From f6c0ddf45ca10d49a572649f1fedc738e734f409 Mon Sep 17 00:00:00 2001 From: Felix Hamann Date: Sat, 25 May 2019 18:45:10 +0200 Subject: [PATCH] port js services to class syntax and move to static/es --- static/es/services/htmlHelpers.js | 43 ++++++++++ static/es/services/httpClient.js | 58 ++++++++++++++ static/es/services/i18n.js | 34 ++++++++ static/es/services/utilRegistry.js | 121 +++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+) create mode 100644 static/es/services/htmlHelpers.js create mode 100644 static/es/services/httpClient.js create mode 100644 static/es/services/i18n.js create mode 100644 static/es/services/utilRegistry.js diff --git a/static/es/services/htmlHelpers.js b/static/es/services/htmlHelpers.js new file mode 100644 index 000000000..60b9f1471 --- /dev/null +++ b/static/es/services/htmlHelpers.js @@ -0,0 +1,43 @@ +class HtmlHelpers { + + // `parseResponse` takes a raw HttpClient response and an options object. + // Returns an object with `element` being an contextual fragment of the + // HTML in the response and `ifPrefix` being the prefix that was used to + // "unique-ify" the ids of the received HTML. + // Original Response IDs can optionally be kept by adding `keepIds: true` + // to the `options` object. + parseResponse(response, options = {}) { + return response.text() + .then( + (responseText) => { + const docFrag = document.createRange().createContextualFragment(responseText); + let idPrefix; + if (!options.keepIds) { + idPrefix = this._getIdPrefix(); + this._prefixIds(docFrag, idPrefix); + } + return Promise.resolve({ idPrefix, element: docFrag }); + }, + Promise.reject, + ).catch(console.error); + } + + _prefixIds(element, idPrefix) { + const idAttrs = ['id', 'for', 'data-conditional-input', 'data-modal-trigger']; + + idAttrs.forEach((attr) => { + Array.from(element.querySelectorAll('[' + attr + ']')).forEach((input) => { + const value = idPrefix + input.getAttribute(attr); + input.setAttribute(attr, value); + }); + }); + } + + _getIdPrefix() { + // leading 'r'(andom) to overcome the fact that IDs + // starting with a numeric value are not valid in CSS + return 'r' + Math.floor(Math.random() * 100000) + '__'; + } +} + +window.HtmlHelpers = new HtmlHelpers(); diff --git a/static/es/services/httpClient.js b/static/es/services/httpClient.js new file mode 100644 index 000000000..a0199a8fa --- /dev/null +++ b/static/es/services/httpClient.js @@ -0,0 +1,58 @@ +class HttpClient { + + static ACCEPT = { + TEXT_HTML: 'text/html', + JSON: 'application/json', + }; + + _responseInterceptors = []; + + addResponseInterceptor(interceptor) { + if (typeof interceptor === 'function') { + this._responseInterceptors.push(interceptor); + } + } + + get(args) { + args.method = 'GET'; + return this._fetch(args); + } + + post(args) { + args.method = 'POST'; + return this._fetch(args); + } + + _fetch(options) { + const requestOptions = { + credentials: 'same-origin', + ...options, + }; + + return fetch(options.url, requestOptions) + .then( + (response) => { + this._responseInterceptors.forEach((interceptor) => interceptor(response, options)); + return Promise.resolve(response); + }, + Promise.reject, + ).catch(console.error); + } +} + +window.HttpClient = new HttpClient(); + +// HttpClient ships with its own little interceptor to throw an error +// if the response does not match the expected content-type +function contentTypeInterceptor(response, options) { + if (!options || !options.accept) { + return; + } + + const contentType = response.headers.get("content-type"); + if (!contentType.match(options.accept)) { + throw new Error('Server returned with "' + contentType + '" when "' + options.accept + '" was expected'); + } +} + +window.HttpClient.addResponseInterceptor(contentTypeInterceptor); diff --git a/static/es/services/i18n.js b/static/es/services/i18n.js new file mode 100644 index 000000000..f36de73ab --- /dev/null +++ b/static/es/services/i18n.js @@ -0,0 +1,34 @@ + /** + * I18n + * + * This module stores and serves translated strings, according to the users language settings. + * + * Translations are stored in /messages/frontend/*.msg. + * + * To make additions to any of these files accessible to JavaScrip Utilities + * you need to add them to the respective *.msg file and to the list of FrontendMessages + * in /src/Utils/Frontend/I18n.hs. + * + */ + +class I18n { + + translations = {}; + + add(id, translation) { + this.translations[id] = translation; + } + + addMany(manyTranslations) { + Object.keys(manyTranslations).forEach((key) => this.add(key, manyTranslations[key])); + } + + get(id) { + if (!this.translations[id]) { + throw new Error('I18N Error: Translation missing for »' + id + '«!'); + } + return this.translations[id]; + } +} + +window.I18n = new I18n(); diff --git a/static/es/services/utilRegistry.js b/static/es/services/utilRegistry.js new file mode 100644 index 000000000..712721cc6 --- /dev/null +++ b/static/es/services/utilRegistry.js @@ -0,0 +1,121 @@ +const DEBUG_MODE = /localhost/.test(window.location.href) && 0; + +class UtilRegistry { + + _registeredUtils = []; + _activeUtilInstances = []; + + constructor() { + document.addEventListener('DOMContentLoaded', () => this.setupAll()); + } + + /** + * function registerUtil + * + * utils need to have at least these properties: + * name: string | utils name, e.g. 'example' + * selector: string | utils selector, e.g. '[uw-example]' + * setup: Function | utils setup function, see below + * + * setup function must return instance object with at least these properties: + * name: string | utils name + * element: HTMLElement | element the util is applied to + * destroy: Function | function to destroy the util and remove any listeners + * + * @param util Object Utility that should be added to the registry + */ + register(util) { + if (DEBUG_MODE > 2) { + console.log('registering util "' + util.name + '"'); + console.log({ util }); + } + console.log('register...', { this: this }); + this._registeredUtils.push(util); + } + + deregister(name, destroy) { + const utilIndex = this._findUtilIndex(name); + + if (utilIndex >= 0) { + if (destroy === true) { + this._destroyUtilInstances(name); + } + + this._registeredUtils.splice(utilIndex, 1); + } + } + + setupAll = (scope) => { + console.log('setupAll', { scope }); + if (DEBUG_MODE > 1) { + console.info('registered js utilities:'); + console.table(this._registeredUtils); + } + + this._registeredUtils.forEach((util) => this.setup(util, scope)); + } + + setup(util, scope = document.body) { + if (DEBUG_MODE > 2) { + console.log('setting up util', { util }); + } + + if (util && typeof util.setup === 'function') { + const elements = this._findUtilElements(util, scope); + + elements.forEach((element) => { + let utilInstance = null; + + try { + utilInstance = util.setup(element); + } catch(err) { + if (DEBUG_MODE > 0) { + console.warn('Error while trying to initialize a utility!', { util , element, err }); + } + } + + if (utilInstance) { + if (DEBUG_MODE > 2) { + console.info('Got utility instance for utility "' + util.name + '"', { utilInstance }); + } + + this._activeUtilInstances.push(utilInstance); + } + }); + } + } + + find(name) { + return this._registeredUtils.find((util) => util.name === name); + } + + _findUtilElements(util, scope) { + if (scope && scope.matches(util.selector)) { + return [scope]; + } + return Array.from(scope.querySelectorAll(util.selector)); + } + + _findUtilIndex(name) { + return this._registeredUtils.findIndex((util) => util.name === name); + } + + _destroyUtilInstances(name) { + this._activeUtilInstances + .map((util, index) => ({ + util: util, + index: index, + })) + .filter((activeUtil) => activeUtil.util.name === name) + .forEach((activeUtil) => { + // destroy util instance + activeUtil.util.destroy(); + delete this._activeUtilInstances[activeUtil.index]; + }); + + // get rid of now empty array slots + this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util); + } +} + +window.UtilRegistry = new UtilRegistry();