')\n .appendTo(this.$container)\n .append(ResizeHandle);\n\n this.resizeDragger = new Garnish.BaseDrag($resizeDragHandle, {\n onDragStart: this._handleResizeStart.bind(this),\n onDrag: this._handleResize.bind(this),\n });\n }\n\n this.addListener(this.$container, 'click', function (ev) {\n ev.stopPropagation();\n });\n\n // Show it if we're late to the party\n if (this.visible) {\n this.show();\n }\n },\n\n show: function () {\n // Close other modals as needed\n if (\n this.settings.closeOtherModals &&\n Garnish.Modal.visibleModal &&\n Garnish.Modal.visibleModal !== this\n ) {\n Garnish.Modal.visibleModal.hide();\n }\n\n if (this.$container) {\n // Move it to the end of so it gets the highest sub-z-index\n this.$shade.appendTo(Garnish.$bod).velocity('stop');\n this.$container.appendTo(Garnish.$bod).velocity('stop');\n\n this.$container.show();\n this.updateSizeAndPosition();\n\n this.$shade.velocity('fadeIn', {\n duration: 50,\n complete: function () {\n this.$container.velocity('fadeIn', {\n complete: function () {\n this.updateSizeAndPosition();\n Garnish.setFocusWithin(this.$container);\n this.onFadeIn();\n }.bind(this),\n });\n }.bind(this),\n });\n\n if (this.settings.hideOnShadeClick) {\n this.addListener(this.$shade, 'click', 'hide');\n }\n\n // Add focus trap\n Garnish.trapFocusWithin(this.$container);\n\n this.addListener(Garnish.$win, 'resize', '_handleWindowResize');\n }\n\n this.enable();\n\n if (!this.visible) {\n this.visible = true;\n Garnish.Modal.visibleModal = this;\n\n Garnish.uiLayerManager.addLayer(this.$container);\n Garnish.hideModalBackgroundLayers();\n\n if (this.settings.hideOnEsc) {\n Garnish.uiLayerManager.registerShortcut(Garnish.ESC_KEY, () => {\n this.trigger('escape');\n this.hide();\n });\n }\n\n this.onShow();\n }\n },\n\n onShow: function () {\n this.trigger('show');\n this.settings.onShow();\n },\n\n quickShow: function () {\n this.show();\n\n if (this.$container) {\n this.$container.velocity('stop');\n this.$container.show().css('opacity', 1);\n\n this.$shade.velocity('stop');\n this.$shade.show().css('opacity', 1);\n }\n },\n\n hide: function (ev) {\n if (!this.visible) {\n return;\n }\n\n this.disable();\n\n if (ev) {\n ev.stopPropagation();\n }\n\n if (this.$container) {\n this.$container\n .velocity('stop')\n .velocity('fadeOut', {duration: Garnish.FX_DURATION});\n this.$shade.velocity('stop').velocity('fadeOut', {\n duration: Garnish.FX_DURATION,\n complete: this.onFadeOut.bind(this),\n });\n\n if (this.settings.hideOnShadeClick) {\n this.removeListener(this.$shade, 'click');\n }\n\n this.removeListener(Garnish.$win, 'resize');\n }\n\n this.$triggerElement.focus();\n\n this.visible = false;\n Garnish.Modal.visibleModal = null;\n Garnish.uiLayerManager.removeLayer();\n Garnish.resetModalBackgroundLayerVisibility();\n this.onHide();\n },\n\n onHide: function () {\n this.trigger('hide');\n this.settings.onHide();\n },\n\n quickHide: function () {\n this.hide();\n\n if (this.$container) {\n this.$container.velocity('stop');\n this.$container.css('opacity', 0).hide();\n\n this.$shade.velocity('stop');\n this.$shade.css('opacity', 0).hide();\n }\n },\n\n updateSizeAndPosition: function () {\n if (!this.$container) {\n return;\n }\n\n this.$container.css({\n width: this.desiredWidth ? Math.max(this.desiredWidth, 200) : '',\n height: this.desiredHeight ? Math.max(this.desiredHeight, 200) : '',\n 'min-width': '',\n 'min-height': '',\n });\n\n // Set the width first so that the height can adjust for the width\n this.updateSizeAndPosition._windowWidth = Garnish.$win.width();\n this.updateSizeAndPosition._width = Math.min(\n this.getWidth(),\n this.updateSizeAndPosition._windowWidth - this.settings.minGutter * 2\n );\n\n this.$container.css({\n width: this.updateSizeAndPosition._width,\n 'min-width': this.updateSizeAndPosition._width,\n left: Math.round(\n (this.updateSizeAndPosition._windowWidth -\n this.updateSizeAndPosition._width) /\n 2\n ),\n });\n\n // Now set the height\n this.updateSizeAndPosition._windowHeight = Garnish.$win.height();\n this.updateSizeAndPosition._height = Math.min(\n this.getHeight(),\n this.updateSizeAndPosition._windowHeight - this.settings.minGutter * 2\n );\n\n this.$container.css({\n height: this.updateSizeAndPosition._height,\n 'min-height': this.updateSizeAndPosition._height,\n top: Math.round(\n (this.updateSizeAndPosition._windowHeight -\n this.updateSizeAndPosition._height) /\n 2\n ),\n });\n\n this.trigger('updateSizeAndPosition');\n },\n\n onFadeIn: function () {\n this.trigger('fadeIn');\n this.settings.onFadeIn();\n },\n\n onFadeOut: function () {\n this.trigger('fadeOut');\n this.settings.onFadeOut();\n },\n\n getHeight: function () {\n if (!this.$container) {\n throw 'Attempted to get the height of a modal whose container has not been set.';\n }\n\n if (!this.visible) {\n this.$container.show();\n }\n\n this.getHeight._height = this.$container.outerHeight();\n\n if (!this.visible) {\n this.$container.hide();\n }\n\n return this.getHeight._height;\n },\n\n getWidth: function () {\n if (!this.$container) {\n throw 'Attempted to get the width of a modal whose container has not been set.';\n }\n\n if (!this.visible) {\n this.$container.show();\n }\n\n // Chrome might be 1px shy here for some reason\n this.getWidth._width = this.$container.outerWidth() + 1;\n\n if (!this.visible) {\n this.$container.hide();\n }\n\n return this.getWidth._width;\n },\n\n _handleWindowResize: function (ev) {\n // ignore propagated resize events\n if (ev.target === window) {\n this.updateSizeAndPosition();\n }\n },\n\n _handleResizeStart: function () {\n this.resizeStartWidth = this.getWidth();\n this.resizeStartHeight = this.getHeight();\n },\n\n _handleResize: function () {\n if (Garnish.ltr) {\n this.desiredWidth =\n this.resizeStartWidth + this.resizeDragger.mouseDistX * 2;\n } else {\n this.desiredWidth =\n this.resizeStartWidth - this.resizeDragger.mouseDistX * 2;\n }\n\n this.desiredHeight =\n this.resizeStartHeight + this.resizeDragger.mouseDistY * 2;\n\n this.updateSizeAndPosition();\n },\n\n /**\n * Destroy\n */\n destroy: function () {\n if (this.$container) {\n this.$container.removeData('modal').remove();\n }\n\n if (this.$shade) {\n this.$shade.remove();\n }\n\n if (this.dragger) {\n this.dragger.destroy();\n }\n\n if (this.resizeDragger) {\n this.resizeDragger.destroy();\n }\n\n Garnish.Modal.instances = Craft.Preview.instances.filter(\n (o) => o !== this\n );\n\n this.base();\n },\n },\n {\n relativeElemPadding: 8,\n defaults: {\n autoShow: true,\n draggable: false,\n dragHandleSelector: null,\n resizable: false,\n minGutter: 10,\n onShow: $.noop,\n onHide: $.noop,\n onFadeIn: $.noop,\n onFadeOut: $.noop,\n closeOtherModals: false,\n hideOnEsc: true,\n hideOnShadeClick: true,\n triggerElement: null,\n shadeClass: 'modal-shade',\n },\n\n /**\n * @type {Garnish.Modal[]}\n */\n instances: [],\n\n /**\n * @type {?Garnish.Modal}\n */\n visibleModal: null,\n }\n);\n","const ResizeHandle =\n '
';\n\nexport default ResizeHandle;\n","import Garnish from './Garnish.js';\nimport Base from './Base.js';\nimport $ from 'jquery';\n\n/**\n * Multi-Function Button\n */\nexport default Base.extend(\n {\n $btn: null,\n $btnLabel: null,\n $liveRegion: null,\n\n defaultMessage: null,\n busyMessage: null,\n failureMessage: null,\n retryMessage: null,\n successMessage: null,\n\n init: function (button, settings) {\n this.setSettings(settings, Garnish.MultiFunctionBtn.defaults);\n this.$btn = $(button);\n\n // Is this already a multi-function button?\n if (this.$btn.data('multifunction-btn')) {\n console.warn(\n 'Double-instantiating a multi-function button on an element'\n );\n this.$btn.data('multifunction-btn').destroy();\n }\n\n this.$btnLabel = this.$btn.find('.label');\n this.defaultMessage = this.$btnLabel.text();\n\n if (this.$btn.prev().attr('role') === 'status') {\n this.$liveRegion = this.$btn.prev();\n } else {\n this.$liveRegion = $('
', {\n class: 'visually-hidden',\n role: 'status',\n });\n this.$btn.before(this.$liveRegion);\n }\n\n this.busyMessage = this.$btn.data('busy-message')\n ? this.$btn.data('busy-message')\n : Craft.t('app', 'Loading');\n this.failureMessage = this.$btn.data('failure-message');\n this.retryMessage = this.$btn.data('retry-message');\n this.successMessage = this.$btn.data('success-message')\n ? this.$btn.data('success-message')\n : Craft.t('app', 'Success');\n },\n\n busyEvent: function () {\n this.$btn.addClass(this.settings.busyClass);\n\n if (this.busyMessage) {\n this.updateMessages(this.busyMessage);\n }\n },\n\n failureEvent: function () {\n this.endBusyState();\n\n if (!this.failureMessage && !this.retryMessage) return;\n\n if (this.failureMessage) {\n this.updateMessages(this.failureMessage);\n }\n\n if (this.retryMessage) {\n // If there was a failure message, ensure there's a delay before showing retry message\n if (this.failureMessage) {\n setTimeout(() => {\n this.updateMessages(this.retryMessage);\n }, this.settings.failureMessageDuration);\n } else {\n this.updateMessages(this.retryMessage);\n }\n }\n },\n\n successEvent: function () {\n this.endBusyState();\n\n if (this.successMessage) {\n this.updateMessages(this.successMessage);\n }\n },\n\n updateMessages: function (message) {\n this.$liveRegion.text(message);\n\n if (this.settings.changeButtonText) {\n this.$btnLabel.text(message);\n }\n\n // Empty live region so a SR user navigating with virtual cursor doesn't find outdated message\n setTimeout(() => {\n // Bail out if there's now a different message in the live region\n if (this.$liveRegion.text() !== message) return;\n\n this.$liveRegion.empty();\n }, this.settings.clearLiveRegionTimeout);\n },\n\n endBusyState: function () {\n this.$btn.removeClass(this.settings.busyClass);\n },\n\n destroy: function () {\n this.$btn.removeData('multifunction-btn');\n this.base();\n },\n },\n {\n defaults: {\n busyClass: 'loading',\n clearLiveRegionTimeout: 2500,\n failureMessageDuration: 3000,\n changeButtonText: false,\n },\n }\n);\n","import Garnish from './Garnish.js';\nimport Base from './Base.js';\nimport $ from 'jquery';\n\n/**\n * Nice Text\n */\nexport default Base.extend(\n {\n $input: null,\n $hint: null,\n $stage: null,\n $charsLeft: null,\n autoHeight: null,\n maxLength: null,\n showCharsLeft: false,\n showingHint: false,\n val: null,\n inputBoxSizing: 'content-box',\n width: null,\n height: null,\n minHeight: null,\n initialized: false,\n\n init: function (input, settings) {\n this.$input = $(input);\n this.settings = $.extend({}, Garnish.NiceText.defaults, settings);\n\n if (this.isVisible()) {\n this.initialize();\n } else {\n this.addListener(Garnish.$win, 'resize', 'initializeIfVisible');\n }\n },\n\n isVisible: function () {\n return this.$input.height() > 0;\n },\n\n initialize: function () {\n if (this.initialized) {\n return;\n }\n\n this.initialized = true;\n this.removeListener(Garnish.$win, 'resize');\n\n this.maxLength = this.$input.attr('maxlength');\n\n if (this.maxLength) {\n this.maxLength = parseInt(this.maxLength);\n }\n\n if (\n this.maxLength &&\n (this.settings.showCharsLeft ||\n Garnish.hasAttr(this.$input, 'data-show-chars-left'))\n ) {\n this.showCharsLeft = true;\n\n // Remove the maxlength attribute\n this.$input.removeAttr('maxlength');\n }\n\n // Is this already a transparent text input?\n if (this.$input.data('nicetext')) {\n console.warn(\n 'Double-instantiating a transparent text input on an element'\n );\n this.$input.data('nicetext').destroy();\n }\n\n this.$input.data('nicetext', this);\n\n this.getVal();\n\n this.autoHeight =\n this.settings.autoHeight && this.$input.prop('nodeName') === 'TEXTAREA';\n\n if (this.autoHeight) {\n this.minHeight = this.getHeightForValue('');\n this.updateHeight();\n\n // Update height when the window resizes\n this.width = this.$input.width();\n this.addListener(Garnish.$win, 'resize', 'updateHeightIfWidthChanged');\n }\n\n if (this.settings.hint) {\n this.$hintContainer = $(\n '
'\n ).insertBefore(this.$input);\n this.$hint = $(\n '
' + this.settings.hint + '
'\n ).appendTo(this.$hintContainer);\n this.$hint.css({\n top:\n parseInt(this.$input.css('borderTopWidth')) +\n parseInt(this.$input.css('paddingTop')),\n left:\n parseInt(this.$input.css('borderLeftWidth')) +\n parseInt(this.$input.css('paddingLeft')) +\n 1,\n });\n Garnish.copyTextStyles(this.$input, this.$hint);\n\n if (this.val) {\n this.$hint.hide();\n } else {\n this.showingHint = true;\n }\n\n // Focus the input when clicking on the hint\n this.addListener(this.$hint, 'mousedown', function (ev) {\n ev.preventDefault();\n this.$input.focus();\n });\n }\n\n if (this.showCharsLeft) {\n this.$charsLeft = $(\n '
'\n ).insertAfter(this.$input);\n this.updateCharsLeft();\n }\n\n this.addListener(this.$input, 'textchange', 'onTextChange');\n this.addListener(this.$input, 'keydown', 'onKeyDown');\n },\n\n initializeIfVisible: function () {\n if (this.isVisible()) {\n this.initialize();\n }\n },\n\n getVal: function () {\n this.val = this.$input.val();\n return this.val;\n },\n\n showHint: function () {\n this.$hint.velocity('fadeIn', {\n complete: Garnish.NiceText.hintFadeDuration,\n });\n\n this.showingHint = true;\n },\n\n hideHint: function () {\n this.$hint.velocity('fadeOut', {\n complete: Garnish.NiceText.hintFadeDuration,\n });\n\n this.showingHint = false;\n },\n\n onTextChange: function () {\n this.getVal();\n\n if (this.$hint) {\n if (this.showingHint && this.val) {\n this.hideHint();\n } else if (!this.showingHint && !this.val) {\n this.showHint();\n }\n }\n\n if (this.autoHeight) {\n this.updateHeight();\n }\n\n if (this.showCharsLeft) {\n this.updateCharsLeft();\n }\n },\n\n onKeyDown: function (ev) {\n // If Ctrl/Command + Return is pressed, submit the closest form\n if (ev.keyCode === Garnish.RETURN_KEY && Garnish.isCtrlKeyPressed(ev)) {\n ev.preventDefault();\n this.$input.closest('form').submit();\n }\n },\n\n buildStage: function () {\n this.$stage = $('
').appendTo(Garnish.$bod);\n\n // replicate the textarea's text styles\n this.$stage.css({\n display: 'block',\n position: 'absolute',\n top: -9999,\n left: -9999,\n });\n\n this.inputBoxSizing = this.$input.css('box-sizing');\n\n if (this.inputBoxSizing === 'border-box') {\n this.$stage.css({\n 'border-top': this.$input.css('border-top'),\n 'border-right': this.$input.css('border-right'),\n 'border-bottom': this.$input.css('border-bottom'),\n 'border-left': this.$input.css('border-left'),\n 'padding-top': this.$input.css('padding-top'),\n 'padding-right': this.$input.css('padding-right'),\n 'padding-bottom': this.$input.css('padding-bottom'),\n 'padding-left': this.$input.css('padding-left'),\n '-webkit-box-sizing': this.inputBoxSizing,\n '-moz-box-sizing': this.inputBoxSizing,\n 'box-sizing': this.inputBoxSizing,\n });\n }\n\n Garnish.copyTextStyles(this.$input, this.$stage);\n },\n\n getHeightForValue: function (val) {\n if (!this.$stage) {\n this.buildStage();\n }\n\n if (this.inputBoxSizing === 'border-box') {\n this.$stage.css('width', this.$input.outerWidth());\n } else {\n this.$stage.css('width', this.$input.width());\n }\n\n if (!val) {\n val = ' ';\n for (var i = 1; i < this.$input.prop('rows'); i++) {\n val += '
';\n }\n } else {\n // Ampersand entities\n val = val.replace(/&/g, '&');\n\n // < and >\n val = val.replace(//g, '>');\n\n // Multiple spaces\n val = val.replace(/ {2,}/g, function (spaces) {\n // TODO: replace with String.repeat() when more broadly available?\n var replace = '';\n for (var i = 0; i < spaces.length - 1; i++) {\n replace += ' ';\n }\n return replace + ' ';\n });\n\n // Line breaks\n val = val.replace(/[\\n\\r]$/g, '
');\n val = val.replace(/[\\n\\r]/g, '
');\n }\n\n this.$stage.html(val);\n\n if (this.inputBoxSizing === 'border-box') {\n this.getHeightForValue._height = this.$stage.outerHeight();\n } else {\n this.getHeightForValue._height = this.$stage.height();\n }\n\n if (this.minHeight && this.getHeightForValue._height < this.minHeight) {\n this.getHeightForValue._height = this.minHeight;\n }\n\n return this.getHeightForValue._height;\n },\n\n updateHeight: function () {\n // has the height changed?\n if (this.height !== (this.height = this.getHeightForValue(this.val))) {\n this.$input.css('min-height', this.height);\n\n if (this.initialized) {\n this.onHeightChange();\n }\n }\n },\n\n updateHeightIfWidthChanged: function () {\n if (\n this.isVisible() &&\n this.width !== (this.width = this.$input.width()) &&\n this.width\n ) {\n this.updateHeight();\n }\n },\n\n onHeightChange: function () {\n this.settings.onHeightChange();\n },\n\n updateCharsLeft: function () {\n this.updateCharsLeft._charsLeft = this.maxLength - this.val.length;\n this.$charsLeft.html(\n Garnish.NiceText.charsLeftHtml(this.updateCharsLeft._charsLeft)\n );\n\n if (this.updateCharsLeft._charsLeft >= 0) {\n this.$charsLeft.removeClass(this.settings.negativeCharsLeftClass);\n } else {\n this.$charsLeft.addClass(this.settings.negativeCharsLeftClass);\n }\n },\n\n /**\n * Destroy\n */\n destroy: function () {\n this.$input.removeData('nicetext');\n\n if (this.$hint) {\n this.$hint.remove();\n }\n\n if (this.$stage) {\n this.$stage.remove();\n }\n\n this.base();\n },\n },\n {\n interval: 100,\n hintFadeDuration: 50,\n charsLeftHtml: function (charsLeft) {\n return charsLeft;\n },\n defaults: {\n autoHeight: true,\n showCharsLeft: false,\n charsLeftClass: 'chars-left',\n negativeCharsLeftClass: 'negative-chars-left',\n onHeightChange: $.noop,\n },\n }\n);\n","import Garnish from './Garnish.js';\nimport Base from './Base.js';\nimport $ from 'jquery';\n\n/**\n * Select\n */\nexport default Base.extend(\n {\n $container: null,\n $items: null,\n $selectedItems: null,\n $focusedItem: null,\n\n mousedownTarget: null,\n mouseUpTimeout: null,\n callbackFrame: null,\n\n $focusable: null,\n $first: null,\n first: null,\n $last: null,\n last: null,\n\n /**\n * Constructor\n */\n init: function (container, items, settings) {\n this.$container = $(container);\n\n // Param mapping\n if (typeof items === 'undefined' && $.isPlainObject(container)) {\n // (settings)\n settings = container;\n container = null;\n items = null;\n } else if (typeof settings === 'undefined' && $.isPlainObject(items)) {\n // (container, settings)\n settings = items;\n items = null;\n }\n\n // Is this already a select?\n if (this.$container.data('select')) {\n console.warn('Double-instantiating a select on an element');\n this.$container.data('select').destroy();\n }\n\n this.$container.data('select', this);\n\n this.setSettings(settings, Garnish.Select.defaults);\n\n this.$items = $();\n this.$selectedItems = $();\n\n this.addItems(items);\n\n // --------------------------------------------------------------------\n\n if (this.settings.allowEmpty && !this.settings.checkboxMode) {\n this.addListener(this.$container, 'click', function () {\n if (this.ignoreClick) {\n this.ignoreClick = false;\n } else {\n // Deselect all items on container click\n this.deselectAll(true);\n }\n });\n }\n },\n\n /**\n * Get Item Index\n */\n getItemIndex: function ($item) {\n return this.$items.index($item[0]);\n },\n\n /**\n * Is Selected?\n */\n isSelected: function (item) {\n if (Garnish.isJquery(item)) {\n if (!item[0]) {\n return false;\n }\n\n item = item[0];\n }\n\n return $.inArray(item, this.$selectedItems) !== -1;\n },\n\n /**\n * Select Item\n */\n selectItem: function ($item, focus, preventScroll) {\n if (!this.settings.multi) {\n this.deselectAll();\n }\n\n this.$first = this.$last = $item;\n this.first = this.last = this.getItemIndex($item);\n\n if (focus) {\n this.setFocusableItem($item);\n this.focusItem($item, preventScroll);\n }\n\n this._selectItems($item);\n },\n\n selectAll: function () {\n if (!this.settings.multi || !this.$items.length) {\n return;\n }\n\n this.first = 0;\n this.last = this.$items.length - 1;\n this.$first = this.$items.eq(this.first);\n this.$last = this.$items.eq(this.last);\n\n this._selectItems(this.$items);\n },\n\n /**\n * Select Range\n */\n selectRange: function ($item, preventScroll) {\n if (!this.settings.multi) {\n return this.selectItem($item, true, true);\n }\n\n this.deselectAll();\n\n this.$last = $item;\n this.last = this.getItemIndex($item);\n\n this.setFocusableItem($item);\n this.focusItem($item, preventScroll);\n\n // prepare params for $.slice()\n var sliceFrom, sliceTo;\n\n if (this.first < this.last) {\n sliceFrom = this.first;\n sliceTo = this.last + 1;\n } else {\n sliceFrom = this.last;\n sliceTo = this.first + 1;\n }\n\n this._selectItems(this.$items.slice(sliceFrom, sliceTo));\n },\n\n /**\n * Deselect Item\n */\n deselectItem: function ($item) {\n var index = this.getItemIndex($item);\n if (this.first === index) {\n this.$first = this.first = null;\n }\n if (this.last === index) {\n this.$last = this.last = null;\n }\n\n this._deselectItems($item);\n },\n\n /**\n * Deselect All\n */\n deselectAll: function (clearFirst) {\n if (clearFirst) {\n this.$first = this.first = this.$last = this.last = null;\n }\n\n this._deselectItems(this.$items);\n },\n\n /**\n * Deselect Others\n */\n deselectOthers: function ($item) {\n this.deselectAll();\n this.selectItem($item, true, true);\n },\n\n /**\n * Toggle Item\n */\n toggleItem: function ($item, preventScroll) {\n if (!this.isSelected($item)) {\n this.selectItem($item, true, preventScroll);\n } else {\n if (this._canDeselect($item)) {\n this.deselectItem($item, true);\n }\n }\n },\n\n clearMouseUpTimeout: function () {\n clearTimeout(this.mouseUpTimeout);\n },\n\n getFirstItem: function () {\n if (this.$items.length) {\n return this.$items.first();\n }\n },\n\n getLastItem: function () {\n if (this.$items.length) {\n return this.$items.last();\n }\n },\n\n isPreviousItem: function (index) {\n return index > 0;\n },\n\n isNextItem: function (index) {\n return index < this.$items.length - 1;\n },\n\n getPreviousItem: function (index) {\n if (this.isPreviousItem(index)) {\n return this.$items.eq(index - 1);\n }\n },\n\n getNextItem: function (index) {\n if (this.isNextItem(index)) {\n return this.$items.eq(index + 1);\n }\n },\n\n getItemToTheLeft: function (index) {\n var func = Garnish.ltr ? 'Previous' : 'Next';\n\n if (this['is' + func + 'Item'](index)) {\n if (this.settings.horizontal) {\n return this['get' + func + 'Item'](index);\n }\n if (!this.settings.vertical) {\n return this.getClosestItem(index, Garnish.X_AXIS, '<');\n }\n }\n },\n\n getItemToTheRight: function (index) {\n var func = Garnish.ltr ? 'Next' : 'Previous';\n\n if (this['is' + func + 'Item'](index)) {\n if (this.settings.horizontal) {\n return this['get' + func + 'Item'](index);\n } else if (!this.settings.vertical) {\n return this.getClosestItem(index, Garnish.X_AXIS, '>');\n }\n }\n },\n\n getItemAbove: function (index) {\n if (this.isPreviousItem(index)) {\n if (this.settings.vertical) {\n return this.getPreviousItem(index);\n } else if (!this.settings.horizontal) {\n return this.getClosestItem(index, Garnish.Y_AXIS, '<');\n }\n }\n },\n\n getItemBelow: function (index) {\n if (this.isNextItem(index)) {\n if (this.settings.vertical) {\n return this.getNextItem(index);\n } else if (!this.settings.horizontal) {\n return this.getClosestItem(index, Garnish.Y_AXIS, '>');\n }\n }\n },\n\n getClosestItem: function (index, axis, dir) {\n var axisProps = Garnish.Select.closestItemAxisProps[axis],\n dirProps = Garnish.Select.closestItemDirectionProps[dir];\n\n var $thisItem = this.$items.eq(index),\n thisOffset = $thisItem.offset(),\n thisMidpoint =\n thisOffset[axisProps.midpointOffset] +\n Math.round($thisItem[axisProps.midpointSizeFunc]() / 2),\n otherRowPos = null,\n smallestMidpointDiff = null,\n $closestItem = null;\n\n // Go the other way if this is the X axis and a RTL page\n var step;\n\n if (Garnish.rtl && axis === Garnish.X_AXIS) {\n step = dirProps.step * -1;\n } else {\n step = dirProps.step;\n }\n\n for (\n var i = index + step;\n typeof this.$items[i] !== 'undefined';\n i += step\n ) {\n var $otherItem = this.$items.eq(i),\n otherOffset = $otherItem.offset();\n\n // Are we on the next row yet?\n if (\n dirProps.isNextRow(\n otherOffset[axisProps.rowOffset],\n thisOffset[axisProps.rowOffset]\n )\n ) {\n // Is this the first time we've seen this row?\n if (otherRowPos === null) {\n otherRowPos = otherOffset[axisProps.rowOffset];\n }\n // Have we gone too far?\n else if (otherOffset[axisProps.rowOffset] !== otherRowPos) {\n break;\n }\n\n var otherMidpoint =\n otherOffset[axisProps.midpointOffset] +\n Math.round($otherItem[axisProps.midpointSizeFunc]() / 2),\n midpointDiff = Math.abs(thisMidpoint - otherMidpoint);\n\n // Are we getting warmer?\n if (\n smallestMidpointDiff === null ||\n midpointDiff < smallestMidpointDiff\n ) {\n smallestMidpointDiff = midpointDiff;\n $closestItem = $otherItem;\n }\n // Getting colder?\n else {\n break;\n }\n }\n // Getting colder?\n else if (\n dirProps.isWrongDirection(\n otherOffset[axisProps.rowOffset],\n thisOffset[axisProps.rowOffset]\n )\n ) {\n break;\n }\n }\n\n return $closestItem;\n },\n\n getFurthestItemToTheLeft: function (index) {\n return this.getFurthestItem(index, 'ToTheLeft');\n },\n\n getFurthestItemToTheRight: function (index) {\n return this.getFurthestItem(index, 'ToTheRight');\n },\n\n getFurthestItemAbove: function (index) {\n return this.getFurthestItem(index, 'Above');\n },\n\n getFurthestItemBelow: function (index) {\n return this.getFurthestItem(index, 'Below');\n },\n\n getFurthestItem: function (index, dir) {\n var $item, $testItem;\n\n while (($testItem = this['getItem' + dir](index))) {\n $item = $testItem;\n index = this.getItemIndex($item);\n }\n\n return $item;\n },\n\n /**\n * totalSelected getter\n */\n get totalSelected() {\n return this.getTotalSelected();\n },\n\n /**\n * Get Total Selected\n */\n getTotalSelected: function () {\n return this.$selectedItems.length;\n },\n\n /**\n * Add Items\n */\n addItems: function (items) {\n var $items = $(items);\n\n for (var i = 0; i < $items.length; i++) {\n var item = $items[i];\n\n // Make sure this element doesn't belong to another selector\n if ($.data(item, 'select')) {\n console.warn('Element was added to more than one selector');\n $.data(item, 'select').removeItems(item);\n }\n\n // Add the item\n $.data(item, 'select', this);\n\n // Get the handle\n var $handle;\n\n if (this.settings.handle) {\n if (typeof this.settings.handle === 'object') {\n $handle = $(this.settings.handle);\n } else if (typeof this.settings.handle === 'string') {\n $handle = $(item).find(this.settings.handle);\n } else if (typeof this.settings.handle === 'function') {\n $handle = $(this.settings.handle(item));\n }\n } else {\n $handle = $(item);\n }\n\n $.data(item, 'select-handle', $handle);\n $handle.data('select-item', item);\n\n // Get the checkbox element\n let $checkbox;\n if (this.settings.checkboxClass) {\n $checkbox = $(item).find(`.${this.settings.checkboxClass}`);\n }\n\n this.addListener($handle, 'mousedown', 'onMouseDown');\n this.addListener($handle, 'mouseup', 'onMouseUp');\n this.addListener($handle, 'click', function () {\n this.ignoreClick = true;\n });\n\n if ($checkbox && $checkbox.length) {\n $checkbox.data('select-item', item);\n this.addListener($checkbox, 'keydown', (event) => {\n if (\n event.keyCode === Garnish.SPACE_KEY ||\n event.keyCode === Garnish.RETURN_KEY\n ) {\n event.preventDefault();\n this.onCheckboxActivate(event);\n }\n });\n }\n\n this.addListener(item, 'keydown', 'onKeyDown');\n }\n\n this.$items = this.$items.add($items);\n this.updateIndexes();\n },\n\n /**\n * Remove Items\n */\n removeItems: function (items) {\n items = $.makeArray(items);\n\n var itemsChanged = false,\n selectionChanged = false;\n\n for (var i = 0; i < items.length; i++) {\n var item = items[i];\n\n // Make sure we actually know about this item\n var index = $.inArray(item, this.$items);\n if (index !== -1) {\n this._deinitItem(item);\n this.$items.splice(index, 1);\n itemsChanged = true;\n\n var selectedIndex = $.inArray(item, this.$selectedItems);\n if (selectedIndex !== -1) {\n this.$selectedItems.splice(selectedIndex, 1);\n selectionChanged = true;\n }\n }\n }\n\n if (itemsChanged) {\n this.updateIndexes();\n\n if (selectionChanged) {\n $(items).removeClass(this.settings.selectedClass);\n this.onSelectionChange();\n }\n }\n },\n\n /**\n * Remove All Items\n */\n removeAllItems: function () {\n for (var i = 0; i < this.$items.length; i++) {\n this._deinitItem(this.$items[i]);\n }\n\n this.$items = $();\n this.$selectedItems = $();\n this.updateIndexes();\n },\n\n /**\n * Update First/Last indexes\n */\n updateIndexes: function () {\n if (this.first !== null) {\n this.first = this.getItemIndex(this.$first);\n this.setFocusableItem(this.$first);\n } else if (this.$items.length) {\n this.setFocusableItem($(this.$items[0]));\n }\n\n if (this.$focusedItem) {\n this.setFocusableItem(this.$focusedItem);\n this.focusItem(this.$focusedItem, true);\n }\n\n if (this.last !== null) {\n this.last = this.getItemIndex(this.$last);\n }\n },\n\n /**\n * Reset Item Order\n */\n resetItemOrder: function () {\n this.$items = $().add(this.$items);\n this.$selectedItems = $().add(this.$selectedItems);\n this.updateIndexes();\n },\n\n /**\n * Sets the focusable item.\n *\n * We only want to have one focusable item per selection list, so that the user\n * doesn't have to tab through a million items.\n *\n * @param {object} $item\n */\n setFocusableItem: function ($item) {\n if (this.settings.makeFocusable) {\n if (this.$focusable) {\n this.$focusable.removeAttr('tabindex');\n }\n\n this.$focusable = $item.attr('tabindex', '0');\n }\n },\n\n /**\n * Sets the focus on an item.\n */\n focusItem: function ($item, preventScroll) {\n if (this.settings.makeFocusable) {\n $item[0].focus({preventScroll: !!preventScroll});\n }\n this.$focusedItem = $item;\n this.trigger('focusItem', {item: $item});\n },\n\n /**\n * Get Selected Items\n */\n getSelectedItems: function () {\n return $(this.$selectedItems.toArray());\n },\n\n /**\n * Destroy\n */\n destroy: function () {\n this.$container.removeData('select');\n this.removeAllItems();\n this.base();\n },\n\n // Events\n // ---------------------------------------------------------------------\n\n /**\n * On Mouse Down\n */\n onMouseDown: function (ev) {\n this.mousedownTarget = null;\n\n // ignore right/ctrl-clicks\n if (!Garnish.isPrimaryClick(ev) && !Garnish.isCtrlKeyPressed(ev)) {\n return;\n }\n\n // Enforce the filter\n if (this.settings.filter && !$(ev.target).is(this.settings.filter)) {\n return;\n }\n\n var $item = $($.data(ev.currentTarget, 'select-item'));\n\n if (this.first !== null && ev.shiftKey) {\n // Shift key is consistent for both selection modes\n this.selectRange($item, true);\n } else if (this._actAsCheckbox(ev) && !this.isSelected($item)) {\n // Checkbox-style selection is handled from onMouseUp()\n this.selectItem($item, true, true);\n } else {\n // Prepare for click handling in onMouseUp()\n this.mousedownTarget = ev.currentTarget;\n }\n },\n\n /**\n * On Mouse Up\n */\n onMouseUp: function (ev) {\n // ignore right clicks\n if (!Garnish.isPrimaryClick(ev) && !Garnish.isCtrlKeyPressed(ev)) {\n return;\n }\n\n // Enforce the filter\n if (this.settings.filter && !$(ev.target).is(this.settings.filter)) {\n return;\n }\n\n var $item = $($.data(ev.currentTarget, 'select-item'));\n\n // was this a click?\n if (!ev.shiftKey && ev.currentTarget === this.mousedownTarget) {\n // If this is already selected, wait a moment to see if this is a double click before making any rash decisions\n if (this.isSelected($item)) {\n this.clearMouseUpTimeout();\n\n this.mouseUpTimeout = setTimeout(\n function () {\n if (this._actAsCheckbox(ev)) {\n this.deselectItem($item);\n } else {\n this.deselectOthers($item);\n }\n }.bind(this),\n 300\n );\n } else if (!this._actAsCheckbox(ev)) {\n // Checkbox-style deselection is handled from onMouseDown()\n this.deselectAll();\n this.selectItem($item, true, true);\n }\n }\n },\n\n onCheckboxActivate: function (ev) {\n ev.stopImmediatePropagation();\n const $item = $($.data(event.currentTarget, 'select-item'));\n\n if (!this.isSelected($item)) {\n this.selectItem($item);\n } else {\n this.deselectItem($item);\n }\n },\n\n /**\n * On Key Down\n */\n onKeyDown: function (ev) {\n // Ignore if the focus isn't on one of our items\n if (ev.target !== ev.currentTarget) {\n return;\n }\n\n var ctrlKey = Garnish.isCtrlKeyPressed(ev);\n var shiftKey = ev.shiftKey;\n\n var anchor, $item;\n\n if (!this.settings.checkboxMode || !this.$focusable.length) {\n anchor = ev.shiftKey ? this.last : this.first;\n } else {\n anchor = $.inArray(this.$focusable[0], this.$items);\n\n if (anchor === -1) {\n anchor = 0;\n }\n }\n\n // Ok, what are we doing here?\n switch (ev.keyCode) {\n case Garnish.LEFT_KEY: {\n ev.preventDefault();\n\n // Select the last item if none are selected\n if (this.first === null) {\n if (Garnish.ltr) {\n $item = this.getLastItem();\n } else {\n $item = this.getFirstItem();\n }\n } else {\n if (ctrlKey) {\n $item = this.getFurthestItemToTheLeft(anchor);\n } else {\n $item = this.getItemToTheLeft(anchor);\n }\n }\n\n break;\n }\n\n case Garnish.RIGHT_KEY: {\n ev.preventDefault();\n\n // Select the first item if none are selected\n if (this.first === null) {\n if (Garnish.ltr) {\n $item = this.getFirstItem();\n } else {\n $item = this.getLastItem();\n }\n } else {\n if (ctrlKey) {\n $item = this.getFurthestItemToTheRight(anchor);\n } else {\n $item = this.getItemToTheRight(anchor);\n }\n }\n\n break;\n }\n\n case Garnish.UP_KEY: {\n ev.preventDefault();\n\n // Select the last item if none are selected\n if (this.first === null) {\n if (this.$focusable) {\n $item = this.$focusable.prev();\n }\n\n if (!this.$focusable || !$item.length) {\n $item = this.getLastItem();\n }\n } else {\n if (ctrlKey) {\n $item = this.getFurthestItemAbove(anchor);\n } else {\n $item = this.getItemAbove(anchor);\n }\n\n if (!$item) {\n $item = this.getFirstItem();\n }\n }\n\n break;\n }\n\n case Garnish.DOWN_KEY: {\n ev.preventDefault();\n\n // Select the first item if none are selected\n if (this.first === null) {\n if (this.$focusable) {\n $item = this.$focusable.next();\n }\n\n if (!this.$focusable || !$item.length) {\n $item = this.getFirstItem();\n }\n } else {\n if (ctrlKey) {\n $item = this.getFurthestItemBelow(anchor);\n } else {\n $item = this.getItemBelow(anchor);\n }\n\n if (!$item) {\n $item = this.getLastItem();\n }\n }\n\n break;\n }\n\n case Garnish.SPACE_KEY: {\n if (!ctrlKey && !shiftKey) {\n ev.preventDefault();\n\n if (this.isSelected(this.$focusable)) {\n if (this._canDeselect(this.$focusable)) {\n this.deselectItem(this.$focusable);\n }\n } else {\n this.selectItem(this.$focusable, true, false);\n }\n }\n\n break;\n }\n\n case Garnish.A_KEY: {\n if (ctrlKey) {\n ev.preventDefault();\n this.selectAll();\n }\n\n break;\n }\n }\n\n // Is there an item queued up for focus/selection?\n if ($item && $item.length) {\n if (!this.settings.checkboxMode) {\n // select it\n if (this.first !== null && ev.shiftKey) {\n this.selectRange($item, false);\n } else {\n this.deselectAll();\n this.selectItem($item, true, false);\n }\n } else {\n // just set the new item to be focusable\n this.setFocusableItem($item);\n if (this.settings.makeFocusable) {\n $item.focus();\n }\n this.$focusedItem = $item;\n this.trigger('focusItem', {item: $item});\n }\n }\n },\n\n /**\n * Set Callback Timeout\n */\n onSelectionChange: function () {\n if (this.callbackFrame) {\n Garnish.cancelAnimationFrame(this.callbackFrame);\n this.callbackFrame = null;\n }\n\n this.callbackFrame = Garnish.requestAnimationFrame(\n function () {\n this.callbackFrame = null;\n this.trigger('selectionChange');\n this.settings.onSelectionChange();\n }.bind(this)\n );\n },\n\n // Private methods\n // ---------------------------------------------------------------------\n\n _actAsCheckbox: function (ev) {\n if (Garnish.isCtrlKeyPressed(ev)) {\n return !this.settings.checkboxMode;\n } else {\n return this.settings.checkboxMode;\n }\n },\n\n _canDeselect: function ($items) {\n return this.settings.allowEmpty || this.totalSelected > $items.length;\n },\n\n _selectItems: function ($items) {\n $items.addClass(this.settings.selectedClass);\n\n if (this.settings.checkboxClass) {\n const $checkboxes = $items.find(`.${this.settings.checkboxClass}`);\n $checkboxes.attr('aria-checked', 'true');\n }\n\n this.$selectedItems = this.$selectedItems.add($items);\n this.onSelectionChange();\n },\n\n _deselectItems: function ($items) {\n $items.removeClass(this.settings.selectedClass);\n\n if (this.settings.checkboxClass) {\n const $checkboxes = $items.find(`.${this.settings.checkboxClass}`);\n $checkboxes.attr('aria-checked', 'false');\n }\n\n this.$selectedItems = this.$selectedItems.not($items);\n this.onSelectionChange();\n },\n\n /**\n * Deinitialize an item.\n */\n _deinitItem: function (item) {\n var $handle = $.data(item, 'select-handle');\n\n if ($handle) {\n $handle.removeData('select-item');\n this.removeAllListeners($handle);\n }\n\n $.removeData(item, 'select');\n $.removeData(item, 'select-handle');\n\n if (this.$focusedItem && this.$focusedItem[0] === item) {\n this.$focusedItem = null;\n }\n },\n },\n {\n defaults: {\n selectedClass: 'sel',\n checkboxClass: 'checkbox',\n multi: false,\n allowEmpty: true,\n vertical: false,\n horizontal: false,\n handle: null,\n filter: null,\n checkboxMode: false,\n makeFocusable: false,\n onSelectionChange: $.noop,\n },\n\n closestItemAxisProps: {\n x: {\n midpointOffset: 'top',\n midpointSizeFunc: 'outerHeight',\n rowOffset: 'left',\n },\n y: {\n midpointOffset: 'left',\n midpointSizeFunc: 'outerWidth',\n rowOffset: 'top',\n },\n },\n\n closestItemDirectionProps: {\n '<': {\n step: -1,\n isNextRow: function (a, b) {\n return a < b;\n },\n isWrongDirection: function (a, b) {\n return a > b;\n },\n },\n '>': {\n step: 1,\n isNextRow: function (a, b) {\n return a > b;\n },\n isWrongDirection: function (a, b) {\n return a < b;\n },\n },\n },\n }\n);\n","import Garnish from './Garnish.js';\nimport CustomSelect from './CustomSelect.js';\nimport $ from 'jquery';\n\n/**\n * Select Menu\n */\nexport default CustomSelect.extend(\n {\n /**\n * Constructor\n */\n init: function (btn, options, settings, callback) {\n // argument mapping\n if (typeof settings === 'function') {\n // (btn, options, callback)\n callback = settings;\n settings = {};\n }\n\n settings = $.extend({}, Garnish.SelectMenu.defaults, settings);\n\n this.base(btn, options, settings, callback);\n\n this.selected = -1;\n },\n\n /**\n * Build\n */\n build: function () {\n this.base();\n\n if (this.selected !== -1) {\n this._addSelectedOptionClass(this.selected);\n }\n },\n\n /**\n * Select\n */\n select: function (option) {\n // ignore if it's already selected\n if (option === this.selected) {\n return;\n }\n\n if (this.dom.ul) {\n if (this.selected !== -1) {\n this.dom.options[this.selected].className = '';\n }\n\n this._addSelectedOptionClass(option);\n }\n\n this.selected = option;\n\n // set the button text to the selected option\n this.setBtnText($(this.options[option].label).text());\n\n this.base(option);\n },\n\n /**\n * Add Selected Option Class\n */\n _addSelectedOptionClass: function (option) {\n this.dom.options[option].className = 'sel';\n },\n\n /**\n * Set Button Text\n */\n setBtnText: function (text) {\n this.dom.$btnLabel.text(text);\n },\n },\n {\n defaults: {\n ulClass: 'menu select',\n },\n }\n);\n","import Garnish from './Garnish.js';\nimport Base from './Base.js';\nimport $ from 'jquery';\n\n/**\n * UI Layer Manager class\n *\n * This is used to manage the visible UI “layers”, including the base document, and any open modals, HUDs, slideouts, or menus.\n */\nexport default Base.extend({\n layers: null,\n\n init: function () {\n this.layers = [\n {\n $container: Garnish.$bod,\n shortcuts: [],\n options: {\n bubble: false,\n },\n },\n ];\n this.addListener(Garnish.$bod, 'keydown', 'triggerShortcut');\n },\n\n get layer() {\n return this.layers.length - 1;\n },\n\n get currentLayer() {\n return this.layers[this.layer];\n },\n\n get modalLayers() {\n return this.layers.filter((layer) => layer.isModal === true);\n },\n\n get highestModalLayer() {\n return this.modalLayers.pop();\n },\n\n /**\n * Registers a new UI layer.\n *\n * @param {jQuery|HTMLElement} [container]\n * @param {Object} [options]\n */\n addLayer: function (container, options) {\n if ($.isPlainObject(container)) {\n options = container;\n container = null;\n }\n\n options = Object.assign(\n {\n bubble: false,\n },\n options || {}\n );\n\n this.layers.push({\n $container: container ? $(container) : null,\n shortcuts: [],\n isModal: container ? $(container).attr('aria-modal') === 'true' : false,\n options: options,\n });\n this.trigger('addLayer', {\n layer: this.layer,\n $container: this.currentLayer.$container,\n options: options,\n });\n return this;\n },\n\n removeLayer: function (layer) {\n if (this.layer === 0) {\n throw 'Can’t remove the base layer.';\n }\n\n if (layer) {\n const layerIndex = this.getLayerIndex(layer);\n if (layerIndex) {\n this.removeLayerAtIndex(layerIndex);\n }\n } else {\n this.layers.pop();\n this.trigger('removeLayer');\n return this;\n }\n },\n\n getLayerIndex: function (layer) {\n layer = $(layer).get(0);\n let layerIndex;\n\n $(this.layers).each(function (index) {\n if (this.$container !== null && this.$container.get(0) === layer) {\n layerIndex = index;\n return false;\n }\n });\n\n return layerIndex;\n },\n\n removeLayerAtIndex: function (index) {\n this.layers.splice(index, 1);\n this.trigger('removeLayer');\n return this;\n },\n\n registerShortcut: function (shortcut, callback, layer) {\n shortcut = this._normalizeShortcut(shortcut);\n if (typeof layer === 'undefined') {\n layer = this.layer;\n }\n this.layers[layer].shortcuts.push({\n key: JSON.stringify(shortcut),\n shortcut: shortcut,\n callback: callback,\n });\n return this;\n },\n\n unregisterShortcut: function (shortcut, layer) {\n shortcut = this._normalizeShortcut(shortcut);\n const key = JSON.stringify(shortcut);\n if (typeof layer === 'undefined') {\n layer = this.layer;\n }\n const index = this.layers[layer].shortcuts.findIndex((s) => s.key === key);\n if (index !== -1) {\n this.layers[layer].shortcuts.splice(index, 1);\n }\n return this;\n },\n\n _normalizeShortcut: function (shortcut) {\n if (typeof shortcut === 'number') {\n shortcut = {keyCode: shortcut};\n }\n\n if (typeof shortcut.keyCode !== 'number') {\n throw 'Invalid shortcut';\n }\n\n return {\n keyCode: shortcut.keyCode,\n ctrl: !!shortcut.ctrl,\n shift: !!shortcut.shift,\n alt: !!shortcut.alt,\n };\n },\n\n triggerShortcut: function (ev, layerIndex) {\n if (typeof layerIndex === 'undefined') {\n layerIndex = this.layer;\n }\n const layer = this.layers[layerIndex];\n const shortcut = layer.shortcuts.find(\n (s) =>\n s.shortcut.keyCode === ev.keyCode &&\n s.shortcut.ctrl === Garnish.isCtrlKeyPressed(ev) &&\n s.shortcut.shift === ev.shiftKey &&\n s.shortcut.alt === ev.altKey\n );\n\n if (shortcut) {\n ev.preventDefault();\n shortcut.callback(ev);\n } else if (layer.options.bubble && layerIndex > 0) {\n this.triggerShortcut(ev, layerIndex - 1);\n }\n },\n});\n","import $ from 'jquery';\nimport Base from './Base.js';\nimport BaseDrag from './BaseDrag.js';\nimport CheckboxSelect from './CheckboxSelect.js';\nimport ContextMenu from './ContextMenu.js';\nimport CustomSelect from './CustomSelect.js';\nimport DisclosureMenu from './DisclosureMenu.js';\nimport Drag from './Drag.js';\nimport DragDrop from './DragDrop.js';\nimport DragMove from './DragMove.js';\nimport DragSort from './DragSort.js';\nimport EscManager from './EscManager.js';\nimport HUD from './HUD.js';\nimport MenuBtn from './MenuBtn.js';\nimport MixedInput from './MixedInput.js';\nimport Modal from './Modal.js';\nimport MultiFunctionBtn from './MultiFunctionBtn.js';\nimport NiceText from './NiceText.js';\nimport Select from './Select.js';\nimport SelectMenu from './SelectMenu.js';\nimport UiLayerManager from './UiLayerManager.js';\n\n/**\n * @namespace Garnish\n */\n\n// Bail if Garnish is already defined\nif (typeof Garnish !== 'undefined') {\n throw 'Garnish is already defined!';\n}\n\nlet Garnish = {\n // jQuery objects for common elements\n $win: $(window),\n $doc: $(document),\n $bod: $(document.body),\n};\n\nGarnish.rtl = Garnish.$bod.hasClass('rtl');\nGarnish.ltr = !Garnish.rtl;\n\nGarnish = $.extend(Garnish, {\n $scrollContainer: Garnish.$win,\n\n // Key code constants\n BACKSPACE_KEY: 8,\n TAB_KEY: 9,\n CLEAR_KEY: 12,\n RETURN_KEY: 13,\n SHIFT_KEY: 16,\n CTRL_KEY: 17,\n ALT_KEY: 18,\n ESC_KEY: 27,\n SPACE_KEY: 32,\n PAGE_UP_KEY: 33,\n PAGE_DOWN_KEY: 34,\n END_KEY: 35,\n HOME_KEY: 36,\n LEFT_KEY: 37,\n UP_KEY: 38,\n RIGHT_KEY: 39,\n DOWN_KEY: 40,\n DELETE_KEY: 46,\n A_KEY: 65,\n S_KEY: 83,\n CMD_KEY: 91,\n META_KEY: 224,\n\n // ARIA hidden classes\n JS_ARIA_CLASS: 'garnish-js-aria',\n JS_ARIA_TRUE_CLASS: 'garnish-js-aria-true',\n JS_ARIA_FALSE_CLASS: 'garnish-js-aria-false',\n\n // Mouse button constants\n PRIMARY_CLICK: 1,\n SECONDARY_CLICK: 3,\n\n // Axis constants\n X_AXIS: 'x',\n Y_AXIS: 'y',\n\n FX_DURATION: 200,\n\n // Node types\n TEXT_NODE: 3,\n\n /**\n * Logs a message to the browser's console, if the browser has one.\n *\n * @param {string} msg\n * @deprecated\n */\n log: function (msg) {\n if (typeof console !== 'undefined' && typeof console.log === 'function') {\n console.log(msg);\n }\n },\n\n _isMobileBrowser: null,\n _isMobileOrTabletBrowser: null,\n\n /**\n * Returns whether this is a mobile browser.\n * Detection script courtesy of http://detectmobilebrowsers.com\n *\n * Last updated: 2014-11-24\n *\n * @param {boolean} detectTablets\n * @return {boolean}\n */\n isMobileBrowser: function (detectTablets) {\n var key = detectTablets ? '_isMobileOrTabletBrowser' : '_isMobileBrowser';\n\n if (Garnish[key] === null) {\n var a = navigator.userAgent || navigator.vendor || window.opera;\n Garnish[key] =\n new RegExp(\n '(android|bbd+|meego).+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap|windows ce|xda|xiino' +\n (detectTablets ? '|android|ipad|playbook|silk' : ''),\n 'i'\n ).test(a) ||\n /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\\-|your|zeto|zte\\-/i.test(\n a.substring(0, 4)\n );\n }\n\n return Garnish[key];\n },\n\n /**\n * Returns whether user prefers reduced motion\n *\n * @return {boolean}\n */\n prefersReducedMotion: function () {\n // Grab the prefers reduced media query.\n const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');\n\n // Check if the media query matches or is not available.\n return !mediaQuery || mediaQuery.matches;\n },\n\n /**\n * Returns either '0' or a set duration, based on a user's prefers-reduced-motion setting\n * Used to set the duration inside the Velocity.js options object in a way that respects user preferences\n * @param {string|integer} duration Either a ms duration or a named jQuery duration (i.e. 'fast', 'slow')\n * @return {string|integer}\n */\n getUserPreferredAnimationDuration: function (duration) {\n return Garnish.prefersReducedMotion() ? 0 : duration;\n },\n\n /**\n * Returns whether a variable is an array.\n *\n * @param {object} val\n * @return {boolean}\n * @deprecated\n */\n isArray: function (val) {\n return Array.isArray(val);\n },\n\n /**\n * Returns whether a variable is a jQuery collection.\n *\n * @param {object} val\n * @return {boolean}\n */\n isJquery: function (val) {\n return val instanceof $;\n },\n\n /**\n * Returns whether a variable is a string.\n *\n * @param {object} val\n * @return {boolean}\n */\n isString: function (val) {\n return typeof val === 'string';\n },\n\n /**\n * Returns whether an element has an attribute.\n *\n * @see http://stackoverflow.com/questions/1318076/jquery-hasattr-checking-to-see-if-there-is-an-attribute-on-an-element/1318091#1318091\n */\n hasAttr: function (elem, attr) {\n var val = $(elem).attr(attr);\n return typeof val !== 'undefined' && val !== false;\n },\n\n /**\n * Returns whether something is a text node.\n *\n * @param {object} elem\n * @return {boolean}\n */\n isTextNode: function (elem) {\n return elem.nodeType === Garnish.TEXT_NODE;\n },\n\n /**\n * Returns the offset of an element within the scroll container, whether that's the window or something else\n */\n getOffset: function (elem) {\n this.getOffset._offset = $(elem).offset();\n\n if (Garnish.$scrollContainer[0] !== Garnish.$win[0]) {\n this.getOffset._offset.top += Garnish.$scrollContainer.scrollTop();\n this.getOffset._offset.left += Garnish.$scrollContainer.scrollLeft();\n }\n\n return this.getOffset._offset;\n },\n\n /**\n * Returns the distance between two coordinates.\n *\n * @param {number} x1 The first coordinate's X position.\n * @param {number} y1 The first coordinate's Y position.\n * @param {number} x2 The second coordinate's X position.\n * @param {number} y2 The second coordinate's Y position.\n * @return {number}\n */\n getDist: function (x1, y1, x2, y2) {\n return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));\n },\n\n /**\n * Returns whether an element is touching an x/y coordinate.\n *\n * @param {number} x The coordinate's X position.\n * @param {number} y The coordinate's Y position.\n * @param {object} elem Either an actual element or a jQuery collection.\n * @return {boolean}\n */\n hitTest: function (x, y, elem) {\n Garnish.hitTest._$elem = $(elem);\n Garnish.hitTest._offset = Garnish.hitTest._$elem.offset();\n Garnish.hitTest._x1 = Garnish.hitTest._offset.left;\n Garnish.hitTest._y1 = Garnish.hitTest._offset.top;\n Garnish.hitTest._x2 =\n Garnish.hitTest._x1 + Garnish.hitTest._$elem.outerWidth();\n Garnish.hitTest._y2 =\n Garnish.hitTest._y1 + Garnish.hitTest._$elem.outerHeight();\n\n return (\n x >= Garnish.hitTest._x1 &&\n x < Garnish.hitTest._x2 &&\n y >= Garnish.hitTest._y1 &&\n y < Garnish.hitTest._y2\n );\n },\n\n /**\n * Returns whether the cursor is touching an element.\n *\n * @param {object} ev The mouse event object containing pageX and pageY properties.\n * @param {object} elem Either an actual element or a jQuery collection.\n * @return {boolean}\n */\n isCursorOver: function (ev, elem) {\n return Garnish.hitTest(ev.pageX, ev.pageY, elem);\n },\n\n /**\n * Copies text styles from one element to another.\n *\n * @param {object} source The source element. Can be either an actual element or a jQuery collection.\n * @param {object} target The target element. Can be either an actual element or a jQuery collection.\n */\n copyTextStyles: function (source, target) {\n var $source = $(source),\n $target = $(target);\n\n $target.css({\n fontFamily: $source.css('fontFamily'),\n fontSize: $source.css('fontSize'),\n fontWeight: $source.css('fontWeight'),\n letterSpacing: $source.css('letterSpacing'),\n lineHeight: $source.css('lineHeight'),\n textAlign: $source.css('textAlign'),\n textIndent: $source.css('textIndent'),\n whiteSpace: $source.css('whiteSpace'),\n wordSpacing: $source.css('wordSpacing'),\n wordWrap: $source.css('wordWrap'),\n });\n },\n\n /**\n * Adds modal ARIA and role attributes to a container\n *\n * @param {object} container The container element. Can be either an actual element or a jQuery collection.\n */\n addModalAttributes: function (container) {\n const $container = $(container);\n\n $(container).attr({\n 'aria-modal': 'true',\n role: 'dialog',\n });\n },\n\n /**\n * Hide immediate descendants of the body element from screen readers\n *\n */\n hideModalBackgroundLayers: function () {\n const topmostLayer = Garnish.uiLayerManager.currentLayer.$container.get(0);\n\n Garnish.$bod.children().each(function () {\n // If element is modal or already has jsAria class, do nothing\n if (Garnish.hasJsAriaClass(this) || this === topmostLayer) return;\n\n if (!Garnish.isScriptOrStyleElement(this)) {\n Garnish.ariaHide(this);\n }\n });\n },\n\n /**\n * Un-hide elements based on currently active layers\n *\n */\n resetModalBackgroundLayerVisibility: function () {\n const highestModalLayer = Garnish.uiLayerManager.highestModalLayer;\n const hiddenLayerClasses = [\n Garnish.JS_ARIA_CLASS,\n Garnish.JS_ARIA_TRUE_CLASS,\n Garnish.JS_ARIA_FALSE_CLASS,\n ];\n\n // If there is another modal, make it accessible to AT\n if (highestModalLayer) {\n highestModalLayer.$container\n .removeClass(hiddenLayerClasses)\n .removeAttr('aria-hidden');\n return;\n }\n\n // If no more modals in DOM, loop through hidden elements and un-hide them\n const hiddenLayerSelector = hiddenLayerClasses\n .map((name) => '.' + name)\n .join(', ');\n const hiddenElements = $(hiddenLayerSelector);\n\n $(hiddenElements).each(function () {\n if ($(this).hasClass(Garnish.JS_ARIA_CLASS)) {\n $(this).removeClass(Garnish.JS_ARIA_CLASS);\n $(this).removeAttr('aria-hidden');\n } else if ($(this).hasClass(Garnish.JS_ARIA_FALSE_CLASS)) {\n $(this).removeClass(Garnish.JS_ARIA_FALSE_CLASS);\n $(this).attr('aria-hidden', false);\n } else if ($(this).hasClass(Garnish.JS_ARIA_TRUE_CLASS)) {\n $(this).removeClass(Garnish.JS_ARIA_TRUE_CLASS);\n $(this).attr('aria-hidden', true);\n }\n });\n },\n\n /**\n * Apply aria-hidden=\"true\" to element and store previous value as class\n *\n * @param {object} element The element. Can be either an actual element or a jQuery collection.\n */\n ariaHide: function (element) {\n const ariaHiddenAttribute = $(element).attr('aria-hidden');\n\n // Capture initial aria-hidden values in an applied class\n if (!ariaHiddenAttribute) {\n $(element).addClass(Garnish.JS_ARIA_CLASS);\n } else if (ariaHiddenAttribute === 'false') {\n $(element).addClass(Garnish.JS_ARIA_FALSE_CLASS);\n } else if (ariaHiddenAttribute === 'true') {\n $(element).addClass(Garnish.JS_ARIA_TRUE_CLASS);\n }\n\n $(element).attr('aria-hidden', 'true');\n },\n\n /**\n * Checks to see if element is