lia-resource.623df6113d7d1.min.js.file

/**
 * Base class for any custom autowired nodes
 *
 */
Autowire = class {

    /**
     *
     * @param node a node to attach to or querySelector to create a new node from an existing template node
     * @param args to pass to onCreate(){} and onAttach(){}
     */
    constructor(node, ...args){
        if (typeof "string" === typeof node){
            node = this.fromTemplate(node);
			//@TODO probably report an error
            if (node == null)return;
        }
        this.readyCalled = false;
        
        this.name = this.constructor.name;
        this.node = node;
        this.n = node;
        this.onCreate(...args);
        for (const extKey of (this.extensions || [])){
            const extClass = Autowire.extensions[extKey];
            if (extClass['onExtAttach']!==null){
                extClass['onExtAttach'].apply(this,args);
            }
        }
        this.constructor.attach(this);
        this.onAttach(...args);
    }

    /**
     * shorthand for this.node.innerHTML;
     * @tag shorthand
     */
    get innerHTML(){
        return this.node.innerHTML;
    }
    /**
     * shorthand for this.node.innerHTML = value;
     * @tag shorthand
     */
    set innerHTML(value){
        this.node.innerHTML = value;
    }
    /**
     * Called before extensions are applied & listeners are setup. this.name & this.node are available
     * Receives any args passed to the class constructor
     * @tag override
     */
    onCreate(){}
    /**
     * Called after the node is completely ready. Other nodes may not be ready yet.
     *
     * @tag override
     */
    onAttach(){}
    /**
     * Called when there are no pending calls to autowire.
     * This does not run if you create an autowire object directly (`new Autowire(node)`).
     * Not sure if it gets called after initial page load... I think so?
     *
     * @todo add onBatchReady(){} <- when autowire('someSelector'); has finished wiring all selected nodes
     * @todo add onPageReady(){} <- after all autowire objects are wired (post page-load)
     *    - for MyObj.autowire('.someNewNode') called AFTER page has already loaded, each MyObj.onPageReady(){} will be called right after onBatchReady(){}
     * @todo add onObjectAdded(object){} <- Maybe only called AFTER pageload...
     * @todo deprecate onReady() in favor of these other ready-methods
     *
     * @tag override
     */
    onReady(){}

    /**
     * run querySelector() on this object's node
     * @return a node
     * @tag shorthand, featured
     */
    q(selector){
        return this.n.querySelector(selector);
    }
    /**
     * run querySelectorAll() on this object's node
     * @return a node
     * @tag shorthand, featured
     */
    qa(selector){
        return this.n.querySelectorAll(selector);
    }
    /**
     * get an object if exists or null and is a child of this node
     * @param objectName The class name
     * @todo allow non-class name
     * @return an autowire object
     * @tag shorthand, featured
     * @todo add get() to do the same thing
     */
    g(objectName){
        return this.ga(objectName)[0] || null;
    }
    /**
     * get objects that are children of this node
     * @param objectName the class name
     * @return an array of autowire objects
     *
     * @todo add a 'query-for-node, but-get-object-back' function
     * @todo add getAll() to do the same thing
     *
     * @tag shorthand, featured
     *
     */
    ga(objectName){
        return this.constructor.getObjectsFromName(objectName,this);
    }
    /**
     * Get an object anywhere in the dom (not necessarily a child)
     * @param objectName the class name
     * @return an autowire object
     * @featured
     */
	getAny(objectName){
		return this.getAnyList(objectName)[0] || null;
	}
    /**
     * Get objects anywhere in the dom (not necessarily children)
     * @param objectName the class name
     * @return an array of autowire objects
     * @featured
     */
	getAnyList(objectName){
		return this.constructor.getObjectsFromName(objectName);
	}

    /**
     * Get a new node object from an existing node. Handles templates & non-templates
     *
     * If non-template: Simply clone the node
     * If template: Clone the template's content node & wrap it in a div if there are multiple root nodes in the template.
     *
     * @param querySelector a query selector to find the template/placeholder node
     *
     * @todo make this method static?
     * @tag setup
     *
     * @return a Node
     */
    fromTemplate(querySelector){
        let node = null;
        const template = document.querySelector(querySelector);
        if (template == null){
            console.error(querySelector + " is not a valid query selector");
            return null;
        }
        
        if (template.tagName.toUpperCase()=='TEMPLATE'){
            if (template.content.children.length>1){
                node = document.createElement('div');
                node.appendChild(template.content.cloneNode(true));
            } else {
                node = template.content.children[0].cloneNode(true);
            }
        } else {
            node = template.cloneNode(true);
        }
        return node;
    }

    /**
     * get text from a web request
     * @param url a url
     * @param params array of paramaters to send with your request
     * @param method the http verb, like POST, GET, PUT, etc
     * @tag fetch, shorthand, featured
     */
    async ft(url, params=[], method="POST"){
        return await this.fetchText(url, params, method);
    }
    /**
     * get json from a web request
     * @see ft()
     * @tag fetch, shorthand, featured
     */
    async fj(url, params=[], method="POST"){
        return await this.fetchJson(url, params, method);
    }
    /**
     * get json from a web request
     * @see ft()
     * @tag fetch, shorthand, featured
     */
    async fetchJson(url, params=[], method="POST"){
        const res = await this.fetch(url,params,method);
        const json = await res.json();
        return json;
    }
    /**
     * get text from a web request
     * @see ft()
     *
     * @tag fetch, shorthand, featured
     */
    async fetchText(url, params=[], method="POST"){
        const res = await this.fetch(url,params,method);
        const text = await res.text();
        return text;
    }
    /**
     * Do a web request 
     *
     * @see ft()
     * @return a response promise. Need to call response.text() or response.json()
     *
     * @tag fetch, internals
     */
    async fetch(url, params, method="POST"){
        var formData = new FormData();
        this.addParamsToFormData(formData, params);
        const result = await fetch(url, {
            method: method, 
            mode: "cors",
            body: formData
        })
        return result;
    }

    addParamsToFormData(formData, params){
        for(var key in params){
            const param = params[key];
            if (typeof param == typeof []){
                key = `${key}[]`;
                for(const val of param){
                    if (typeof [] === typeof val){

                    } else {
                        formData.append(key,val);
                    }
                }
            } else formData.append(key,params[key]);
        }
    }
    /**
     * Bind to a NodeList
     *
     * @see bindTo
     */
    bindToAll(nodeList){
        for (const node of nodeList){
            this.bindTo(node);
        }
    }
    /**
     * Bind `this` to each attribute-listener on node. 
     * So an `onclick` declared in html will have the called class as `this`, rather than the node.
     *
     * @arg node a node or element
     */
    bindTo(node){
        for (const attr of node.attributes){
            if (attr.name.substring(0,2)=='on'){
                node[attr.name] = node[attr.name].bind(this);
            }
        }
    }

    /**
     * Get all objects for the given name
     * @param objectName the class name / constructor name
     * @param parentObj An Autowire object (not a node). Its a reference object to only get children, or null to search full DOM
     *
     * @tag setup
     * @see getAny()
     */
    static getObjectsFromName(objectName=null,parentObject=null){ 
        
        objectName = objectName || this.prototype.constructor.name;
        const all = [];
        for (const item of Autowire.wiredList){
            if (item.obj.constructor.name == objectName){
                all.push(item.obj);
            }
        }
		if (parentObject==null)return all;
		const parentNode = parentObject.node;
		const filtered = [];
		for (const child of all){
			let check = child.node;
			do {
				check = check.parentNode;
				if (check==parentNode){
					filtered.push(child);
				}
			}
			while (check!=document.body&&check!=parentNode);
		}
        return filtered;
    }

    /**
     * Get the autowire object attached to a node
     * @param node a node
     * @return an autowire object or null
     * @tag featured, setup
     */
    static getObjectFromNode(node){
        for (const row of Autowire.wiredList){
            if (row.node===node)return row.obj;
        }
        return null;
    }

    /**
     * Shorthand for `autowire(null, true, ...args);`
     *
     * @tag featured, setup
     */
    static aw(...args){
        this.autowire(null,true, ...args);
    }

    /**
     * @param querySelector query selector or null to use .TheJSClassName
     * @param onPageLoad true to attach after page has loaded. Will attach immediately if page has already loaded
     * @param ...args arguments to pass to your Autowire onCreate(...args) & onAttach(...args)
     *
     * @tag featured, setup
     * @todo return an array of wired nodes (which will only work if page is already loaded)
     */
    static autowire(querySelector=null, onPageLoad=true, ...args) {
        const qs = querySelector || ('.'+this.prototype.constructor.name);
        Autowire.readyCount++;
        if (onPageLoad){
            this.onPageLoad(this.wireSelector,this,qs,true,...args);
        } else {
            this.wireSelector(qs,false,...args);
        }
        
    }

    /**
     * Wire to all select nodes. Actually calls `onReady()` on the target nodes. (whereas `new YourClass(node)` does not)
     * Prefer you call autowire()
     *
     * @param querySelector a query selector
     * @param isPageLoad <- pass false. does nothing
     * @param ...args <- args to pass to your onCreate() & onReady(...args)
     *
     * @todo remove isPageLoad paramater
     * @todo return nodes
     * @tag setup, internals
     */
    static wireSelector(querySelector, isPageLoad=false,...args){
        Autowire.readyCount--;
        let aw = null;
        for (const node of document.querySelectorAll(querySelector)){
            if (this.getObjectFromNode(node)!==null)continue;
            aw = new this(node, ...args);
            Autowire.waitingForReady.push(aw);
        }
        if (Autowire.readyCount==0){
            this.readyUpAllObjects();
        }
    }

    /**
     * call onReady() on all wired nodes that have not previously readied up
     *
     */
    static readyUpAllObjects(){
        const waiting = Autowire.waitingForReady;
        Autowire.waitingForReady = [];
        for (const aw of waiting){
            if (aw.readyCalled)continue;
            aw.readyCalled = true;
            aw.onReady();
        }
    }


    /**
     * Setup event listeners on the attached node.
     * @todo first time a class is created, cache the list of event names to bind to, to save scanning all methods of the class
     */
    static attach(obj){
        Autowire.wiredList.push({'node':obj.node,'obj':obj});

        for (const methodName of this.getMethods(obj)){
            if (typeof obj[methodName] !== 'function'
                ||methodName.substring(0,2)!=='on'
                ||methodName=='onCreate'
                ||methodName=='onAttach'
                ||methodName=='onReady')continue;
            const method = obj[methodName].bind(obj);
            const eventName = methodName.substring(2);
            obj.node.addEventListener(eventName,method);
        }
    }

    /**
     * Run the function on page load, or immediately if page is already loaded
     * @param func a function
     * @param thisArg what `this` should refer to inside `func`
     * @pram ..args to pass to func
     */
    static onPageLoad(func, thisArg, ...args) {
        if (document.readyState == 'interactive' || document.readyState == 'complete') {
            func.apply(thisArg, args);
        } else if (document.addEventListener != null) {
            document.addEventListener("DOMContentLoaded", function () {
                if (document.readyState == "interactive") {
                    func.apply(thisArg, args);
                }
            });
        } else {
            window.onload = function () {
                func.apply(thisArg, args);

            };
        }
    }

    /**
     * Get an array of methods
     */
    static getMethods(object){
        const properties = new Set();
        let currentObj = object;
        do {
            Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
        } while ((currentObj = Object.getPrototypeOf(currentObj)))
        return [...properties.keys()].filter(item => typeof object[item] === 'function')

    }

    /**
     * Make an extension available to use
     *
     * @param extensionKey the name of the extension
     * @param uninstantiatedClassObject the extension class object (not instantiated)
     * @tag extension
     */
    static expose(extensionKey, uninstantiatedClassObject){
        Autowire.extensions[extensionKey] = uninstantiatedClassObject;
    }
    /**
     * Apply the extension to the class
     * @param extensionKey the name of the extension for your class to use
     *
     * @tag extension, faetured
     * @todo add an onExtensionsReady() and/or onExtensionAdded() methods
     * @todo create an "interface" for extensions
     */
    static use(extensionKey){
        const properties = new Set();
        const extClass = Autowire.extensions[extensionKey];
        let currentObj = extClass.prototype;
        do {
            Object.getOwnPropertyNames(currentObj).map(item => properties.add(item))
        } while ((currentObj = Object.getPrototypeOf(currentObj)));
        for (const prop of properties){
            if (this.prototype[prop]==null)
                this.prototype[prop] = extClass.prototype[prop];
        }
        this.prototype.extensions = this.prototype.extensions || [];
        this.prototype.extensions.push(extensionKey);
    }
}
Autowire.readyCount = 0;
Autowire.waitingForReady = [];
Autowire.wiredList = [];
Autowire.extensions = {};


