Page Handler Update

This commit is contained in:
Josef 2025-03-25 07:42:27 +01:00
parent 7a05d26162
commit 1238a1ff7c
27 changed files with 4412 additions and 16 deletions

212
browser/app/App.ts Normal file
View File

@ -0,0 +1,212 @@
import { nextFrame } from "../animation/nextFrame";
import { OnAnimationFrame } from "../animation/OnAnimationFrame";
import { DateMath } from "../date/DateMath";
import { ActivityAnalyser } from "../dom/ActivityAnalyser";
import { Insight } from "../dom/Insight";
import { LandscapeScale } from "../dom/LandscapeScale";
import { PageHandler, PageHandlerMode } from "../dom/PageHandler";
import { UserAgentDeviceTypes } from "../dom/UserAgentDeviceType";
import { TemplatesManager, TemplatesManagerMode } from "../templates/TemplatesManager";
import { AppPathConverter } from "./AppPathConverter";
export class AppInitializerData
{
localAdress:string;
webAdress:string;
templatesList:string[];
addTemplateStyles:boolean = false;
pageRoot:string;
pagesPath?:string;
disableForceHTTPS?:boolean;
htmlMode?:boolean;
allowWWWSubdomain?:boolean;
}
export class ElectronAppInitializerData
{
appLocation:string;
templatesList:string[];
pageRoot:string;
disableForceHTTPS?:boolean;
htmlMode?:boolean;
allowWWWSubdomain:boolean;
}
export class App
{
readonly activityAnalyser = new ActivityAnalyser();
readonly onAnimationFrame = new OnAnimationFrame();
readonly templatesManager = new TemplatesManager();
readonly insight = new Insight();
readonly landscapeScale = new LandscapeScale();
readonly pathConverter = new AppPathConverter( this );
private _pageHandler:PageHandler;
private _initializerData:AppInitializerData;
private _electronInitializerData:ElectronAppInitializerData;
protected _loaded = false;
get loaded()
{
return this._loaded;
}
protected _loadedTime:Date = null;
get timeElapsedSinceLoaded()
{
return DateMath.getDifferenceMs( new Date(), this._loadedTime ) / 1000;
}
setLoaded()
{
this._loaded = true;
this._loadedTime = new Date();
}
get initializerData(){ return this._initializerData; }
get pageHandler(){ return this._pageHandler; }
get webAdress()
{
return this._initializerData.webAdress;
}
get isHTMLMode()
{
return this._initializerData.htmlMode === true;
}
grabHash()
{
let hash = window.location.hash;
return hash;
}
get isLocal()
{
return false;
}
async initializeApp( data:AppInitializerData|ElectronAppInitializerData ):Promise<void>
{
if ( document.readyState === "loading" )
{
document.addEventListener(
"DOMContentLoaded",
()=>
{
this.setLoaded();
}
);
}
else
{
this.setLoaded();
}
setTimeout( ()=>{ this.setLoaded() }, 3000 )
if ( ! data.allowWWWSubdomain )
{
let url = window.location + "";
if ( url.startsWith( "https://www." ) )
{
let realURL = url.replace( /^https:\/\/www\./, "https://" );
window.location.assign( realURL );
return Promise.resolve();
}
}
UserAgentDeviceTypes.setOnBody();
if ( ( data as ElectronAppInitializerData ).appLocation )
{
console.log( "ELECTRON APP" );
this._electronInitializerData = data as ElectronAppInitializerData;
}
else
{
this._initializerData = data as AppInitializerData;
}
if ( data.templatesList )
{
let templatesList = data.templatesList;
let templateStylesMode = this.initializerData.addTemplateStyles ?
TemplatesManagerMode.ADD_STYLES_TO_HEAD :
TemplatesManagerMode.IGNORE_STYLES;
this.templatesManager.setMode( templateStylesMode );
templatesList.forEach( c => this.templatesManager.addTemplateHTML( c ) );
}
if ( this._initializerData )
{
//console.log( "PAGE HANDLER FOR WEB" );
let appData = this._initializerData;
this._pageHandler = new PageHandler( appData.localAdress, appData.webAdress, data.disableForceHTTPS );
}
else
{
//console.log( "PAGE HANDLER FOR APP" );
let electronData = this._electronInitializerData;
this._pageHandler = new PageHandler( electronData.appLocation, null, electronData.disableForceHTTPS, PageHandlerMode.ELECTRON );
}
this.pageHandler.setPageRootTag( data.pageRoot );
if ( this._initializerData.pagesPath )
{
this.pageHandler.setPagesPath( this._initializerData.pagesPath );
}
this.onAnimationFrame.run();
this.onAnimationFrame.addListener(
()=>
{
this.landscapeScale.update();
this.insight.update();
}
)
this.activityAnalyser.start();
await this.pageHandler.initialize();
this.initializePage();
}
async initializePage()
{
while ( ! this.loaded )
{
await nextFrame();
}
//console.log( "App.initializePage" );
this.insight.grabElements();
this.templatesManager.processChildren( document.body );
this.pageHandler.replaceLinks();
return Promise.resolve();
}
static updateAll()
{
Insight.updateAll();
}
}

View File

@ -0,0 +1,34 @@
import { RootPathResolver } from "../dom/RootPathResolver";
import { App } from "./App";
export class AppPathConverter
{
#app:App;
// root: relative to root => ::/en/store
// absolute: internet/localhost => https://rokojori.com/en/store || localhost/en/store
// relative: to current page => ../en/store
constructor( app:App )
{
this.#app = app;
}
rootToRelative( rootPath:string )
{
// ::/en/store/
// -> ../../en/store
let page = this.#app.pageHandler.currentPage;
let rootToken = RootPathResolver.rootToken;
let pathToRoot = this.#app.pageHandler.rootPathResolver.getRootPath( page );
console.log( "PathToRoot", page, pathToRoot );
let value = rootPath;
value = value.replace( rootToken, pathToRoot );
return value;
}
}

View File

@ -0,0 +1,29 @@
export class DateHelper
{
static now()
{
let date = new Date();
return date;
}
static today()
{
let date = new Date();
date.setHours( 0, 0, 0 );
return date;
}
static createYMD( year:number, month:number = 1, day:number = 1, hours:number = 0, minutes:number = 0, seconds:number = 0 )
{
let date = new Date();
date.setFullYear( year );
date.setMonth( month - 1 );
date.setDate( day );
date.setHours( hours, minutes, seconds );
return date;
}
}

154
browser/date/DateMath.ts Normal file
View File

@ -0,0 +1,154 @@
import { DateHelper } from "./DateHelper";
export class DateMath
{
static sort( dates:Date[] )
{
dates.sort( DateMath.sortDate );
}
static sortDate( a:Date, b:Date )
{
return DateMath.isBefore( a, b ) ? -1 : 1;
}
static isNowOlderThanMS( d:Date, ms:number )
{
let difference = this.getDifferenceMs( DateHelper.now(), d );
return difference > ms;
}
static isBeforeNow( d:Date )
{
return d.getTime() < DateHelper.now().getTime();
}
static isInThePast( d:Date )
{
return DateMath.isBeforeNow( d );
}
static isExpired( d:Date )
{
return DateMath.isInThePast( d );
}
static isExpiredMS( dateMS:number, durationMS:number )
{
let end = dateMS + durationMS;
return end <= DateHelper.now().getTime();
}
static isBefore( a:Date, b:Date )
{
return a.getTime() < b.getTime();
}
static isBeforeOrEquals( a:Date, b:Date )
{
return a.getTime() <= b.getTime();
}
static isAfter( a:Date, b:Date )
{
return a.getTime() > b.getTime();
}
static isAfterNow( d:Date )
{
return d.getTime() > DateHelper.now().getTime();
}
static isInTheFuture( d:Date )
{
return DateMath.isAfterNow( d );
}
static getDifferenceMs( a:Date, b:Date )
{
return a.getTime() - b.getTime();
}
static isAfterOrEquals( a:Date, b:Date )
{
return a.getTime() >= b.getTime();
}
static addMilliseconds( a:Date, durationMS:number )
{
let dateMS = a.getTime();
let milliseconds = dateMS + durationMS;
return new Date( milliseconds );
}
static fromNowAddDays( days:number )
{
return DateMath.addDays( new Date(), days );
}
static fromNowAddHours( hours:number )
{
return DateMath.addHours( new Date(), hours );
}
static fromNowAddMinutes( minutes:number )
{
return DateMath.addMinutes( new Date(), minutes );
}
static addSeconds( a:Date, duration:number )
{
return this.addMilliseconds( a, duration * 1000 );
}
static addMinutes( a:Date, durationMinutes:number )
{
return this.addSeconds( a, durationMinutes * 60 );
}
static addHours( a:Date, durationHours:number )
{
return this.addMinutes( a, durationHours * 60 );
}
static addDays( a:Date, durationDays:number )
{
return this.addHours( a, durationDays * 24 );
}
static nextMonth( date:Date )
{
date = new Date( date );
if ( date.getMonth() == 11 )
{
date.setFullYear( date.getFullYear() + 1, 0 );
}
else
{
date.setMonth( date.getMonth() + 1 );
}
return date;
}
static isOnSameDay( a:Date, b:Date )
{
if ( a.getFullYear() !== b.getFullYear() )
{
return false;
}
if ( a.getMonth() !== b.getMonth() )
{
return false;
}
return a.getDate() === b.getDate();
}
}

