PageSourceSearch

https://lpar.net/js/modal.js

js lpar.net collected 2026-09-28 05:49:34 UTC 10,718 bytes, 369 lines download raw bytes

1// (c) 2025 EasyChair Ltd
2
3"use strict";
4
5const storageName = 'mdl';
6
7/**
8 * @typedef ModalData
9 * @type {Object}
10 * @property {'overlay'|'popup'} type - The type of modal window.
11 * @property {string} content - A valid html code.
12 * @property {string|undefined} title - Title of the modal block.
13 * @property {boolean|undefined} x - Should self-close of modal be blocked.
14 * @property {boolean|undefined} refresh - Refresh the page after closing of modal.
15 * @property {boolean|undefined} save - Should modal be saved in session storage.
16 * @property {string[]|undefined} js - Array with js files that required for this modal.
17 * @property {string[]|undefined} css - Array with css files that required for this modal.
18 * @property {'default'|'wide'|'full'} width - The width of the modal window.
19 */
20
21class Modal {
22  /**
23   * Initialize and show modal window with content.
24   * @param {string} id Modal id.
25   * @param {ModalData} data - A modal data.
26   */
27  constructor(id, {
28    content,
29    title,
30    x: hideX,
31    refresh,
32    type,
33    width,
34  }) {
35    try {
36      this.id = id;
37      this.refresh = refresh;
38      /** @type {HTMLElement} */
39      this.modalEl = null;
40      /** @type {HTMLElement?} */
41      this.modalWrapperEl = null;
42      this.changedTabIndex = new Map();
43      /**
44       * Parameters of click and modal window when user clicked to the movable part.
45       * @type {{ offsetX: number, offsetY: number, height: number, width: number }}
46       */
47      this.modalBox = { offsetX: 0, offsetY: 0, height: 0, width: 0 };
48
49      this.onKeyDown = this.onKeyDown.bind(this);
50      this.onHeadMouseDown = this.onHeadMouseDown.bind(this);
51      this.onHeadMouseUp = this.onHeadMouseUp.bind(this);
52      this.onHeadMouseMove = this.onHeadMouseMove.bind(this);
53
54      let modalWidthClass;
55      if (width === 'default') { 
56        modalWidthClass = '';
57      } else if (width === 'wide') {
58        modalWidthClass = 'modal_wide';
59      } else if (width == 'full') {
60        modalWidthClass = 'modal_full';
61      } else {
62        throw new Error(`Invalid modal width: ${width}`);
63      }
64
65      if (type === 'overlay') {
66        this.modalWrapperEl = $_('div', { className: 'modal_wrapper' }, false, document.body);
67        $_('div', { className: 'modal_spacer_top' }, false, this.modalWrapperEl);
68        this.modalEl = $_('div', { className: `modal ${modalWidthClass}` }, false, this.modalWrapperEl);
69        $_('div', { className: 'modal_spacer_bottom' }, false, this.modalWrapperEl);
70      } else if (type === 'popup') {
71        this.modalEl = $_('div', { className: `modal modal_popup ${modalWidthClass}` }, false, document.body);
72      } else {
73        throw new Error(`Wrong modal type: ${type}`);
74      }
75
76      const head = $_('div', { className: `modal_head ${type === 'popup' ? 'movable' : ''}` },
77        false, this.modalEl);
78      if (title || !hideX) {
79        $_('span', { className: 'modal_title' }, title, head);
80
81        if (!hideX) {
82          const closeBtn = $_('button', {
83            className: 'modal_close',
84            onclick: ModalManager.hide,
85            title: 'close this window'
86          }, false, head);
87          $_('img', {}, false, closeBtn);
88        }
89
90        if (title) {
91          $_('hr', false, false, this.modalEl);
92        }
93      }
94      $_('div', { className: 'modal_content' }, content, this.modalEl);
95
96      if (type === 'overlay') {
97        this.disableTabUnderElement();
98      } else if (type === 'popup' || type === 'popup_wide') {
99        centerInWindow(this.modalEl);
100        head.addEventListener('mousedown', this.onHeadMouseDown);
101      }
102
103      if (!hideX) {
104        document.addEventListener('keydown', this.onKeyDown);
105      }
106    } catch (err) {
107      logError(err, 'Modal.constructor');
108    }
109  }
110
111  destroy() {
112    this.restoreTabIndex();
113
114    // wrapper can be missed in dependence of modal type
115    removeElement(this.modalWrapperEl || this.modalEl);
116    this.modalWrapperEl = null;
117    this.modalEl = null;
118
119    document.removeEventListener('keydown', this.onKeyDown);
120  }
121
122  /**
123   * Change tabindex on all elements under the blocking popup,
124   * otherwise they are accessible
125   * @private
126   */
127  disableTabUnderElement() {
128    try {
129      // do not change tabindex in the element itself
130      const self = new Set();
131      const selfEls = this.modalEl.querySelectorAll('[href], input, select, textarea, button, iframe, object, embed, [tabindex], [contenteditable]');
132      for (let i = selfEls.length - 1; i >= 0; i--) {
133        self.add(selfEls[i]);
134      }
135
136      this.changedTabIndex = new Map();
137      const all = document.querySelectorAll('[href], input, select, textarea, button, iframe, object, embed, [tabindex], [contenteditable]');
138      for (let i = all.length - 1; i >= 0; i--) {
139        let el = all[i];
140        if (!self.has(el)) {
141          this.changedTabIndex.set(el, el.tabIndex);
142          el.tabIndex = -1;
143        }
144      }
145
146      document.activeElement?.blur();
147    } catch (err) {
148      logError(err, 'Modal.disableTabUnderElement');
149    }
150  }
151
152  /**
153   * Restore all tabindex to values that what before disabling.
154   * @private
155   */
156  restoreTabIndex() {
157    try {
158      this.changedTabIndex.forEach((index, el) => {
159        el.tabIndex = index;
160      });
161      this.changedTabIndex = new Map();
162    }
163    catch (err) {
164      logError(err, 'Modal.restoreTabIndex');
165    }
166  }
167
168  /**
169   * On key down handler.
170   * @param {KeyboardEvent} ev
171   * @private
172   */
173  onKeyDown(ev) {
174    try {
175      if (ev.key === 'Escape') {
176        ModalManager.hide();
177      }
178    } catch (err) {
179      logError(err, 'Modal.onKeyDown');
180    }
181  }
182
183  /**
184   * @param {MouseEvent} ev
185   * @private
186   */
187  onHeadMouseDown(ev) {
188    try {
189      ev.preventDefault();
190
191      const box = this.modalEl.getBoundingClientRect();
192      this.modalBox = {
193        offsetX: ev.clientX - box.x,
194        offsetY: ev.clientY - box.y,
195        height: box.height,
196        width: box.width
197      };
198
199      document.addEventListener('mousemove', this.onHeadMouseMove);
200      document.addEventListener('mouseup', this.onHeadMouseUp);
201    } catch (err) {
202      logError(err, 'Modal.onHeadMouseDown');
203    }
204  }
205
206  /** @private */
207  onHeadMouseUp() {
208    try {
209      document.removeEventListener('mousemove', this.onHeadMouseMove);
210      document.removeEventListener('mouseup', this.onHeadMouseUp);
211    } catch (err) {
212      logError(err, 'Modal.onHeadMouseUp');
213    }
214  }
215
216  /**
217   * @param {MouseEvent} ev
218   * @private
219   */
220  onHeadMouseMove(ev) {
221    try {
222      const pageHeight = Math.max(
223        document.body.scrollHeight, document.documentElement.scrollHeight,
224        document.body.offsetHeight, document.documentElement.offsetHeight,
225        document.body.clientHeight, document.documentElement.clientHeight
226      );
227      const pageWidth = Math.max(
228        document.body.scrollWidth, document.documentElement.scrollWidth,
229        document.body.offsetWidth, document.documentElement.offsetWidth,
230        document.body.clientWidth, document.documentElement.clientWidth
231      );
232      const box = this.modalBox;
233
234      let x = ev.pageX - box.offsetX;
235      if (x < 0) {
236        x = 0;
237      } else if (x + box.width > pageWidth) {
238        x = pageWidth - box.width;
239      }
240      this.modalEl.style.left = x + 'px';
241
242      let y = ev.pageY - box.offsetY;
243      if (y < 0) {
244        y = 0;
245      } else if (y + box.height > pageHeight) {
246        y = pageHeight - box.height;
247      }
248      this.modalEl.style.top = y + 'px';
249    } catch (err) {
250      logError(err, 'Modal.onHeadMouseMove');
251    }
252  }
253}
254
255window.ModalManager = {
256  modal: null,
257  /**
258   * Shows modal window.
259   * @param {ModalData} data Modal's data.
260   * @param {string | undefined} restoringId Id of modal window if we restore
261   * it from storage and should not save it and change page hash.
262   * Undefined if we create it.
263   */
264  show: function (data, restoringId) {
265    try {
266      const { js = [], css = [], save } = data;
267      css.forEach(file => { EC.loadCSS(file) });
268      Promise.all(js.map(file => EC.loadJS(file))).then(() => {
269        ModalManager.hide();
270
271        const modalId = restoringId || Date.now();
272        const modal = new Modal(modalId, data);
273        ModalManager.modal = modal;
274
275        if (!restoringId && save) {
276          EC.setPageHash({
277            ...EC.pageHash(),
278            mdl: modalId
279          });
280          ModalManager.store(modalId, data);
281        }
282
283        onLoad.emit(modal.modalEl);
284      });
285    } catch (err) {
286      logError(err, 'ModalManager.show');
287    }
288  },
289  /** Hide current modal window and remove it from storage and page hash. */
290  hide: function () {
291    try {
292      const { modal } = ModalManager;
293      if (modal === null) {
294        return;
295      }
296
297      ModalManager.removeFromStorage(modal.id);
298      modal.destroy();
299      ModalManager.modal = null;
300      const pageHash = EC.pageHash();
301      delete pageHash[storageName];
302      EC.setPageHash(pageHash);
303
304      if (modal.refresh) {
305        location.reload();
306      }
307    } catch (err) {
308      logError(err, 'ModalManager.hide');
309    }
310  },
311  /**
312   * Checking modal data in the storage and return it if found.
313   * @param {string} modalId
314   * @returns {Object?} Returns data if it exist or null.
315   * @private
316   */
317  getFromStorage: function (modalId) {
318    try {
319      const { pathname } = location;
320      const modalStorage
321        = JSON.parse(sessionStorage.getItem(storageName)) || {};
322      const current = modalStorage[pathname] || {};
323      return current[modalId] || null;
324    } catch (err) {
325      return null;
326    }
327  },
328  /**
329   * Try to store modal's data in the storage.
330   * @param {string} modalId
331   * @param {Object} data
332   * @private
333   */
334  store: function (modalId, data) {
335    try {
336      const { pathname } = location;
337      const modalStorage
338        = JSON.parse(sessionStorage.getItem(storageName)) || {};
339      const current = modalStorage[pathname] || {};
340      sessionStorage.setItem(storageName, JSON.stringify({
341        ...modalStorage,
342        [pathname]: {
343          ...current,
344          [modalId]: data
345        }
346      }));
347    } catch (err) { }
348  },
349  /**
350   * Trys to remove data for modal with id modalId from the storage.
351   * @param {string} modalId
352   * @private
353   */
354  removeFromStorage(modalId) {
355    try {
356      const { pathname } = location;
357      const modalStorage
358        = JSON.parse(sessionStorage.getItem(storageName)) || {};
359      const current = modalStorage[pathname] || {};
360      delete current[modalId];
361      sessionStorage.setItem(storageName, JSON.stringify({
362        ...modalStorage,
363        [pathname]: current
364      }));
365    } catch (err) { }
366  },
367};
368
369addDebug('modal', 'loaded');

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.