/**
* Fragment
* The base class for all code fragments in Codestrates
*
* Copyright 2020, 2021 Rolf Bagge, Janus B. Kristensen, CAVI,
* Center for Advanced Visualization and Interaction, Aarhus University
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/* global cQuery, webstrate, HTMLElement, NodeList, UUIDGenerator, Observer, WPMv2, Text, DIFF_INSERT, DIFF_DELETE, wpm */
const dmp = new diff_match_patch();
let legacyWarningCounter = 0;
/**
* @namespace Fragments
*/
/**
* Codestrate Fragment representing a <code-fragment></code-fragment> tag in codestrates.
* @abstract
* @hideconstructor
* @memberof Fragments
*/
class Fragment {
get html(){
if (legacyWarningCounter<3){
try {
throw new Error("Legacy read access of private field Fragment.html");
} catch (ex){
console.info("Reading cQuery object from private field .html directly on Fragment will break in the future, use .element to get the DOMElement directly instead. Only the first 3 warnings are shown, printing a stack trace...",ex);
}
}
legacyWarningCounter++;
return cQuery(this.element);
}
set html(cQueryHTML){
try {
throw new Error("Legacy write access to private field Fragment.html");
} catch (ex){
console.warn("Writing a cQuery object to private field .html directly on Fragment will not be possible in the future, printing a stack trace...",ex);
}
this.element = cQueryHTML[0];
}
/**
* Create a new fragment using the given cQuery object as its base.
* @param {cQuery} cQueryHTMl - The cQuery object to use as base.
*/
constructor(cQueryHTMl) {
let self = this;
// Legacy cQuery html element
cQueryHTMl.data("Fragment", this); // Legacy
this.element = cQueryHTMl[0];
this.element.fragment = this;
this.textInsertedCallbacks = [];
this.textDeletedCallbacks = [];
this.fragmentChangedCallbacks = [];
this.fragmentUnloadedCallbacks = [];
this.fragmentClassChangedCallbacks = [];
this.fragmentAutoChangedCallbacks = [];
this.uuid = UUIDGenerator.generateUUID("fragment-");
//Setup autodom and make it able to wait until it is complete.
this.autoDomDirty = true;
this.autoDomReady = false;
this.setupAutoDomHandling();
this.element.setAttribute("transient-fragment-uuid", this.uuid);
this.setupObservers();
this.startObserver();
}
/**
* Get the node that holds the text content of this fragment
* @protected
* @ignore
* @returns {Text}
*/
getTextContentNode() {
let textContentNode = this.element;
this.checkTextContentNode(textContentNode);
return textContentNode;
}
/**
* Checks the given node for its feasibility of being the textContentNode
* @protected
* @ignore
* @param {Text} textContentNode - The text node to check
*/
checkTextContentNode(textContentNode) {
if(textContentNode.childNodes.length > 1) {
console.warn("More than 1 childnode...", textContentNode.childNodes);
}
}
/**
* Triggers all callback handlers for the given type insert/delete
* @private
* @param {number} pos - The position the event happened
* @param {string} val - The value of the event
* @param {insert|delete} type - The type of the event
*/
insertDeleteCallback(pos, val, type) {
switch (type) {
case "insert":
{
this.textInsertedCallbacks.forEach((callback) => {
callback(pos, val);
});
break;
}
case "delete":
{
this.textDeletedCallbacks.forEach((callback) => {
callback(pos, val);
});
break;
}
}
}
/**
* Handle the given mutations
* @private
* @param {Mutation[]} mutations - The mutations to handle
*/
mutationCallback(mutations) {
let self = this;
let characterDataTargets = [];
let sendUpdateCallback = false;
mutations.forEach((mutation) => {
if(mutation.type === "attributes") {
sendUpdateCallback = true;
}
if (mutation.type === "attributes" && mutation.attributeName === "auto" && mutation.target === self.element) {
//If auto attribute changed, trigger onAutoChanged
self.onAutoChanged(self.auto);
sendUpdateCallback = false;
} else if (mutation.type === "attributes" && mutation.attributeName === "class" && mutation.target === self.element) {
//If auto attribute changed, trigger onAutoChanged
self.onClassChanged(this.element.classList);
sendUpdateCallback = false;
} else if(mutation.type === "characterData") {
if(mutation.target.characterDataAlreadyHandled) {
return;
}
sendUpdateCallback = true;
characterDataTargets.push(mutation.target);
let newValue = mutation.target.nodeValue;
let oldValue = mutation.oldValue;
mutation.target.characterDataAlreadyHandled = true;
//If characterData mutation, generate insert/delete ops
let patches = dmp.patch_make(oldValue, newValue);
Array.from(patches).forEach((patch)=>{
let offset = patch.start1;
patch.diffs.forEach((diff) => {
let type = diff[0];
let value = diff[1];
switch(type) {
case DIFF_INSERT:
self.insertDeleteCallback(offset, value, "insert");
offset += value.length;
break;
case DIFF_DELETE:
self.insertDeleteCallback(offset, value, "delete");
break;
case DIFF_EQUAL:
offset += value.length;
break;
}
});
});
} else if(mutation.type === "childList") {
sendUpdateCallback = true;
}
});
characterDataTargets.forEach((target)=>{
target.characterDataAlreadyHandled = false;
});
//Only send
if(this.element.parentNode != null && sendUpdateCallback) {
//Dont do changed callbacks if we are not in the dom?
this.triggerFragmentChanged(this);
}
}
/**
* @private
* @param context
*/
triggerFragmentChanged(context) {
this.fragmentChangedCallbacks.slice().forEach((callback) => {
try {
callback(context);
} catch(e) {
console.group("Error: "+e);
console.log("Callback:", callback);
console.log("Context:", context);
console.groupCollapsed("Trace");
console.trace();
console.groupEnd();
console.groupEnd();
}
});
}
/**
* Sets up the mutation observer for this fragment
* @ignore
* @protected
*/
setupObservers() {
let self = this;
this.mutationHandler = (mutations) => {
self.mutationCallback(mutations);
};
this.observer = new MutationObserver(this.mutationHandler);
}
/**
* Starts this fragments mutation observer
* @ignore
* @protected
*/
startObserver() {
if(this.observer == null) {
return;
}
this.observer.observe(this.element, {
attributes: true,
childList: true,
subtree: true,
characterData: true,
characterDataOldValue: true
});
}
/**
* Stops this fragments mutation observer, handling any mutations that is queued before stopping.
* @ignore
* @protected
*/
stopObserver() {
if(this.observer == null) {
return;
}
let mutations = this.observer.takeRecords();
if (mutations.length > 0) {
this.mutationCallback(mutations);
}
this.observer.disconnect();
}
/**
* Run the given method without triggering the mutation observer on this fragment, then trigger fragment changed callbacks with the given context
* @ignore
* @protected
* @param {Function} method - Method to call. Important: cannot be async or return a promise, the observer will be restarted as soon as this method returns.
* @param {Object} context - Context to pass along to the callbacks
*/
executeObserverless(method, context, skipChangeCheck=false) {
this.stopObserver();
let before = null;
if(!skipChangeCheck) {
before = this.raw;
}
//Run our method, potentially adding mutations
method();
this.startObserver();
if(skipChangeCheck || before !== this.raw) {
this.triggerFragmentChanged(context);
}
}
/**
* The raw representation of this fragment, can be used to set/get the raw value.
*
* @example
* //Get the raw value of a fragment
* let fragmentValue = myFragment.raw;
*
* @example
* //Set the raw value of a fragment
* myFragment.raw = myNewFragmentValue;
*
* @type {string}
*/
get raw() {
if(this.getTextContentNode().firstChild instanceof Text) {
return this.getTextContentNode().firstChild.nodeValue;
} else {
return this.getTextContentNode().textContent;
}
}
set raw(content) {
if(this.getTextContentNode().firstChild instanceof Text) {
this.getTextContentNode().firstChild.nodeValue = content;
} else {
this.getTextContentNode().textContent = content;
}
}
/**
* The auto attribute of this fragment, toggles automatic behaviour on/off
* @type {boolean}
*/
get auto() {
return this.element.hasAttribute("auto");
}
set auto(auto) {
if (auto) {
this.element.setAttribute("auto", "");
} else {
this.element.removeAttribute("auto");
}
}
/**
* @callback Fragments.Fragment~fragmentChangedCallback
* @param {Fragment|Object} context - The context that called the callback
*/
/**
* Register a callback to be run when this fragments content changes.
*
* @example
* Fragment.one("#myFragment").registerOnFragmentChangedHandler((context)=>{
* //Fragment has changed
* });
*
* @param {Fragments.Fragment~fragmentChangedCallback} callback - The callback that is run when fragment content changes
*/
registerOnFragmentChangedHandler(callback) {
let self = this;
this.fragmentChangedCallbacks.push(callback);
return {
delete: ()=>{
self.unRegisterOnFragmentChangedHandler(callback);
}
}
}
/**
* Unregister a callback handler
* @param {Fragments.Fragment~fragmentChangedCallback} callback - The callback to unregister
*/
unRegisterOnFragmentChangedHandler(callback) {
this.fragmentChangedCallbacks.splice(this.fragmentChangedCallbacks.indexOf(callback), 1);
}
/**
* @callback Fragments.Fragment~fragmentUnloadedCallback
* @param {Fragments.Fragment} fragment - The fragment that called the callback
*/
/**
* Register a callback to run when this fragment is unloaded
*
* @example
* Fragment.one("#myFragment").registerOnFragmentUnloadedHandler(()=>{
* //Fragment is unloaded
* });
*
* @param {Fragments.Fragment~fragmentUnloadedCallback} callback - The callback to run when the fragment is unloaded
*/
registerOnFragmentUnloadedHandler(callback) {
let self = this;
this.fragmentUnloadedCallbacks.push(callback);
return {
delete: ()=>{
self.unRegisterOnFragmentUnloadedHandler(callback);
}
}
}
/**
* Unregister a callback handler
* @param {Fragments.Fragment~fragmentUnloadedCallback} callback - The callback to unregister
*/
unRegisterOnFragmentUnloadedHandler(callback) {
this.fragmentUnloadedCallbacks.splice(this.fragmentUnloadedCallbacks.indexOf(callback), 1);
}
/**
* @callback Fragments.Fragment~autoChangedCallback
* @param {Fragments.Fragment} fragment - The fragment
* @param {boolean} auto - The new value of auto
*/
/**
* Register a callback to run when auto attribute changes on fragment
*
* @example
* Fragment.one("#myFragment").registerOnAutoChangedHandler((fragment, auto)=>{
* //Fragment auto attribute changed
* });
*
* @param {Fragments.Fragment~autoChangedCallback} callback - The callback to run when auto changes
*/
registerOnAutoChangedHandler(callback) {
let self = this;
this.fragmentAutoChangedCallbacks.push(callback);
return {
delete: ()=>{
self.unRegisterOnAutoChangedHandler(callback);
}
}
}
/**
* Unregister a callback handler
* @param {Fragments.Fragment~autoChangedCallback} callback - The callback to unregister
*/
unRegisterOnAutoChangedHandler(callback) {
this.fragmentAutoChangedCallbacks.splice(this.fragmentAutoChangedCallbacks.indexOf(callback), 1);
}
/**
* @callback Fragments.Fragment~textInsertedCallback
* @param {number} position - The position where the text was inserted
* @param {string} value - The value of inserted text
*/
/**
* Register a callback to run when text is inserted into this fragment
*
* @example
* Fragment.one("#myFragment").registerOnTextInsertedHandler((position, value)=>{
* //Text "value" has been inserted into this fragment at "position"
* });
*
* @param {Fragments.Fragment~textInsertedCallback} callback - The callback to run when text is inserted
*/
registerOnTextInsertedHandler(callback) {
let self = this;
this.textInsertedCallbacks.push(callback);
return {
delete: ()=>{
self.textInsertedCallbacks.splice(self.textInsertedCallbacks.indexOf(callback), 1);
}
}
}
/**
* @callback Fragments.Fragment~textDeletedCallback
* @param {number} position - The position where the text was deleted
* @param {string} value - The value of deleted text
*/
/**
* Register a callback to run when text is deleted from this fragment
*
* @example
* Fragment.one("#myFragment").registerOnTextDeletedHandler((position, value)=>{
* //Text "value" has been deleted from this fragment at "position"
* });
*
* @param {Fragments.Fragment~textDeletedCallback} callback - The callback to run when text is deleted
*/
registerOnTextDeletedHandler(callback) {
let self = this;
this.textDeletedCallbacks.push(callback);
return {
delete: ()=>{
self.textDeletedCallbacks.splice(self.textDeletedCallbacks.indexOf(callback), 1);
}
}
}
/**
* @callback Fragments.Fragment~fragmentClassChangedCallback
* @param {string[]} classes - The classes that changed
*/
/**
* Register a callback to run when the classes of this fragment changes
*
* @example
* Fragment.one("#myFragment").registerOnClassChangedHandler((classes)=>{
* //Some classes changed on this fragment
* });
*
* @param {Fragments.Fragment~fragmentClassChangedCallback} callback - The callback to run when classes change on the fragment
*/
registerOnClassChangedHandler(callback) {
let self = this;
this.fragmentClassChangedCallbacks.push(callback);
return {
delete: ()=>{
self.fragmentClassChangedCallbacks.splice(self.fragmentClassChangedCallbacks.indexOf(callback), 1);
}
}
}
/**
* Handles when classes change on the fragment
* @private
* @param classes
*/
onClassChanged(classes) {
this.fragmentClassChangedCallbacks.slice().forEach((cb)=>{
cb(classes);
});
}
/**
* The type of this fragment
* @type {string}
* @readonly
*/
get type() {
return this.element.getAttribute("data-type");
}
/**
* Require this fragment and return the result. What require does depends on what type of fragment it is.
* @example
* let result = await Fragment.one("#myFragment").require()
*
* @abstract
* @param {json} [options] - The options to pass to require
* @returns {*} result of the require action
*/
async require(options = {}) {
//Ovewritten in subclass
}
/**
* Tell this fragment to unload itself
* @example
* Fragment.one("#myFragment").unload();
*/
unload() {
let self = this;
this.fragmentUnloadedCallbacks.slice().forEach((callback)=>{
callback(self);
});
if(this.supportsAutoDom()) {
this.clearAutoDom();
}
this.stopObserver();
}
/**
* Called when all fragments are loaded
* @private
*/
async onFragmentsLoaded() {
if (this.auto && !Fragment.disableAutorun) {
await this.insertAutoDom();
}
}
/**
* @private
* @returns {boolean} - True/False depending on if this fragments supports automatic behaviour
*/
supportsAuto() {
return this.supportsAutoDom();
}
/**
* @private
* @returns {boolean} True/False depending on if this fragment supports automatic dom insertion
*/
supportsAutoDom() {
//Override in subclass
return false;
}
/**
* Setup handling of automatic dom insertion
* @private
* @returns {Promise<void>}
*/
setupAutoDomHandling() {
if(!this.supportsAutoDom()) {
return;
}
let self = this;
this.registerOnFragmentChangedHandler((context) => {
self.autoDomDirty = true;
if (self.auto && !Fragment.disableAutorun) {
self.insertAutoDom();
}
});
}
/**
* Called when auto attribute is changed on this fragment
* @private
* @param {boolean} auto - The new state of auto
*/
onAutoChanged(auto) {
this.fragmentAutoChangedCallbacks.forEach((cb)=>{
cb(this, auto);
});
if(!this.supportsAutoDom()) {
return;
}
if (auto && !Fragment.disableAutorun) {
this.insertAutoDom();
} else {
this.clearAutoDom();
}
}
/**
* Create an instance of the automatic dom
* @private
* @returns {Promise<*>}
*/
async createAutoDom() {
if(!this.supportsAutoDom()) {
return;
}
try {
return await this.require();
} catch(e) {
return null;
}
}
/**
* Ask this fragment to insert its automatic dom (regardless of Fragment.disableAutorun)
* @ignore
* @returns {Promise<void>} - Promise that resolves when the automatic dom is inserted into the document
*/
insertAutoDom() {
let self = this;
if(!this.supportsAutoDom()) {
return;
}
if(!this.autoDomDirty) {
console.debug("Not inserting autoDom, as it is already present and not flagged dirty:", this.uuid);
return new Promise(async (resolve)=>{
while(!self.autoDomReady) {
await new Promise((timeoutPromiseResolve)=>{
setTimeout(()=>{
timeoutPromiseResolve();
}, 10);
});
}
resolve();
});
}
this.autoDomDirty = false;
this.autoDomReady = false;
return new Promise(async (resolve, reject)=>{
try {
let autoDomContent = await this.createAutoDom();
let oldTransient = cQuery("transient.autoDom#" + self.uuid);
if(oldTransient.length > 0) {
oldTransient[0].setAttribute("class", "autoDom");
//Fix missing classes
self.element.classList.forEach((c)=>{
if (!oldTransient[0].classList.contains(c)) oldTransient[0].classList.add(c);
});
try {
diff.innerHTML(oldTransient[0], autoDomContent, { parser: { strict: true } });
} catch (ex){
console.error("Failed to perform autoDOM diffing", ex);
diff.release(oldTransient[0]); // Reset state trackers since the patch was not applied
}
function cssPath(element, path= []) {
if(element.parentNode == null) {
// Document fragment is the top node
return path.reverse().join(" > ");
}
const parent = element.parentNode;
const childIndex = Array.from(parent.children).indexOf(element) + 1;
path.push(element.nodeName.toLowerCase()+":nth-child("+childIndex+")");
return cssPath(parent, path);
}
// Update innerHTML for each template, as this is not part of the dom, and would not be updated otherwise
cQuery(autoDomContent).find("template").forEach((template)=>{
let path = cssPath(template);
oldTransient[0].querySelector(path).innerHTML = autoDomContent.querySelector(path).innerHTML;
});
} else {
let transient = cQuery("<transient></transient>");
transient[0].setAttribute("id", self.uuid);
transient.addClass("autoDom");
self.element.classList.forEach((c)=>{
transient.addClass(c);
});
if (autoDomContent != null && autoDomContent !== "") {
transient.append(autoDomContent);
}
self.element.parentNode.insertBefore(transient[0], self.element.nextSibling);
}
self.autoDomReady = true;
resolve();
} catch(e) {
console.warn("Unable to insertAutoDom: ", e);
self.autoDomDirty = true;
reject();
}
});
}
/**
* Clear this fragments automatic dom from the document
* @ignore
*/
clearAutoDom() {
if(!this.supportsAutoDom()) {
return;
}
cQuery("transient.autoDom#" + this.uuid).remove();
this.autoDomDirty = true;
this.autoDomReady = false;
}
/**
* Returns whether this fragment supports the run flag
* @private
* @returns {boolean}
*/
supportsRun() {
return false;
}
/**
* Returns a dompath for finding this fragment
*/
getDomPath() {
let child = this.element;
let parent = this.element.parentNode;
let domPath = [];
while(parent.parentNode != null) {
let children = Array.from(parent.childNodes);
let childIndex = children.indexOf(child);
domPath.push({
parent: parent.tagName,
childIndex: childIndex
});
child = parent;
parent = parent.parentNode;
}
domPath.reverse()
return domPath;
}
static findFromDomPath(domPath) {
let currentParent = document.querySelector(domPath[0].parent);
for(let dp of domPath) {
if(currentParent.tagName !== dp.parent) {
throw new Error("DomPath invalid, should have seen "+dp.parent+" saw "+currentParent.tagName);
}
currentParent = Array.from(currentParent.childNodes)[dp.childIndex];
}
return currentParent;
}
/**
* Create a fragment of the given type.
*
* If no Fragment is registered for the given type, null is returned.
*
* @example
* let myJSFragment = Fragment.create("text/javascript");
*
* @param {string} type the type of fragment to create
* @returns {Fragments.Fragment} the created fragment, or null
*/
static create(type) {
if (!Fragment.fragmentTypes.has(type)) {
console.error("Creating fragment of unregistered type:", type);
return null;
}
let fragmentDom = cQuery("<code-fragment data-type='" + type + "'></code-fragment>");
Fragment.setupFragment(fragmentDom);
return fragmentDom[0].fragment;
}
/**
* Registers a new fragment type
* @ignore
* @param {string} fragmentClass the fragment type to register
*/
static registerFragmentType(fragmentClass) {
if (Fragment.fragmentTypes.has(fragmentClass.type())) {
console.error("Already have registered fragment type:", fragmentClass.type());
return;
}
Fragment.fragmentTypes.set(fragmentClass.type(), fragmentClass);
return Fragment.loadUnknownFragments(fragmentClass.type());
}
/**
* Unregisters a fragment type, this also triggers unload on all fragments of this type that is currently loaded
* @ignore
* @param {string} fragmentClass the fragment type to unregister
*/
static unRegisterFragmentType(fragmentClass) {
Fragment.fragmentTypes.delete(fragmentClass.type());
// Remove and reinsert all fragments of this type as unknown
document.querySelectorAll("code-fragment[data-type='" + fragmentClass.type() + "']").forEach((fragmentElement) => {
fragmentElement.fragment?.unload();
Fragment.saveUnknownFragment(fragmentElement, fragmentClass.type());
});
}
/**
* Sets up the given fragment
* @private
* @param {cQuery} fragment - the fragment to set up
*/
static setupFragment(fragment) {
if (fragment[0].fragment != null) return; //Already setup as fragment
let fragmentType = fragment[0].getAttribute("data-type");
if (!Fragment.fragmentTypes.has(fragmentType)) {
//Unknown fragment type
if (fragmentType != null) {
Fragment.saveUnknownFragment(fragment, fragmentType);
}
return null;
}
let fragmentClass = Fragment.fragmentTypes.get(fragmentType);
return new fragmentClass(fragment);
}
/**
* Tries to setup all current and future fragments on the DOM
* @ignore
*/
static async setupFragments() {
let foundFragments = [];
//Check fragments already in DOM
cQuery("code-fragment").forEach((fragmentDom) => {
fragmentDom = cQuery(fragmentDom);
let fragment = Fragment.setupFragment(fragmentDom);
if (fragment !== null) {
foundFragments.push(fragment);
}
});
await Fragment.runFragmentsLoaded();
//Observe newly added fragments, and deleted fragments
let observer = new MutationObserver(async (mutations) => {
let foundFragments = [];
mutations.forEach((mutation) => {
Array.from(mutation.addedNodes).forEach((node) => {
node = cQuery(node);
if (node.is("code-fragment")) {
let fragment = Fragment.setupFragment(node);
if(fragment != null) {
foundFragments.push(fragment);
}
} else {
if (node[0].querySelector != null) {
node.find("code-fragment").forEach((child) => {
child = cQuery(child);
let fragment = Fragment.setupFragment(child);
if(fragment != null) {
foundFragments.push(fragment);
}
});
}
}
});
Array.from(mutation.removedNodes).forEach((node) => {
if(node.matches != null && node.matches("code-fragment")) {
node.fragment?.unload();
} else if(node.querySelector != null) {
node.querySelectorAll("code-fragment").forEach((child)=>{
child.fragment?.unload();
});
}
});
});
await Fragment.runFragmentsLoaded();
});
observer.observe(document, {
attributes: false,
subtree: true,
childList: true
});
}
/**
* Loads all currently unloaded fragments
* @ignore
* @returns {Promise<void>} - Promise that resolves when all unloaded fragments are done loading
*/
static async runFragmentsLoaded() {
if(!Fragment.allInstalledRun) {
return;
}
//Check if currently loading
while (Fragment.currentlyLoadingFragments) {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 0);
});
}
Fragment.currentlyLoadingFragments = true;
let unloadedFragments = Fragment.find("code-fragment").filter((fragment)=>{
let isLoaded = fragment.isLoaded;
fragment.isLoaded = true;
return !isLoaded;
});
for(let fragment of unloadedFragments) {
fragment.isLoaded = true;
await fragment.onFragmentsLoaded();
}
Fragment.currentlyLoadingFragments = false;
}
/**
* Saves the given fragment for later loading when its no longer unknown
* @private
* @param {cQuery} fragment - the fragment to save
* @param {string} type - the type of the fragment
*/
static saveUnknownFragment(fragment, type) {
//Unpack cQuery/jQuery objects, we want unique check of Set to work.
if (fragment[0] != null) {
fragment = fragment[0];
}
let fragmentsOfType = Fragment.unknownFragments.get(type);
if (fragmentsOfType == null) {
fragmentsOfType = new Set();
Fragment.unknownFragments.set(type, fragmentsOfType);
}
fragmentsOfType.add(fragment);
}
/**
* Loads all unknown fragments of a given type
* @private
* @param {type} type the fragment type to load
*/
static loadUnknownFragments(type) {
let unknownFragments = Fragment.unknownFragments.get(type);
if (unknownFragments != null) {
let unknownFragmentsCopy = Array.from(unknownFragments);
//Clear the Set, fragments will be readded if not handled
unknownFragments.clear();
unknownFragmentsCopy.forEach((fragment) => {
let frag = Fragment.setupFragment(cQuery(fragment));
});
return Fragment.runFragmentsLoaded();
}
return Promise.resolve();
}
/**
* Returns the first fragment that is found from the given query
*
* This is the equivalent of taking the first result of Fragment.find(query)
*
* @example
* let myFragment = Fragment.one("#myFragment");
*
* @param {string|cQuery|Array|Node} query - The query used to find fragments. Can be a css selector, a cQuery object, a dom element or an array of dom elements.
* @returns {Fragments.Fragment} - the found fragment, or null if none could be found
*/
static one(query) {
let fragments = Fragment.find(query);
if (fragments.length > 0) {
return fragments[0];
}
return null;
}
/**
* Finds all fragments based on a given query
*
* @example
* let fragments = Fragment.find(".someClass");
*
* @param {string|cQuery|Array|Node} query - The query used to find fragments. Can be a css selector, a cQuery object, a dom element or an array of dom elements.
* @returns {Fragments.Fragment[]} The found fragments
*/
static find(query) {
let fragments = [];
if (query != null) {
if (typeof query === "string") {
cQuery(query).forEach((result) => {
let fragment = result.fragment;
if (fragment != null) {
fragments.push(fragment);
}
});
} else if (Array.isArray(query) || query instanceof Array) {
query.forEach((item) => {
fragments = fragments.concat(Fragment.find(item));
});
} else if (typeof query === "object") {
if (query instanceof Fragment) {
fragments.push(query);
} else if (query instanceof HTMLElement) {
let fragment = query.fragment;
if (fragment != null) {
fragments.push(fragment);
}
} else if (query instanceof NodeList) {
fragments = fragments.concat(Fragment.find(Array.from(query)));
}
}
}
return fragments;
}
static fromFragmentUUID(uuid) {
let node = document.querySelector("code-fragment[data-uuid='"+uuid.replace("_", "-")+"']");
return node?.fragment;
}
static addAllFragmentsLoadedCallback(callback) {
if (Fragment.initialLoadComplete){
callback(); // callbacks added late are called immedaitely
} else {
Fragment.allFragmentsLoadedCallbacks.push(callback);
}
}
}; window.Fragment = Fragment;
Fragment.allInstalledRun = false;
Fragment.initialLoadComplete = false;
Fragment.fragmentTypes = new Map();
Fragment.unknownFragments = new Map();
Fragment.disableAutorun = false;
Fragment.currentlyLoadingFragments = false;
Fragment.allFragmentsLoadedCallbacks = [];
const urlParams = new URLSearchParams(location.search);
Fragment.disableAutorun = (urlParams.get("codestrates") === "false");
if(window.disableCodestratesFragmentsAutorun === true) {
Fragment.disableAutorun = true;
}
Fragment.setupFragments();
wpm.onAllInstalled(()=>{
Fragment.allInstalledRun = true;
Fragment.runFragmentsLoaded().then(()=>{
Fragment.initialLoadComplete = true;
Fragment.allFragmentsLoadedCallbacks.forEach((callback)=>{
callback();
});
});
});