@atlassian/aui
Version:
Atlassian User Interface Framework
690 lines (604 loc) • 23.6 kB
JavaScript
import $ from './jquery';
import '../../js-vendor/jquery/plugins/jquery.aop';
import * as deprecate from './internal/deprecation';
import * as logger from './internal/log';
import globalize from './internal/globalize';
/**
* Displays a drop down, typically used for menus.
*
* @param obj {jQuery Object|String|Array} object to populate the drop down from.
* @param usroptions optional dropdown configuration. Supported properties are:
* <li>alignment - "left" or "right" alignment of the drop down</li>
* <li>escapeHandler - function to handle on escape key presses</li>
* <li>activeClass - class name to be added to drop down items when 'active' ie. hover over</li>
* <li>selectionHandler - function to handle when drop down items are selected on</li>
* <li>hideHandler - function to handle when the drop down is hidden</li>
* When an object of type Array is passed in, you can also configure:
* <li>isHiddenByDefault - set to true if you would like to hide the drop down on initialisation</li>
* <li>displayHandler - function to display text in the drop down</li>
* <li>useDisabled - If set to true, the dropdown will not appear if a class of disabled is added to aui-dd-parent</li>
*
* @returns {Array} an array of jQuery objects, referring to the drop down container elements
*/
function dropDown (obj, usroptions) {
var dd = null;
var result = [];
var moving = false;
var $doc = $(document);
var options = {
item: 'li:has(a)',
activeClass: 'active',
alignment: 'right',
displayHandler: function (obj) {
return obj.name;
},
escapeHandler: function () {
this.hide('escape');
return false;
},
hideHandler: function () {},
moveHandler: function () {},
useDisabled: false
};
$.extend(options, usroptions);
options.alignment = {left: 'left',right: 'right'}[options.alignment.toLowerCase()] || 'left';
if (obj && obj.jquery) { // if $
dd = obj;
} else if (typeof obj === 'string') { // if $ selector
dd = $(obj);
} else if (obj && obj.constructor === Array) { // if JSON
dd = $('<div></div>').addClass('aui-dropdown').toggleClass('hidden', !!options.isHiddenByDefault);
for (var i = 0, ii = obj.length; i < ii; i++) {
var ol = $('<ol></ol>');
for (var j = 0, jj = obj[i].length; j < jj; j++) {
var li = $('<li></li>');
var properties = obj[i][j];
if (properties.href) {
li.append($('<a></a>')
.html('<span>' + options.displayHandler(properties) + '</span>')
.attr({href: properties.href})
.addClass(properties.className));
// deprecated - use the properties on the li, not the span
$.data($('a > span', li)[0], 'properties', properties);
} else {
li.html(properties.html).addClass(properties.className);
}
if (properties.icon) {
li.prepend($('<img />').attr('src', properties.icon));
}
if (properties.insideSpanIcon){
li.children('a').prepend($('<span></span>').attr('class','icon'));
}
if (properties.iconFontClass) {
li.children('a').prepend($('<span></span>').addClass('aui-icon aui-icon-small aui-iconfont-' + properties.iconFontClass));
}
$.data(li[0], 'properties', properties);
ol.append(li);
}
if (i === ii - 1) {
ol.addClass('last');
}
dd.append(ol);
}
$('body').append(dd);
} else {
throw new Error('dropDown function was called with illegal parameter. Should be $ object, $ selector or array.');
}
var moveDown = function () {
move(+1);
};
var moveUp = function () {
move(-1);
};
var move = function (dir) {
var trigger = !moving;
var cdd = dropDown.current.$[0];
var links = dropDown.current.links;
var oldFocus = cdd.focused;
moving = true;
if (links.length === 0) {
// Nothing to move focus to. Abort.
return;
}
cdd.focused = (typeof oldFocus === 'number') ? oldFocus : -1;
if (!dropDown.current) {
logger.log('move - not current, aborting');
return true;
}
cdd.focused += dir;
// Resolve out of bounds values:
if (cdd.focused < 0) {
cdd.focused = links.length - 1;
} else if (cdd.focused >= links.length) {
cdd.focused = 0;
}
options.moveHandler($(links[cdd.focused]), dir < 0 ? 'up' : 'down');
if (trigger && links.length) {
$(links[cdd.focused]).addClass(options.activeClass);
moving = false;
} else if (!links.length) {
moving = false;
}
};
var moveFocus = function (e) {
if (!dropDown.current) {
return true;
}
var c = e.which;
var cdd = dropDown.current.$[0];
var links = dropDown.current.links;
dropDown.current.cleanActive();
switch (c) {
case 40: {
moveDown();
break;
}
case 38:{
moveUp();
break;
}
case 27:{
return options.escapeHandler.call(dropDown.current, e);
}
case 13:{
if (cdd.focused >= 0) {
if (!options.selectionHandler){
if ($(links[cdd.focused]).attr('nodeName') != 'a'){
return $('a', links[cdd.focused]).trigger('focus'); //focus on the "a" within the parent item elements
} else {
return $(links[cdd.focused]).trigger('focus'); //focus on the "a"
}
} else {
return options.selectionHandler.call(dropDown.current, e, $(links[cdd.focused])); //call the selection handler
}
}
return true;
}
default:{
if (links.length) {
$(links[cdd.focused]).addClass(options.activeClass);
}
return true;
}
}
e.stopPropagation();
e.preventDefault();
return false;
};
var hider = function (e) {
if (!((e && e.which && (e.which == 3)) || (e && e.button && (e.button == 2)) || false)) { // right click check
if (dropDown.current) {
dropDown.current.hide('click');
}
}
};
var active = function (i) {
return function () {
if (!dropDown.current) {
return;
}
dropDown.current.cleanFocus();
this.originalClass = this.className;
$(this).addClass(options.activeClass);
dropDown.current.$[0].focused = i;
};
};
var handleClickSelection = function (e) {
if (e.button || e.metaKey || e.ctrlKey || e.shiftKey) {
return true;
}
if (dropDown.current && options.selectionHandler) {
options.selectionHandler.call(dropDown.current, e, $(this));
}
};
var isEventsBound = function (el) {
var bound = false;
if (el.data('events')) {
$.each(el.data('events'), function (i, handler) {
$.each(handler, function (type, handler) {
if (handleClickSelection === handler) {
bound = true;
return false;
}
});
});
}
return bound;
};
dd.each(function () {
var cdd = this;
var $cdd = $(this);
var res = {};
var methods = {
reset: function () {
res = $.extend(res, {
$: $cdd,
links: $(options.item || 'li:has(a)', cdd),
cleanActive: function () {
if (cdd.focused + 1 && res.links.length) {
$(res.links[cdd.focused]).removeClass(options.activeClass);
}
},
cleanFocus: function () {
res.cleanActive();
cdd.focused = -1;
},
moveDown: moveDown,
moveUp: moveUp,
moveFocus: moveFocus,
getFocusIndex: function () {
return (typeof cdd.focused === 'number') ? cdd.focused : -1;
}
});
res.links.each(function (i) {
var $this = $(this);
if (!isEventsBound($this)) {
$this.hover(active(i), res.cleanFocus);
$this.click(handleClickSelection);
}
});
},
appear: function (dir) {
if (dir) {
$cdd.removeClass('hidden');
//handle left or right alignment
$cdd.addClass('aui-dropdown-' + options.alignment);
} else {
$cdd.addClass('hidden');
}
},
fade: function (dir) {
if (dir) {
$cdd.fadeIn('fast');
} else {
$cdd.fadeOut('fast');
}
},
scroll: function (dir) {
if (dir) {
$cdd.slideDown('fast');
} else {
$cdd.slideUp('fast');
}
}
};
res.reset = methods.reset;
res.reset();
/**
* Uses Aspect Oriented Programming (AOP) to wrap a method around another method
* Allows control of the execution of the wrapped method.
* specified method has returned @see $.aop
* @method addControlProcess
* @param {String} methodName - Name of a public method
* @param {Function} callback - Function to be executed
* @return {Array} weaved aspect
*/
res.addControlProcess = function (method, process) {
$.aop.around({target: this, method: method}, process);
};
/**
* Uses Aspect Oriented Programming (AOP) to insert callback <em>after</em> the
* specified method has returned @see $.aop
* @method addCallback
* @param {String} methodName - Name of a public method
* @param {Function} callback - Function to be executed
* @return {Array} weaved aspect
*/
res.addCallback = function (method, callback) {
return $.aop.after({target: this, method: method}, callback);
};
res.show = function (method) {
if (options.useDisabled && this.$.closest('.aui-dd-parent').hasClass('disabled')) {
return
}
this.alignment = options.alignment;
hider();
dropDown.current = this;
this.method = method || this.method || 'appear';
this.timer = setTimeout(function () {
$doc.click(hider);
}, 0);
$doc.keydown(moveFocus);
if (options.firstSelected && this.links[0]) {
active(0).call(this.links[0]);
}
$(cdd.offsetParent).css({zIndex: 2000});
methods[this.method](true);
$(document).trigger('showLayer', ['dropdown', dropDown.current]);
};
res.hide = function (causer) {
this.method = this.method || 'appear';
$($cdd.get(0).offsetParent).css({zIndex: ''});
this.cleanFocus();
methods[this.method](false);
$doc.unbind('click', hider).unbind('keydown', moveFocus);
$(document).trigger('hideLayer', ['dropdown', dropDown.current]);
dropDown.current = null;
return causer;
};
res.addCallback('reset', function () {
if (options.firstSelected && this.links[0]) {
active(0).call(this.links[0]);
}
});
if (!dropDown.iframes) {
dropDown.iframes = [];
}
dropDown.createShims = (function createShims () {
$('iframe').each(function (idx) {
var iframe = this;
if (!iframe.shim) {
iframe.shim = $('<div />')
.addClass('shim hidden')
.appendTo('body');
dropDown.iframes.push(iframe);
}
});
return createShims;
}());
res.addCallback('show', function () {
$(dropDown.iframes).each(function () {
var $this = $(this);
if ($this.is(':visible')) {
var offset = $this.offset();
offset.height = $this.height();
offset.width = $this.width();
this.shim.css({
left: offset.left + 'px',
top: offset.top + 'px',
height: offset.height + 'px',
width: offset.width + 'px'
}).removeClass('hidden');
}
});
});
res.addCallback('hide', function () {
$(dropDown.iframes).each(function () {
this.shim.addClass('hidden');
});
options.hideHandler();
});
result.push(res);
});
return result;
};
/**
* For the given item in the drop down get the value of the named additional property. If there is no
* property with the specified name then null will be returned.
*
* @method getAdditionalPropertyValue
* @namespace dropDown
* @param item {Object} jQuery Object of the drop down item. An LI element is expected.
* @param name {String} name of the property to retrieve
*/
dropDown.getAdditionalPropertyValue = function (item, name) {
var el = item[0];
if (!el || (typeof el.tagName !== 'string') || el.tagName.toLowerCase() !== 'li') {
// we are moving the location of the properties and want to deprecate the attachment to the span
// but are unsure where and how its being called so for now we just log
logger.log('dropDown.getAdditionalPropertyValue : item passed in should be an LI element wrapped by jQuery');
}
var properties = $.data(el, 'properties');
return properties ? properties[name] : null;
};
/**
* Only here for backwards compatibility
* @method removeAllAdditionalProperties
* @namespace dropDown
* @deprecated Since 3.0
*/
dropDown.removeAllAdditionalProperties = function (item) {
};
/**
* Base dropdown control. Enables you to identify triggers that when clicked, display dropdown.
*
* @class Standard
* @constructor
* @namespace dropDown
* @param {Object} usroptions
* @return {Object
*/
dropDown.Standard = function (usroptions) {
var res = [];
var options = {
selector: '.aui-dd-parent',
dropDown: '.aui-dropdown',
trigger: '.aui-dd-trigger'
};
var dropdownParents;
// extend defaults with user options
$.extend(options, usroptions);
var hookUpDropDown = function ($trigger, $parent, $dropdown, ddcontrol) {
// extend to control to have any additional properties/methods
$.extend(ddcontrol, {trigger: $trigger});
// flag it to prevent additional dd controls being applied
$parent.addClass('dd-allocated');
//hide dropdown if not already hidden
$dropdown.addClass('hidden');
//show the dropdown if isHiddenByDefault is set to false
if (options.isHiddenByDefault == false) {
ddcontrol.show();
}
ddcontrol.addCallback('show', function () {
$parent.addClass('active');
});
ddcontrol.addCallback('hide', function () {
$parent.removeClass('active');
});
};
var handleEvent = function (event, $trigger, $dropdown, ddcontrol) {
if (ddcontrol != dropDown.current) {
$dropdown.css({top: $trigger.outerHeight()});
ddcontrol.show();
event.stopImmediatePropagation();
}
event.preventDefault();
};
if (options.useLiveEvents) {
// cache arrays so that we don't have to recalculate the dropdowns. Since we can't store objects as keys in a map,
// we have two arrays: keysCache stores keys of dropdown triggers; valuesCache stores a map of internally used objects
var keysCache = [];
var valuesCache = [];
$(options.trigger).live('click', function (event) {
var $trigger = $(this);
var $parent;
var $dropdown;
var ddcontrol;
// if we're cached, don't recalculate the dropdown and do all that funny shite.
var index;
if ((index = $.inArray(this, keysCache)) >= 0) {
var val = valuesCache[index];
$parent = val.parent;
$dropdown = val.dropdown;
ddcontrol = val.ddcontrol;
} else {
$parent = $trigger.closest(options.selector);
$dropdown = $parent.find(options.dropDown);
// Sanity checking
if ($dropdown.length === 0) {
return;
}
ddcontrol = dropDown($dropdown, options)[0];
// Sanity checking
if (!ddcontrol) {
return;
}
// cache
keysCache.push(this);
val = {
parent: $parent,
dropdown: $dropdown,
ddcontrol: ddcontrol
};
hookUpDropDown($trigger, $parent, $dropdown, ddcontrol);
valuesCache.push(val);
}
handleEvent(event, $trigger, $dropdown, ddcontrol);
});
} else {
// handling for jQuery collections
if (this instanceof $) {
dropdownParents = this;
// handling for selectors
} else {
dropdownParents = $(options.selector);
}
// a series of checks to ensure we are dealing with valid dropdowns
dropdownParents = dropdownParents
.not('.dd-allocated')
.filter(':has(' + options.dropDown + ')')
.filter(':has(' + options.trigger + ')');
dropdownParents.each(function () {
var $parent = $(this);
var $dropdown = $(options.dropDown, this);
var $trigger = $(options.trigger, this);
var ddcontrol = dropDown($dropdown, options)[0];
// extend to control to have any additional properties/methods
$.extend(ddcontrol, {trigger: $trigger});
hookUpDropDown($trigger, $parent, $dropdown, ddcontrol);
$trigger.click(function (e) {
handleEvent(e, $trigger, $dropdown, ddcontrol);
});
// add control to the response
res.push(ddcontrol);
});
}
return res;
};
/**
* A NewStandard dropdown, however, with the ability to populate its content's via ajax.
*
* @class Ajax
* @constructor
* @namespace dropDown
* @param {Object} options
* @return {Object} dropDown instance
*/
dropDown.Ajax = function (usroptions) {
var dropdowns;
var options = {cache: true};
// extend defaults with user options
$.extend(options, usroptions || {});
// we call with "this" in case we are called in the context of a jQuery collection
dropdowns = dropDown.Standard.call(this, options);
$(dropdowns).each(function () {
var ddcontrol = this;
$.extend(ddcontrol, {
getAjaxOptions: function (opts) {
var success = function (response) {
if (options.formatResults) {
response = options.formatResults(response);
}
if (options.cache) {
ddcontrol.cache.set(ddcontrol.getAjaxOptions(), response);
}
ddcontrol.refreshSuccess(response);
};
if (options.ajaxOptions) {
if ($.isFunction(options.ajaxOptions)) {
return $.extend(options.ajaxOptions.call(ddcontrol), {success: success});
} else {
return $.extend(options.ajaxOptions, {success: success});
}
}
return $.extend(opts, {success: success});
},
refreshSuccess: function (response) {
this.$.html(response);
},
cache: (function () {
var c = {};
return {
get: function (ajaxOptions) {
var data = ajaxOptions.data || '';
return c[(ajaxOptions.url + data).replace(/[\?\&]/gi,'')];
},
set: function (ajaxOptions, responseData) {
var data = ajaxOptions.data || '';
c[(ajaxOptions.url + data).replace(/[\?\&]/gi,'')] = responseData;
},
reset: function () {
c = {};
}
};
}()),
show: (function (superMethod) {
return function () {
if (options.cache && !!ddcontrol.cache.get(ddcontrol.getAjaxOptions())) {
ddcontrol.refreshSuccess(ddcontrol.cache.get(ddcontrol.getAjaxOptions()));
superMethod.call(ddcontrol);
} else {
$($.ajax(ddcontrol.getAjaxOptions())).throbber({target: ddcontrol.$,
end: function () {
ddcontrol.reset();
}
});
superMethod.call(ddcontrol);
if (ddcontrol.iframeShim) {
ddcontrol.iframeShim.hide();
}
}
};
}(ddcontrol.show)),
resetCache: function () {
ddcontrol.cache.reset();
}
});
ddcontrol.addCallback('refreshSuccess', function () {
ddcontrol.reset();
});
});
return dropdowns;
};
// OMG. No. Just no.
$.fn.dropDown = function (type, options) {
type = (type || 'Standard').replace(/^([a-z])/, function (match) {
return match.toUpperCase();
});
return dropDown[type].call(this, options);
};
$.fn.dropDown = deprecate.construct($.fn.dropDown, 'Dropdown constructor', {
alternativeName: 'Dropdown2'
});
globalize('dropDown', dropDown);
export default dropDown;