From baf8b18dc3049559891647ab2ef43a23a982cbdb Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Mon, 18 May 2020 14:08:58 +0200 Subject: [PATCH] feat(util-registry): ensure specific start ordering Fixes #587 --- .../services/util-registry/util-registry.js | 92 +++++++++++++++++-- .../src/utils/form/navigate-away-prompt.js | 38 +++++--- package-lock.json | 3 +- package.json | 1 + 4 files changed, 115 insertions(+), 19 deletions(-) diff --git a/frontend/src/services/util-registry/util-registry.js b/frontend/src/services/util-registry/util-registry.js index 3fcfb99d5..346dda06f 100644 --- a/frontend/src/services/util-registry/util-registry.js +++ b/frontend/src/services/util-registry/util-registry.js @@ -1,9 +1,11 @@ +import * as toposort from 'toposort'; + const DEBUG_MODE = /localhost/.test(window.location.href) ? 2 : 0; export class UtilRegistry { - _registeredUtils = []; - _activeUtilInstances = []; + _registeredUtils = new Array(); + _activeUtilInstances = new Array(); _appInstance; /** @@ -50,12 +52,35 @@ export class UtilRegistry { this._appInstance = appInstance; } - initAll(scope) { - let startedInstances = []; + initAll(scope = document.body) { + let startedInstances = new Array(); const setupInstances = this._registeredUtils.map((util) => this.setup(util, scope)).flat(); - setupInstances.forEach((utilInstance) => { + const orderedInstances = setupInstances.filter(_isStartOrdered); + + if (DEBUG_MODE > 3) { + console.log({ setupInstances, orderedInstances }); + } + + const startDependencies = new Array(); + for (const utilInstance of orderedInstances) { + for (const otherInstance of setupInstances) { + const startOrder = _startOrder(utilInstance, otherInstance); + if (typeof startOrder !== 'undefined') + startDependencies.push(startOrder); + } + } + + if (DEBUG_MODE > 2) { + console.log('starting instances', { setupInstances, startDependencies, order: toposort.array(setupInstances, startDependencies) }); + } + + toposort.array(setupInstances, startDependencies).forEach((utilInstance) => { if (utilInstance) { + if (DEBUG_MODE > 2) { + console.log('starting utilInstance', { util: utilInstance.util.name, utilInstance }); + } + const instance = utilInstance.instance; if (instance && typeof instance.start === 'function') { instance.start.bind(instance)(); @@ -77,7 +102,7 @@ export class UtilRegistry { console.log('setting up util', { util }); } - let instances = []; + let instances = new Array(); if (util) { const elements = this._findUtilElements(util, scope); @@ -140,3 +165,58 @@ export class UtilRegistry { this._activeUtilInstances = this._activeUtilInstances.filter((util) => !!util); } } + +function _startOrder(utilInstance, otherInstance) { + if (utilInstance.element !== otherInstance.element && !(utilInstance.element.contains(otherInstance.element) || otherInstance.element.contains(utilInstance.element))) + return undefined; + + if (utilInstance === otherInstance) + return undefined; + + if (!_isStartOrdered(utilInstance) || !otherInstance.instance || !otherInstance.util) + return undefined; + + function orderParam(name) { + if (typeof utilInstance.instance[name] === 'function') + return !!utilInstance.instance[name](otherInstance.instance); + if (typeof utilInstance.util[name] === 'function') + return !!utilInstance.util[name](otherInstance.instance); + else if (Array.isArray(utilInstance.instance[name])) + return utilInstance.instance[name].some(constr => otherInstance.util === constr); + else if (Array.isArray(utilInstance.util[name])) + return utilInstance.util[name].some(constr => otherInstance.util === constr); + + return false; + } + + const after = orderParam('startAfter'); + const before = orderParam('startBefore'); + + if (DEBUG_MODE > 3) { + console.log('compared instances for ordering', { utilInstance, otherInstance }, { after, before }); + } + + if (after && before) { + console.error({ utilInstance, otherInstance }); + throw new Error(`Incompatible start ordering: ${utilInstance.instance.constructor.name} and ${otherInstance.instance.constructor.name}`); + } else if (after) + return [otherInstance, utilInstance]; + else if (before) + return [utilInstance, otherInstance]; + + return undefined; +} + +function _isStartOrdered(utilInstance) { + if (!utilInstance || !utilInstance.instance || !utilInstance.util) + return false; + + function isOrderParam(name) { + return typeof utilInstance.instance[name] === 'function' || + typeof utilInstance.util[name] === 'function' || + Array.isArray(utilInstance.instance[name]) || + Array.isArray(utilInstance.util[name]); + } + + return isOrderParam('startBefore') || isOrderParam('startAfter'); +} diff --git a/frontend/src/utils/form/navigate-away-prompt.js b/frontend/src/utils/form/navigate-away-prompt.js index f55eaba78..e063809ee 100644 --- a/frontend/src/utils/form/navigate-away-prompt.js +++ b/frontend/src/utils/form/navigate-away-prompt.js @@ -2,6 +2,9 @@ import { Utility } from '../../core/utility'; import { AUTO_SUBMIT_BUTTON_UTIL_SELECTOR } from './auto-submit-button'; import { AUTO_SUBMIT_INPUT_UTIL_SELECTOR } from './auto-submit-input'; +import { InteractiveFieldset } from './interactive-fieldset'; +import { Datepicker } from './datepicker'; + /** * Key generator from an arbitrary number of FormData objects. * @param {...any} formDatas FormData objects @@ -31,37 +34,50 @@ export class NavigateAwayPrompt { } this._element = element; - this._initFormData = new FormData(this._element); if (this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS)) { - return false; + return; } // ignore forms that get submitted automatically if (this._element.querySelector(AUTO_SUBMIT_BUTTON_UTIL_SELECTOR) || this._element.querySelector(AUTO_SUBMIT_INPUT_UTIL_SELECTOR)) { - return false; + return; } if (this._element.matches(NAVIGATE_AWAY_PROMPT_UTIL_OPTOUT)) { - return false; + return; } - window.addEventListener('beforeunload', this._beforeUnloadHandler); + // mark initialized + this._element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); + } + + static startAfter = [ Datepicker, InteractiveFieldset ]; + + start() { + if (!this._isActive()) + return; + + this._initFormData = new FormData(this._element); + window.addEventListener('beforeunload', this._beforeUnloadHandler.bind(this)); this._element.addEventListener('submit', () => { this._unloadDueToSubmit = true; }); - - // mark initialized - this._element.classList.add(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); - } destroy() { - window.removeEventListener('beforeunload', this._beforeUnloadHandler); + this._element.classList.remove(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); } - _beforeUnloadHandler = (event) => { + _isActive() { + return this._element.classList.contains(NAVIGATE_AWAY_PROMPT_INITIALIZED_CLASS); + } + + _beforeUnloadHandler(event) { + if (!this._isActive() || !this._initFormData) + return; + // compare every value of the current FormData with every corresponding value of the initial FormData and set formDataHasChanged to true if there is at least one change const currentFormData = new FormData(this._element); let formDataHasChanged = false; diff --git a/package-lock.json b/package-lock.json index 12bbff701..45e4c2beb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19553,8 +19553,7 @@ "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=", - "dev": true + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" }, "tough-cookie": { "version": "2.4.3", diff --git a/package.json b/package.json index 661edb323..80d2349b3 100644 --- a/package.json +++ b/package.json @@ -125,6 +125,7 @@ "npm": "^6.14.5", "sodium-javascript": "^0.5.6", "tail.datetime": "git+ssh://git@gitlab2.rz.ifi.lmu.de/uni2work/tail.DateTime.git#master", + "toposort": "^2.0.2", "whatwg-fetch": "^3.0.0" } }