/**
* 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');