View File

@ -0,0 +1,72 @@
import { EventSlot } from "../events/EventSlot";
import { OnBlur, OnFocus, OnMouseDown, OnMouseLeave, OnMouseMove, OnMouseUp, OnTouchCancel, OnTouchEnd, OnTouchMove, OnTouchStart } from "../dom/EventListeners";
export class ActivityAnalyser
{
readonly onActive = new EventSlot();
readonly onPassive = new EventSlot();
private _timeOutDurationMS = 3000;
private _active = false;
get active(){ return this._active; }
private _timeOutCallback:any = null;
start()
{
let setActive = ()=>
{
this._cancelPassiveState();
if ( ! this._active )
{
this._active = true;
this.onActive.dispatch( null );
}
this._reschedulePassiveState();
}
let activeEvents =
[
OnFocus, OnBlur,
OnMouseMove, OnMouseDown, OnMouseUp, OnMouseLeave,
OnTouchStart, OnTouchMove, OnTouchCancel, OnTouchEnd
];
activeEvents.forEach(
( onEvent )=>
{
onEvent.add( document.body, setActive );
}
)
}
private _cancelPassiveState()
{
if ( this._timeOutCallback )
{
clearTimeout( this._timeOutCallback );
this._timeOutCallback = null;
}
}
private _reschedulePassiveState()
{
this._cancelPassiveState();
this._timeOutCallback = setTimeout( ()=>{ this._setPassive(); }, this._timeOutDurationMS );
}
private _setPassive()
{
if ( ! this._active )
{
return;
}
this._active = false;
this.onPassive.dispatch( null );
}
}

View File

@ -0,0 +1,161 @@
import { DOMEditor } from "./DOMEditor";
import { ElementAttribute } from "./ElementAttribute";
export enum AttributeValueMatchMode
{
EXACTLY,
STARTS_WITH
}
export class AttributeValue
{
private _value:string;
private _attribute:ElementAttribute;
private _mode:AttributeValueMatchMode = AttributeValueMatchMode.EXACTLY;
static get type_checkbox(){ return new AttributeValue( ElementAttribute.type, "checkbox" ); }
static get type_email(){ return new AttributeValue( ElementAttribute.type, "email" ); }
static get type_text(){ return new AttributeValue( ElementAttribute.type, "text" ); }
static get type_file(){ return new AttributeValue( ElementAttribute.type, "file" ); }
static get type_number(){ return new AttributeValue( ElementAttribute.type, "number" ); }
static get type_time(){ return new AttributeValue( ElementAttribute.type, "time" ); }
static get type_date(){ return new AttributeValue( ElementAttribute.type, "date" ); }
static get type_password(){ return new AttributeValue( ElementAttribute.type, "password" ); }
static get target_blank(){ return new AttributeValue( ElementAttribute.target, "_blank" ); }
static get preloadAuto() { return new AttributeValue( ElementAttribute.preload, "auto" ); }
static get preloadNone() { return new AttributeValue( ElementAttribute.preload, "none" ); }
static get preloadMetaData() { return new AttributeValue( ElementAttribute.preload, "metadata" ); }
static parseAttributeValue( source:string )
{
let seperator = source.indexOf( "=" );
let attributeName = source.substring( 0, seperator ).trim();
let value = source.substring( seperator + 1 ).trim();
if ( value.startsWith( "\"" ) || value.startsWith( "'") )
{
value = value.substring( 1, value.length - 1 );
}
let attribute = new ElementAttribute( attributeName, false );
return new AttributeValue( attribute, value );
}
constructor( attribute:ElementAttribute, value:string, mode:AttributeValueMatchMode = AttributeValueMatchMode.EXACTLY )
{
this._value = value;
this._attribute = attribute;
this._mode = mode || AttributeValueMatchMode.EXACTLY;
}
get attribute()
{
return this._attribute;
}
get value()
{
return this._value;
}
get selector():string
{
switch ( this._mode )
{
case AttributeValueMatchMode.EXACTLY:
{
return this._attribute.selectorEquals( this._value );
}
case AttributeValueMatchMode.STARTS_WITH:
{
return this._attribute.selectorStartsWith( this._value );
}
}
console.log( "No selector" );
return null;
}
find( elements:Element[] )
{
for ( let i = 0; i < elements.length; i++ )
{
if ( this.in( elements[ i ] ) )
{
return elements[ i ];
}
}
return null;
}
query<T extends Element>( element:Element )
{
return element.querySelector( this.selector ) as T;
}
queryDoc<T extends Element>()
{
return document.querySelector( this.selector ) as T;
}
queryAll( element:Element )
{
return DOMEditor.nodeListToArray( element.querySelectorAll( this.selector ) );
}
queryAllInDoc()
{
return DOMEditor.nodeListToArray( document.querySelectorAll( this.selector ) );
}
set( element:Element )
{
this._attribute.to( element, this._value );
}
removeFrom( element:Element )
{
this._attribute.removeFrom( element );
}
toggle( element:Element )
{
if ( this.in( element ) )
{
this.removeFrom( element );
}
else
{
this.set( element );
}
}
isSelected( selectElement:HTMLSelectElement )
{
return selectElement.value == this.value;
}
in( element:Element )
{
let attValue = this._attribute.from( element );
switch ( this._mode )
{
case AttributeValueMatchMode.EXACTLY: return attValue === this._value;
case AttributeValueMatchMode.STARTS_WITH: return attValue.startsWith( this._value );
}
return false;
}
}

12
browser/dom/Cursor.ts Normal file
View File

@ -0,0 +1,12 @@
export class Cursor
{
static lock( type:string = null )
{
}
static free()
{
}
}

138
browser/dom/DOMHitTest.ts Normal file
View File

@ -0,0 +1,138 @@
import { Box2 } from "../geometry/Box2";
import { Vector2 } from "../geometry/Vector2";
export class DOMHitTest
{
static get scrollOffset():Vector2
{
return new Vector2( window.scrollX, window.scrollY );
}
static getPageRect( e:Element ):ClientRect
{
let pageBox = this.getPageBox( e );
return Box2.toDomRect( pageBox );
}
static other()
{
return true;
}
static isPointerOver( me:MouseEvent|TouchEvent, e:Element )
{
if ( ! me || ! e )
{
return false;
}
let relative = DOMHitTest.getNormalizedPointerPosition( me, e );
if ( relative.x < 0 || relative.x > 1)
{
return false;
}
if ( relative.y < 0 || relative.y > 1 )
{
return false;
}
return true;
}
static getPageBox( e:Element ):Box2
{
let pageBox = Box2.fromClientRect( e.getBoundingClientRect() );
pageBox.translate( this.scrollOffset );
return pageBox;
}
private static _lastPageX:number = null;
private static _lastPageY:number = null;
static getPointerPosition( e:MouseEvent|TouchEvent ):Vector2
{
let isMouseEvent = /mouse|click/.test( e.type );
console.log( e.type );
let mouseEvent = isMouseEvent ? e as MouseEvent : null;
let touchEvent = isMouseEvent ? null : ( e as TouchEvent );
let pageX = isMouseEvent ? mouseEvent.pageX : touchEvent.touches.length === 0 ? null : touchEvent.touches[ 0 ].pageX;
let pageY = isMouseEvent ? mouseEvent.pageY : touchEvent.touches.length === 0 ? null : touchEvent.touches[ 0 ].pageY;
// if ( isMouseEvent && CustomMouse.active )
// {
// pageX = CustomMouse.x;
// pageY = CustomMouse.y;
// }
if ( pageX === null )
{
pageX = DOMHitTest._lastPageX === undefined ? window.innerWidth / 2 : DOMHitTest._lastPageX;
}
if ( pageY === null )
{
pageY = DOMHitTest._lastPageY === undefined ? window.innerHeight / 2 : DOMHitTest._lastPageY;
}
DOMHitTest._lastPageX = pageX;
DOMHitTest._lastPageY = pageY;
return new Vector2( pageX, pageY );
}
static getRelativePointerPosition( e:MouseEvent|TouchEvent, element:Element )
{
let position = this.getPointerPosition( e );
let pageRect = this.getPageBox( element );
return position.sub( pageRect.min );
}
static getRelativeWindowPosition( e:MouseEvent|TouchEvent )
{
let position = this.getPointerPosition( e );
let windowBox = new Box2( new Vector2( 0, 0 ), new Vector2( window.innerWidth, window.innerHeight ) );
position.sub( windowBox.min );
let size = windowBox.size;
position.x /= size.x;
position.y /= size.y;
return position;
}
static getNormalizedPointerPosition( e:MouseEvent|TouchEvent, element:Element )
{
let position = this.getPointerPosition( e );
let pageRect = this.getPageBox( element );
position.sub( pageRect.min );
let size = pageRect.size;
position.x /= size.x;
position.y /= size.y;
return position;
}
static getNormalizedPosition( position:Vector2, element:Element )
{
let pageRect = this.getPageBox( element );
position.sub( pageRect.min );
let size = pageRect.size;
position.x /= size.x;
position.y /= size.y;
return position;
}
}

