library-ts/browser/dom/Insight.ts

373 lines
8.8 KiB
TypeScript
Raw Normal View History

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