UNPKG

@schukai/monster

Version:

Monster is a simple library for creating fast, robust and lightweight websites.

251 lines (215 loc) 6.47 kB
/** * Copyright © schukai GmbH 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 schukai GmbH. * * 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 schukai GmbH * @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; }