View File

@ -0,0 +1,23 @@
export enum DOMOrientationType
{
Portrait,
Landscape
}
export class DOMOrientation
{
static get type()
{
return this.isLandscape ? DOMOrientationType.Landscape : DOMOrientationType.Portrait;
}
static get isLandscape()
{
return window.innerWidth > window.innerHeight;
}
static get isPortrait()
{
return window.innerWidth < window.innerHeight;
}
}

View File

@ -0,0 +1,932 @@
import { Func } from "../tools/TypeUtilities";
import { Cursor } from "./Cursor";
import { DOMHitTest } from "./DOMHitTest";
export type click = "click";
export type contextmenu = "contextmenu";
export type touchstart = "touchstart";
export type touchmove = "touchmove";
export type touchcancel = "touchcancel";
export type touchend = "touchend";
export type mousedown = "mousedown";
export type mousemove = "mousemove";
export type mouseup = "mouseup";
export type mouseleave = "mouseleave";
export type dblclick = "dblclick";
export type mouseover = "mouseover";
export type mouseenter = "mouseenter";
export type wheel = "wheel";
export type keydown = "keydown";
export type keyup = "keyup";
export type blur = "blur";
export type focus = "focus";
export type change = "change";
export type input = "input";
export type submit = "submit";
export type resize = "resize";
export type pause = "pause";
export type play = "play";
export type ended = "ended";
export type timeupdate = "timeupdate";
export type pointerlockchange = "pointerlockchange";
export type pointerlockerror = "pointerlockerror";
export type load = "load";
export type selectstart = "selectstart";
// export type (\w+).+
// $1 |
export type EventListenerType =
click |
contextmenu |
touchstart |
touchmove |
touchcancel |
touchend |
mousedown |
mousemove |
mouseup |
mouseleave |
dblclick |
mouseover |
mouseenter |
wheel |
keydown |
keyup |
blur |
focus |
change |
input |
submit |
resize |
pause |
play |
ended |
timeupdate |
pointerlockchange |
pointerlockerror |
load |
selectstart
;
// export type (\w+)\s*=\s*"(.+)"\s*;
// static readonly $1:$1 = "$2";
export class Events
{
static readonly MouseButtonLeft = 0;
static readonly MouseButtonMiddle = 1;
static readonly MouseButtonRight = 2;
static readonly click:click = "click";
static readonly contextmenu:contextmenu = "contextmenu";
static readonly touchstart:touchstart = "touchstart";
static readonly touchmove:touchmove = "touchmove";
static readonly touchcancel:touchcancel = "touchcancel";
static readonly touchend:touchend = "touchend";
static readonly mousedown:mousedown = "mousedown";
static readonly mousemove:mousemove = "mousemove";
static readonly mouseup:mouseup = "mouseup";
static readonly mouseleave:mouseleave = "mouseleave";
static readonly dblclick:dblclick = "dblclick";
static readonly mouseover:mouseover = "mouseover";
static readonly mouseenter:mouseenter = "mouseenter";
static readonly wheel:wheel = "wheel";
static readonly keydown:keydown = "keydown";
static readonly keyup:keyup = "keyup";
static readonly blur:blur = "blur";
static readonly focus:focus = "focus";
static readonly change:change = "change";
static readonly input:input = "input";
static readonly submit:submit = "submit";
static readonly resize:resize = "resize";
static readonly pause:pause = "pause";
static readonly play:play = "play";
static readonly ended:ended = "ended";
static readonly timeupdate:timeupdate = "timeupdate";
static readonly pointerlockchange:pointerlockchange = "pointerlockchange";
static readonly pointerlockerror:pointerlockerror = "pointerlockerror";
static readonly load = "load";
static readonly selectstart:selectstart = "selectstart";
static copyMouseEvent( e:MouseEvent )
{
return new MouseEvent( e.type, e );
}
static copyTouchEvent( e:TouchEvent )
{
let te = new TouchEvent( e.type, e as any );
return te;
}
static copyMouseOrTouchEvent( e:TouchEvent|MouseEvent )
{
if ( this.isMouseEvent( e ) )
{
return this.copyMouseEvent( e as MouseEvent );
}
return this.copyTouchEvent( e as TouchEvent );
}
static is( e:Event, ...types:EventListenerType[] )
{
for ( let t of types )
{
if ( e.type === t )
{
return true;
}
}
return false;
}
static isType( e:string, ...types:EventListenerType[] )
{
for ( let t of types )
{
if ( e === t )
{
return true;
}
}
return false;
}
static isEventOneOf( e:string|Event, ...types:EventListenerType[] )
{
if ( typeof e === "string" )
{
for ( let t of types )
{
if ( e === t )
{
return true;
}
}
return false;
}
for ( let t of types )
{
if ( e.type === t )
{
return true;
}
}
return false;
}
static isMouseEvent( e:MouseEvent|TouchEvent )
{
return /^mouse/.test( e.type ) || ( e.type === Events.dblclick );
}
static îsTouchEvent( e:MouseEvent|TouchEvent )
{
return /^touch/.test( e.type )
}
static toMouseEvent( e:MouseEvent|TouchEvent )
{
if ( ! this.isMouseEvent( e ) )
{
console.log( "Not mouse event:", e.type );
return null;
}
return e as MouseEvent;
}
static toTouchEvent( e:MouseEvent|TouchEvent )
{
if ( this.îsTouchEvent( e ) )
{
return null;
}
return e as TouchEvent;
}
static isStart( e:MouseEvent|TouchEvent|string )
{
return Events.isEventOneOf( e, Events.mousedown, Events.touchstart );
}
static isMove( e:MouseEvent|TouchEvent|string )
{
return Events.isEventOneOf( e, Events.mousemove, Events.touchmove );
}
static isEnd( e:MouseEvent|TouchEvent|string )
{
return Events.isEventOneOf( e, Events.mouseup, Events.touchend, Events.touchcancel );
}
static silent( e:Event )
{
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
}
static once<T>( target:EventTarget, type:EventListenerType, callback:(t:T)=>void )
{
let onceCallback = ( e:T )=>
{
try
{
callback( e );
}
catch( ex )
{
console.error( ex );
}
target.removeEventListener( type, onceCallback as any );
}
target.addEventListener( type, onceCallback as any );
}
static disableSelection( e:HTMLElement ):Func<void>
{
let preventer = ( ev:any ) => { ev.preventDefault() };
OnMouseDown.add( e, preventer );
OnSelectStart.add( e, preventer );
let remover = ()=>
{
OnMouseDown.remove( e, preventer );
OnSelectStart.remove( e, preventer );
}
return remover;
}
}
export class OnPointerLockChange
{
static add( element:HTMLElement|Document, callback:( e:Event )=>void, options:any = undefined )
{
document.addEventListener( Events.pointerlockchange, callback, options );
}
static remove( element:HTMLElement|Document, callback:( e:Event )=>void )
{
document.removeEventListener( Events.pointerlockchange, callback );
}
}
export class OnWheel
{
static add( element:HTMLElement, callback:( e:WheelEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.wheel, callback, options );
}
static remove( element:HTMLElement, callback:( e:WheelEvent )=>void )
{
element.removeEventListener( Events.wheel, callback );
}
}
export type MouseEventCallback = ( e:MouseEvent )=>void;
export class WrappedCallback
{
element:HTMLElement;
wrappedCallback:MouseEventCallback;
}
export class OnClick
{
static add( element:HTMLElement, callback:MouseEventCallback, options:any = undefined )
{
element.addEventListener( Events.click, callback, options );
}
static remove( element:HTMLElement, callback:MouseEventCallback )
{
element.removeEventListener( Events.click, callback );
}
}
export class OnMouseDown
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.mousedown, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.mousedown, callback );
}
}
export class OnContextMenu
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.contextmenu, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.contextmenu, callback );
}
}
export class OnMouseDrag
{
static noHold = false;
static add( element:HTMLElement, callback:( e:MouseEvent )=>void|boolean, options:any = undefined )
{
let noHold = OnMouseDrag.noHold;
let isHoldInteraction = null;
let downStart = 0;
let holdInteractionTreshold = 500;
let checkAutomaticSwitch = (e:MouseEvent) =>
{
let time = new Date().getTime();
let elapsed = time - downStart;
if ( elapsed > holdInteractionTreshold )
{
removeEvents( e );
}
else
{
Cursor.lock();
//UIComponent.lockCursor( UICursors.releaseDragging + "" );
}
}
let removeEvents = ( e:MouseEvent )=>
{
if ( noHold )
{
let mouseUpEvent = new MouseEvent( Events.mouseup, e );
callback( mouseUpEvent );
}
else
{
callback( e );
}
OnMouseMove.remove( document.body, callback );
if ( noHold )
{
OnMouseUp.remove( document.body, checkAutomaticSwitch );
OnMouseDown.remove( document.body, removeEvents );
//UIComponent.freeCursor();
Cursor.free();
}
else
{
OnMouseUp.remove( document.body, removeEvents );
}
}
let addEvents = ()=>
{
OnMouseMove.add( document.body, callback );
if ( noHold )
{
OnMouseUp.add( document.body, checkAutomaticSwitch );
OnMouseDown.add( document.body, removeEvents );
}
else
{
OnMouseUp.add( document.body, removeEvents );
}
}
let onDown = ( e:MouseEvent )=>
{
let result = callback( e );
if ( result === false )
{
return;
}
downStart = new Date().getTime();
isHoldInteraction = true;
addEvents();
}
OnMouseDown.add( element, onDown );
return onDown;
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
OnMouseDown.remove( element, callback );
}
}
export class OnMouseOver
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.mouseover, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.mouseover, callback );
}
}
export class OnMouseEnter
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.mouseenter, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.mouseenter, callback );
}
}
export class OnDoubleClick
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.dblclick, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.dblclick, callback );
}
}
export class OnMouseMove
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.mousemove, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.mousemove, callback );
}
}
export class OnMouseUp
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.mouseup, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.mouseup, callback );
}
}
export class OnMouseLeave
{
static add( element:HTMLElement, callback:( e:MouseEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.mouseleave, callback, options );
}
static remove( element:HTMLElement, callback:( e:MouseEvent )=>void )
{
element.removeEventListener( Events.mouseleave, callback );
}
}
export class OnKeyDown
{
static add( element:HTMLElement, callback:( e:KeyboardEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.keydown, callback, options );
}
static remove( element:HTMLElement, callback:( e:KeyboardEvent )=>void )
{
element.removeEventListener( Events.keydown, callback );
}
}
export class OnKeyUp
{
static add( element:HTMLElement, callback:( e:KeyboardEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.keyup, callback, options );
}
static remove( element:HTMLElement, callback:( e:KeyboardEvent )=>void )
{
element.removeEventListener( Events.keyup, callback );
}
}
export class OnTouchStart
{
static add( element:HTMLElement, callback:( e:TouchEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.touchstart, callback, options );
}
static remove( element:HTMLElement, callback:( e:TouchEvent )=>void )
{
element.removeEventListener( Events.touchstart, callback );
}
}
export class OnTouchEnd
{
static add( element:HTMLElement, callback:( e:TouchEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.touchend, callback, options );
}
static remove( element:HTMLElement, callback:( e:TouchEvent )=>void )
{
element.removeEventListener( Events.touchend, callback );
}
}
export class OnTouchCancel
{
static add( element:HTMLElement, callback:( e:TouchEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.touchcancel, callback, options );
}
static remove( element:HTMLElement, callback:( e:TouchEvent )=>void )
{
element.removeEventListener( Events.touchcancel, callback );
}
}
export class OnTouchMove
{
static add( element:HTMLElement, callback:( e:TouchEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.touchmove, callback, options );
}
static remove( element:HTMLElement, callback:( e:TouchEvent )=>void )
{
element.removeEventListener( Events.touchmove, callback );
}
}
export class OnTouchDrag
{
static add( element:HTMLElement, callback:( e:TouchEvent )=>void, options:any = undefined )
{
OnTouchStart.add( element, callback, options );
OnTouchMove.add( element, callback, options );
OnTouchEnd.add( element, callback, options );
}
static remove( element:HTMLElement, callback:( e:TouchEvent )=>void )
{
OnTouchStart.remove( element, callback );
OnTouchMove.remove( element, callback );
OnTouchEnd.remove( element, callback );
}
}
export class OnDrag
{
static add( element:HTMLElement, callback:( e:TouchEvent|MouseEvent )=>void, options:any = undefined )
{
OnTouchDrag.add( element, callback );
let mouseCallback = OnMouseDrag.add( element, callback );
return { touch:callback, mouse:mouseCallback };
}
static remove( element:HTMLElement, callbackData:{ touch:()=>void, mouse:()=>void } )
{
OnTouchDrag.add( element, callbackData.touch );
OnMouseDrag.add( element, callbackData.mouse );
}
}
export type HORIZONTAL = "HORIZONTAL";
export type VERTICAL = "VERTICAL";
export type BOTH = "BOTH";
export type SwipeType = HORIZONTAL | VERTICAL | BOTH;
export class OnSwipe
{
static readonly HORIZONTAL:HORIZONTAL = "HORIZONTAL";
static readonly VERTICAL:VERTICAL = "VERTICAL";
static readonly BOTH:BOTH = "BOTH";
static add( type:SwipeType, treshold:number, element:HTMLElement, callback:( e:TouchEvent )=>void, options:any = undefined )
{
let onStart =
( startEvent:TouchEvent )=>
{
let checkingMovement = true;
let active = false;
let startPosition = DOMHitTest.getPointerPosition( startEvent );
let onDrag = ( dragEvent:TouchEvent )=>
{
if ( checkingMovement )
{
let position = DOMHitTest.getPointerPosition( dragEvent );
let difference = position.sub( startPosition );
let moved = difference.length > treshold;
if ( moved )
{
if ( OnSwipe.HORIZONTAL === type )
{
active = Math.abs( difference.x ) > Math.abs( difference.y );
}
else if ( OnSwipe.VERTICAL === type )
{
active = Math.abs( difference.y ) > Math.abs( difference.x );
}
else
{
active = true;
}
checkingMovement = false;
callback( startEvent );
}
}
if ( ! active )
{
return;
}
callback( dragEvent );
dragEvent.preventDefault();
dragEvent.stopImmediatePropagation();
dragEvent.stopPropagation();
}
let removeListeners = ( endEvent :TouchEvent ) =>
{
if ( active )
{
callback( endEvent );
endEvent.preventDefault();
endEvent.stopImmediatePropagation();
endEvent.stopPropagation();
}
OnTouchMove.remove( element, onDrag );
OnTouchCancel.remove( element, removeListeners );
OnTouchEnd.remove( element, removeListeners );
}
OnTouchMove.add( element, onDrag );
OnTouchCancel.add( element, removeListeners );
OnTouchEnd.add( element, removeListeners );
};
OnTouchStart.add( element, onStart );
return onStart;
}
static remove( element:HTMLElement, callback:( e:TouchEvent )=>void )
{
OnTouchStart.remove( element, callback );
}
}
export class OnBlur
{
static add( element:HTMLElement, callback:( e:FocusEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.blur, callback, options );
}
static remove( element:HTMLElement, callback:( e:FocusEvent )=>void )
{
element.removeEventListener( Events.blur, callback );
}
}
export class OnFocus
{
static add( element:HTMLElement, callback:( e:FocusEvent )=>void, options:any = undefined )
{
element.addEventListener( Events.focus, callback, options );
}
static remove( element:HTMLElement, callback:( e:FocusEvent )=>void )
{
element.removeEventListener( Events.focus, callback );
}
}
export class OnChange
{
static add( element:HTMLElement|HTMLInputElement, callback:( e:Event )=>void, options:any = undefined )
{
element.addEventListener( Events.change, callback, options );
}
static remove( element:HTMLElement|HTMLInputElement, callback:( e:Event )=>void )
{
element.removeEventListener( Events.change, callback );
}
}
export class OnInput
{
static add( element:HTMLElement, callback:( e:InputEvent )=>void, options:any = undefined )
{
( element as any ).addEventListener( Events.input, callback, options );
}
static remove( element:HTMLElement, callback:( e:InputEvent )=>void )
{
( element as any ).removeEventListener( Events.input, callback );
}
}
export class OnSubmit
{
static add( element:HTMLFormElement, callback:( e:Event )=>void, options:any = undefined )
{
element.addEventListener( Events.submit, callback, options );
}
static remove( element:HTMLFormElement, callback:( e:Event )=>void )
{
element.removeEventListener( Events.submit, callback );
}
}
export class OnResize
{
static add( window:Window, callback:( e:Event )=>void, options:any = undefined )
{
window.addEventListener( Events.resize, callback, options );
}
static remove( window:Window, callback:( e:Event )=>void )
{
window.removeEventListener( Events.resize, callback );
}
}
export class OnPlay
{
static add( audioElement:HTMLAudioElement, callback:( e:Event )=>void, options:any = undefined )
{
audioElement.addEventListener( Events.play, callback, options );
}
static remove( audioElement:HTMLAudioElement, callback:( e:Event )=>void )
{
audioElement.removeEventListener( Events.play, callback );
}
}
export class OnPause
{
static add( audioElement:HTMLAudioElement, callback:( e:Event )=>void, options:any = undefined )
{
audioElement.addEventListener( Events.pause, callback, options );
}
static remove( audioElement:HTMLAudioElement, callback:( e:Event )=>void )
{
audioElement.removeEventListener( Events.pause, callback );
}
}
export class OnEnded
{
static add( audioElement:HTMLAudioElement, callback:( e:Event )=>void, options:any = undefined )
{
audioElement.addEventListener( Events.ended, callback, options );
}
static remove( audioElement:HTMLAudioElement, callback:( e:Event )=>void )
{
audioElement.removeEventListener( Events.ended, callback );
}
}
export class OnTimeUpdate
{
static add( audioElement:HTMLAudioElement, callback:( e:Event )=>void, options:any = undefined )
{
audioElement.addEventListener( Events.timeupdate, callback, options );
}
static remove( audioElement:HTMLAudioElement, callback:( e:Event )=>void )
{
audioElement.removeEventListener( Events.timeupdate, callback );
}
}
export class OnLoad
{
static add( element:HTMLScriptElement|FileReader, callback:( e:Event )=>void, options:any = undefined )
{
element.addEventListener( Events.load, callback, options );
}
static remove( element:HTMLScriptElement|FileReader, callback:( e:Event )=>void )
{
element.removeEventListener( Events.load, callback );
}
}
export class OnSelectStart
{
static add( element:HTMLElement, callback:( e:Event )=>void, options:any = undefined )
{
element.addEventListener( Events.selectstart, callback, options );
}
static remove( element:HTMLElement, callback:( e:Event )=>void )
{
element.removeEventListener( Events.selectstart, callback );
}
}

