/** * This file is part of the jps-like-websites lib * URL: https://git.labos.goip.de/chris/jpc-like-websites * @copyright by its creator Christian Martin */ /** * Represents the most basic and simple form of a Component. * It is mainly a collection of wrapper methods * around the HTMLElement methods to make them chainable. * It serves as base for further functionallity extensions. * @property {Map} _style */ class Component extends StyleAndScriptStoringComponent { /** * @type {boolean} */ #isCompel; /** * @type {WebTrinity} */ _wenity; /** * @type {Array} */ _toRegister; /** * @type {boolean} */ _isContextMenu; /** * Initializes the component * @param {HTMLElement} element the base element * @param {Map} attr Specific already known attributes */ constructor(element, attr = {}) { super(element, attr); this.#isCompel = false; this._isContextMenu = false; this._modifier = new Modifier() .margin(new Sides().all(0)); this._modifier._modifications['display'] = "flex"; this._modifier._modifications["box-sizing"] = "border-box"; this._toRegister = []; } /** * Adds a class to classList via HTMLElement.classList.add() method. * Further collects rules in a property until generate is called. * * @CAUGHTION implementation is not safe to use, ignoring extStore is recommended; * * @todo difference between stylings and classes, extStore logic in combination with the Page.register... logic * * @override * * @param {string} styleClass (without the '.' in the front) * @param {string|Modifier|map} styling * @param {ExtStorage|ExtStoreType|ExtStorePosition|OverwriteBehaviour|EXPosConfer|ESOverwriteConfer} extStore * if a unique definition is desired, all constants or configurator objects are allowed - they will be processed accordingly * @returns {Component} this component object */ addStyleClass(styleClass, styling = null, extStore = null) { if (!extStore) { extStore = this._styleClassesExtStore; } else if (extStore.isMissing()) { extStore = extStore.fillBy(this._styleClassesExtStore); } if (styling) { if (styling instanceof Modifier) { styling = styling._modifications; } Page.registerStyling('.' + styleClass, styling); } this._element.classList.add(styleClass); return this; } /** * * @param {boolean} vertical Defines if the Component should overflow vertically (default: true) * @param {boolean} horizontal Defines if the Component should overflow horizontally (default: false) * @returns {Component} */ overflow(vertical = true, horizontal = false) { if (vertical) { this.subscribeOnGenerate(CommonCompelGroups.OVERFLOWING); this._modifier._modifications["overflow-y"] = "auto"; } if (horizontal) { this.subscribeOnGenerate(CommonCompelGroups.OVERFLOWING); this._modifier._modifications["overflow-x"] = "auto"; } return this; } /** * * @param {boolean} untilFound * @returns {Component} */ hidden(untilFound = false) { Page.registerStyling(".compel-mech-hidden", { "hidden": "hidden" }); this._modifier.removeStyleRule("display"); this.setAttribute( "hidden", (untilFound ? "until-found" : "hidden") ); this.subscribeOnGenerate(CommonCompelGroups.HIDDEN_ON_START); return this; } /** * * @returns {Component} */ isHigherComponent() { this.subscribeOnGenerate(CommonCompelGroups.HIGHER_COMPEL); this.#isCompel = true; return this.setAttribute("data-compel-isHCompel", "true") } /** * Collects the given List in the _toRegister attribute. * When generate() is called, * the created Element will be registered (added) in every list * within the list. * @param {*|string|Array<*>} listName */ subscribeOnGenerate(listName) { this._toRegister.push(listName); return this; } /** * * @returns {Component} */ registerAsContextMenu() { this.subscribeOnGenerate(CommonCompelGroups.IS_CONTEXT_MENU); this._isContextMenu = true; return this.addStyleClass('contextmenu') .hidden(); } /** * @todo Positioning of the contextmenu element * @todo extract into an extra function(allity) provider * * @param {Component} component * @param {Sides|Function => Sides} getRefPos * @param {ExtStorage} [extStore=null] * @returns {Component} */ contextMenu(component, getRefPos = null, extStore = null) { if (!component._isContextMenu) { component.registerAsContextMenu(); } if (!getRefPos) { getRefPos = function (cmEvent) { return new Sides() .left(cmEvent.pageX) .top(cmEvent.pageY); } } this.subscribeOnGenerate(CommonCompelGroups.HAS_CONTEXT_MENU); let identifier = component._compName; this.addEventListener( "contextmenu", DefaultContextMenu.openContextMenuAction(identifier, getRefPos) ); return this.childContext(component); } /** * * @param {DragAndDropImplementation} dadImpl * @returns {Component} */ draggable(dadImpl = new DragAndDropImplementation()) { this.subscribeOnGenerate(CommonCompelGroups.DRAGGABLE); this.subscribeOnGenerate(CommonCompelGroups.HAS_DRAG_EVENT); let addedClass = "comp-el-mech-draggable"; let selector = this._element.getAttribute("data-autocompel"); selector = `.${addedClass}[data-autocompel="${selector}"]`; return this.addStyleClass(addedClass) .setAttribute("draggable", "true") .setAttribute("dropEffect", "none") .addEventListener( CommonEvents.DRAG_START, dadImpl.dragStartAction("text/plain", selector) ); } /** * * @param {EventDrag} dragEvent * @param {Function} action */ onDrag(dragEvent, action = (e) => { e.preventDefault(); }) { this.subscribeOnGenerate(CommonCompelGroups.HAS_DRAG_EVENT); let selector = `comp-el-mech-drag${dragEvent}`; return this.addEventListener( 'drag' + dragEvent, action ); } /** * * @param {DragAndDropImplementation} dadImpl * @returns {Component} */ dropTarget(dadImpl = new DragAndDropImplementation()) { this.subscribeOnGenerate(CommonCompelGroups.DROP_TARGET); let specialClass = "comp-el-mech-droptarget"; this.addStyleClass(specialClass) .onDrag(EventDrag.OVER); let selector = `.${specialClass}[data-autocompel="${this._compName}"]` this._element.addEventListener( "drop", dadImpl.dropEventAction("text/plain", selector) ); return this; } /** * An echo of Scope-Functions from kotlin for convenience * * Executes a given function injects this component into the function. * @param {Function} func * @returns {Component} */ apply(func) { func(this); return this; } /** * An echo of Scope-Functions from kotlin for convenience * * Executes a given function injects the htmlelement of this component into the function. * @param {Function} func * @returns {Component} */ applyToEl(func) { func(this._element) return this; } /** * Defines how a child Component is to be appended. * @param {Component} component the child component to add it. * @returns {HTMLElement} */ _appendChildComponent(component) { let child = new WebTrinity(); if (component instanceof Component) { child = component.generate(); } if (component instanceof WebTrinity) { child = component; } if (component instanceof HTMLElement) { console.log("No wenity set - htmlEl was given"); child.html = component; } this._element.append(child.html); return child; } _processStyles(extStore = null) { if (!extStore) { extStore = this._stylesExtStore.setupForGeneralStyling(); } else { extStore.setupForGeneralStyling(); } /** * @todo very likely code dupplication - but kept for the time being * for error tracking. */ if (extStore === ExtStoreType.INTERNALIZED_WITHIN) { let sizings = Object.keys(this._modifier._modifications) .filter(e => e.includes("width") || e.includes("height")) .filter(e => this._modifier._modifications[e].includes("calc")) .reduce((a, c) => a.add( c, this._modifier ._modifications[c] .split('(')[1] .split(' - ')[0] ), new ObjectAccessObject()); fillAttrsInContainerByCb( this._modifier._modifications, this._element, (key, val, el) => { el.style[key] = val; } ); let hasElSizing = sizings.keys.some(k => Object.keys(this._element.style).includes(k)); if (sizings.keys.length > 0 && !hasElSizing) { console.log("Fixing sizing - because not supported 'calc'", sizings); fillAttrsInContainerByCb( sizings, this._element, (key, val, el) => { el.style[key] = val; } ); } } else { /* ADDS ELEMENT MODIFIER TO this._styles list for styles processing */ let modifierSSD = new SStoreDefinition(); modifierSSD._identifier = "." + this._compName; modifierSSD._definition = this._modifier._modifications; modifierSSD._extStore = extStore; this._styles.unshift(modifierSSD); } let forCollection = []; let counter = 0; for (let i = 0; i < this._styles.length; i++) { const ssd = this._styles[i]; /* Make sure that the type is unified for later processing */ if (ssd._definition instanceof Modifier) { ssd._definition = ssd._definition._modifications; } /* Check/Ensure proper ExtStorageType for following comparison */ let refESType = ( ssd._extStore && ssd._extStore._type ? ssd._extStore.setupForGeneralStyling()._type : extStore._type ); switch (refESType) { case ExtStoreType.INTERNALIZED_WITHIN: fillAttrsInContainerByCb( ssd._definition, this._element, (key, val, el) => { el.style[key] = val; } ) break; case ExtStoreType.INDIVIDUALLY_DOC_HEAD: let container = generateAndFillStyleTag([ssd]); container.setAttribute("data-compel-individually-nr", counter++); Page.addElementToPage(container, refESType); break; case ExtStoreType.COLLECTED_DOC_HEAD: forCollection.push(ssd); break; case ExtStoreType.CENTRALIZED_DOC_HEAD: Page.registerStyling(ssd._identifier, ssd._definition); break; } } return forCollection; } _processFunctions(extStore = null) { if (!extStore) { extStore = this._functionsExtStore.setupForFunctions(); } else { extStore.setupForFunctions(); } const forCollection = new Map(); const collectForBefore = []; let counter = 0; for (let i = 0; i < this._functions.length; i++) { const ssd = this._functions[i]; /* Make sure that the type is unified for later processing */ let refESType = ( ssd._extStore && ssd._extStore._type ? ssd._extStore.setupForFunctions()._type : extStore._type ); switch (refESType) { case ExtStoreType.CENTRALIZED_DOC_HEAD: case ExtStoreType.CENTRALIZED_SEGMENT_BEGIN: case ExtStoreType.CENTRALIZED_DOC_FOOTER: Page.registerPageFunction(ssd._identifier, ssd._definition); break; case ExtStoreType.INDIVIDUALLY_WITHIN: case ExtStoreType.INDIVIDUALLY_BEFORE: case ExtStoreType.INDIVIDUALLY_SEGMENT_BEGIN: case ExtStoreType.INDIVIDUALLY_DOC_FOOTER: case ExtStoreType.INDIVIDUALLY_DOC_HEAD: let container = document.createElement("script"); container.setAttribute("data-compel-individually-nr", counter++); container.innerText += getScriptTagInjectionText( clearFunctionDeclarationText(ssd._definition), ssd._identifier ); Page.addElementToPage(container, refESType, this._element); break; case ExtStoreType.COLLECTED_BEFORE: collectForBefore.push(ssd); break; case ExtStoreType.COLLECTED_SEGMENT_BEGIN: case ExtStoreType.COLLECTED_DOC_FOOTER: case ExtStoreType.COLLECTED_DOC_HEAD: if (!forCollection.has(refESType)) { forCollection.set(refESType, []); } forCollection.get(refESType).push(ssd); break; } } return forCollection; } /** * Ends chain. * Applies all modifications on the element. * Processes alls stored additions. * Returns the constructed HTMLElement of this Component. * * * * @param {ExtStorage} * @returns {WebTrinity} the constructed HTMLElement of this Component. */ generate(styleStore = null, functionStore = null) { this._wenity = new WebTrinity(); /* DEAL WITH COMPONENT MODIFICATION FIRST */ this._styles.push(new SStoreDefinition(this._compName, this._modifier, this._stylesExtStore)); /* DEAL WITH CHILDREN */ let collectedWenities = []; for (let i = 0; i < this._children.length; i++) { /** * @type {Component} */ let child = this._children[i]; if (child instanceof ChainableModifier) { child = child.toComponent(); } if (Page._useCssCalc) { child._modifier._updateDimensionsBy(this._modifier._paddingValues); } child = child.generate(); let wenity = this._appendChildComponent(child); if (!wenity.isSSEmpty()) { collectedWenities.push(wenity); } } /* DEAL WITH STYLING AND PROCESSING */ /** * @type {Array} */ let styleCollection = this._processStyles(styleStore); /** * @type {Map>} */ const funcCollections = this._processFunctions(functionStore); /** * * @param {Map>} source * @param {Map>} target * @param {ExtStoreType} extStoreType * @returns */ function transferCollectedFunctions(source, target, extStoreType) { if (source) { if (source.has(extStoreType)) { if (funcCollections.has(extStoreType)) { target.get(extStoreType) .push(source.get(extStoreType)) } else { target.set( extStoreType, source.get(extStoreType) ); } } } return target; } function executeOnExtStoreTypeCollectedTriple(func) { return new Map([ { [ExtStoreType.COLLECTED_SEGMENT_BEGIN]: func(ExtStoreType.COLLECTED_SEGMENT_BEGIN) }, { [ExtStoreType.COLLECTED_DOC_HEAD]: func(ExtStoreType.COLLECTED_DOC_HEAD) }, { [ExtStoreType.COLLECTED_DOC_FOOTER]: func(ExtStoreType.COLLECTED_DOC_FOOTER) } ]); } for (let i = 0; i < collectedWenities.length; i++) { const child = collectedWenities[i]; if (child.js) { executeOnExtStoreTypeCollectedTriple( (extstoretype) => transferCollectedFunctions(child.js, funcCollections, extstoretype) ); } } if (this.#isCompel) { function dealCollected(map, extStoreType) { if (map.has(extStoreType)) { let collectionScriptTag = generateAndFillScriptTag(map.get(extStoreType)); if (extStoreType === ExtStoreType.COLLECTED_SEGMENT_BEGIN) { this._element.insertAdjacentElement( "afterbegin", generateAndFillScriptTag(segment) ); } else { Page.addElementToPage( collectionScriptTag, extStoreType ); } } } executeOnExtStoreTypeCollectedTriple((est) => dealCollected(funcCollections, est)); } else { this._wenity.js = funcCollections; this._wenity.css = styleCollection; } this._wenity.html = this._element; for (let i = 0; i < this._toRegister.length; i++) { const group = this._toRegister[i]; Page.subscribeComponentToGroup(group, this._compName) } return this._wenity; } }