/** * ESAggregation := Extensions Storage Aggregation (method) */ const ESAggregation = Object.freeze({ INTERNALIZED: "intern", INDIVIDUALLY: "individual", COLLECTED: "collected", CENTRALIZED: "centralized" }); /** * ExtStoragePos := Extensions Storage Position * * Determines where the extensions are positioned. * Only relevant if ExtStorage is not 'internalized'. * Determines where the tag (if individually) or the extensions are positioned. */ const ExtStorePosition = Object.freeze({ WITHIN: "WITHIN", BEFORE: "BEFORE", SEGMENT_BEGIN: "SEGMENT_BEGIN", DOC_HEAD: "DOC_HEAD", DOC_FOOTER: "DOC_FOOTER" }); /** * Defines how an identified dupplication should be "resolved"/dealt with. * REPLACE: * RENAME: * RENAME_OLD: * DROP_NEW: * MOVE_ELEMENT_SPECIFIC: @ATTENTION implementation pending */ const OverwriteBehaviour = Object.freeze({ REPLACE: "REPLACE", RENAME: "RENAME", RENAME_OLD: "RENAME_OLD", DROP_NEW: "DROP_NEW", MOVE_ELEMENT_SPECIFIC: "MOVE_ELEMENT_SPECIFIC" }); /** * Is supposed to shrink all empty strings to length 1 * @param {Function} func * @returns {string} */ function clearFunctionDeclarationText(func) { function shrinkEmptyStrings(text) { for (let i = 1; i < 10; i++) { text = text.replaceAll(" ".slice(i), ' '); } return text; } return shrinkEmptyStrings( func.toString() .replaceAll('\r\n', ' ') .replaceAll('\n\r', ' ') .replaceAll('\n', ' ') .replaceAll('\r', ' ') ); } /** * * @param {Function} func * @param {string} registrationName * @returns {string} */ function getScriptTagInjectionText(func, registrationName) { let funcHasName; if (typeof func === 'function') { funcHasName = ((func.name) && func.name.trim() !== ''); } if (func.startsWith('function')) { let label = ` function ${registrationName}`; let isNameInFuncText = func.startsWith(label); if (isNameInFuncText) { return func; } else { return [label, '(', func.split('(').slice(1).join('(')].join('') } } else { return ` const ${registrationName} = ${func}; `; } } /** * Stores a function until generate is called. * Then the additional informations of the store wil be applied * and the funcitons added to the page. */ class FunctionStoreBuffer { /** * Stores a function until generate is called. * Then the additional informations of the store wil be applied * and the funcitons added to the page. * @param {Function} func the function that will be stored * @param {Array} args additional arguments that will be given to the function * @param {boolean} repeats weither the funciton is supposed to execute repeatedly * @param {number} interval the time in milliseconds between executions * @param {boolean} execAfterStart weither the function is supposed to be executed after pageload * @param {number} delay the time in milliseconds the execution will be delayed */ constructor( func, args = [], repeats = false, interval = -1, execAfterStart = false, delay = -1 ) { this.func = func; this.args = args; this.execAfterStart = execAfterStart; this.delay = delay; this.repeats = repeats; this.interval = interval; } } /** * Extracted this super class to differentiate between * internal and external store. */ class ExtStorage { constructor( aggregation = ESAggregation.INTERNALIZED, position = ExtStorePosition.WITHIN, behaviour = OverwriteBehaviour.DROP_NEW ) { /** * @type {ESAggregation} */ this._aggregation = aggregation; /** * @type {ExtStorePosition} */ this._position = position; /** * @type {OverwriteBehaviour} */ this._overwriteBehaviour = behaviour; } /** * * @param {ESAggregation} position */ setExtStoreAggregation(aggregation) { this._aggregation = aggregation; return this; } /** * * @param {ExtStoreType} position */ setExtStorePosition(position) { this._position = position; return this; } /** * * @param {OverwriteBehaviour} behave * @returns {ExtStorage} */ setOverwriteBehaviour(behave) { this._overwriteBehaviour = behave; return this; } /** * * @param {ExtStorage} extStore * @returns {boolean} */ equals(extStore = null) { if (!extStore) return false; return extStore._type === this._type && extStore._overwriteBehaviour === this._overwriteBehaviour; } /** * * @returns {boolean} */ isMissing() { return this._type === null || this._overwriteBehaviour === null; } /** * * @returns {boolean} */ isNotInternalOrIndividual() { return !( this._aggregation === ESAggregation.INTERNALIZED || this._aggregation === ESAggregation.INDIVIDUALLY ); } /** * * @param {ExtStorage} otherExtStore * @returns {ExtStorage} */ fillBy(otherExtStore) { if (this._type === null) { this._type = otherExtStore._type; } if (this._overwriteBehaviour === null) { this._overwriteBehaviour = otherExtStore._overwriteBehaviour; } return this; } /** * @todo check if still implemented correctly * Takes the singleValue and an ExtStore object to copy all values from. * Then the singleValue will be compared to the three enums for the type of value. * After the type is identified the corresponding (copied) value will be updated. * @param {ExtStoreType|ExtStorePosition|OverwriteBehaviour} singleValue * @param {ExtStorage} extStoreToClone * @returns {ExtStorage} */ setSingleValueToClone(singleValue, extStoreToClone) { this._type = extStoreToClone._type; this._position = extStoreToClone._position; this._overwriteBehaviour = extStoreToClone._overwriteBehaviour; let target = [ ...Object.values(ExtStoreType).map(text => Object({ "value": text, "ref": "type" })), ...Object.values(ExtStorePosition).map(text => Object({ "value": text, "ref": "pos" })), ...Object.values(OverwriteBehaviour).map(text => Object({ "value": text, "ref": "over" })) ] .find(compareObj => compareObj["value"] === singleValue); if (target) { switch (target["ref"]) { case "type": this._type = singleValue; break; case "pos": this._position = singleValue; break; case "over": this._overwriteBehaviour = singleValue; break; } } return this; } /** * * @returns {ExtStorage} this extStore (updated if rules were used, that don't work for functions) */ setupForFunctions() { if (this._type === ExtStoreType.INTERNALIZED_WITHIN) { console.log("Updated Functions extstore from INTERNALIZED_WITHIN to INDIVIDUALLY_BEFORE") this._type = ExtStoreType.INDIVIDUALLY_BEFORE; } return this; } /** * * @returns {ExtStorage} */ setupForGeneralStyling() { if (this === ExtStoreType.INTERNALIZED_WITHIN) { this._position = ExtStorePosition.WITHIN; this._aggregation = ESAggregation.INTERNALIZED; return this; } this._position = ExtStorePosition.DOC_HEAD; switch (this) { case ExtStoreType.INDIVIDUALLY_WITHIN: case ExtStoreType.INDIVIDUALLY_BEFORE: case ExtStoreType.INDIVIDUALLY_SEGMENT_BEGIN: case ExtStoreType.INDIVIDUALLY_DOC_FOOTER: case ExtStoreType.INDIVIDUALLY_DOC_HEAD: this._aggregation = ESAggregation.INDIVIDUALLY; break; case ExtStoreType.COLLECTED_BEFORE: case ExtStoreType.COLLECTED_SEGMENT_BEGIN: case ExtStoreType.COLLECTED_DOC_FOOTER: case ExtStoreType.COLLECTED_DOC_HEAD: this._aggregation = ESAggregation.COLLECTED; this._aggregation = ESAggregation.COLLECTED; break; case ExtStoreType.CENTRALIZED_DOC_HEAD: case ExtStoreType.CENTRALIED_SEGMENT_BEGIN: case ExtStoreType.CENTRALIZED_DOC_FOOTER: default: this._aggregation = ESAggregation.CENTRALIZED; break } return this; } /** * Currently it works the same as the "updateForFunctions()" since the same rules won't work. * @returns {ExtStorage} this extStore (updated if rules were used, that won't work for StyleClasses) */ setupForStyleClass() { /* const positionedAfter = [ COLLECTED_DOC_FOOTER, INDIVIDUALLY_DOC_FOOTER, CENTRALIZED_DOC_FOOTER ]; if (positionedAfter.includes(this._type)) { this._type = ExtStoreType.INTERNALIZED_WITHIN; } */ return this.setupForGeneralStyling(); } /** * * @returns {InsertPosition} */ getRelativePositioning() { switch (this._position) { case ExtStorePosition.BEFORE: return "beforebegin" case ExtStorePosition.SEGMENT_BEGIN: return "afterbegin"; case ExtStorePosition.DOC_HEAD: case ExtStorePosition.DOC_FOOTER: return "beforeend" case ExtStorePosition.WITHIN: default: return "afterbegin"; } } /** * Expects a reference element for the positions before and segment_begin. * Otherwise will return head, footer or element accordingly. * @param {HTMLLIElement|Component} element * @returns {HTMLElement} */ getRefElement(element = null) { let ensuredElement = element; if (!element) { console.log("ExtStorePosition defines a relative position, but no reference Element is given - using head!") return document.querySelector('head'); } if (element instanceof Component) { ensuredElement = element.generate().compext; } switch (this._position) { case ExtStorePosition.BEFORE: case ExtStorePosition.SEGMENT_BEGIN: return ensuredElement.closest('[data-compel-isHCompel="true"]'); case ExtStorePosition.DOC_HEAD: return document.querySelector('head'); case ExtStorePosition.DOC_FOOTER: return document.querySelector('footer'); case ExtStorePosition.WITHIN: default: return ensuredElement; } } insertElementAccordingly(element) { this.getRefElement(element) .insertAdjacentElement( this.getRelativePositioning(), this.getRefElement(element) ) } /** * Returns a function that will setup the distribution of a given styling. * @returns {function(SStoreDefinition,HTMLElement,number): boolean} */ getStylingDistribution() { switch (this._aggregation) { case ESAggregation.INDIVIDUALLY: return function (ssd, orgElement, counter) { let container = generateAndFillStyleTag([ssd]); container.setAttribute("data-compel-individually-nr", counter++); Page.addElementToPage(container, this); return false; } case ESAggregation.COLLECTED: return function (ssd, orgElement) { return true; } case ESAggregation.CENTRALIZED: return function (ssd, orgElement) { Page.registerStyling(ssd._identifier, ssd._definition); return false; } case ESAggregation.INTERNALIZED: default: return function (ssd, orgElement) { helperFun.fillAttrsInContainerByCb( ssd._definition, orgElement, (key, val, el) => { el.style[key] = val; } ); return false; } } } /** * * @returns {function(SStoreDefinition, Map, number): boolean} */ getFunctionDistribution() { switch (this._aggregation) { case ESAggregation.INTERNALIZED: case ESAggregation.INDIVIDUALLY: return function (ssd, counter) { let container = document.createElement("script"); container.setAttribute("data-compel-individually-nr", counter++); container.innerText += getScriptTagInjectionText( clearFunctionDeclarationText(ssd._definition), ssd._identifier ); Page.addElementToPage(container, refESType); return false; } case ESAggregation.COLLECTED: return function () { return true; } case ESAggregation.CENTRALIZED: default: return function (ssd) { Page.registerPageFunction(ssd._identifier, ssd._definition); return false; } } } } /** * ExtStorage := Extensions storage (type) * Extensions in this context are stylings and scripts (currently only javascript). * internalized: the extensions are part of the element code/attributes - works obviously only with styling * individually: an individual tag is created/used * collected: the extension can/will be collected with others in a higher position of the element hierarchy * (but not document - root) * centralized: the extensions are send to the Page to be joined in a centralized tag/position of the document * (either head or footer tag) */ const ExtStoreType = Object.freeze({ INTERNALIZED_WITHIN: new ExtStorage(ESAggregation.INDIVIDUALLY, ExtStorePosition.WITHIN), INDIVIDUALLY_WITHIN: new ExtStorage(ESAggregation.INDIVIDUALLY, ExtStorePosition.WITHIN), INDIVIDUALLY_BEFORE: new ExtStorage(ESAggregation.INDIVIDUALLY, ExtStorePosition.BEFORE), INDIVIDUALLY_SEGMENT_BEGIN: new ExtStorage(ESAggregation.INDIVIDUALLY, ExtStorePosition.SEGMENT_BEGIN), INDIVIDUALLY_DOC_HEAD: new ExtStorage(ESAggregation.INDIVIDUALLY, ExtStorePosition.DOC_HEAD), INDIVIDUALLY_DOC_FOOTER: new ExtStorage(ESAggregation.INDIVIDUALLY, ExtStorePosition.DOC_FOOTER), COLLECTED_BEFORE: new ExtStorage(ESAggregation.COLLECTED, ExtStorePosition.BEFORE), COLLECTED_SEGMENT_BEGIN: new ExtStorage(ESAggregation.COLLECTED, ExtStorePosition.SEGMENT_BEGIN), COLLECTED_DOC_HEAD: new ExtStorage(ESAggregation.COLLECTED, ExtStorePosition.DOC_HEAD), COLLECTED_DOC_FOOTER: new ExtStorage(ESAggregation.COLLECTED, ExtStorePosition.DOC_FOOTER), CENTRALIZED_DOC_HEAD: new ExtStorage(ESAggregation.CENTRALIZED, ExtStorePosition.DOC_HEAD), CENTRALIZED_SEGMENT_BEGIN: new ExtStorage(ESAggregation.CENTRALIZED, ExtStorePosition.SEGMENT_BEGIN), CENTRALIZED_DOC_FOOTER: new ExtStorage(ESAggregation.CENTRALIZED, ExtStorePosition.DOC_FOOTER) }); /** * Style or Script Store Definition * @property {string} _identifier; * @property {any} _definition; * @property {any} _additionaly; * @property {ExtStorage} _extStore; */ class SStoreDefinition { constructor(identifier, definition, extStore = null, additions = null) { /** * Usually the name or the selector * @type {string} _identifier; */ this._identifier = identifier; /** * the values * @type {any} _definition; */ this._definition = definition; /** * additional values, if needed. E.g. funciton args * @type {any} _additionaly; */ this._additions = additions; /** * The corresponding extStore * @type {ExtStorage} _extStore; */ this._extStore = extStore; } } /** * Resolves an overwrite case for a map/object. * @param {string} key * @param {Map|Object} container * @param {OverwriteBehaviour} overwriteBehaviour * @returns {string} the key to be used */ function resolveOverwrite(key, container, overwriteBehaviour) { let dealAsMap = container instanceof Map; let occurances = [...( dealAsMap ? container.keys() : Object.keys(container) ) .filter(e => e.includes(key) )].length; switch (overwriteBehaviour) { case OverwriteBehaviour.REPLACE: break; case OverwriteBehaviour.RENAME_OLD: nameForOld = `${key}${occurances}`; if (dealAsMap) { container.set(nameForOld, container.get(key)); container.delete(key); } else { container[nameForOld] = container[key]; delete container[key]; } break; case OverwriteBehaviour.RENAME: default: key = `${key}${occurances}`; break; } return key; } /** * Will resolve the compareKey according to the overwriteBehaviour * and add the newValue to the targetContainer with it. * @param {Object} targetContainer * @param {string} compareKey * @param {Object} newValue * @param {OverwriteBehaviour} overwriteBehaviour * @returns {string} the "resolved" compareKey */ function identifyAndResolveOverwrite(targetContainer, compareKey, newValue, overwriteBehaviour) { let keys = Object.keys(targetContainer); if (keys.includes(compareKey)) { if (overwriteBehaviour === OverwriteBehaviour.DROP_NEW) { console.log("Not Adding, because overwrite is set to DROP_NEW"); return compareKey; } compareKey = resolveOverwrite(compareKey, targetContainer, overwriteBehaviour); } targetContainer[compareKey] = newValue; return compareKey; } /** * Creates a new Script Tag * and then fills the given css rules into it. * @param {Array} ssdArray * @returns {HTMLScriptElement} */ function generateAndFillScriptTag(ssdArray) { let tag = document.createElement("script"); tag.setAttribute("data-compel-gen", "true"); for (let i = 0; i < ssdArray.length; i++) { const ssd = ssdArray[i]; tag.innerText += getScriptTagInjectionText( clearFunctionDeclarationText(ssd._definition), ssd._identifier ); } return tag; } /** * * @param {string} selector * @param {Map} stylingMap * @returns {string} */ function getStylingInjectionText(selector, stylingMap) { function keyValueToString(key) { return `${key}: ${stylingMap[key]}; `; } return `${selector } { ${Object.keys(stylingMap) .map(keyValueToString) .join(" ") } }; `; } /** * * @param {Array} ssdArray * @returns {HTMLStyleElement} */ function generateAndFillStyleTag(ssdArray) { let tag = document.createElement("style"); tag.setAttribute("data-compel-gen", "true"); for (let i = 0; i < ssdArray.length; i++) { const ssd = ssdArray[i]; tag.innerText += getStylingInjectionText(ssd._identifier, ssd._definition); } return tag; } /** * Executes the given function upon the delegating ExtStoreTypes * @param {Function} func * @returns {Map