372
browser/dom/Insight.ts Normal file
View File

@ -0,0 +1,372 @@
import { ClassFlag } from "./ClassFlag";
import { HTMLNodeTreeWalker } from '../graphs/HTMLNodeTreeWalker';
import { Box2 } from "../geometry/Box2";
import { Vector2 } from "../geometry/Vector2";
import { ElementAttribute } from "./ElementAttribute";
import { MapList } from "../tools/MapList";
export type above_sight = "above-sight";
export type in_sight = "in-sight";
export type below_sight = "below-sight";
export type InsighStateType = above_sight | in_sight | below_sight;
export class InsighState
{
static readonly above_sight:above_sight = "above-sight";
static readonly in_sight:in_sight = "in-sight";
static readonly below_sight:below_sight = "below-sight";
}
export class InsightData
{
insight:Insight;
isInsight = false;
wasInsight = false;
viewBox:Box2 = null;
elementBox:Box2 = new Box2();
changeTime:number = 0;
}
export class InsightElement extends Element
{
_insightData:InsightData;
static ensure( element:Element )
{
let insightElement = element as InsightElement;
if ( ! insightElement._insightData )
{
insightElement._insightData = new InsightData();
}
return insightElement;
}
static forAllInsightElements( root:Element, callback:( insightElement:InsightElement )=>void )
{
let walker = HTMLNodeTreeWalker.$;
walker.forAll( root,
n =>
{
let insightElement = n as InsightElement;
if ( insightElement._insightData )
{
callback( insightElement );
}
}
);
}
}
export type InsightCallback = ()=>void;
export type CallbackMapList = Map<Element,InsightCallback[]>;
export class Insight
{
static attribute = new ElementAttribute( "insight" );
static wasSeenFlag = new ClassFlag( "was-in-sight" );
static maxHeight = new ElementAttribute( "max-insight-height" );
static readonly above_sight = new ClassFlag( InsighState.above_sight );
static readonly in_sight = new ClassFlag( InsighState.in_sight );
static readonly below_sight = new ClassFlag( InsighState.below_sight );
static changeFrequencyMS = 200;
private _elements:Element[] = [];
private _offsightCallbackElements:CallbackMapList;
private _insightCallbackElements:CallbackMapList;
private _viewBoxSize:Vector2 = null;
private _hasScreenUpdate = false;
private _lastYOffset = 0;
private _yTresholdForChanges = 15;
private _lastGrabbedTime = 0;
private _afterGrabingUpdateDurationMS = 3000;
static _allInstances:Insight[] = [];
static updateAll()
{
this._allInstances.forEach( i => { i._hasScreenUpdate = true; i.update() } );
}
constructor( publishGlobally:boolean = true )
{
if ( publishGlobally )
{
Insight._allInstances.push( this );
}
}
onInsight( element:Element, callback:InsightCallback )
{
MapList.add( this._insightCallbackElements, element, callback );
}
onOffsight( element:Element, callback:InsightCallback )
{
MapList.add( this._offsightCallbackElements, element, callback );
}
grabElements()
{
this._lastGrabbedTime = new Date().getTime();
this._elements.forEach(
e =>
{
let element = InsightElement.ensure( e );
element._insightData.insight = null;
}
);
this._elements = Insight.attribute.queryAll( document.body );
this._offsightCallbackElements = new Map<Element,InsightCallback[]>();
this._insightCallbackElements = new Map<Element,InsightCallback[]>();
this._elements.forEach(
e =>
{
let element = InsightElement.ensure( e );
element._insightData.insight = this;
}
);
//console.log( this._elements );
}
update()
{
this._updateViewBox();
let currentTime = new Date().getTime();
let elapsedSinceGrabbing = currentTime - this._lastGrabbedTime;
if ( elapsedSinceGrabbing < this._afterGrabingUpdateDurationMS )
{
this._hasScreenUpdate = true;
}
let yChange = window.pageYOffset - this._lastYOffset;
if ( Math.abs( yChange ) > this._yTresholdForChanges )
{
this._hasScreenUpdate = true;
}
if ( ! this._hasScreenUpdate )
{
return;
}
this._lastYOffset = window.pageYOffset;
this._hasScreenUpdate = false;
let now = new Date().getTime();
this._elements.forEach( e => this.updateElement( e, now ) );
}
updateElement( e:Element, updateTime:number )
{
let insightElement = InsightElement.ensure( e );
let insightData = insightElement._insightData;
let viewBox = insightData.viewBox;
if ( ! viewBox )
{
viewBox = this._getViewBox( e );
insightData.viewBox = viewBox;
}
let elementBox = insightData.elementBox;
Box2.updatefromClientRect( insightData.elementBox, e.getBoundingClientRect() );
if ( Insight.maxHeight.in( e ) )
{
let maxHeight = Insight.maxHeight.from( e );
let isPercentage = /\%/.test( maxHeight );
let maxHeightPixel = parseFloat( maxHeight.replace( /\%|px/, "" ) );
if ( isPercentage )
{
maxHeightPixel *= window.innerHeight / 100;
}
let size = elementBox.size;
if ( size.y > maxHeightPixel )
{
let center = elementBox.center;
let offset = maxHeightPixel / 2;
elementBox.min.y = center.y - offset;
elementBox.max.y = center.y + offset;
}
}
let insight = viewBox.intersectsBox( elementBox );
/*
console.log(
"Is in sight", insight,
"DOCUMENT VIEW BOX:", viewBox,
"ELEMENT BOX:", elementBox,
"ELEMENT", e
);*/
if ( insight === insightData.isInsight )
{
return;
}
let timeElapsedSinceLastChange = updateTime - insightData.changeTime;
if ( timeElapsedSinceLastChange <= Insight.changeFrequencyMS )
{
return;
}
insightData.changeTime = updateTime;
insightData.isInsight = insight;
if ( insight )
{
Insight.in_sight.setAs( e, true );
Insight.above_sight.setAs( e, false);
Insight.below_sight.setAs( e, false );
if ( this._insightCallbackElements.has( e ) )
{
this._insightCallbackElements.get( e ).forEach( c => c() );
}
}
else
{
let isAbove = elementBox.max.y < viewBox.min.y;
Insight.in_sight.setAs( e, false );
Insight.above_sight.setAs( e, isAbove);
Insight.below_sight.setAs( e, ! isAbove );
if ( this._offsightCallbackElements.has( e ) )
{
this._offsightCallbackElements.get( e ).forEach( c => c() );
}
}
if ( insightData.wasInsight )
{
return;
}
setTimeout( ()=>{ Insight.wasSeenFlag.set( e ); }, 200 );
}
private _updateViewBox()
{
if ( ! this._elements )
{
return;
}
if ( ! this._needsViewBoxUpdate() )
{
return;
}
this._hasScreenUpdate = true;
this._viewBoxSize.x = window.innerWidth;
this._viewBoxSize.y = window.innerHeight;
this._elements.forEach(
e =>
{
let insightElement = InsightElement.ensure( e );
let data = insightElement._insightData;
data.viewBox = this._getViewBox( e );;
}
);
}
private _needsViewBoxUpdate()
{
if ( this._viewBoxSize == null )
{
this._viewBoxSize = new Vector2( 0, 0 );
return true;
}
if ( this._viewBoxSize.x !== window.innerWidth || this._viewBoxSize.y !== window.innerHeight )
{
return true;
}
return false;
}
private _getViewBox( e:Element ):Box2
{
let relativeStart = 15;
let relativeEnd = 100 - relativeStart;
let coords = Insight.attribute.from( e );
if ( coords && coords.length > 0 )
{
let numbers = coords.split( "," );
if ( numbers.length == 1 )
{
relativeStart = parseFloat( numbers[ 0 ] );
relativeEnd = 100 - relativeStart;
}
else if ( numbers.length == 2 )
{
relativeStart = parseFloat( numbers[ 0 ] );
relativeEnd = 100 - parseFloat( numbers[ 1 ] );
}
}
if ( isNaN( relativeStart ) )
{
relativeStart = 15;
}
if ( isNaN( relativeEnd ) )
{
relativeEnd = 100 - relativeStart;
}
//console.log( "VIEW BOX START/END", relativeStart, relativeEnd );
let startY = window.innerHeight * relativeStart / 100;
let endY = window.innerHeight * relativeEnd / 100;
return new Box2( new Vector2( -10000, startY ), new Vector2( 10000, endY ) );
}
}

