/** * 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._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._modifier._modifications["overflow-y"] = "auto"; } if (horizontal) { this._modifier._modifications["overflow-x"] = "auto"; } return this; } /** * * @param {boolean} untilFound * @returns {Component} */ hidden(untilFound = false) { this._modifier.removeStyleRule("display"); this.setAttribute( "hidden", (untilFound ? "until-found" : "hidden") ); return this; } /** * * @returns {Component} */ isHigherComponent() { 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 {Array} listName */ subscribeOnGenerate(listName) { this._toRegister.push(listName); return this; } /** * * @returns {Component} */ registerAsContextMenu() { this._isContextMenu = true; this.addStyleClass('contextmenu') .hidden(); return this; } /** * @todo Positioning of the contextmenu element * @todo extract into an extra function(allity) provider * * @param {Component} component * @param {Function => Sides} getRefPos * @param {null|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); } } let cMenuWenity = component.generate(); let identifier = cMenuWenity.html.getAttribute("data-autocompel"); function hideCMenu(el) { el.setAttribute("hidden", "hidden"); el.style.display = "none"; } function hideOnEscape(event) { if (event.key === "Escape") { let menu = document.querySelector(`[data-autocompel="${identifier}"`); hideCMenu(menu); document.removeEventListener("keyup") } } function hideOnClickOutsideOfBounds(event) { let menu = document.querySelector(`[data-autocompel="${identifier}"`); let area = getEnclosingBounds(menu); if (!areXYInArea(area, event.clientX, event.clientY)) { //if (event.target.offsetParent != menu) { hideCMenu(menu); document.removeEventListener("click") } } this.addEventListener( "contextmenu", function (event) { event.preventDefault(); /** * @type {Sides} */ const pos = getRefPos(event); let menu = document.querySelector(`[data-autocompel="${identifier}"`); menu.style.left = `${pos.getByIndex(SideDirections.LEFT)}px`; menu.style.top = `${pos.getByIndex(SideDirections.TOP)}px`; menu.style["display"] = "block"; menu.removeAttribute("hidden"); document.addEventListener("click", hideOnClickOutsideOfBounds); document.addEventListener("keyup", hideOnEscape); } ); return this; } /** * * @param {*} dndGroup * @returns {Component} */ draggable(dndGroup = null) { let selector = this._element.getAttribute("data-autocompel"); return this.addStyleClass("comp-el-mech-draggable") .setAttribute("draggable", "true") .setAttribute("dropEffect", "none") .addEventListener( CommonEvents.DRAG_START, function (event) { console.log("DragEvent", event, "on", selector); e.dataTransfer .setData( "text/plain", selector ); } ); } /** * * @param {EventDrag} dragEvent * @param {Function} action */ onDrag(dragEvent, action) { let selector = `comp-el-mech-drag${dragEvent}`; return this.addEventListener( dragEvent, function (event) { event.preventDefault(); } ); } /** * * @param {*} dndGroup * @returns {Component} */ dropTarget(dndGroup = null) { let selector = "comp-el-mech-droptarget"; return this.onDrag(EventDrag.OVER) .addStyleClass(selector) .addEventListener( CommonEvents.DROP, function (event) { event.preventDefault(); let draggedKey = event.dataTransfer.getData("text"); let draggedElement = document.querySelector(`[data-autocompel="${draggedKey}"]`); let target = e.target .closest('.' + selector); if (![...target.childNodes].includes(draggedElement)) { target .appendChild(draggedElement); } } ); } /** * 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.updateForGeneralStyling(); } else { extStore.updateForGeneralStyling(); } /** * @todo very likely code dupplication - but kept for the time being * for error tracking. */ if (extStore._type === 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.updateForGeneralStyling()._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.updateForFunctions(); } else { extStore.updateForFunctions(); } 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.updateForFunctions()._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._modifier._modifications["box-sizing"] = "border-box"; this._modifier._modifications["justify-content"] = this._arrangement; this._modifier._modifications["align-content"] = this._alignment; this._modifier._modifications["align-items"] = this._alignment; this._modifier._modifications["text-align"] = this._alignment; 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); } } /** * @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; return this._wenity; } }