@atlassian/aui
Version:
Atlassian User Interface library
385 lines (341 loc) • 12.1 kB
JavaScript
import $ from './jquery';
import Alignment from './internal/alignment';
import amdify from './internal/amdify';
import attributes from './internal/attributes';
import enforce from './internal/enforcer';
import globalize from './internal/globalize';
import layer, {EVENT_PREFIX} from './layer';
import skate from './internal/skate';
import state from './internal/state';
import {doIfTrigger, forEachTrigger, getTrigger, setTrigger} from './trigger';
import {ifGone} from './internal/elements';
const DEFAULT_HOVEROUT_DELAY = 1000;
function changeTrigger(element, newTrigger) {
doIfTrigger(element, function(oldTrigger) {
oldTrigger.setAttribute('aria-expanded', 'false');
newTrigger.setAttribute('aria-expanded', element.open);
});
setTrigger(element, newTrigger);
}
function enableAlignment (element, trigger) {
if (element._auiAlignment){
element._auiAlignment.changeTarget(trigger);
element._auiAlignment.enable();
element._auiAlignment.scheduleUpdate();
} else {
element._auiAlignment = new Alignment(element, trigger, {
overflowContainer: element.getAttribute('contained-by') === 'viewport' ? 'viewport' : 'window'
});
element._auiAlignment.enable();
}
}
function disableAlignment (element) {
if (element._auiAlignment) {
element._auiAlignment.disable();
}
}
function destroyAlignment (element) {
if (element._auiAlignment) {
element._auiAlignment.destroy();
delete element._auiAlignment;
}
}
function handleMessage (element, message) {
var messageTypeMap = {
toggle: ['click'],
hover: ['mouseenter', 'mouseleave', 'focus', 'blur']
};
var messageList = messageTypeMap[element.respondsTo];
if (messageList && messageList.indexOf(message.type) > -1) {
messageHandler[message.type](element, message);
}
}
var messageHandler = {
click: function (element, e) {
if (element.open && !layer(element).isPersistent()) {
element.open = false;
} else {
changeTrigger(element, e.currentTarget);
element.open = true;
}
},
mouseenter: function (element, e) {
var newTrigger = e.currentTarget;
if (newTrigger) {
changeTrigger(element, newTrigger);
enableAlignment(element, newTrigger);
}
if (!element.open) {
element.open = true;
}
if (element._clearMouseleaveTimeout) {
element._clearMouseleaveTimeout();
}
},
mouseleave: function (element) {
if (!element.open || layer(element).isPersistent()) {
return;
}
if (element._clearMouseleaveTimeout) {
element._clearMouseleaveTimeout();
}
var timeout = setTimeout(function () {
if (!state(element).get('mouse-inside')) {
element.open = false;
}
}, DEFAULT_HOVEROUT_DELAY);
element._clearMouseleaveTimeout = function () {
clearTimeout(timeout);
element._clearMouseleaveTimeout = null;
};
},
focus: function (element, e) {
if (!element.open) {
changeTrigger(element, e.currentTarget);
element.open = true;
}
},
blur: function (element) {
if (!layer(element).isPersistent() && element.open) {
element.open = false;
}
}
};
function onMouseEnter(e) {
var element = e.target;
state(element).set('mouse-inside', true);
element.message({
type: 'mouseenter'
});
}
function onMouseLeave(e) {
var element = e.target;
state(element).set('mouse-inside', false);
element.message({
type: 'mouseleave'
});
}
function rebindMouseEvents(el) {
state(el).set('mouse-inside', undefined);
el.removeEventListener('mouseenter', onMouseEnter);
el.removeEventListener('mouseleave', onMouseLeave);
if (el.respondsTo === 'hover') {
state(el).set('mouse-inside', false);
el.addEventListener('mouseenter', onMouseEnter);
el.addEventListener('mouseleave', onMouseLeave);
}
}
function namespaceEvent(eventName, elId) {
return `${eventName}.nested-layer-${elId}`
}
function handleNestedLayers(el) {
let $el = $(el);
const elId = el.id;
const noNestedTriggers = e => {
return $el.find(getTrigger(e.target)).length < 1;
};
// Temporary timeout variable to resolve AUI-5025 issue
// as described in further detail below.
const selectCloseTimeout = 150;
$(document)
.on(namespaceEvent('aui-layer-show', elId), e => {
if (noNestedTriggers(e)) {
return;
}
$el.attr('persistent', '');
})
.on(namespaceEvent('aui-layer-hide', elId), e => {
if (noNestedTriggers(e)) {
return;
}
$el.removeAttr('persistent');
})
.on(namespaceEvent('select2-opening', elId), () => {
$el.attr('persistent', '');
})
// Relates to solving AUI-5025
// Temporary solution to race condition with select2,
// where this runs before select2's dropdown is closed.
.on(namespaceEvent('select2-close', elId), () => {
setTimeout(() => {
$el.removeAttr('persistent');
}, selectCloseTimeout);
});
}
function cleanupNestedLayerHandler(elId) {
$(document)
.off(namespaceEvent('aui-layer-hide', elId))
.off(namespaceEvent('aui-layer-show', elId))
.off(namespaceEvent('select2-opening', elId))
.off(namespaceEvent('select2-close', elId));
}
function cleanupOnHide(el) {
cleanupNestedLayerHandler(el.id);
disableAlignment(el);
}
function showInlineDialog(el) {
layer(el).show();
if (layer(el).isVisible()) {
$(el).on(`${EVENT_PREFIX}-hide`, () => cleanupOnHide(el));
handleNestedLayers(el);
doIfTrigger(el, function (trigger) {
enableAlignment(el, trigger);
trigger.setAttribute('aria-expanded', 'true');
});
} else {
el.open = false;
}
}
function hideInlineDialog(el) {
layer(el).hide();
if (!layer(el).isVisible()) {
cleanupOnHide(el);
doIfTrigger(el, function (trigger) {
trigger.setAttribute('aria-expanded', 'false');
});
} else {
el.open = true;
}
setTrigger(el, null);
}
function reflectOpenness(el) {
const isInitalizing = !el.hasAttribute('aria-hidden');
const shouldBeOpen = el.hasAttribute('open');
if (isInitalizing || el.open !== shouldBeOpen) {
if (shouldBeOpen) {
state(el).set('is-processing-show', true);
showInlineDialog(el);
state(el).set('is-processing-show', false);
} else {
hideInlineDialog(el);
}
}
}
const RESPONDS_TO_ATTRIBUTE_ENUM = {
attribute: 'responds-to',
values: ['toggle', 'hover'],
missingDefault: 'toggle',
invalidDefault: 'toggle'
};
const InlineDialogEl = skate('aui-inline-dialog', {
prototype: {
/**
* Returns whether the inline dialog is open.
*/
get open() {
return layer(this).isVisible();
},
/**
* Opens or closes the inline dialog, returning whether the dialog is
* open or closed as a result (since event handlers can prevent either
* action).
*
* You should check the value of open after setting this
* value since the before show/hide events may have prevented it.
*/
set open(value) {
// TODO AUI-3726 Revisit double calls to canceled event handlers.
// Explicitly calling reflectOpenness(…) in this setter means
// that in native we'll get two sync calls to reflectOpenness(…)
// and in polyfill one sync (here) and one async (attr change
// handler). The latter of the two calls, for both cases, will
// usually be a noop (except when show/hide events are cancelled).
attributes.setBooleanAttribute(this, 'open', value);
reflectOpenness(this);
},
get persistent() {
return this.hasAttribute('persistent');
},
set persistent(value) {
attributes.setBooleanAttribute(this, 'persistent', value);
},
get respondsTo() {
var attr = RESPONDS_TO_ATTRIBUTE_ENUM.attribute;
return attributes.computeEnumValue(RESPONDS_TO_ATTRIBUTE_ENUM, this.getAttribute(attr));
},
set respondsTo(value) {
const oldComputedValue = this.respondsTo;
attributes.setEnumAttribute(this, RESPONDS_TO_ATTRIBUTE_ENUM, value);
if (oldComputedValue !== this.respondsTo) {
rebindMouseEvents(this);
}
},
/**
* Handles the receiving of a message from another component.
*
* @param {Object} msg The message to act on.
*
* @returns {HTMLElement}
*/
message: function (msg) {
handleMessage(this, msg);
return this;
}
},
created: function (element) {
state(element).set('is-processing-show', false);
},
attributes: {
'aria-hidden': function (element, change) {
if (change.newValue === 'true') {
doIfTrigger(element, function(trigger) {
trigger.setAttribute('aria-expanded', 'false');
});
}
// Whenever layer manager hides us, we need to sync the open attribute.
attributes.setBooleanAttribute(element, 'open', change.newValue === 'false');
},
open: function (element) {
// skate runs the created callback for attributes before the
// element is attached to the DOM, so guard against that.
if (document.body.contains(element)) {
reflectOpenness(element);
}
},
'responds-to': function (element, change) {
const oldComputedValue = attributes.computeEnumValue(RESPONDS_TO_ATTRIBUTE_ENUM, change.oldValue);
const newComputedValue = attributes.computeEnumValue(RESPONDS_TO_ATTRIBUTE_ENUM, change.newValue);
if (oldComputedValue !== newComputedValue) {
rebindMouseEvents(element);
}
}
},
attached: function (element) {
enforce(element).attributeExists('id');
if (element.hasAttribute('open')) {
// show() can cause the element to be reattached (to the <body>),
// so guard against a nested show() call that blows up the layer
// manager (since it sees us pushing the same element twice).
if (!state(element).get('is-processing-show')) {
reflectOpenness(element);
}
} else {
reflectOpenness(element);
}
rebindMouseEvents(element);
doIfTrigger(element, function (trigger) {
trigger.setAttribute('aria-expanded', element.open);
});
forEachTrigger(element, function (trigger) {
trigger.setAttribute('aria-haspopup', 'true');
});
},
detached: function (element) {
ifGone(element).then(() => {
destroyAlignment(element);
forEachTrigger(element, function (trigger) {
trigger.removeAttribute('aria-haspopup');
trigger.removeAttribute('aria-expanded');
})
});
},
template: function (element) {
var elem = $('<div class="aui-inline-dialog-contents"></div>').append(element.childNodes);
$(element)
.addClass('aui-layer')
.html(elem);
}
});
amdify('aui/inline-dialog2', InlineDialogEl);
globalize('InlineDialog2', InlineDialogEl);
export default InlineDialogEl;