View File

@ -0,0 +1,105 @@
import { DOMOrientation, DOMOrientationType } from "../dom/DOMOrientation";
import { EventSlot } from "../events/EventSlot";
import { ElementType } from "./ElementType";
export class LandscapeScale
{
private _styleElement:Element;
private _lastWidth:number;
private _lastHeight:number;
private _lastOrientation:DOMOrientationType;
private _ultraWideRatio:number = 2.2;
private _sw:number;
private _sh:number;
private _swidth:number;
private _sheight:number;
private _sx:number;
private _sy:number;
private _spx:number;
get sw()
{
return this._sw;
}
onScreenSizeChange = new EventSlot<LandscapeScale>();
onOrientationChange = new EventSlot<LandscapeScale>();
update()
{
let newWidth = window.innerWidth;
let newHeight = window.innerHeight;
let newOrientation = DOMOrientation.type;
if ( newWidth === this._lastWidth && newHeight === this._lastHeight )
{
return;
}
this.onScreenSizeChange.dispatch( this );
if ( this._lastOrientation != newOrientation )
{
this._lastOrientation = newOrientation;
this.onOrientationChange.dispatch( this );
}
if ( this._styleElement == null )
{
this._styleElement = ElementType.style.create();
document.body.appendChild( this._styleElement );
}
let ratio = newWidth / newHeight;
this._lastWidth = newWidth;
this._lastHeight = newHeight;
this._sw = newWidth / 100;
if ( ratio > this._ultraWideRatio )
{
this._sw *= ( this._ultraWideRatio / ratio );
this._sh = this._sw / 16 * 9;
this._sx = ( newWidth - ( this._sw * 100 ) ) / 2;
this._sy = 0;
this._spx = ratio / this._ultraWideRatio;
}
else
{
this._spx = 1;
this._sx = 0;
this._sy = 0;
}
this._swidth = this._sw * 100;
this._sheight = this._sh * 100;
this._styleElement.innerHTML =
`
:root
{
--sw:${this._sw}px;
--sh:${this._sh}px;
--sx:${this._sx}px;
--sy:${this._sy}px;
--swidth:${this._swidth}px;
--sheight:${this._sheight}px;
--spx:${this._spx}px;
--spx-raw:${this._spx};
--spx-inv:${1/this._spx}px;
}
`
}
}

