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 ) ); } }