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.