View File

@ -0,0 +1,74 @@
export enum LinkType
{
RELATIVE,
ROOT,
ABSOLUTE
}
export class Link
{
type:LinkType;
path:string;
clone()
{
let c = new Link();
c.type = this.type;
c.path = this.path;
return c;
}
prettify()
{
let clone = this.clone();
if ( clone.path.endsWith( "index.php" ) )
{
clone.path = clone.path.replace( /index\.php$/, "" );
}
if ( clone.path.endsWith( ".php" ) )
{
clone.path = clone.path.replace( /\.php$/, "" );
}
if ( clone.path.endsWith( "/" ) )
{
clone.path = clone.path.replace( /\/$/, "" );
}
return clone;
}
static Root( path:string )
{
let c = new Link();
c.type = LinkType.ROOT;
c.path = path;
return c;
}
static Absolute( path:string )
{
let c = new Link();
c.type = LinkType.ABSOLUTE;
c.path = path;
return c;
}
static Relative( path:string )
{
let c = new Link();
c.type = LinkType.RELATIVE;
c.path = path;
return c;
}
}

167
browser/dom/PageData.ts Normal file
View File

@ -0,0 +1,167 @@
import { Loader } from "../xhttp/Loader";
import { PageHandler } from "./PageHandler";
export enum PageDataState
{
INITIAL,
LOADING,
SUCCESSED,
FAILED
}
export class PageData
{
private _pageHandler:PageHandler;
private _rawHTML:string;
get rawHTML(){ return this._rawHTML;}
private _pageRootStartIndex:number;
private _pageRootEndIndex:number;
private _title:string;
private _state:PageDataState = PageDataState.INITIAL;
private _absolutePath:string;
private _relativePath:string;
private _cacheTime:number;
private _language:string = null;
scrollTop:number;
constructor( pageHandler:PageHandler, absolutePath:string, relativePath:string )
{
this._pageHandler = pageHandler;
this._absolutePath = absolutePath;
this._relativePath = relativePath;
this._state = PageDataState.INITIAL;
this.scrollTop = 0;
}
async load():Promise<void>
{
if ( this.isLoading )
{
return await this._waitUntilLoaded();
}
this._state = PageDataState.LOADING;
let path = this._absolutePath;
let rawHTML:string = null;
try
{
let pageParameters = this._pageHandler.pageParameters;
this._cacheTime = new Date().getTime();
let pageURL = path + pageParameters + "&time=" + this._cacheTime;
console.log( pageURL );
rawHTML = await Loader.loadText( pageURL );
this._rawHTML = rawHTML;
this._state = PageDataState.SUCCESSED;
}
catch( e )
{
this._rawHTML = null;
this._state = PageDataState.FAILED;
}
return Promise.resolve();
}
private async _waitUntilLoaded():Promise<void>
{
let pageData = this;
let promise = new Promise<void>
(
( resolve, reject ) =>
{
let checkForReadiness = ()=>
{
if ( pageData.ready )
{
resolve();
return;
}
setTimeout( checkForReadiness, 100 );
}
checkForReadiness();
}
);
return Promise.resolve( promise );
}
get ready()
{
return this._state === PageDataState.SUCCESSED || this._state === PageDataState.FAILED;
}
get isLoading()
{
return this._state === PageDataState.LOADING;
}
get title()
{
if ( ! this._title )
{
let titleStartTag = "<title>";
let titleEndTag = "</title>";
let titleStart = this._rawHTML.indexOf( titleStartTag ) + titleStartTag.length;
let titleEnd = this._rawHTML.indexOf( titleEndTag, titleStart )
this._title = this._rawHTML.substring( titleStart, titleEnd );
}
return this._title;
}
get language()
{
if ( ! this._language )
{
let langStartMatcher = "<html lang=\"";
let langEndMatcher = "\"";
let langStart = this._rawHTML.indexOf( langStartMatcher );
if ( langStart === -1 )
{
this._language = "en";
return this._language;
}
let langEnd = this._rawHTML.indexOf( langEndMatcher, langStart + langStartMatcher.length + 1 );
let snippet = this._rawHTML.substring( langStart, langEnd );
let result = /\"(.+)/.exec( snippet );
this._language = result[ 1 ];
}
return this._language;
}
get pageRootInnerHTML()
{
if ( ! this._pageRootStartIndex )
{
let pageRootTag = this._pageHandler.pageRootTag;
let pageRootStartTagBegin = this._rawHTML.indexOf( `<${pageRootTag}` );
let pageRootStartEnd = this._rawHTML.indexOf( ">", pageRootStartTagBegin ) + 1;
let pageRootEnd = this._rawHTML.lastIndexOf( `</${pageRootTag}>` );
this._pageRootStartIndex = pageRootStartEnd;
this._pageRootEndIndex = pageRootEnd;
}
return this._rawHTML.substring( this._pageRootStartIndex, this._pageRootEndIndex );
}
}

