|
7 | 7 |
|
8 | 8 | Icinga.Behaviors = Icinga.Behaviors || {}; |
9 | 9 |
|
| 10 | + let functions = null; |
| 11 | + |
| 12 | + try { |
| 13 | + functions = require('icinga/icinga-php-library/functions'); |
| 14 | + } catch (error) { |
| 15 | + console.error('Failed to require library:', error); |
| 16 | + } |
| 17 | + |
10 | 18 | /** |
11 | 19 | * Behavior for modal dialogs. |
12 | 20 | * |
|
18 | 26 | this.icinga = icinga; |
19 | 27 | this.$layout = $('#layout'); |
20 | 28 | this.$ghost = $('#modal-ghost'); |
| 29 | + this.hasChanges = false; |
| 30 | + this._wobbleTimeout = null; |
21 | 31 |
|
22 | 32 | this.on('submit', '#modal form', this.onFormSubmit, this); |
23 | 33 | this.on('change', '#modal form select.autosubmit', this.onFormAutoSubmit, this); |
24 | 34 | this.on('change', '#modal form input.autosubmit', this.onFormAutoSubmit, this); |
25 | 35 | this.on('click', '[data-icinga-modal][href]', this.onModalToggleClick, this); |
26 | 36 | this.on('mousedown', '#layout > #modal', this.onModalLeave, this); |
27 | 37 | this.on('click', '.modal-header > button', this.onModalClose, this); |
28 | | - this.on('keydown', this.onKeyDown, this); |
| 38 | + this.on('paste', '#modal form', this.onPaste, this); |
| 39 | + this.on('change', '#modal form', this.onChange, this); |
| 40 | + this.on('keydown', '#modal form', this.onKeyDown, this); |
| 41 | + this.on('keydown', this.onEscapeKey, this); |
29 | 42 | }; |
30 | 43 |
|
31 | 44 | Modal.prototype = new Icinga.EventListener(); |
|
181 | 194 | var $target = $(event.target); |
182 | 195 |
|
183 | 196 | if ($target.is('#modal')) { |
184 | | - _this.hide($target); |
| 197 | + if (_this.hasChanges) { |
| 198 | + _this.wobble($target); |
| 199 | + } else { |
| 200 | + _this.hide($target); |
| 201 | + } |
| 202 | + } |
| 203 | + }; |
| 204 | + |
| 205 | + /** |
| 206 | + * Event handler for closing the modal. Closes it when the user pushes ESC. |
| 207 | + * |
| 208 | + * @param event {KeyboardEvent} The `keydown` event triggered by pushing a key |
| 209 | + */ |
| 210 | + Modal.prototype.onEscapeKey = function(event) { |
| 211 | + if (event.key !== 'Escape') { |
| 212 | + return; |
| 213 | + } |
| 214 | + |
| 215 | + const _this = event.data.self; |
| 216 | + const $modal = _this.$layout.children('#modal'); |
| 217 | + if (! $modal.length) { |
| 218 | + return; |
| 219 | + } |
| 220 | + |
| 221 | + if (_this.hasChanges) { |
| 222 | + _this.wobble($modal); |
| 223 | + } else if (! event.isDefaultPrevented()) { |
| 224 | + _this.hide($modal); |
185 | 225 | } |
186 | 226 | }; |
187 | 227 |
|
|
197 | 237 | }; |
198 | 238 |
|
199 | 239 | /** |
200 | | - * Event handler for closing the modal. Closes it when the user pushed ESC. |
| 240 | + * Event handler for pasting into the modal form. Sets the hasChanges flag to true. |
201 | 241 | * |
202 | | - * @param event {Event} The `keydown` event triggered by pushing a key |
| 242 | + * @param event The `paste` event triggered by pasting into the form |
| 243 | + */ |
| 244 | + Modal.prototype.onPaste = function(event) { |
| 245 | + const _this = event.data.self; |
| 246 | + |
| 247 | + /** @type {ClipboardEvent} */ |
| 248 | + const originalEvent = event.originalEvent; |
| 249 | + if (originalEvent.clipboardData.types.length) { |
| 250 | + // Only set hasChanges flag if clipboard data is present |
| 251 | + _this.hasChanges = true; |
| 252 | + } |
| 253 | + }; |
| 254 | + |
| 255 | + /** |
| 256 | + * Event handler for input into the modal form. Sets the hasChanges flag to true. |
| 257 | + * |
| 258 | + * This is needed to detect changes in the form, as the `change` event is not always reliable. |
| 259 | + * Unless a text input or textarea is blurred, the `change` event might not be triggered. |
| 260 | + * Pushing Escape in this case would still close the modal without this. |
| 261 | + * |
| 262 | + * @param event {KeyboardEvent} The `keydown` event triggered by pushing a key |
203 | 263 | */ |
204 | 264 | Modal.prototype.onKeyDown = function(event) { |
205 | | - var _this = event.data.self; |
| 265 | + const _this = event.data.self; |
206 | 266 |
|
207 | | - if (! event.isDefaultPrevented() && event.key === 'Escape') { |
208 | | - let $modal = _this.$layout.children('#modal'); |
209 | | - if ($modal.length) { |
210 | | - _this.hide($modal); |
211 | | - } |
| 267 | + if (! functions?.isSpecialKeyPress(event)) { |
| 268 | + _this.hasChanges = true; |
212 | 269 | } |
213 | 270 | }; |
214 | 271 |
|
| 272 | + /** |
| 273 | + * Event handler to register whether the modal form has been changed. |
| 274 | + * |
| 275 | + * In addition to `onKeyDown`, this is needed because checkboxes or select elements |
| 276 | + * do only trigger the `change` event, but at least rather reliably. |
| 277 | + * |
| 278 | + * @param event {Event} The change event |
| 279 | + */ |
| 280 | + Modal.prototype.onChange = function(event) { |
| 281 | + const _this = event.data.self; |
| 282 | + _this.hasChanges = true; |
| 283 | + }; |
| 284 | + |
215 | 285 | /** |
216 | 286 | * Make final preparations and add the modal to the DOM |
217 | 287 | * |
|
240 | 310 | this.icinga.ui.focusElement($modal.find('.modal-window')); |
241 | 311 | }; |
242 | 312 |
|
| 313 | + /** |
| 314 | + * Wobble the modal |
| 315 | + * |
| 316 | + * @param $modal {jQuery} The modal element |
| 317 | + */ |
| 318 | + Modal.prototype.wobble = function($modal) { |
| 319 | + const modal = $modal[0]; |
| 320 | + let timingOffset = 0; |
| 321 | + if (this._wobbleTimeout !== null) { |
| 322 | + clearTimeout(this._wobbleTimeout); |
| 323 | + // Do not interrupt the animation by removing the class too early. |
| 324 | + // This is done by identifying the running animation and synchronizing the timeout with it. |
| 325 | + for (const animation of modal.getAnimations({ subtree: true })) { |
| 326 | + if (animation.effect?.target?.matches('.modal-window')) { |
| 327 | + timingOffset = animation.currentTime; |
| 328 | + |
| 329 | + break; |
| 330 | + } |
| 331 | + } |
| 332 | + } else { |
| 333 | + modal.classList.add("wobble"); |
| 334 | + } |
| 335 | + |
| 336 | + const _this = this; |
| 337 | + this._wobbleTimeout = setTimeout(function () { |
| 338 | + modal.classList.remove("wobble"); |
| 339 | + _this._wobbleTimeout = null; |
| 340 | + }, 1000 - timingOffset); |
| 341 | + }; |
| 342 | + |
243 | 343 | /** |
244 | 344 | * Hide the modal and remove it from the DOM |
245 | 345 | * |
|
249 | 349 | // Remove pointerEvent none style to make the button clickable again |
250 | 350 | this.modalOpener.style.pointerEvents = ''; |
251 | 351 | this.modalOpener = null; |
| 352 | + this.hasChanges = false; |
252 | 353 |
|
253 | 354 | $modal.removeClass('active'); |
254 | 355 | // Using `setTimeout` here to let the transition finish |
|
0 commit comments