UNPKG

@atlassian/aui

Version:

Atlassian User Interface Framework

690 lines (604 loc) 23.6 kB
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;