1262
browser/dom/PageHandler.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
import { PageHandler } from "./PageHandler";
export abstract class PageTransitionHandler
{
abstract onTransitionOut( pageHandler:PageHandler ):Promise<void>;
abstract onTransitionIn( pageHandler:PageHandler ):Promise<void>;
}

View File

@ -0,0 +1,228 @@
import { HTMLNodeTreeWalker } from "../graphs/HTMLNodeTreeWalker";
import { DOMNameSpaces } from "./DOMNameSpaces";
import { ElementAttribute } from "./ElementAttribute";
export class ResolvablePathAttribute
{
private _selector:string;
private _attribute:ElementAttribute
constructor( selector:string, attribute:string, namespace?:string )
{
this._selector = selector;
this._attribute = new ElementAttribute( attribute, false, namespace );
}
get combinedSelector()
{
let combined = `${this._selector}${this._attribute.selector}`;
return combined;
}
get useNodeNameMatcher()
{
return this._selector === "image";
}
getFrom( target:Element|Document )
{
if ( this.useNodeNameMatcher )
{
let matchedElements:Element[] = [];
let walker = new HTMLNodeTreeWalker();
let walkable = target;
if ( ( target as Document ).documentElement )
{
walkable = ( target as Document).documentElement;
}
walker.forAll( walkable,
n =>
{
if ( n.nodeName === this._selector )
{
matchedElements.push( n as Element );
}
}
);
return matchedElements;
}
let elements = target.querySelectorAll( this.combinedSelector );
return Array.prototype.slice.call( elements );
}
process( element:Element, rootToken:string, rootPath:string )
{
let value = this._attribute.from( element );
if ( value === null )
{
if ( this._selector === "image" )
{
console.log( "no value for image" );
this.lgGetAttribute( element, "href" );
this.lgGetAttribute( element, "xlink:href" );
}
return;
}
else if ( this._selector === "image" )
{
}
/*if ( ! value.startsWith( rootToken ) )
{
if ( this._selector === "image" )
{
let shortValue = value;
if ( value.length > 100 )
{
shortValue = value.substring( 0, 100 );
}
}
else if ( this._attribute.name === "style" )
{
value = value.replace( rootToken, rootPath )
}
return;
}*/
value = value.replace( rootToken, rootPath )
this._attribute.to( element, value );
}
lgGetAttribute( element:Element, attribute:string )
{
this.lg( `${attribute} >> ${element.getAttribute( attribute )} `);
this.lg( `${attribute} NS=null >> ${element.getAttributeNS( null, attribute )} `);
this.lg( `${attribute} NS="" >> ${element.getAttributeNS( "", attribute )} `);
this.lg( `${attribute} NS=XLink >> ${element.getAttributeNS( DOMNameSpaces.XLink, attribute )} `);
this.lg( `${attribute} NS=SVG >> ${element.getAttributeNS( DOMNameSpaces.SVG, attribute )} `);
}
lg( value:string )
{
let shortValue = value;
if ( value.length > 100 )
{
shortValue = value.substring( 0, 100 ) + "...";
}
console.log( shortValue );
}
}
export class RootPathResolver
{
static readonly customResolve = new ElementAttribute( "resolve-root-path" );
static readonly resolvables:ResolvablePathAttribute[] =
[
new ResolvablePathAttribute( "link", "href" ),
new ResolvablePathAttribute( "script", "src" ),
new ResolvablePathAttribute( "a", "href" ),
new ResolvablePathAttribute( "img", "src" ),
new ResolvablePathAttribute( "audio", "src" ),
new ResolvablePathAttribute( "source", "src" ),
new ResolvablePathAttribute( "video", "poster" ),
new ResolvablePathAttribute( "video", "src" ),
new ResolvablePathAttribute( "*", "style" ),
new ResolvablePathAttribute( "image", "xlink:href" ),
new ResolvablePathAttribute( "*", "data-load-audio-element" ),
new ResolvablePathAttribute( "meta", "content" ),
new ResolvablePathAttribute( "*", "data-root-href" ),
]
private static _rootToken = "::/";
static clearRootPath( rootPath:string )
{
return rootPath.replace( /^\:\:\//, "" );
}
static isRootPath( path:string )
{
return path.startsWith( RootPathResolver._rootToken );
}
static get rootToken()
{
return RootPathResolver._rootToken;
}
getRootPath( relativeFilePath:string )
{
let normalizedPath = relativeFilePath.replace( /\\/g, "/" );
if ( normalizedPath.startsWith( "/" ) )
{
normalizedPath = normalizedPath.substring( 1 );
}
let numParentDirectories = normalizedPath.split( "/" ).length - 1;
let rootPath = "";
for ( let i = 0; i < numParentDirectories; i++ )
{
rootPath += "../";
}
return rootPath;
}
process( target:Element|Document, relativeFilePath:string )
{
let rootPath = this.getRootPath( relativeFilePath );
RootPathResolver.resolvables.forEach(
( resolvable )=>
{
let elements = resolvable.getFrom( target );
elements.forEach(
( e:Element ) =>
resolvable.process( e, RootPathResolver.rootToken, rootPath )
);
}
)
RootPathResolver.customResolve.forAll(
target,
( e:Element ) =>
{
let attName = RootPathResolver.customResolve.from( e );
let att = new ElementAttribute( attName, false );
let path = att.from( e );
console.log( `Processing custom root path "${attName}" = "${path}"` );
if ( path === null )
{
console.log( "Root Path Resolve: No path found in ", attName);
return;
}
path = path.replace( RootPathResolver.rootToken, rootPath );
att.to( e, path );
}
)
}
}

View File

@ -0,0 +1,121 @@
import { ElementAttribute } from "./ElementAttribute";
import { UserAgentInfo } from "./UserAgentInfo";
export type Android = "android";
export type Tablet = "tablet";
export type iPhone = "iphone";
export type iPad = "ipad";
export type Mac = "mac";
export type PC = "pc";
export type TV = "tv";
export type Other = "Other";
// export type (\w+)\s*=\s*".+;
// $1 |
export type UserAgentDeviceType =
Android |
Tablet |
iPhone |
iPad |
Mac |
PC |
TV |
Other
;
export class UserAgentDeviceTypes
{
// export type (\w+)\s*=\s*"(.+)".*;
// static readonly $1:$1 = "$2";
static readonly iPhone:iPhone = "iphone";
static readonly iPad:iPad = "ipad";
static readonly Android:Android = "android";
static readonly Tablet:Tablet = "tablet";
static readonly Mac:Mac = "mac";
static readonly PC:PC = "pc";
static readonly TV:TV = "tv";
static readonly Other:Other = "Other";
private static _deviceType:UserAgentDeviceType = null;
static get current()
{
if ( UserAgentDeviceTypes._deviceType !== null )
{
return UserAgentDeviceTypes._deviceType;
}
UserAgentDeviceTypes._deviceType = UserAgentDeviceTypes._guess();
return UserAgentDeviceTypes._deviceType;
}
static readonly attribute = new ElementAttribute( "device-type" );
static setOnBody()
{
UserAgentDeviceTypes.attribute.to( document.body, UserAgentDeviceTypes.current );
}
private static _guess():UserAgentDeviceType
{
if ( UserAgentInfo.isIPad )
{
return UserAgentDeviceTypes.iPad;
}
if ( UserAgentInfo.isIPhone )
{
return UserAgentDeviceTypes.iPhone;
}
if ( UserAgentInfo.isMac )
{
if ( UserAgentInfo.isTV )
{
return UserAgentDeviceTypes.TV;
}
return UserAgentDeviceTypes.Mac;
}
if ( UserAgentInfo.isAndroid )
{
if ( UserAgentInfo.isTV )
{
return UserAgentDeviceTypes.TV;
}
if ( UserAgentInfo.isTablet )
{
return UserAgentDeviceTypes.Tablet;
}
return UserAgentDeviceTypes.Android;
}
if ( UserAgentInfo.isTablet )
{
return UserAgentDeviceTypes.Tablet;
}
if ( UserAgentInfo.isTV )
{
return UserAgentDeviceTypes.TV;
}
if ( UserAgentInfo.isOther )
{
return UserAgentDeviceTypes.Other;
}
return UserAgentDeviceTypes.PC;
}
}

View File

@ -0,0 +1,50 @@
export class UserAgentInfo
{
static is( regex:RegExp )
{
return regex.test( navigator.userAgent );
}
static get isIPhone()
{
return UserAgentInfo.is( /iphone/i );
}
static get isIPad()
{
return UserAgentInfo.is( /ipad/i );
}
static get isMac()
{
return UserAgentInfo.is( /macintosh/i );
}
static get isAndroid()
{
return UserAgentInfo.is( /android/i );
}
static get isTablet()
{
return UserAgentInfo.is( /tablet/i );
}
static get isTV()
{
let isTV = UserAgentInfo.is( /smart\-?tv/i ) || UserAgentInfo.is( /net\-?cast/i ) ||
UserAgentInfo.is( /apple\s*tv/i ) || UserAgentInfo.is( /android\s*tv/i ) ||
UserAgentInfo.is( /opera\s*tv/i );
return isTV;
}
static get isOther()
{
let other = UserAgentInfo.is( /roku/i ) || UserAgentInfo.is( /tizen/i ) ||
UserAgentInfo.is( /netflix/i ) || UserAgentInfo.is( /crkey/i );
return other;
}
}

69
browser/geometry/Box2.ts Normal file
View File

@ -0,0 +1,69 @@
import { Range } from "./Range";
import { Vector2 } from "./Vector2";
export class Box2
{
min:Vector2;
max:Vector2;
constructor( min:Vector2 = null, max:Vector2 = null )
{
this.min = min;
this.max = max;
}
get size():Vector2
{
return this.max.clone().sub( this.min );
}
get center():Vector2
{
return this.max.clone().add( this.min ).multiply( 0.5 );
}
get rangeX():Range
{
return new Range( this.min.x, this.max.x );
}
get rangeY():Range
{
return new Range( this.min.y, this.max.y );
}
intersectsBox( box:Box2 )
{
return this.rangeX.overlaps( box.rangeX ) && this.rangeY.overlaps( box.rangeY );
}
translate( offset:Vector2 )
{
this.min.add( offset );
this.max.add( offset );
}
static fromClientRect( clientRect:ClientRect|DOMRect )
{
let min = new Vector2( clientRect.left, clientRect.top );
let max = new Vector2( clientRect.right, clientRect.bottom );
return new Box2( min, max );
}
static toDomRect( box:Box2 )
{
let size = box.size;
let domRect = new DOMRect( box.min.x, box.min.y, size.x, size.y );
return domRect;
}
static updatefromClientRect( box:Box2, clientRect:ClientRect|DOMRect )
{
box.min.set( clientRect.left, clientRect.top );
box.max.set( clientRect.right, clientRect.bottom );
}
}

View File

@ -0,0 +1,52 @@
export class Vector2
{
x:number = 0;
y:number = 0;
constructor( x:number = 0, y:number = 0 )
{
this.x = x;
this.y = y;
}
clone():Vector2
{
return new Vector2( this.x, this.y );
}
set( x:number, y:number )
{
this.x = x;
this.y = y;
}
add( other:Vector2 )
{
this.x += other.x;
this.y += other.y;
return this;
}
sub( other:Vector2 )
{
this.x -= other.x;
this.y -= other.y;
return this;
}
multiply( value:number )
{
this.x *= value;
this.y *= value;
return this;
}
get length()
{
return Math.sqrt( this.x * this.x + this.y * this.y );
}
}

View File

@ -834,7 +834,6 @@ export class RegExpUtility
static createRegExp( regexp:RegExp, matching:string, replacement:string )
{
let source = regexp.source;
let flags = regexp.flags;
source = source.replace( matching, replacement );

14
browser/tools/MapList.ts Normal file
View File

@ -0,0 +1,14 @@
export class MapList
{
static add<K,V>( map:Map<K,V[]>, k:K, v:V )
{
if ( ! map.has( k ) )
{
map.set( k, [] );
}
map.get( k ).push( v );
}
}

View File

@ -83,7 +83,7 @@ export class Loader
xhr.open( method, url, true );
xhr.responseType = "json";
console.log( "xhr", url, method );
// console.log( "xhr", url, method );
xhr.onload=()=>
{
@ -129,7 +129,7 @@ export class Loader
xhr.onload = () =>
{
console.log( "load", url, xhr );
// console.log( "load", url, xhr );
if ( xhr.status !== 200 )
{

View File

@ -2,7 +2,6 @@
import { RegExpUtility } from "../../browser/text/RegExpUtitlity";
import { LogColors } from "./LogColors";
let pr = ( window as any ).process;
export class RJLog
{
@ -100,7 +99,7 @@ export class RJLog
static error( ...params:any[] )
{
if ( RJLog.logAlwaysLineInfo || typeof pr === "object" )
if ( RJLog.logAlwaysLineInfo || typeof process === "object" )
{
let lineInfo = RJLog.getLineInfo( RJLog.errorColor );
console.log( "\n" + lineInfo );
@ -112,7 +111,7 @@ export class RJLog
static warn( ...params:any[] )
{
if ( RJLog.logAlwaysLineInfo || typeof pr === "object" )
if ( RJLog.logAlwaysLineInfo || typeof process === "object" )
{
let lineInfo = RJLog.getLineInfo( RJLog.warnColor );
console.log( "\n" + lineInfo );
@ -124,7 +123,7 @@ export class RJLog
static log( ...params:any[] )
{
if ( RJLog.logAlwaysLineInfo || typeof pr === "object" )
if ( RJLog.logAlwaysLineInfo || typeof process === "object" )
{
let lineInfo = RJLog.getLineInfo();
console.log( "\n" + lineInfo );

View File

@ -1,10 +1,9 @@
{
"compilerOptions":
{
"lib": ["ES2020"],
"types": ["node"], // Includes Node.js types
},
"include": ["./*"]
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"lib": ["ESNext"],
"moduleResolution": "Node",
"types": ["node"]
}
}

View File

@ -0,0 +1,111 @@
import * as fs from "fs";
import * as path from "path";
import { RJLog } from "../log/RJLog";
export class PagesInfo
{
pages:string[] = [];
}
export class PHPPagesBuilder
{
inputDir:string;
outputDir:string;
constructor( source:string, build:string )
{
this.inputDir = source;
this.outputDir = build;
}
filterFiles( fileName:string )
{
if ( fileName.startsWith( "__" ) )
{
return false;
}
return fileName.endsWith( ".html" );
}
modify( content:string )
{
return `<?php ?>\n${content}`;
}
apply( compiler:any )
{
compiler.hooks.afterCompile.tapAsync( "PHPPagesBuilder",
( compilation:any, callback:any ) =>
{
if ( fs.existsSync( this.inputDir ) )
{
fs.readdirSync( this.inputDir )
.filter( this.filterFiles )
.forEach(
( file ) =>
{
let fullPath = path.join(this.inputDir, file);
compilation.fileDependencies.add(fullPath); // Mark for watching
}
);
}
callback();
}
);
compiler.hooks.emit.tapAsync( "PHPPagesBuilder",
( compilation:any, callback:any ) =>
{
fs.readdir( this.inputDir,
( err, files ) =>
{
if ( err )
{
RJLog.log( "Error", this.inputDir );
return callback( err );
}
let pages = new PagesInfo();
files.filter( this.filterFiles ).forEach( ( file ) =>
{
let filePath = path.join( this.inputDir, file );
let content = fs.readFileSync( filePath, "utf-8" );
let modifiedContent = this.modify( content );
let phpFileName = file.replace( ".html", ".php" );
let outputFileName = path.join( this.outputDir, phpFileName );
pages.pages.push( phpFileName );
compilation.assets[ outputFileName ] =
{
source: () => modifiedContent,
size: () => modifiedContent.length,
};
}
);
let pagesPath = path.join( this.outputDir, "pages.json" );
let pagesJSON = JSON.stringify( pages, null, " " );
compilation.assets[ pagesPath ] =
{
source: () => pagesJSON,
size: () => pagesJSON.length,
};
callback();
}
);
}
);
}
}