/**
 * A shorthand
 */
Aw = Autowire;

class BranchSelector extends Autowire {

    get popup(){return this.getAny('BranchPopup')}

    onclick(){
        this.popup.show();
    }

}

BranchSelector.autowire('.taeluf-wiki .branch-selector button');

class DialogCancel extends Autowire {
    get popup(){return this.getAny('BranchPopup')}

    onclick(event){
        if (event.target==this.node)this.popup.hide();
    }
}
DialogCancel.autowire('.taeluf-wiki .DialogX');

class BranchPopup extends DialogCancel {

    get branchSelector(){return this.getAny('BranchSelector');}

    __attach(){
        this.firstButton = this.node.querySelector('button:first-of-type');
        this.lastButton = this.node.querySelector('a:last-of-type');
        this.node.querySelector('.BranchDialog').addEventListener('click',
            function(event){

                if (event.target.tagName!='BUTTON'&&event.target.tagName!='A'){
                    this.querySelector('button').focus();
                }
            }
        );
    }

    show(){
        this.node.style.display = 'flex';
        this.node.querySelector('button').focus();
    }
    hide(){
        this.node.style.display = 'none';
        this.branchSelector.node.focus();
    }


    onkeydown(event){
        if (event.keyCode===27){
            this.hide();
        }
        const curButton = document.activeElement;
        const isTab = (event.key === 'Tab');
        const isSpace = (event.code === 'Space' || event.code === 32);
        const isShift = event.shiftKey
        if (isTab && isShift && curButton == this.firstButton){
            this.lastButton.focus();
            event.preventDefault();
            event.stopPropagation();
        } else if (isTab && !isShift && curButton == this.lastButton){
            this.firstButton.focus();
            event.preventDefault();
            event.stopPropagation();
        } else if (isSpace && (curButton.tagName.toUpperCase()=='A'||curButton.tagName.toUpperCase()=='BUTTON')){
            // curButton.click();
            event.preventDefault();
            event.stopPropagation();
        }
    }

    onkeyup(event){
        const isSpace = (event.code === 'Space' || event.code === 32);
        const curButton = document.activeElement;
        if (isSpace && (curButton.tagName.toUpperCase()=='A'||curButton.tagName.toUpperCase()=='BUTTON')){
            curButton.click();
            event.preventDefault();
            event.stopPropagation();
        }
    }
}
BranchPopup.autowire('.taeluf-wiki .BranchPopup');