@atlassian/aui
Version:
Atlassian User Interface Framework
1,575 lines (1,322 loc) • 854 kB
JavaScript
'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 = {
'<': '<',
'>': '>',
'&': '&',
'"': '"',
'\'': ''',
'`': '`',
};
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