@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
251 lines (215 loc) • 6.48 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { internalStateSymbol, internalSymbol } from "../constants.mjs";
import { extend } from "../data/extend.mjs";
import { BaseWithOptions } from "../types/basewithoptions.mjs";
import { getGlobalObject } from "../types/global.mjs";
import { ID } from "../types/id.mjs";
import { isString } from "../types/is.mjs";
import { Observer } from "../types/observer.mjs";
import { ProxyObserver } from "../types/proxyobserver.mjs";
import {
ATTRIBUTE_CLASS,
ATTRIBUTE_ID,
ATTRIBUTE_TITLE,
} from "./constants.mjs";
import { instanceSymbol } from "../constants.mjs";
export { Resource, KEY_DOCUMENT, KEY_QUERY, referenceSymbol };
/**
* @private
* @type {string}
*/
const KEY_DOCUMENT = "document";
/**
* @private
* @type {string}
*/
const KEY_QUERY = "query";
/**
* @private
* @type {string}
*/
const KEY_TIMEOUT = "timeout";
/**
* @private
* @type {symbol}
*/
const referenceSymbol = Symbol("reference");
/**
* This class is the base class for all resources to be loaded.
*
* @license AGPLv3
* @since 1.25.0
* @copyright Volker Schukai
* @summary A Resource class
*/
class Resource extends BaseWithOptions {
/**
*
* @param {Object|undefined} options
*/
constructor(options) {
super(options);
let uri = this.getOption(this.constructor.getURLAttribute());
if (uri === undefined) {
throw new Error("missing source");
} else if (uri instanceof URL) {
uri = uri.toString();
} else if (!isString(uri)) {
throw new Error("unsupported url type");
}
this[internalSymbol][this.constructor.getURLAttribute()] = uri;
this[internalStateSymbol] = new ProxyObserver({
loaded: false,
error: undefined,
});
this[referenceSymbol] = undefined;
}
/**
* @return {boolean}
*/
isConnected() {
if (this[referenceSymbol] instanceof HTMLElement) {
return this[referenceSymbol].isConnected;
}
return false;
}
/**
* This method is overridden by the special classes and creates the DOM object.
* This method is also called implicitly, if not yet done explicitly, by calling `connect()`.
*
* @throws {Error} this method must be implemented by derived classes
* @return {Monster.DOM.Resource}
*/
create() {
throw new Error("this method must be implemented by derived classes");
}
/**
* This method appends the HTMLElement to the specified document.
* If the element has not yet been created, `create()` is called implicitly.
*
* throws {Error} target not found
* @return {Monster.DOM.Resource}
*/
connect() {
if (!(this[referenceSymbol] instanceof HTMLElement)) {
this.create();
}
appendToDocument.call(this);
return this;
}
/**
* @property {Document} document the document object into which the node is to be appended
* @property {string} src/href url to the corresponding resource
* @property {string} query defines the location where the resource is to be hooked into the dom.
* @property {string} id element attribute id
* @property {string} title element attribute title
* @property {string} class element attribute class
* @property {int} timeout timeout
*/
get defaults() {
return extend({}, super.defaults, {
[this.constructor.getURLAttribute()]: undefined,
[KEY_DOCUMENT]: getGlobalObject("document"),
[KEY_QUERY]: "head",
[KEY_TIMEOUT]: 10000,
[ATTRIBUTE_ID]: new ID("resource").toString(),
[ATTRIBUTE_CLASS]: undefined,
[ATTRIBUTE_TITLE]: undefined,
});
}
/**
* With `available()` you can check if a resource is available.
* This is the case when the tag is included and the resource is loaded.
*
* @return {Promise}
*/
available() {
const self = this;
if (!(self[referenceSymbol] instanceof HTMLElement)) {
return Promise.reject("no element");
}
if (!self.isConnected()) {
return Promise.reject("element not connected");
}
if (self[internalStateSymbol].getSubject()["loaded"] === true) {
if (self[internalStateSymbol].getSubject()["error"] !== undefined) {
return Promise.reject(self[internalStateSymbol].getSubject()["error"]);
}
return Promise.resolve();
}
return new Promise(function (resolve, reject) {
const timeout = setTimeout(() => {
reject("timeout");
}, self.getOption("timeout"));
const observer = new Observer(() => {
clearTimeout(timeout);
self[internalStateSymbol].detachObserver(observer);
resolve();
});
self[internalStateSymbol].attachObserver(observer);
});
}
/**
* @return {string}
*/
static getURLAttribute() {
throw new Error("this method must be implemented by derived classes");
}
/**
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for("@schukai/monster/dom/resource");
}
}
/**
* @private
* @return {Promise}
* throws {Error} target not found
*/
function appendToDocument() {
const targetNode = document.querySelector(this.getOption(KEY_QUERY, "head"));
if (!(targetNode instanceof HTMLElement)) {
throw new Error("target not found");
}
addEvents.call(this);
targetNode.appendChild(this[referenceSymbol]);
return this;
}
/**
* @private
* @return {addEvents}
*/
function addEvents() {
const onError = () => {
this[referenceSymbol].removeEventListener("error", onError);
this[referenceSymbol].removeEventListener("load", onLoad);
this[internalStateSymbol].setSubject({
loaded: true,
error: `${
this[referenceSymbol][this.constructor.getURLAttribute()]
} is not available`,
});
return;
};
const onLoad = () => {
this[referenceSymbol].removeEventListener("error", onError);
this[referenceSymbol].removeEventListener("load", onLoad);
this[internalStateSymbol].getSubject()["loaded"] = true;
};
this[referenceSymbol].addEventListener("load", onLoad, false);
this[referenceSymbol].addEventListener("error", onError, false);
return this;
}