You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

653 lines
20 KiB

/**
* 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<any>} 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<ExtStorage, Array<SStoreDefinition>, 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<SStoreDefinition>} 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<string,string>} stylingMap
* @returns {string}
*/
function getStylingInjectionText(selector, stylingMap) {
function keyValueToString(key) {
return `${key}: ${stylingMap[key]}; `;
}
return `${selector
} { ${Object.keys(stylingMap)
.map(keyValueToString)
.join(" ")
} }; `;
}
/**
*
* @param {Array<SStoreDefinition>} 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<ExtStoreType, *}
*/
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) }
]);
}