UNPKG

@atlassian/aui

Version:

Atlassian User Interface Framework

1,575 lines (1,322 loc) 854 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var skate = _interopDefault(require('skatejs')); var _ = _interopDefault(require('underscore')); var Tether = _interopDefault(require('tether')); var template = _interopDefault(require('skatejs-template-html')); var Backbone = _interopDefault(require('backbone')); require('jquery'); var cssEscape = _interopDefault(require('css.escape')); // iconfont v1: ADG 1 + 2 // these icons are deprecated. // import '../../src/less/adg-icons.less'; /* eslint no-cond-assign: off */ /** * Replaces tokens in a string with arguments, similar to Java's MessageFormat. * Tokens are in the form {0}, {1}, {2}, etc. * * This version also provides support for simple choice formats (excluding floating point numbers) of the form * {0,choice,0#0 issues|1#1 issue|1<{0,number} issues} * * Number format is currently not implemented, tokens of the form {0,number} will simply be printed as {0} * * @method format * @param message the message to replace tokens in * @param arg (optional) replacement value for token {0}, with subsequent arguments being {1}, etc. * @return {String} the message with the tokens replaced * @usage formatString("This is a {0} test", "simple"); */ function formatString (message) { var apos = /'(?!')/g; // founds "'", but not "''" // TODO: does not work for floating point numbers! var simpleFormat = /^\d+$/; var numberFormat = /^(\d+),number$/; // TODO: incomplete, as doesn't support floating point numbers var choiceFormat = /^(\d+)\,choice\,(.+)/; var choicePart = /^(\d+)([#<])(.+)/; // we are caching RegExps, so will not spend time on recreating them on each call // formats a value, currently choice and simple replacement are implemented, proper var getParamValue = function (format, args) { // simple substitute var res = ''; var match; if (match = format.match(simpleFormat)) { // TODO: heavy guns for checking whether format is a simple number... res = args.length > ++format ? args[format] : ''; // use the argument as is, or use '' if not found } // number format else if (match = format.match(numberFormat)) { // TODO: doesn't actually format the number... res = args.length > ++match[1] ? args[match[1]] : ''; } // choice format else if (match = format.match(choiceFormat)) { // format: "0,choice,0#0 issues|1#1 issue|1<{0,number} issues" // match[0]: "0,choice,0#0 issues|1#1 issue|1<{0,number} issues" // match[1]: "0" // match[2]: "0#0 issues|1#1 issue|1<{0,number} issues" // get the argument value we base the choice on var value = (args.length > ++match[1] ? args[match[1]] : null); if (value !== null) { // go through all options, checking against the number, according to following formula, // if X < the first entry then the first entry is returned, if X > last entry, the last entry is returned // // X matches j if and only if limit[j] <= X < limit[j+1] // var options = match[2].split('|'); var prevOptionValue = null; // holds last passed option for (var i = 0; i < options.length; i++) { // option: "0#0 issues" // part[0]: "0#0 issues" // part[1]: "0" // part[2]: "#" // part[3]" "0 issues"; var parts = options[i].match(choicePart); // if value is smaller, we take the previous value, or the current if no previous exists var argValue = parseInt(parts[1], 10); if (value < argValue) { if (prevOptionValue) { res = prevOptionValue; break; } else { res = parts[3]; break; } } // if value is equal the condition, and the match is equality match we accept it if (value == argValue && parts[2] == '#') { res = parts[3]; break; } // check whether we are the last option, in which case accept it even if the option does not match if (i == options.length - 1) { res = parts[3]; } // retain current option prevOptionValue = parts[3]; } // run result through format, as the parts might contain substitutes themselves var formatArgs = [res].concat(Array.prototype.slice.call(args, 1)); res = formatString.apply(null, formatArgs); } } return res; }; // drop in replacement for the token regex // splits the message to return the next accurance of a i18n placeholder. // Does not use regexps as we need to support nested placeholders // text between single ticks ' are ignored var _performTokenRegex = function (message) { var tick = false; var openIndex = -1; var openCount = 0; for (var i = 0; i < message.length; i++) { // handle ticks var c = message.charAt(i); if (c == "'") { // toggle tick = !tick; } // skip if we are between ticks if (tick) { continue; } // check open brackets if (c === '{') { if (openCount === 0) { openIndex = i; } openCount++; } else if (c === '}') { if (openCount > 0) { openCount--; if (openCount === 0) { // we found a bracket match - generate the result array ( var match = []; match.push(message.substring(0, i + 1)); // from begin to match match.push(message.substring(0, openIndex)); // everything until match start match.push(message.substring(openIndex + 1, i)); // matched content return match; } } } } return null; }; var _formatString = function (message) { var args = arguments; var res = ''; if (!message) { return res; } var match = _performTokenRegex(message); while (match) { // reduce message to string after match message = message.substring(match[0].length); // add value before match to result res += match[1].replace(apos, ''); // add formatted parameter res += getParamValue(match[2], args); // check for next match match = _performTokenRegex(message); //message.match(token); } // add remaining message to result res += message.replace(apos, ''); return res; }; return _formatString.apply(null, arguments); } /** * @fileOverview * Provides the `AJS.format` function, which powers * all code transformed through the WRM's jsI18n transformation. * @note This behaviour really should be a part of the WRM itself. * @see https://ecosystem.atlassian.net/browse/PLUGWEB-109 */ // @note: this value is set via webpack and gulp // eslint-disable-next-line var version = "8.0.0-alpha.1"; var $ = window.jQuery || window.Zepto; function polyfillConsole (prop) { return function () { if (typeof console !== 'undefined' && console[prop]) { Function.prototype.apply.call(console[prop], console, arguments); } }; } var log = polyfillConsole('log'); var warn = polyfillConsole('warn'); var error = polyfillConsole('error'); /** * Triggers events on the window object. See jQuery trigger documentation for * more details. Exceptions are caught and logged. */ function trigger (eventType, extraParameters) { try { return $(window).trigger(eventType, extraParameters); } catch (e) { log('error while triggering: ' + e.message); } } const special = { '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', '\'': '&#39;', '`': '&#96;', }; const expr = new RegExp(`[${Object.keys(special).join('')}]`, 'g'); function escapeHtml (str) { return str.replace(expr, (str) => special[str]); } /** * @fileOverview * Includes the most minimal set of JavaScript * possible to make an Atlassian UI "work". * * This entry-point should *not* include anything that causes * a global side-effect. The only exception to this rule * is registering a global variable or AMD module. */ window.AJS; var deprecationCalls = []; var deprecatedSelectorMap = []; function toSentenceCase (str) { str += ''; if (!str) { return ''; } return str.charAt(0).toUpperCase() + str.substring(1); } function getDeprecatedLocation (printFrameOffset) { var err = new Error(); var stack = err.stack || err.stacktrace; var stackMessage = (stack && stack.replace(/^Error\n/, '')) || ''; if (stackMessage) { stackMessage = stackMessage.split('\n'); return stackMessage[printFrameOffset + 2]; } return stackMessage; } function logger () { if (typeof console !== 'undefined' && console.warn) { Function.prototype.apply.call(console.warn, console, arguments); } } /** * Return a function that logs a deprecation warning to the console the first time it is called from a certain location. * It will also print the stack frame of the calling function. * * @param {string} displayName the name of the thing being deprecated * @param {object} options * @param {string} options.removeInVersion the version this will be removed in * @param {string} options.alternativeName the name of an alternative to use * @param {string} options.sinceVersion the version this has been deprecated since * @param {string} options.extraInfo extra information to be printed at the end of the deprecation log * @param {string} options.extraObject an extra object that will be printed at the end * @param {string} options.deprecationType type of the deprecation to append to the start of the deprecation message. e.g. JS or CSS * @return {Function} that logs the warning and stack frame of the calling function. Takes in an optional parameter for the offset of * the stack frame to print, the default is 0. For example, 0 will log it for the line of the calling function, * -1 will print the location the logger was called from */ function getShowDeprecationMessage (displayName, options) { // This can be used internally to pas in a showmessage fn if (typeof displayName === 'function') { return displayName; } var called = false; options = options || {}; return function (printFrameOffset) { var deprecatedLocation = getDeprecatedLocation(printFrameOffset ? printFrameOffset : 1) || ''; // Only log once if the stack frame doesn't exist to avoid spamming the console/test output if (!called || deprecationCalls.indexOf(deprecatedLocation) === -1) { deprecationCalls.push(deprecatedLocation); called = true; var deprecationType = (options.deprecationType + ' ') || ''; var message = 'DEPRECATED ' + deprecationType + '- ' + toSentenceCase(displayName) + ' has been deprecated' + (options.sinceVersion ? ' since ' + options.sinceVersion : '') + ' and will be removed in ' + (options.removeInVersion || 'a future release') + '.'; if (options.alternativeName) { message += ' Use ' + options.alternativeName + ' instead. '; } if (options.extraInfo) { message += ' ' + options.extraInfo; } if (deprecatedLocation === '') { deprecatedLocation = ' \n ' + 'No stack trace of the deprecated usage is available in your current browser.'; } else { deprecatedLocation = ' \n ' + deprecatedLocation; } if (options.extraObject) { message += '\n'; logger(message, options.extraObject, deprecatedLocation); } else { logger(message, deprecatedLocation); } } }; } function logCssDeprecation (selectorMap, newNode) { var displayName = selectorMap.options.displayName; displayName = displayName ? ' (' + displayName + ')' : ''; var options = $.extend({ deprecationType: 'CSS', extraObject: newNode }, selectorMap.options); getShowDeprecationMessage('\'' + selectorMap.selector + '\' pattern' + displayName, options)(); } /** * Returns a wrapped version of the function that logs a deprecation warning when the function is used. * @param {Function} fn the fn to wrap * @param {string} displayName the name of the fn to be displayed in the message * @param {string} options.removeInVersion the version this will be removed in * @param {string} options.alternativeName the name of an alternative to use * @param {string} options.sinceVersion the version this has been deprecated since * @param {string} options.extraInfo extra information to be printed at the end of the deprecation log * @return {Function} wrapping the original function */ function deprecateFunctionExpression(fn, displayName, options) { options = options || {}; options.deprecationType = options.deprecationType || 'JS'; var showDeprecationMessage = getShowDeprecationMessage(displayName || fn.name || 'this function', options); return function () { showDeprecationMessage(); return fn.apply(this, arguments); }; } /** * Returns a wrapped version of the constructor that logs a deprecation warning when the constructor is instantiated. * @param {Function} constructorFn the constructor function to wrap * @param {string} displayName the name of the fn to be displayed in the message * @param {string} options.removeInVersion the version this will be removed in * @param {string} options.alternativeName the name of an alternative to use * @param {string} options.sinceVersion the version this has been deprecated since * @param {string} options.extraInfo extra information to be printed at the end of the deprecation log * @return {Function} wrapping the original function */ function deprecateConstructor(constructorFn, displayName, options) { options = options || {}; options.deprecationType = options.deprecationType || 'JS'; var deprecatedConstructor = deprecateFunctionExpression(constructorFn, displayName, options); deprecatedConstructor.prototype = constructorFn.prototype; $.extend(deprecatedConstructor, constructorFn); //copy static methods across; return deprecatedConstructor; } var supportsProperties = false; try { if (Object.defineProperty) { Object.defineProperty({}, 'blam', {get: function () {}, set: function () {}}); supportsProperties = true; } } catch (e) { /* IE8 doesn't support on non-DOM elements */ } /** * Wraps a "value" object property in a deprecation warning in browsers supporting Object.defineProperty * @param {Object} obj the object containing the property * @param {string} prop the name of the property to deprecate * @param {string} options.removeInVersion the version this will be removed in * @param {string} options.displayName the display name of the property to deprecate (optional, will fall back to the property name) * @param {string} options.alternativeName the name of an alternative to use * @param {string} options.sinceVersion the version this has been deprecated since * @param {string} options.extraInfo extra information to be printed at the end of the deprecation log */ function deprecateValueProperty(obj, prop, options) { if (supportsProperties) { var oldVal = obj[prop]; options = options || {}; options.deprecationType = options.deprecationType || 'JS'; var displayNameOrShowMessageFn = options.displayName || prop; var showDeprecationMessage = getShowDeprecationMessage(displayNameOrShowMessageFn, options); Object.defineProperty(obj, prop, { get: function () { showDeprecationMessage(); return oldVal; }, set: function (val) { oldVal = val; showDeprecationMessage(); return val; } }); } } /** * Wraps an object property in a deprecation warning, if possible. functions will always log warnings, but other * types of properties will only log in browsers supporting Object.defineProperty * @param {Object} obj the object containing the property * @param {string} prop the name of the property to deprecate * @param {string} options.removeInVersion the version this will be removed in * @param {string} options.displayName the display name of the property to deprecate (optional, will fall back to the property name) * @param {string} options.alternativeName the name of an alternative to use * @param {string} options.sinceVersion the version this has been deprecated since * @param {string} options.extraInfo extra information to be printed at the end of the deprecation log */ function deprecateObjectProperty(obj, prop, options) { if (typeof obj[prop] === 'function') { options = options || {}; options.deprecationType = options.deprecationType || 'JS'; var displayNameOrShowMessageFn = options.displayName || prop; obj[prop] = deprecateFunctionExpression(obj[prop], displayNameOrShowMessageFn, options); } else { deprecateValueProperty(obj, prop, options); } } function matchesSelector(el, selector) { return (el.matches || el.msMatchesSelector || el.webkitMatchesSelector || el.mozMatchesSelector || el.oMatchesSelector).call(el, selector); } function testAndHandleDeprecation(newNode) { return function (selectorMap) { if (matchesSelector(newNode, selectorMap.selector)) { logCssDeprecation(selectorMap, newNode); } }; } if (window.MutationObserver) { var observer = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { // TODO - should this also look at class changes, if possible? var addedNodes = mutation.addedNodes; for (var i = 0; i < addedNodes.length; i++) { var newNode = addedNodes[i]; if (newNode.nodeType === 1) { deprecatedSelectorMap.forEach(testAndHandleDeprecation(newNode)); } } }); }); var config = { childList: true, subtree: true }; observer.observe(document, config); } /** * Force a re-compute of the style of an element. * * This is useful for CSS transitions and animations that need computed style changes to occur. * CSS transitions will fire when the computed value of the property they are transitioning changes. * This may not occur if the style changes get batched into one style change event by the browser. * We can force the browser to recognise the two different computed values by calling this function when we want it * to recompute the styles. * * For example, consider a transition on the opacity property. * * With recomputeStyle: * $parent.append($el); //opacity=0 * recomputeStyle($el); * $el.addClass('visible'); //opacity=1 * //Browser calculates value of opacity=0, and then transitions it to opacity=1 * * Without recomputeStyle: * $parent.append($el); //opacity=0 * $el.addClass('visible'); //opacity=1 * //Browser calculates value of opacity=1 but no transition * * @param el The DOM or jQuery element for which style should be recomputed */ function recomputeStyle (el) { el = el.jquery ? el[0] : el; window.getComputedStyle(el, null).getPropertyValue('left'); } var overflowEl; var _hiddenByAui = []; /** * Dims the screen using a blanket div * @param useShim deprecated, it is calculated by dim() now */ function dim (useShim, zIndex) { //if we're blanketing the page it means we want to hide the whatever is under the blanket from the screen readers as well function hasAriaHidden(element) { return element.hasAttribute('aria-hidden'); } function isAuiLayer(element) { return element.classList.contains('aui-layer'); } Array.prototype.forEach.call(document.body.children, function (element) { if (!hasAriaHidden(element) && !isAuiLayer(element)) { element.setAttribute('aria-hidden', 'true'); _hiddenByAui.push(element); } }); if (!overflowEl) { overflowEl = document.body; } if (useShim === true) { useShimDeprecationLogger(); } var isBlanketShowing = (!!dim.$dim) && dim.$dim.attr('aria-hidden') === 'false'; if (!!dim.$dim) { dim.$dim.remove(); dim.$dim = null; } dim.$dim = $('<div></div>').addClass('aui-blanket'); dim.$dim.attr('tabindex', '0'); //required, or the last element's focusout event will go to the browser dim.$dim.appendTo(document.body); if (!isBlanketShowing) { //recompute after insertion and before setting aria-hidden=false to ensure we calculate a difference in //computed styles recomputeStyle(dim.$dim); dim.cachedOverflow = { overflow: overflowEl.style.overflow, overflowX: overflowEl.style.overflowX, overflowY: overflowEl.style.overflowY }; overflowEl.style.overflowX = 'hidden'; overflowEl.style.overflowY = 'hidden'; overflowEl.style.overflow = 'hidden'; } dim.$dim.attr('aria-hidden', 'false'); if (zIndex) { dim.$dim.css({zIndex: zIndex}); } return dim.$dim; } /** * Removes semitransparent DIV * @see dim */ function undim () { _hiddenByAui.forEach(function (element) { element.removeAttribute('aria-hidden'); }); _hiddenByAui = []; if (dim.$dim) { dim.$dim.attr('aria-hidden', 'true'); if (overflowEl) { overflowEl.style.overflow = dim.cachedOverflow.overflow; overflowEl.style.overflowX = dim.cachedOverflow.overflowX; overflowEl.style.overflowY = dim.cachedOverflow.overflowY; } } } var useShimDeprecationLogger = getShowDeprecationMessage('useShim', { extraInfo: 'useShim has no alternative as it is now calculated by dim().' }); (function initSelectors () { /* :tabbable and :focusable functions from jQuery UI v 1.10.4 renamed to :aui-tabbable and :aui-focusable to not clash with jquery-ui if it's included. Code modified slightly to be compatible with jQuery < 1.8 .addBack() replaced with .andSelf() $.curCSS() replaced with $.css() */ function visible (element) { return ($.css(element, 'visibility') === 'visible'); } function focusable (element, isTabIndexNotNaN) { var nodeName = element.nodeName.toLowerCase(); if (nodeName === 'aui-select') { return true; } if (nodeName === 'area') { var map = element.parentNode; var mapName = map.name; var imageMap = $('img[usemap=#' + mapName + ']').get(); if (!element.href || !mapName || map.nodeName.toLowerCase() !== 'map') { return false; } return imageMap && visible(imageMap); } var isFormElement = /input|select|textarea|button|object/.test(nodeName); var isAnchor = nodeName === 'a'; var isAnchorTabbable = (element.href || isTabIndexNotNaN); return ( isFormElement ? !element.disabled : (isAnchor ? isAnchorTabbable : isTabIndexNotNaN) ) && visible(element); } function tabbable (element) { var tabIndex = $.attr(element, 'tabindex'); var isTabIndexNaN = isNaN(tabIndex); var hasTabIndex = (isTabIndexNaN || tabIndex >= 0); return hasTabIndex && focusable(element, !isTabIndexNaN); } $.extend($.expr[ ':' ], { 'aui-focusable': function (element) { return focusable(element, !isNaN($.attr(element, 'tabindex'))); }, 'aui-tabbable': tabbable }); }()); var RESTORE_FOCUS_DATA_KEY = '_aui-focus-restore'; function FocusManager() { this._focusTrapStack = []; $(document).on('focusout', {focusTrapStack: this._focusTrapStack}, focusTrapHandler); } FocusManager.defaultFocusSelector = ':aui-tabbable'; FocusManager.prototype.enter = function ($el) { // remember focus on old element $el.data(RESTORE_FOCUS_DATA_KEY, $(document.activeElement)); // focus on new selector if ($el.attr('data-aui-focus') !== 'false') { var focusSelector = $el.attr('data-aui-focus-selector') || FocusManager.defaultFocusSelector; var $focusEl = $el.is(focusSelector) ? $el : $el.find(focusSelector); $focusEl.first().focus(); } if (elementTrapsFocus($el)) { trapFocus($el, this._focusTrapStack); } }; function trapFocus($el, focusTrapStack) { focusTrapStack.push($el); } function untrapFocus(focusTrapStack) { focusTrapStack.pop(); } function elementTrapsFocus($el) { return $el.is('.aui-dialog2'); } FocusManager.prototype.exit = function ($el) { if (elementTrapsFocus($el)) { untrapFocus(this._focusTrapStack); } // AUI-1059: remove focus from the active element when dialog is hidden var activeElement = document.activeElement; if ($el[0] === activeElement || $el.has(activeElement).length) { $(activeElement).blur(); } var $restoreFocus = $el.data(RESTORE_FOCUS_DATA_KEY); if ($restoreFocus && $restoreFocus.length) { $el.removeData(RESTORE_FOCUS_DATA_KEY); $restoreFocus.focus(); } }; function focusTrapHandler(event) { var focusTrapStack = event.data.focusTrapStack; if (!event.relatedTarget) { //Does not work in firefox, see https://bugzilla.mozilla.org/show_bug.cgi?id=687787 return; } if (focusTrapStack.length === 0) { return; } var $focusTrapElement = focusTrapStack[focusTrapStack.length - 1]; var focusOrigin = event.target; var focusTo = event.relatedTarget; var $tabbableElements = $focusTrapElement.find(':aui-tabbable'); var $firstTabbableElement = $($tabbableElements.first()); var $lastTabbableElement = $($tabbableElements.last()); var elementContainsOrigin = $focusTrapElement.has(focusTo).length === 0; var focusLeavingElement = elementContainsOrigin && focusTo; if (focusLeavingElement) { if ($firstTabbableElement.is(focusOrigin)) { $lastTabbableElement.focus(); } else if ($lastTabbableElement.is(focusOrigin)) { $firstTabbableElement.focus(); } } } FocusManager.global = new FocusManager(); var keyCode = { ALT: 18, BACKSPACE: 8, CAPS_LOCK: 20, COMMA: 188, COMMAND: 91, // cmd COMMAND_LEFT: 91, COMMAND_RIGHT: 93, LEFT_SQUARE_BRACKET: 91, //This is 91 for keypress and 219 for keydown/keyup CONTROL: 17, DELETE: 46, DOWN: 40, END: 35, ENTER: 13, ESCAPE: 27, HOME: 36, INSERT: 45, LEFT: 37, // right cmd MENU: 93, NUMPAD_ADD: 107, NUMPAD_DECIMAL: 110, NUMPAD_DIVIDE: 111, NUMPAD_ENTER: 108, NUMPAD_MULTIPLY: 106, NUMPAD_SUBTRACT: 109, PAGE_DOWN: 34, PAGE_UP: 33, PERIOD: 190, RIGHT: 39, SHIFT: 16, SPACE: 32, TAB: 9, UP: 38, // cmd WINDOWS: 91 }; /** * @param {string} name The name of the widget to use in any messaging. * @param {function(new:{ $el: jQuery }, ?jQuery, ?Object)} Ctor * A constructor which will only ever be called with "new". It must take a JQuery object as the first * parameter, or generate one if not provided. The second parameter will be a configuration object. * The returned object must have an $el property and a setOptions function. * @constructor */ function widget (name, Ctor) { var dataAttr = '_aui-widget-' + name; return function (selectorOrOptions, maybeOptions) { var selector; var options; if ($.isPlainObject(selectorOrOptions)) { options = selectorOrOptions; } else { selector = selectorOrOptions; options = maybeOptions; } var $el = selector && $(selector); var widget; if (!$el || !$el.data(dataAttr)) { widget = new Ctor($el, options || {}); $el = widget.$el; $el.data(dataAttr, widget); } else { widget = $el.data(dataAttr); // options are discarded if $el has already been constructed } return widget; }; } let CustomEvent; (function () { if (window.CustomEvent) { // Some browsers don't support constructable custom events yet. try { const ce = new window.CustomEvent('name', { bubbles: false, cancelable: true, detail: { x: 'y' } }); ce.preventDefault(); if (ce.defaultPrevented !== true) { throw new Error('Could not prevent default'); } if (ce.type !== 'name') { throw new Error('Could not set custom name'); } if (ce.detail.x !== 'y') { throw new Error('Could not set detail'); } CustomEvent = window.CustomEvent; return; } catch (e) { // polyfill it } } /** * @type CustomEvent * @param {String} event - the name of the event. * @param {Object} [params] - optional configuration of the custom event. * @param {Boolean} [params.cancelable=false] - A boolean indicating whether the event is cancelable (i.e., can call preventDefault and set the defaultPrevented property). * @param {Boolean} [params.bubbles=false] - A boolean indicating whether the event bubbles up through the DOM or not. * @param {Boolean} [params.detail] - The data passed when initializing the event. * @extends Event * @returns {Event} * @constructor */ CustomEvent = function(event, params) { params = params || {bubbles: false, cancelable: false, detail: undefined}; var evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, !!params.bubbles, !!params.cancelable, params.detail); var origPrevent = evt.preventDefault; evt.preventDefault = function () { origPrevent.call(this); try { Object.defineProperty(this, 'defaultPrevented', { get: function () { return true; } }); } catch (e) { this.defaultPrevented = true; } }; return evt; }; CustomEvent.prototype = window.Event.prototype; }()); var CustomEvent$1 = CustomEvent; const EVENT_PREFIX = '_aui-internal-layer-'; const GLOBAL_EVENT_PREFIX = '_aui-internal-layer-global-'; const LAYER_EVENT_PREFIX = 'aui-layer-'; const AUI_EVENT_PREFIX = 'aui-'; var $doc = $(document); // AUI-3708 - Abstracted to reflect code implemented upstream. function isTransitioning (el, prop) { var transition = window.getComputedStyle(el).transitionProperty; return transition ? transition.indexOf(prop) > -1 : false; } function onTransitionEnd (el, prop, func, once) { function handler (e) { if (prop !== e.propertyName) { return; } func.call(el); if (once) { el.removeEventListener('transitionend', handler); } } if (isTransitioning(el, prop)) { el.addEventListener('transitionend', handler); } else { func.call(el); } } function oneTransitionEnd (el, prop, func) { onTransitionEnd(el, prop, func, true); } // end AUI-3708 function ariaHide ($el) { $el.attr('aria-hidden', 'true'); } function ariaShow ($el) { $el.attr('aria-hidden', 'false'); } /** * @return {bool} Returns false if at least one of the event handlers called .preventDefault(). Returns true otherwise. */ function triggerEvent ($el, deprecatedName, newNativeName) { var e1 = $.Event(EVENT_PREFIX + deprecatedName); var e2 = $.Event(GLOBAL_EVENT_PREFIX + deprecatedName); // TODO: Remove this 'aui-layer-' prefixed event once it is no longer used by inline dialog and dialog2. var nativeEvent = new CustomEvent$1(LAYER_EVENT_PREFIX + newNativeName, { bubbles: true, cancelable: true }); var nativeEvent2 = new CustomEvent$1(AUI_EVENT_PREFIX + newNativeName, { bubbles: true, cancelable: true }); $el.trigger(e1); $el.trigger(e2, [$el]); $el[0].dispatchEvent(nativeEvent); $el[0].dispatchEvent(nativeEvent2); return !e1.isDefaultPrevented() && !e2.isDefaultPrevented() && !nativeEvent.defaultPrevented && !nativeEvent2.defaultPrevented; } function Layer (selector) { this.$el = $(selector || '<div class="aui-layer" aria-hidden="true"></div>'); this.$el.addClass('aui-layer'); } Layer.prototype = { /** * Returns the layer below the current layer if it exists. * * @returns {jQuery | undefined} */ below: function () { return LayerManager.global.item(LayerManager.global.indexOf(this.$el) - 1); }, /** * Returns the layer above the current layer if it exists. * * @returns {jQuery | undefined} */ above: function () { return LayerManager.global.item(LayerManager.global.indexOf(this.$el) + 1); }, /** * Sets the width and height of the layer. * * @param {Integer} width The width to set. * @param {Integer} height The height to set. * * @returns {Layer} */ changeSize: function (width, height) { this.$el.css('width', width); this.$el.css('height', height === 'content' ? '' : height); return this; }, /** * Binds a layer event. * * @param {String} event The event name to listen to. * @param {Function} fn The event handler. * * @returns {Layer} */ on: function (event, fn) { this.$el.on(EVENT_PREFIX + event, fn); return this; }, /** * Unbinds a layer event. * * @param {String} event The event name to unbind=. * @param {Function} fn Optional. The event handler. * * @returns {Layer} */ off: function (event, fn) { this.$el.off(EVENT_PREFIX + event, fn); return this; }, /** * Shows the layer. * * @returns {Layer} */ show: function () { if (this.isVisible()) { ariaShow(this.$el); return this; } if (!triggerEvent(this.$el, 'beforeShow', 'show')) { return this; } // AUI-3708 // Ensures that the display property is removed if it's been added // during hiding. if (this.$el.css('display') === 'none') { this.$el.css('display', ''); } LayerManager.global.push(this.$el); return this; }, /** * Hides the layer. * * @returns {Layer} */ hide: function () { if (!this.isVisible()) { ariaHide(this.$el); return this; } if (!triggerEvent(this.$el, 'beforeHide', 'hide')) { return this; } // AUI-3708 const thisLayer = this; oneTransitionEnd(this.$el.get(0), 'opacity', function () { if (!thisLayer.isVisible()) { this.style.display = 'none'; } }); LayerManager.global.popUntil(this.$el); return this; }, /** * Checks to see if the layer is visible. * * @returns {Boolean} */ isVisible: function () { return this.$el.attr('aria-hidden') === 'false'; }, /** * Removes the layer and cleans up internal state. * * @returns {undefined} */ remove: function () { this.hide(); this.$el.remove(); this.$el = null; }, /** * Returns whether or not the layer is blanketed. * * @returns {Boolean} */ isBlanketed: function () { return this.$el.attr('data-aui-blanketed') === 'true'; }, /** * Returns whether or not the layer is persistent. * * @returns {Boolean} */ isPersistent: function () { var modal = this.$el.attr('modal') || this.$el.attr('data-aui-modal'); var isPersistent = this.$el[0].hasAttribute('persistent'); return modal === 'true' || isPersistent; }, _hideLayer: function (triggerBeforeEvents) { if (this.isPersistent() || this.isBlanketed()) { FocusManager.global.exit(this.$el); } if (triggerBeforeEvents) { triggerEvent(this.$el, 'beforeHide', 'hide'); } this.$el.attr('aria-hidden', 'true'); this.$el.css('z-index', this.$el.data('_aui-layer-cached-z-index') || ''); this.$el.data('_aui-layer-cached-z-index', ''); this.$el.trigger(EVENT_PREFIX + 'hide'); this.$el.trigger(GLOBAL_EVENT_PREFIX + 'hide', [this.$el]); }, _showLayer: function (zIndex) { if (!this.$el.parent().is('body')) { this.$el.appendTo(document.body); } this.$el.data('_aui-layer-cached-z-index', this.$el.css('z-index')); this.$el.css('z-index', zIndex); this.$el.attr('aria-hidden', 'false'); if (this.isPersistent() || this.isBlanketed()) { FocusManager.global.enter(this.$el); } this.$el.trigger(EVENT_PREFIX + 'show'); this.$el.trigger(GLOBAL_EVENT_PREFIX + 'show', [this.$el]); } }; var createLayer = widget('layer', Layer); createLayer.on = function (eventName, selector, fn) { $doc.on(GLOBAL_EVENT_PREFIX + eventName, selector, fn); return this; }; createLayer.off = function (eventName, selector, fn) { $doc.off(GLOBAL_EVENT_PREFIX + eventName, selector, fn); return this; }; // Layer Manager // ------------- /** * Manages layers. * * There is a single global layer manager. * Additional instances can be created however this should generally only be used in tests. * * Layers are added by the push($el) method. Layers are removed by the * popUntil($el) method. * * popUntil's contract is that it pops all layers above & including the given * layer. This is used to support popping multiple layers. * Say we were showing a dropdown inside an inline dialog inside a dialog - we * have a stack of dialog layer, inline dialog layer, then dropdown layer. Calling * popUntil(dialog.$el) would hide all layers above & including the dialog. */ function getTrigger ($layer) { return $('[aria-controls="' + $layer.attr('id') + '"]'); } function hasTrigger ($layer) { return getTrigger($layer).length > 0; } function topIndexWhere (layerArr, fn) { var i = layerArr.length; while (i--) { if (fn(layerArr[i])) { return i; } } return -1; } function layerIndex (layerArr, $el) { return topIndexWhere(layerArr, function ($layer) { return $layer[0] === $el[0]; }); } function topBlanketedIndex (layerArr) { return topIndexWhere(layerArr, function ($layer) { return createLayer($layer).isBlanketed(); }); } function nextZIndex (layerArr) { var _nextZIndex; if (layerArr.length) { var $topEl = layerArr[layerArr.length - 1]; var zIndex = parseInt($topEl.css('z-index'), 10); _nextZIndex = (isNaN(zIndex) ? 0 : zIndex) + 100; } else { _nextZIndex = 0; } return Math.max(3000, _nextZIndex); } function updateBlanket (stack, oldBlanketIndex) { var newTopBlanketedIndex = topBlanketedIndex(stack); if (oldBlanketIndex !== newTopBlanketedIndex) { if (newTopBlanketedIndex > -1) { dim(false, stack[newTopBlanketedIndex].css('z-index') - 20); } else { undim(); } } } function popLayers (stack, stopIndex, forceClosePersistent) { if (stopIndex < 0) { return; } for (var a = stack.length - 1; a >= stopIndex; a--) { var $layer = stack[a]; var layer = createLayer($layer); if (forceClosePersistent || !layer.isPersistent()) { layer._hideLayer(true); stack.splice(a, 1); } } } function getParentLayer ($childLayer) { var $layerTrigger = getTrigger($childLayer); if ($layerTrigger.length > 0) { return $layerTrigger.closest('.aui-layer'); } } function LayerManager () { this._stack = []; } LayerManager.prototype = { /** * Pushes a layer onto the stack. The same element cannot be opened as a layer multiple times - if the given * element is already an open layer, this method throws an exception. * * @param {HTMLElement | String | jQuery} element The element to push onto the stack. * * @returns {LayerManager} */ push: function (element) { var $el = (element instanceof $) ? element : $(element); if (layerIndex(this._stack, $el) >= 0) { throw new Error('The given element is already an active layer.'); } this.popLayersBeside($el); var layer = createLayer($el); var zIndex = nextZIndex(this._stack); layer._showLayer(zIndex); if (layer.isBlanketed()) { dim(false, zIndex - 20); } this._stack.push($el); return this; }, popLayersBeside: function (element) { var $layer = (element instanceof $) ? element : $(element); if (!hasTrigger($layer)) { // We can't find this layer's trigger, we will pop all non-persistent until a blanket or the document var blanketedIndex = topBlanketedIndex(this._stack); popLayers(this._stack, ++blanketedIndex, false); return; } var $parentLayer = getParentLayer($layer); if ($parentLayer) { var parentIndex = this.indexOf($parentLayer); popLayers(this._stack, ++parentIndex, false); } else { popLayers(this._stack, 0, false); } }, /** * Returns the index of the specified layer in the layer stack. * * @param {HTMLElement | String | jQuery} element The element to find in the stack. * * @returns {Number} the (zero-based) index of the element, or -1 if not in the stack. */ indexOf: function (element) { return layerIndex(this._stack, $(element)); }, /** * Returns the item at the particular index or false. * * @param {Number} index The index of the element to get. * * @returns {jQuery | Boolean} */ item: function (index) { return this._stack[index]; }, /** * Hides all layers in the stack. * * @returns {LayerManager} */ hideAll: function () { this._stack.reverse().forEach(function (element) { var layer = createLayer(element); if (layer.isBlanketed() || layer.isPersistent()) { return; } layer.hide(); }); return this; }, /** * Gets the previous layer below the given layer, which is non modal and non persistent. If it finds a blanketed layer on the way * it returns it regardless if it is modal or not * * @param {HTMLElement | String | jQuery} element layer to start the search from. * * @returns {jQuery | null} the next matching layer or null if none found. */ getNextLowerNonPersistentOrBlanketedLayer: function (element) { var $el = (element instanceof $) ? element : $(element); var index = layerIndex(this._stack, $el); if (index < 0) { return null; } var $nextEl; index--; while (index >= 0) { $nextEl = this._stack[index]; var layer = createLayer($nextEl); if (!layer.isPersistent() || layer.isBlanketed()) { return $nextEl; } index--; } return null; }, /** * Gets the next layer which is neither modal or blanketed, from the given layer. * * @param {HTMLElement | String | jQuery} element layer to start the search from. * * @returns {jQuery | null} the next non modal non blanketed layer or null if none found. */ getNextHigherNonPeristentAndNonBlanketedLayer: function (element) { var $el = (element instanceof $) ? element : $(element); var index = layerIndex(this._stack, $el); if (index < 0) { return null; } var $nextEl; index++; while (index < this._stack.length) { $nextEl = this._stack[index]; var layer = createLayer($nextEl); if (!(layer.isPersistent() || layer.isBlanketed())) { return $nextEl; } index++; } return null; }, /** * Removes all non-modal layers above & including the given element. If the given element is not an active layer, this method * is a no-op. The given element will be removed regardless of whether or not it is modal. * * @param {HTMLElement | String | jQuery} element layer to pop. * * @returns {jQuery} The last layer that was popped, or null if no layer matching the given $el was found. */ popUntil: function (element) { var $el = (element instanceof $) ? element : $(element); var index = layerIndex(this._stack, $el); if (index === -1) { return null; } var oldTopBlanketedIndex = topBlanketedIndex(this._stack); // Removes all layers above the current one. popLayers(this._stack, index + 1, createLayer($el).isBlanketed()); // Removes the current layer. createLayer($el)._hideLayer(); this._stack.splice(index, 1); updateBlanket(this._stack, oldTopBlanketedIndex); return $el; }, /** * Gets the top layer, if it exists. * * @returns The layer on top of the stack, if it exists, otherwise null. */ getTopLayer: function () { if (!this._stack.length) { return null; } var $topLayer = this._stack[this._stack.length - 1]; return $topLayer; }, /** * Pops the top layer, if it exists and it is non modal and non persistent. * * @returns The layer that was popped, if it was popped. */ popTopIfNonPersistent: function () { var $topLayer = this.getTopLayer(); var layer = createLayer($topLayer); if (!$topLayer || layer.isPersistent()) { return null; } return this.popUntil($topLayer); }, /** * Pops all layers above and including the top blanketed layer. If layers exist but none are blanketed, this method * does nothing. * * @returns The blanketed layer that was popped, if it exists, otherwise null. */ popUntilTopBlanketed: function () { var i = topBlanketedIndex(this._stack); if (i < 0) { return null; } var $topBlanketedLayer = this._stack[i]; var layer = createLayer($topBlanketedLayer); if (layer.isPersistent()) { // We can't pop the blanketed layer, only the things ontop var $next = this.getNextHigherNonPeristentAndNonBlanketedLayer($topBlanketedLayer); if ($next) { var stopIndex = layerIndex(this._stack, $next); popLayers(this._stack, stopIndex, true); return $next; } return null; } popLayers(this._stack, i, true); updateBlanket(this._stack, i); return $topBlanketedLayer; }, /** * Pops all layers above and including the top persistent layer. If layers exist but none are persistent, this method * does nothing. */ popUntilTopPersistent: function () { var $toPop = LayerManager.global.getTopLayer(); if (!$toPop) { return; } var stopIndex; var oldTopBlanketedIndex = topBlanketedIndex(this._stack); var toPop = createLayer($toPop); if (toPop.isPersistent()) { if (toPop.isBlanketed()) { return; } else { // Get the closest non modal layer below, stop at the first blanketed layer though, we don't want to pop below that $toPop = LayerManager.global.getNextLowerNonPersistentOrBlanketedLayer($toPop); toPop = createLayer($toPop); if ($toPop && !toPop.isPersistent()) { stopIndex = layerIndex(this._stack, $toPop); popLay