595 lines
13 KiB
JavaScript
595 lines
13 KiB
JavaScript
import CSSQuery from './core/utils/css/CSSQuery';
|
|
import CSSRuleVR from './core/utils/css/CSSRuleVR';
|
|
import CSSMediaQuery from './core/utils/css/CSSMediaQuery';
|
|
import UpdateManager from '../../src/components/core/UpdateManager';
|
|
import HTMTextElement from 'three-mesh-ui/examples/hyperthreemesh/core/elements/HTMTextElement';
|
|
import HTMInlineElement from 'three-mesh-ui/examples/hyperthreemesh/core/elements/HTMInlineElement';
|
|
import HTMBlockElement from 'three-mesh-ui/examples/hyperthreemesh/core/elements/HTMBlockElement';
|
|
import KeyboardHTM from 'three-mesh-ui/examples/hyperthreemesh/KeyboardHTM';
|
|
import HTMInlineBlockElement from 'three-mesh-ui/examples/hyperthreemesh/core/elements/HTMInlineBlockElement';
|
|
import HTMButton from 'three-mesh-ui/examples/hyperthreemesh/elements/HTMButton';
|
|
import HTMButtonToggle from 'three-mesh-ui/examples/hyperthreemesh/elements/HTMButtonToggle';
|
|
import HTMButtonRadio from 'three-mesh-ui/examples/hyperthreemesh/elements/HTMButtonRadio';
|
|
|
|
|
|
/**
|
|
* querySelectorAll is an entrypoint, logic are deviate to internal _querySelectorAll
|
|
* @param {string|CSSQuery} query
|
|
* @param {MeshUIBaseElement|Array.<MeshUIBaseElement>} [context]
|
|
* @return {Array.<MeshUIBaseElement>}
|
|
*/
|
|
function querySelectorAll( query, context = null ) {
|
|
|
|
if( !( query instanceof CSSQuery ) ) {
|
|
query = CSSQuery.build( query );
|
|
}
|
|
|
|
// if no lookup context is provided, use any root MeshUIComponent
|
|
if ( !context ) {
|
|
context = UpdateManager.elements;
|
|
}
|
|
|
|
// Be sure the provided lookup context is an Array
|
|
if ( !Array.isArray( context ) ) {
|
|
context = [ context ];
|
|
}
|
|
|
|
|
|
let results = [];
|
|
// Run the internal logic on any context
|
|
for ( let i = 0; i < context.length; i++ ) {
|
|
|
|
results = results.concat( _querySelectorAll( context[ i ], query ) );
|
|
|
|
}
|
|
|
|
// The same MeshUIComponent could be found multiple times, be sure they are only output once
|
|
for ( let i = results.length - 1; i >= 0; i-- ) {
|
|
|
|
const firstIndexOf = results.indexOf( results[ i ] );
|
|
if ( firstIndexOf !== i ) {
|
|
|
|
results.splice( i, 1 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
/**
|
|
* Internal logic for querySelectorAll
|
|
* @param {MeshUIBaseElement} target
|
|
* @param {CSSQuery} query
|
|
* @param {boolean} [recursive=true] Traverse all children to process query
|
|
* @return {Array.<MeshUIBaseElement>}
|
|
* @private
|
|
*/
|
|
function _querySelectorAll( target, query, recursive = true ) {
|
|
|
|
let results = [];
|
|
|
|
// propagate the query selection to any children
|
|
if ( recursive ) {
|
|
|
|
for ( let i = 0; i < target._children._uis.length; i++ ) {
|
|
|
|
results = results.concat( _querySelectorAll( target._children._uis[ i ], query ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// check that the target match the first query segment of the list
|
|
// ie: "div.foo" in "div.foo span.bar > p"
|
|
if ( query[ 0 ].match( target ) ) {
|
|
|
|
// push the target as result if there is no more segments in the query
|
|
if ( query.length === 1 ) {
|
|
results.push( target );
|
|
return results;
|
|
}
|
|
|
|
// Or check children and sibling to complete further segments
|
|
const subQuery = query.slice( 1 );
|
|
|
|
// check which css combinator to apply
|
|
if ( subQuery && subQuery !== '' ) {
|
|
|
|
// Descendant combinator, looks in any children, recursively
|
|
if ( !subQuery[ 0 ].combinator || subQuery[ 0 ].combinator === '' ) {
|
|
for ( let i = 0; i < target._children._uis.length; i++ ) {
|
|
|
|
results = results.concat( _querySelectorAll( target._children._uis[ i ], subQuery ) );
|
|
}
|
|
|
|
}
|
|
// direct child combinator, only look in direct children, not recursively
|
|
else if ( subQuery[ 0 ].combinator === '>' ) {
|
|
|
|
for ( let i = 0; i < target._children._uis.length; i++ ) {
|
|
|
|
results = results.concat( _querySelectorAll( target._children._uis[ i ], subQuery, false ) );
|
|
|
|
}
|
|
}
|
|
|
|
// siblings combinator
|
|
else if ( target._parent._value && ( subQuery[ 0 ].combinator === '~' || subQuery[ 0 ].combinator === '+' ) ) {
|
|
|
|
const parentUI = target._parent._value;
|
|
|
|
// retrieve the childIndex of the current target
|
|
const currentIndex = parentUI._children._uis.indexOf( target );
|
|
|
|
// build the siblings list to check
|
|
let adjacentSiblings;
|
|
|
|
// General sibling, next query segments apply on any further children
|
|
if ( subQuery[ 0 ].combinator === '~' ) {
|
|
adjacentSiblings = parentUI._children._uis.slice( currentIndex + 1 );
|
|
} else {
|
|
// adjacent sibling, next query segment apply only to next sibling
|
|
adjacentSiblings = parentUI._children._uis.slice( currentIndex + 1, currentIndex + 2 );
|
|
}
|
|
|
|
// Looks to siblings, not recursively
|
|
for ( let i = 0; i < adjacentSiblings.length; i++ ) {
|
|
|
|
results = results.concat( _querySelectorAll( adjacentSiblings[ i ], subQuery, false ) );
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error( `UIDocument::querySelectorAll() - The provided css combinator('${subQuery[ 0 ].combinator}') is not implemented` );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @type {Array.<CSSRuleVR>}
|
|
* @private
|
|
*/
|
|
let _rules = [];
|
|
let _conditions = [];
|
|
let _observers = [];
|
|
let _media = 'three-mesh-ui';
|
|
/**
|
|
* WIP
|
|
* @deprecated until achieved
|
|
* @param media
|
|
* @param listenForChanges
|
|
*/
|
|
function loadSheets( media = 'three-mesh-ui', listenForChanges = false ) {
|
|
|
|
_media = media;
|
|
|
|
// If it should be reactive and a document isset
|
|
if( listenForChanges && document ){
|
|
|
|
// Starts be removing any previously set MutationObservers
|
|
for ( let i = 0; i < _observers.length; i++ ) {
|
|
let previousMutationObserver = _observers[ i ];
|
|
previousMutationObserver.disconnect();
|
|
previousMutationObserver = null;
|
|
}
|
|
|
|
_observers = [];
|
|
|
|
_addMutationObserverOnContainer( document.documentElement );
|
|
|
|
const sheets = document.querySelectorAll(`link[media="${media}"],style[media="${media}"]`);
|
|
for ( let i = 0; i < sheets.length; i++ ) {
|
|
|
|
_addMutationObserverOnStyleSheet( sheets[ i ] );
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// reset elements
|
|
// @TODO: Dispose conditions
|
|
_rules = [];
|
|
_conditions = [];
|
|
|
|
if ( document && document.styleSheets ) {
|
|
|
|
for ( const styleSheet of document.styleSheets ) {
|
|
if ( styleSheet.media.mediaText === media ) {
|
|
|
|
const { rules, conditions } = _importSheet( styleSheet.cssRules );
|
|
|
|
_rules = _rules.concat( rules );
|
|
_conditions = _conditions.concat( conditions );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for ( let j = 0; j < _rules.length; j++ ) {
|
|
_rules[ j ].order = j;
|
|
}
|
|
|
|
_rules.sort( _sortXSSRules );
|
|
|
|
for ( const condition of _conditions ) {
|
|
condition.init( () => { _needsUpdate = true; } );
|
|
}
|
|
|
|
}
|
|
|
|
_applyRules();
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {Array.<MutationRecord>} mutations
|
|
* @private
|
|
*/
|
|
function _addOrRemoveStylesheetMutation( mutations ){
|
|
let reload = false;
|
|
mutations.forEach(function(mutation) {
|
|
|
|
for (let i = 0; i < mutation.addedNodes.length; i++){
|
|
if( _matchVRStyleSheet(mutation.addedNodes[i]) ){
|
|
reload = true;
|
|
}
|
|
|
|
}
|
|
|
|
for ( let i = 0; i < mutation.removedNodes.length; i++ ) {
|
|
|
|
if( _matchVRStyleSheet(mutation.removedNodes[ i ]) ){
|
|
reload = true;
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if( reload ){
|
|
loadSheets( true );
|
|
}
|
|
}
|
|
|
|
function _matchVRStyleSheet( element ) {
|
|
|
|
return element.tagName === 'LINK' || element.tagName === 'STYLE' && element.getAttribute('media') === _media;
|
|
|
|
}
|
|
|
|
function _addMutationObserverOnContainer( container ){
|
|
|
|
const observer = new MutationObserver( _addOrRemoveStylesheetMutation );
|
|
|
|
observer.observe( container, { childList: true, subtree: true });
|
|
_observers.push( observer );
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param cssStyleSheet
|
|
* @private
|
|
*/
|
|
function _addMutationObserverOnStyleSheet( cssStyleSheet ){
|
|
|
|
|
|
// observe the stylesheet itself for content changes
|
|
/* eslint-disable no-unused-vars */
|
|
const observer = new MutationObserver(function(mutations) {
|
|
loadSheets(_media, true);
|
|
});
|
|
/* eslint-enable no-unused-vars */
|
|
|
|
// Pass in the target node, as well as the observer options.
|
|
observer.observe( cssStyleSheet, {
|
|
attributes: true,
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true,
|
|
characterDataOldValue: true
|
|
});
|
|
|
|
_observers.push( observer );
|
|
|
|
}
|
|
|
|
/**
|
|
* When an element has changed its identity
|
|
* - ID
|
|
* - ClassList
|
|
* - Attributes & values
|
|
*
|
|
* Try to apply any css rules to it and its children
|
|
* @param {MeshUIComponent} element
|
|
*/
|
|
export function elementChangeIdentity( element ){
|
|
|
|
_checkAndApplyCSSRules(element);
|
|
|
|
element.traverse( (child) => {
|
|
|
|
if( child.isUI ) {
|
|
|
|
_checkAndApplyCSSRules(child);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {MeshUIComponent} forElement
|
|
* @private
|
|
*/
|
|
function _checkAndApplyCSSRules( forElement ){
|
|
|
|
// to bundle of styles property to set
|
|
let computedStyles = {};
|
|
let found = false;
|
|
|
|
// Loop through each enabled rules
|
|
for ( const rule of _rules ) {
|
|
|
|
if( !rule.enabled ) continue;
|
|
|
|
// If the element match the rule
|
|
if( rule.query.match( forElement ) ) {
|
|
|
|
// append the rules styles properties
|
|
// computedStyles = {...computedStyles,...rule.styles};
|
|
computedStyles = {...computedStyles };
|
|
found = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If at least one rule has matched
|
|
if( found ) {
|
|
|
|
console.log( computedStyles )
|
|
|
|
// Set computed styles
|
|
forElement.set( computedStyles );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
export function computeStyle( element ){
|
|
const elements = [];
|
|
if( element.parentUI ){
|
|
elements.push( element.parentUI );
|
|
}
|
|
|
|
element.traverse( (child) => {
|
|
if( child.isUI ) elements.push( child );
|
|
})
|
|
|
|
for ( const elem of elements ) {
|
|
|
|
let computedStyles = {};
|
|
let found = false;
|
|
for ( const rule of _rules ) {
|
|
|
|
if( !rule.enabled ) continue;
|
|
|
|
if( rule.query.match(elem) ) {
|
|
computedStyles = {...computedStyles,...rule.styles};
|
|
found = true;
|
|
}
|
|
|
|
}
|
|
|
|
if( found ) {
|
|
elem.set( computedStyles );
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
export function _applyRules(){
|
|
|
|
for ( const rule of _rules ) {
|
|
|
|
if( !rule.enabled ) continue;
|
|
|
|
const targets = querySelectorAll( rule.query );
|
|
|
|
for ( const target of targets ) {
|
|
|
|
target.set( rule.styles );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sort the rules by specificity and order
|
|
* Making it sure any overrides is justified
|
|
*
|
|
* @param {CSSRuleVR} a
|
|
* @param {CSSRuleVR} b
|
|
* @returns {number}
|
|
* @private
|
|
*/
|
|
function _sortXSSRules( a, b ){
|
|
|
|
if( a.specificity < b.specificity ){
|
|
return -1;
|
|
}
|
|
|
|
if( a.specificity > b.specificity ){
|
|
return 1;
|
|
}
|
|
|
|
if( a.order < b.order ){
|
|
return -1;
|
|
}
|
|
|
|
if( a.order > b.order ){
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
/**
|
|
* @Todo : Try to reduce as much as possible this table by updating meshUIComponent properties to match
|
|
* as much as possible css valid properties
|
|
*
|
|
* @type {Object.<string,string>}
|
|
* @private
|
|
*/
|
|
const _lookUpTable = {
|
|
// flexDirection: "contentDirection",
|
|
rx: 'offset',
|
|
offsetDistance: 'offset'
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param rulesList
|
|
* @param {string|null} [condition=null]
|
|
* @returns {{rules: *[], conditions: *[]}}
|
|
* @private
|
|
*/
|
|
function _importSheet( rulesList, condition = null ) {
|
|
|
|
let rules = [];
|
|
let mediaQ = null;
|
|
let conditions = [];
|
|
|
|
if( condition && condition !== '' ) {
|
|
|
|
mediaQ = new CSSMediaQuery(condition);
|
|
conditions.push( mediaQ );
|
|
|
|
}
|
|
|
|
for ( let i = 0; i < rulesList.length; i++ ) {
|
|
|
|
const rule = rulesList[ i ];
|
|
|
|
if ( rule.selectorText ) {
|
|
|
|
|
|
const newRule = new CSSRuleVR( rule.selectorText, rule.style, _lookUpTable );
|
|
rules.push( newRule );
|
|
|
|
if( mediaQ ){
|
|
mediaQ.addRule( newRule );
|
|
}
|
|
|
|
} else if ( rule.conditionText ) {
|
|
|
|
let newCondition = condition;
|
|
if( !newCondition || newCondition === '' ){
|
|
newCondition = rule.conditionText;
|
|
}else{
|
|
newCondition += ` and ${rule.conditionText}`;
|
|
}
|
|
|
|
const { rules : addRules, conditions : addConditions } = _importSheet( rule.cssRules , newCondition)
|
|
|
|
rules = rules.concat( addRules );
|
|
conditions = conditions.concat( addConditions );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
return { rules, conditions };
|
|
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param tag
|
|
* @param options
|
|
* @returns {HTMBaseElement}
|
|
*/
|
|
function createElement( tag, options = {} ){
|
|
|
|
if( !options.tagName ) options.tagName = tag;
|
|
switch ( tag.toLowerCase().replace(/\d/g, "") ) {
|
|
|
|
case "p":
|
|
case "h":
|
|
case "label":
|
|
return new HTMTextElement(options);
|
|
|
|
case "button":
|
|
return new HTMButton(options);
|
|
|
|
case "toggle":
|
|
options.tagName = 'button';
|
|
return new HTMButtonToggle(options);
|
|
|
|
case "radio":
|
|
options.tagName = 'button';
|
|
return new HTMButtonRadio(options);
|
|
|
|
case "div":
|
|
case "li":
|
|
case "footer":
|
|
case "header":
|
|
return new HTMBlockElement(options);
|
|
|
|
case "span":
|
|
case "em":
|
|
case "strong":
|
|
case "sup":
|
|
case "sub":
|
|
case "small":
|
|
case "link":
|
|
return new HTMInlineElement(options);
|
|
|
|
case "icon":
|
|
return new HTMInlineBlockElement(options);
|
|
|
|
case "keyboard":
|
|
return new KeyboardHTM( options );
|
|
|
|
default:
|
|
throw new Error("HyperTextMesh::createElement() - The provided tagname is not implemented");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let _needsUpdate = true;
|
|
function requestUpdate() {
|
|
_needsUpdate = true;
|
|
}
|
|
|
|
function update() {
|
|
if( _needsUpdate ) {
|
|
_applyRules();
|
|
_needsUpdate = false;
|
|
}
|
|
}
|
|
|
|
export { loadSheets };
|
|
|
|
export { createElement, requestUpdate, update, querySelectorAll };
|