diff --git a/browser/app/App.ts b/browser/app/App.ts new file mode 100644 index 0000000..fcf88b5 --- /dev/null +++ b/browser/app/App.ts @@ -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 + { + 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(); + } +} \ No newline at end of file diff --git a/browser/app/AppPathConverter.ts b/browser/app/AppPathConverter.ts new file mode 100644 index 0000000..0a080cc --- /dev/null +++ b/browser/app/AppPathConverter.ts @@ -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; + } +} \ No newline at end of file diff --git a/browser/date/DateHelper.ts b/browser/date/DateHelper.ts new file mode 100644 index 0000000..42183bc --- /dev/null +++ b/browser/date/DateHelper.ts @@ -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; + } +} \ No newline at end of file diff --git a/browser/date/DateMath.ts b/browser/date/DateMath.ts new file mode 100644 index 0000000..9600a7e --- /dev/null +++ b/browser/date/DateMath.ts @@ -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(); + } + + +} \ No newline at end of file diff --git a/browser/dom/ActivityAnalyser.ts b/browser/dom/ActivityAnalyser.ts new file mode 100644 index 0000000..57e223a --- /dev/null +++ b/browser/dom/ActivityAnalyser.ts @@ -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 ); + } + +} \ No newline at end of file diff --git a/browser/dom/AttributeValue.ts b/browser/dom/AttributeValue.ts new file mode 100644 index 0000000..d86b885 --- /dev/null +++ b/browser/dom/AttributeValue.ts @@ -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( element:Element ) + { + return element.querySelector( this.selector ) as T; + } + + queryDoc() + { + 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; + + } +} \ No newline at end of file diff --git a/browser/dom/Cursor.ts b/browser/dom/Cursor.ts new file mode 100644 index 0000000..e60d1d1 --- /dev/null +++ b/browser/dom/Cursor.ts @@ -0,0 +1,12 @@ +export class Cursor +{ + static lock( type:string = null ) + { + + } + + static free() + { + + } +} \ No newline at end of file diff --git a/browser/dom/DOMHitTest.ts b/browser/dom/DOMHitTest.ts new file mode 100644 index 0000000..a04917c --- /dev/null +++ b/browser/dom/DOMHitTest.ts @@ -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; + + } + + +} \ No newline at end of file diff --git a/browser/dom/DOMOrientation.ts b/browser/dom/DOMOrientation.ts new file mode 100644 index 0000000..fb4712a --- /dev/null +++ b/browser/dom/DOMOrientation.ts @@ -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; + } +} \ No newline at end of file diff --git a/browser/dom/EventListeners.ts b/browser/dom/EventListeners.ts new file mode 100644 index 0000000..7d48c95 --- /dev/null +++ b/browser/dom/EventListeners.ts @@ -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( 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 + { + 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 ); + } +} \ No newline at end of file diff --git a/browser/dom/Insight.ts b/browser/dom/Insight.ts new file mode 100644 index 0000000..27759f2 --- /dev/null +++ b/browser/dom/Insight.ts @@ -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; + + +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(); + this._insightCallbackElements = new Map(); + + 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 ) ); + + + } +} diff --git a/browser/dom/LandscapeScale.ts b/browser/dom/LandscapeScale.ts new file mode 100644 index 0000000..2097366 --- /dev/null +++ b/browser/dom/LandscapeScale.ts @@ -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(); + onOrientationChange = new EventSlot(); + + + 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; + } + ` + + } +} \ No newline at end of file diff --git a/browser/dom/LinkResolver.ts b/browser/dom/LinkResolver.ts new file mode 100644 index 0000000..4fbde27 --- /dev/null +++ b/browser/dom/LinkResolver.ts @@ -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; + } + + + +} diff --git a/browser/dom/PageData.ts b/browser/dom/PageData.ts new file mode 100644 index 0000000..b3aa891 --- /dev/null +++ b/browser/dom/PageData.ts @@ -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 + { + 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 + { + let pageData = this; + + let promise = new Promise + ( + ( 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 = ""; + let titleEndTag = ""; + + 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 = "", pageRootStartTagBegin ) + 1; + let pageRootEnd = this._rawHTML.lastIndexOf( `` ); + + this._pageRootStartIndex = pageRootStartEnd; + this._pageRootEndIndex = pageRootEnd; + } + + + return this._rawHTML.substring( this._pageRootStartIndex, this._pageRootEndIndex ); + } + +} \ No newline at end of file diff --git a/browser/dom/PageHandler.ts b/browser/dom/PageHandler.ts new file mode 100644 index 0000000..033f3ce --- /dev/null +++ b/browser/dom/PageHandler.ts @@ -0,0 +1,1262 @@ + +import { PageData } from "./PageData"; +import { PageTransitionHandler } from "./PageTransitionHandler"; +import { sleep } from "../animation/sleep"; +import { EventSlot } from "../events/EventSlot"; +import { ElementAttribute } from "./ElementAttribute"; +import { OnClick, OnMouseDown, OnMouseEnter } from "./EventListeners"; +import { LanguageCode } from "../i18n/LanguageCode"; +import { Link, LinkType } from "./LinkResolver"; +import { RootPathResolver } from "./RootPathResolver"; +import { Loader } from "../xhttp/Loader"; +import { ElementType } from "./ElementType"; +import { RegExpUtility } from "../text/RegExpUtitlity"; +import { AttributeValue } from "./AttributeValue"; + +export class FunctionHandleAnchor extends HTMLAnchorElement +{ + _functionCallback:()=>void; + _assignedClickListener = false; +} + +export class PageRequestEvent +{ + lastPage:string; + nextPage:string; +} + +export class PagesJSON +{ + pages:string[]; +} + +export enum PageHandlerMode +{ + PHP, + HTML, + ELECTRON +} + +export class PageHandler +{ + private _pagesPath = "_scripts/pages.json"; + private _pages = new Map(); + private _startPage:string; + get startPage(){ return this._startPage;} + private _currentPage:string; + private _nextPage:string; + private _isLoading:boolean = false; + private _followUpPage:string = null; + private _pageRootTag = "body"; + private _forceHTTPS = true; + private _pageParameters = "?no-page-token=true"; + readonly onPageRequest = new EventSlot(); + get pageRootTag(){ return this._pageRootTag; } + transitionHandler:PageTransitionHandler; + + private _webAdress:string; + private _localAdressExtension:string; + + private _rootPathResolver = new RootPathResolver(); + get rootPathResolver(){ return this._rootPathResolver; } + + setPagesPath( path:string ) + { + this._pagesPath = path; + } + + setAdress( localExtension: string, web:string ) + { + this._localAdressExtension = localExtension; + this._webAdress = web; + } + + setPageRootTag( pageRootTag:string ) + { + this._pageRootTag = pageRootTag; + } + + get pageParameters() + { + return this._pageParameters; + } + + getParameterValue( name:string ) + { + let parameters = window.location.search; + let pageParameters = parameters.length > 0 ? parameters.substring( 1 ) : null; + + if ( ! pageParameters ) + { + return null; + } + + let pairs = pageParameters.split( "&" ); + + for ( let p of pairs ) + { + let keyValue = p.split( "=" ); + + if ( keyValue.length !== 2 ) + { + continue; + } + + if ( keyValue[ 0 ] === name ) + { + return decodeURIComponent( keyValue[ 1 ] ); + } + + } + + return null; + + + } + + + private _preloadImagesFlag:boolean = true; + private _maxPreloadingDurationMS:number = 2000; + + private static _localHost = "http://localhost:8080"; + private static _localNetworkRegexPattern = /^(http:\/\/\d\d\d\.\d\d\d\.\d\d\d\.\d\d\d?:XXXX)/ + private static _localNetworkRegex = RegExpUtility.createRegExp( this._localNetworkRegexPattern, "XXXX", 8080 + "" ); + private _isHTMLMode:boolean = false; + private _fileLocation:string = ""; + private _isFileMode:boolean = false; + + static setLocalHostPort( port:number ) + { + PageHandler._localHost = "http://localhost:" + port; + PageHandler._localNetworkRegex = RegExpUtility.createRegExp( this._localNetworkRegexPattern, "XXXX", port + "" ); + } + + constructor( localAdressExtension:string, webAdress:string, disableForceHTTPS?:boolean, mode:PageHandlerMode = PageHandlerMode.PHP ) + { + if ( disableForceHTTPS === true ) + { + this._forceHTTPS = false; + } + + if ( PageHandlerMode.HTML == mode || PageHandlerMode.ELECTRON == mode ) + { + this._isHTMLMode = true; + } + + if ( PageHandlerMode.ELECTRON == mode ) + { + this.setFileLocation( localAdressExtension ); + } + else + { + this.setAdress( localAdressExtension, webAdress ); + } + + //this._startPage = this.currentPage; + //console.log( "START PAGE:", this._startPage ); + } + + setFileLocation( location:string ) + { + this._fileLocation = location; + this._isFileMode = true; + } + + + get localAdress() + { + let adress = this.trimmedLocation; + + + if ( adress.startsWith( PageHandler._localHost ) ) + { + return PageHandler._localHost + this._localAdressExtension; + } + + if ( PageHandler._localNetworkRegex.test( adress ) ) + { + return PageHandler._localNetworkRegex.exec( adress )[ 1 ] + this._localAdressExtension; + } + + return null; + } + + + + get isLocalNetwork() + { + let adress = this.trimmedLocation; + + if ( adress.startsWith( PageHandler._localHost ) ) + { + return true; + } + + if ( PageHandler._localNetworkRegex.test( adress ) ) + { + return true; + } + + return false; + } + + get isFileMode() + { + return this._isFileMode; + } + + + + + private _pagesLoaded = false; + + async initialize() + { + this._isLoading = true; + + let pagesPath = this.getAbsolutePath( this._pagesPath ) + "?" + new Date().getTime() ; + //console.log( "loading pages", pagesPath ); + + let pages:string[] = []; + + setTimeout( + ()=> + { + // console.log( "PAGES_DATA", ( window as any ).PAGES_DATA ); + }, + 3000 + ); + + if ( ( window as any ).PAGES_DATA ) + { + //console.log( "USING PAGE DATA FROM JS PAGES" ); + pages = ( window as any ).PAGES_DATA as string[]; + } + else + { + console.log( "LOADING PAGE DATA FROM JSON" ); + let pagesData = await Loader.loadJSON( pagesPath ); + pages = pagesData.pages; + } + + + + for ( let i = 0; i < pages.length; i++ ) + { + let page = pages[ i ]; + let absolutePath = this.getAbsolutePath( page ); + //console.log( "adding page: ", page, ">>", absolutePath ); + this._pages.set( page, new PageData( this, absolutePath, page ) ); + } + + this._pagesLoaded = true; + + this._startPage = this.currentPage; + ///console.log( "loading pages", this._startPage ); + this.removeFunctionHandleLink(); + + this.resolveRootPaths( document ); + + this._isLoading = false; + + this._startUpdater(); + + if ( this._followUpPage ) + { + this._loadPage( this._followUpPage ); + } + + return Promise.resolve(); + } + + + private _currentLanguage:string = null; + get currentLanguage() + { + if ( ! this._currentLanguage ) + { + this._currentLanguage = document.querySelector( "html" ).getAttribute( "lang" ) || "en"; + } + + return this._currentLanguage; + } + + get languageCode() + { + return this.currentLanguage as LanguageCode; + } + + resolveRootPaths( target:Element|Document ) + { + let filePath = this.currentPage; + // console.log( "resolveRootPaths: FILEPATH:" + filePath ); + + if ( filePath === null ) + { + return; + } + + this._rootPathResolver.process( target, filePath ); + } + + private async _loadPage( page:string ):Promise + { + if ( this._isLoading ) + { + this._followUpPage = page; + return Promise.resolve(); + } + + this._dispatchPageRequest( this._currentPage, page ); + + this._isLoading = true; + this._followUpPage = null; + + this._nextPage = page; + let next = this._nextPage; + + + if ( this.transitionHandler ) + { + await this.transitionHandler.onTransitionOut( this ); + } + + let pageData = await this._getPageData( next ); + this._currentLanguage = pageData.language; + + let elementType = new ElementType( this.pageRootTag ); + let pageRoot = elementType.query( document.documentElement ); + pageRoot.innerHTML = pageData.pageRootInnerHTML; + + this.resolveRootPaths( pageRoot ); + + this._scrollToY( pageData.scrollTop ); + document.title = pageData.title; + document.querySelector( "html" ).setAttribute( "lang", this._currentLanguage ); + + this._abortPreloadingImages = false; + + if ( this._preloadImagesFlag ) + { + await Promise.race( [ sleep( this._maxPreloadingDurationMS ), this._preloadImages() ]) + } + + this._abortPreloadingImages = true; + + if ( this.transitionHandler ) + { + await this.transitionHandler.onTransitionIn( this ); + } + + + this._currentPage = this._nextPage; + + if ( this._followUpPage ) + { + let page = this._followUpPage; + this._followUpPage = null; + await this._loadPage( page ); + } + + this._isLoading = false; + + return Promise.resolve(); + } + + + private _pageAliases:Map = new Map(); + + public normalizePageLink( page:string ) + { + return this._normalizePageLink( page ); + } + + private _normalizePageLink( page:string ) + { + if ( page === "" ) + { + return this._isHTMLMode ? "index.html" : "index.php"; + } + + if ( this._pages.has( page ) ) + { + return page; + } + + if ( this._pageAliases.has( page ) ) + { + page = this._pageAliases.get( page ); + } + + if ( this._pages.has( page ) ) + { + return page; + } + + if ( ! this._isHTMLMode && page.endsWith( ".html" ) ) + { + let phpPage = page.replace( /\.html$/, ".php" ); + + if ( this._pages.has( phpPage ) ) + { + return phpPage; + } + } + + let ending = this._isHTMLMode ? ".html" : ".php"; + + if ( ! page.endsWith( ending ) ) + { + let indexPage = page + ending; + + if ( this._pages.has( indexPage ) ) + { + return indexPage; + } + + let indexDirectoryPage = page.replace( /\/$/, "" ) + "/index" + ending; + + if ( this._pages.has( indexDirectoryPage ) ) + { + return indexDirectoryPage; + } + } + + + if ( PageHandler.isHiddenPage() ) + { + return null; + } + + // console.log( "Page not found:", page, [...this._pages.keys()].join( ", " ) ); + return null; + } + + static isHiddenPage() + { + let value = PageHandler.pageInfoAttribute.from( document.body ); + return value === "hidden"; + } + + static readonly pageInfoAttribute = new ElementAttribute( "page-info" ); + + getFilteredPages( filter:RegExp ) + { + let pages:string[] = []; + + this._pages.forEach( + ( v, k )=> + { + if ( filter.test( k ) ) + { + pages.push( k ); + } + } + ); + + return pages; + } + + async getPage( page:string ):Promise + { + console.log( "getPage:", page ); + return this._getPageData( page ); + } + + private async _getPageData( page:string ):Promise + { + + if ( ! this._pages.has( page ) ) + { + return Promise.resolve( null ); + } + + let data = this._pages.get( page ); + + if ( ! data.ready ) + { + await data.load(); + } + + + return Promise.resolve( data ); + } + + private _abortPreloadingImages = false; + + private async _preloadImages():Promise + { + + let images = ElementType.image.queryAll( document.body ); + let backgroundImages = document.body.querySelectorAll( `[style*="background-image"]` ); + + //console.log( "preloading images", `found: ${images.length}, [style*="background-image"] ${backgroundImages.length}`, ) + + for ( let i = 0; i < images.length && ! this._abortPreloadingImages; i++ ) + { + let path = images[ i ].getAttribute( "src" ); + + if ( path && path !== "" ) + { + await this._preloadImage( path ); + } + + } + + for ( let i = 0; i < backgroundImages.length && ! this._abortPreloadingImages; i++ ) + { + let htmlElement = backgroundImages[ i ] as HTMLElement; + let backgroundImageValue = htmlElement.style.backgroundImage; + var regexResult = backgroundImageValue.match(/url\(["']?([^"']*)["']?\)/); + + if ( regexResult && regexResult[ 1 ] ) + { + let path = regexResult[ 1 ]; + await this._preloadImage( path ); + } + + } + + return Promise.resolve(); + + + } + + private async _preloadImage( path:string ):Promise + { + try + { + //console.log( "preloading image", path ); + await Loader.loadImage( path ); + } + catch ( e ) + { + + } + + + return Promise.resolve(); + } + + + getAbsolutePath( path:string ) + { + if ( this.isFileMode ) + { + return this._fileLocation + "/" + path + } + else if ( this.isLocalNetwork ) + { + return this.localAdress + "/" + path + } + else + { + return this._webAdress + "/" + path; + } + } + + getRootPath( absolutePath:string ) + { + let pagePath:string = null; + + if ( this.isFileMode ) + { + pagePath = absolutePath.substring( this._fileLocation.length ); + } + else if ( this.isLocalNetwork ) + { + pagePath = absolutePath.substring( this.localAdress.length ); + } + else + { + if ( absolutePath.startsWith( "http:" ) && this._forceHTTPS ) + { + absolutePath = "https:" + absolutePath.substring( "http:".length ); + } + + pagePath = absolutePath.substring( this._webAdress.length ); + } + + if ( pagePath.startsWith( "/" ) ) + { + pagePath = pagePath.substring( 1 ); + } + + return pagePath; + } + + + + get trimmedLocation() + { + let trimmedLocation = window.location.href; + + let hash = /#.+$/.exec( trimmedLocation ); + + if ( hash ) + { + trimmedLocation = trimmedLocation.replace( /#.+$/, "" ); + } + + let search = /\?.+$/.exec( trimmedLocation ); + + if ( search ) + { + trimmedLocation = trimmedLocation.replace( /\?.+$/, "" ); + } + + return trimmedLocation; + } + + get hasFunctionHandleLink() + { + let trimmedLocation = window.location.href; + + let hash = /#\[.+\]$/.exec( trimmedLocation ); + + if ( hash ) + { + return true; + } + + return false; + } + + removeFunctionHandleLink() + { + if ( this.hasFunctionHandleLink ) + { + window.location.href = this.trimmedLocation; + } + } + + get currentPage() + { + let location = this.getRootPath( this.trimmedLocation ); + + let normalized = this._normalizePageLink( location ); + + return normalized; + } + + private _startUpdater() + { + if ( 'scrollRestoration' in window.history) + { + window.history.scrollRestoration = 'manual'; + } + + let page = this.currentPage; + + this._currentPage = page; + this._nextPage = page; + + let updater = () => + { + requestAnimationFrame( + ()=> + { + this._update(); + updater(); + } + ); + } + + updater(); + } + + + + get currentScrollTop() + { + var value= typeof window.pageYOffset !== undefined ? window.pageYOffset: + document.documentElement.scrollTop? document.documentElement.scrollTop: + document.body.scrollTop? document.body.scrollTop:0; + return value; + } + + + + replaceLinks() + { + let anchors = ElementType.anchor.queryAll( document.body ); + + for ( let a of anchors ) + { + this._processLink( a as HTMLAnchorElement ); + } + } + + + + private _dispatchPageRequest( lastPage:string, nextPage:string ) + { + let pageRequestEvent = new PageRequestEvent(); + pageRequestEvent.lastPage = lastPage; + pageRequestEvent.nextPage = nextPage; + + this.onPageRequest.dispatch( pageRequestEvent ); + } + + createAbsoluteLink( link:Link ) + { + let clone = link.clone(); + + if ( LinkType.ABSOLUTE === link.type ) + { + return clone; + } + + clone.type = LinkType.ABSOLUTE; + + if ( LinkType.ROOT === link.type ) + { + clone.path = link.path.replace( "::", window.location.host ); + } + + if ( LinkType.RELATIVE === link.type ) + { + clone.path = new URL( link.path, window.location.href ).href; + } + + + return clone; + } + + createRootLink( link:Link ) + { + if ( link.type === LinkType.ROOT ) + { + return link.clone(); + } + + let absoluteLink = this.createAbsoluteLink( link ); + + let clone = absoluteLink.clone(); + clone.type = LinkType.ROOT; + clone.path = RootPathResolver.rootToken + ( new URL( absoluteLink.path ).pathname ); + + return clone; + } + + getRealPagePath( page:string ) + { + page = page.replace( /^\//, "" ); + + if ( this._pages.has( page ) ) + { + return page; + } + + let phpExtensionPage = page + ".php"; + + if ( this._pages.has( phpExtensionPage ) ) + { + return page; + } + + let directoryWithIndexPage = page + "/index.php"; + + if ( this._pages.get( directoryWithIndexPage ) ) + { + return directoryWithIndexPage; + } + + console.warn( "Page not found:", page ); + + return null; + } + + createRelativeLink( link:Link ) + { + if ( link.type === LinkType.RELATIVE ) + { + return link.clone(); + } + + + let clone = this.createRootLink( link ).clone(); + clone.type = LinkType.RELATIVE; + + let replaced = link.path.replace( RootPathResolver.rootToken, "" ); + let file = RegExpUtility.fileNameOrLastPath( replaced ); + + let pathDirectory = RegExpUtility.parentPath( this._normalizePageLink( replaced ) ); + let currentDirectory = this.currentPath; + + let isDirectoryWithoutSlash = false; + let directoryPrefix = ""; + + + if ( ! window.location.pathname.endsWith( "/" ) ) + { + let windowPath = window.location.pathname.replace( /^\//, "" ); + let windowWithoutSlashPath = windowPath + "/index.php"; + let normalizedPath = this._normalizePageLink( windowPath ); + + isDirectoryWithoutSlash = normalizedPath === windowWithoutSlashPath; + + console.log( "CREATED RELATIVE INFO", windowPath, normalizedPath, isDirectoryWithoutSlash ); + + if ( isDirectoryWithoutSlash ) + { + directoryPrefix = RegExpUtility.fileNameOrLastPath( windowPath ); + } + } + + let path = RegExpUtility.createRelativeDirectoryPath( currentDirectory, pathDirectory ); + + if ( path != "" && ! path.endsWith( "/" ) ) + { + path += "/"; + } + + path += file; + + if ( isDirectoryWithoutSlash ) + { + path = directoryPrefix + "/" + path; + } + + console.log( + "CREATED RELATIVE LINK:", link.path, + ">> from ", currentDirectory, isDirectoryWithoutSlash, "to", pathDirectory, ">>", + path ); + + + + + + clone.path = path; + + return clone; + } + + + + convertLink( link:Link, type:LinkType ) + { + if ( link.type === type ) + { + return link.clone(); + } + + if ( LinkType.ABSOLUTE == type ) + { + return this.createAbsoluteLink( link ); + } + + if ( LinkType.ROOT == type ) + { + return this.createRootLink( link ); + } + + if ( LinkType.RELATIVE == type ) + { + return this.createRelativeLink( link ); + } + + return null; + + + } + + setPage( href:string, loadPage:boolean = true ) + { + let normalizedPath:string = null; + + if ( RootPathResolver.isRootPath( href ) ) + { + let resolvedRootPath = RootPathResolver.clearRootPath( href ); + normalizedPath = this._normalizePageLink( resolvedRootPath ); + this._pages.get( normalizedPath ).scrollTop = 0; + console.log( "LOADING ROOT ADRESS", { href, resolvedRootPath, normalizedPath } ); + + let sourceDirectory = this.currentPath; + let targetDirectory = RegExpUtility.parentPath( normalizedPath ); + let relativePath = RegExpUtility.createRelativeDirectoryPath( sourceDirectory, targetDirectory ); + let targetFile = RegExpUtility.fileNameOrLastPath( normalizedPath ); + let historyAdress = "." + RegExpUtility.join( relativePath, targetFile ); + + let historyAdressShort = historyAdress.replace( /\/?index\.php$/, "" ); + historyAdressShort = historyAdressShort.replace( /\.php$/, "" ); + + console.log( "PUSHING STATE", historyAdress, historyAdressShort ); + history.pushState( { historyAdressShort }, "", historyAdressShort ); + + if ( ! loadPage ) + { + this._nextPage = historyAdressShort; + this._followUpPage = null; + } + + } + else + { + let pageAdress = this.resolveRelativeLink( href ); + normalizedPath = this._normalizePageLink( pageAdress ); + console.log( "LOADING RELATIVE ADRESS", { href, pageAdress, normalizedPath } ); + this._pages.get( normalizedPath ).scrollTop = 0; + history.pushState( { href }, "", href ); + + if ( ! loadPage ) + { + this._nextPage = href; + this._followUpPage = null; + } + } + + + + } + + static pageHandlerLink = new ElementAttribute( "page-handler-link" ); + + makePageHandleLink( a:HTMLAnchorElement ) + { + let originalHref = ElementAttribute.href.from( a, "" ); + + if ( originalHref.startsWith( "https://" ) || originalHref.startsWith( "https://" ) ) + { + AttributeValue.target_blank.set( a ); + return; + } + else + { + AttributeValue.target_blank.removeFrom( a ); + } + + if ( PageHandler.pageHandlerLink.in( a ) ) + { + return; + } + + PageHandler.pageHandlerLink.to( a ); + + + let pageHandlingAttribute = "data-page-handling"; + + a.addEventListener( + "mousedown", + ( e:MouseEvent )=> + { + let hasRequest = e.button === 0; + a.setAttribute( pageHandlingAttribute, hasRequest + "" ); + } + ); + + a.addEventListener( + "click", + ( e:MouseEvent)=> + { + + let href = a.getAttribute( "href" ); + + let hasRequest = a.getAttribute( pageHandlingAttribute ) === "true"; + + if ( hasRequest ) + { + a.removeAttribute( "href" ); + + /* + let pageAdress = this.resolveRelativeLink( href ); + pageAdress = this._normalizePageLink( pageAdress ); + + console.log( "href", `"${href}"`, "pageAdress", `"${pageAdress}"` ); + + + this._pages.get( pageAdress ).scrollTop = 0; + history.pushState( { href }, "", href ); + */ + + this.setPage( href ); + + e.preventDefault(); + e.stopImmediatePropagation(); + e.stopPropagation(); + + setTimeout( + ()=> + { + a.setAttribute( "href", href ); + }, + 200 + ) + } + + + } + ) + } + + static isFunctionHandleLink( link:string ) + { + return link.startsWith( "#[" ) && link.endsWith( "]" ); + } + + static makeFunctionHandleLink( a:HTMLAnchorElement ) + { + let element = a as FunctionHandleAnchor; + + if ( element._assignedClickListener ) + { + return; + } + + element._assignedClickListener = true; + + let hrefValue = ElementAttribute.href.from( a ); + + OnMouseDown.add( a, ( e:MouseEvent )=> + { + ElementAttribute.href.removeFrom( a ); + } + ); + + OnMouseEnter.add( a, ( e:MouseEvent )=> + { + ElementAttribute.href.to( a, hrefValue ); + } + ); + + /* + OnContextMenu.add( a, ( e:MouseEvent )=> + { + e.preventDefault(); + e.stopImmediatePropagation(); + e.stopPropagation(); + } + ); + */ + + OnClick.add( a, ( e:MouseEvent)=> + { + console.log( "FUNCTION HANDLE CLICK", e ); + e.preventDefault(); + e.stopImmediatePropagation(); + e.stopPropagation(); + + if ( element._functionCallback ) + { + element._functionCallback(); + } + + setTimeout( + ()=> + { + ElementAttribute.href.to( a, hrefValue ); + }, + 200 + ) + } + ) + } + + static setFunctionHandleLink( e:Element, name:string, callback:()=>void ) + { + let a = e as HTMLAnchorElement; + this.setFunctionHandleName( a, name ); + this.setFunctionHandleCallback( a, callback ); + this.makeFunctionHandleLink( a ); + + } + + static setFunctionHandleName( a:HTMLAnchorElement, name:string ) + { + ElementAttribute.href.to( a, `#[ ${name} ]`); + } + + static setFunctionHandleCallback( a:HTMLAnchorElement, callback:()=>void ) + { + let handle = a as FunctionHandleAnchor; + handle._functionCallback = callback; + } + + + private _processLink( a:HTMLAnchorElement ) + { + let hrefValue = ElementAttribute.href.from( a ); + + if ( ! hrefValue ) + { + return; + } + + if ( hrefValue.startsWith( "http" ) || hrefValue.startsWith( "mailto" ) ) + { + return; + } + + if ( PageHandler.isFunctionHandleLink( hrefValue ) ) + { + PageHandler.makeFunctionHandleLink( a ); + return; + } + + if ( AttributeValue.target_blank.in( a ) ) + { + return; + } + + this.makePageHandleLink( a ); + + } + + private get currentPath():string + { + let relativePagePath = this.getRootPath( this.trimmedLocation ); + let currentPath = this._normalizePageLink( relativePagePath ); + + if ( currentPath.endsWith( ".html" ) || currentPath.endsWith( ".php" ) ) + { + let end = currentPath.lastIndexOf( "/" ); + + if ( end === -1 ) + { + console.log( "No slash in path", currentPath ); + return ""; + + } + + currentPath = currentPath.substring( 0, end ); + } + + if ( currentPath.startsWith( "/" ) ) + { + currentPath = currentPath.substring( 1 ); + } + + return currentPath; + } + + private resolveRelativeLink( link:string ) + { + let currentPath = this.currentPath; + + if ( ! window.location.pathname.endsWith( "/" ) ) + { + let windowPath = window.location.pathname.replace( /^\//, "" ); + let windowExtendedPath = windowPath + "/index.php"; + let normalizedPath = this._normalizePageLink( windowPath ); + + if ( normalizedPath === windowExtendedPath ) + { + currentPath = RegExpUtility.parentPath( currentPath ); + + console.log( "CREATED SHORTER PATH:", this.currentPath, ">>", currentPath ); + } + else + { + console.log( "CREATED PATH IS OK:", { windowPath, windowExtendedPath, normalizedPath, currentPath } ); + } + + + } + + let pathFragments = currentPath === "" ? [] : currentPath.split( "/" ); + + let linkPathFragments = link.split( "/" ); + + for ( let i = 0; i < linkPathFragments.length; i++ ) + { + if ( linkPathFragments[ i ] === ".." ) + { + pathFragments.pop(); + } + else + { + pathFragments.push( linkPathFragments[ i ] ); + } + } + + let resolvedPath = pathFragments.join( "/" ); + console.log( "resolving: ", this.currentPath, link, ">>", resolvedPath ); + return resolvedPath; + } + + setPageWithoutLoading( rootPath:string ) + { + //this._nextPage = page; + //this._followUpPage = page; + //this.loadPage( page ); + + rootPath = rootPath.replace( /\.php$/, "" ); + + let page = "::/" + rootPath; + let relativeLink = this.createRelativeLink( Link.Root( page ) ); + let relative = relativeLink.path; + this._nextPage = relativeLink.path; + + console.log( "setPageWithoutLoading", { relative, page, rootPath } ); + this.setPage( relative, false ); + } + + getRootPathNextPage() + { + let link = Link.Relative( this._nextPage ); + let abs = this.createRootLink( link ); + + abs.path = abs.path.replace( /^\:\:\/\//, "" ); + + //console.log( link.path, abs.path ); + return this._normalizePageLink( abs.path ); + } + + private _update() + { + if ( this._isLoading ) + { + return; + } + + let location = this.getRootPath( this.trimmedLocation ); + + location = this._normalizePageLink( location ); + + let rootPathNextPage = this.getRootPathNextPage(); + + if ( rootPathNextPage !== location && this._nextPage !== location ) + { + console.log( + { + current: location, + next: this._nextPage, + normalizedNext: rootPathNextPage + } + ); + + this._loadPage( location ); + } + else + { + let pageData = this._pages.get( this._currentPage ); + + if ( pageData ) + { + pageData.scrollTop = this.currentScrollTop; + } + else + { + if ( ! PageHandler.isHiddenPage() ) + { + console.log( this._currentPage, this.currentPage, "NO PAGE DATA" ); + } + + + } + + } + + } + + private _scrollToY( y:number ) + { + document.documentElement.scrollTop = y; + document.body.scrollTop = y; + } + + + + + +} \ No newline at end of file diff --git a/browser/dom/PageTransitionHandler.ts b/browser/dom/PageTransitionHandler.ts new file mode 100644 index 0000000..a924a7b --- /dev/null +++ b/browser/dom/PageTransitionHandler.ts @@ -0,0 +1,7 @@ +import { PageHandler } from "./PageHandler"; + +export abstract class PageTransitionHandler +{ + abstract onTransitionOut( pageHandler:PageHandler ):Promise; + abstract onTransitionIn( pageHandler:PageHandler ):Promise; +} \ No newline at end of file diff --git a/browser/dom/RootPathResolver.ts b/browser/dom/RootPathResolver.ts new file mode 100644 index 0000000..16aeee9 --- /dev/null +++ b/browser/dom/RootPathResolver.ts @@ -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 ); + } + ) + } +} \ No newline at end of file diff --git a/browser/dom/UserAgentDeviceType.ts b/browser/dom/UserAgentDeviceType.ts new file mode 100644 index 0000000..43faf4e --- /dev/null +++ b/browser/dom/UserAgentDeviceType.ts @@ -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; + + } +} + + diff --git a/browser/dom/UserAgentInfo.ts b/browser/dom/UserAgentInfo.ts new file mode 100644 index 0000000..236acc1 --- /dev/null +++ b/browser/dom/UserAgentInfo.ts @@ -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; + } + +} \ No newline at end of file diff --git a/browser/geometry/Box2.ts b/browser/geometry/Box2.ts new file mode 100644 index 0000000..578d684 --- /dev/null +++ b/browser/geometry/Box2.ts @@ -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 ); + } +} \ No newline at end of file diff --git a/browser/geometry/Vector2.ts b/browser/geometry/Vector2.ts new file mode 100644 index 0000000..41e1977 --- /dev/null +++ b/browser/geometry/Vector2.ts @@ -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 ); + } + +} \ No newline at end of file diff --git a/browser/text/RegExpUtitlity.ts b/browser/text/RegExpUtitlity.ts index 18fbeb3..9ab9008 100644 --- a/browser/text/RegExpUtitlity.ts +++ b/browser/text/RegExpUtitlity.ts @@ -833,8 +833,7 @@ export class RegExpUtility static createRegExp( regexp:RegExp, matching:string, replacement:string ) - { - + { let source = regexp.source; let flags = regexp.flags; source = source.replace( matching, replacement ); diff --git a/browser/tools/MapList.ts b/browser/tools/MapList.ts new file mode 100644 index 0000000..de2729e --- /dev/null +++ b/browser/tools/MapList.ts @@ -0,0 +1,14 @@ +export class MapList +{ + static add( map:Map, k:K, v:V ) + { + if ( ! map.has( k ) ) + { + map.set( k, [] ); + } + + map.get( k ).push( v ); + } + + +} \ No newline at end of file diff --git a/browser/xhttp/Loader.ts b/browser/xhttp/Loader.ts index 95ec571..72571b4 100644 --- a/browser/xhttp/Loader.ts +++ b/browser/xhttp/Loader.ts @@ -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 ) { diff --git a/node/log/RJLog.ts b/node/log/RJLog.ts index b401054..1f18674 100644 --- a/node/log/RJLog.ts +++ b/node/log/RJLog.ts @@ -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 ); diff --git a/node/tsconfig.json b/node/tsconfig.json index c15a3d2..bcdd693 100644 --- a/node/tsconfig.json +++ b/node/tsconfig.json @@ -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"] + } } \ No newline at end of file diff --git a/node/webpack/PHPPagesBuilder.ts b/node/webpack/PHPPagesBuilder.ts new file mode 100644 index 0000000..b91e0a0 --- /dev/null +++ b/node/webpack/PHPPagesBuilder.ts @@ -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 `\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(); + + } + ); + } + ); + } +}