diff --git a/.eslintrc b/.eslintrc
index 640956f734..71cbd5cfb6 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -2,6 +2,7 @@ root: true
reportUnusedDisableDirectives: true
ignorePatterns:
+ - /web_src/js/vendor
- /templates/base/head.tmpl
- /templates/repo/activity.tmpl
- /templates/repo/view_file.tmpl
diff --git a/Makefile b/Makefile
index 332e6afe16..f028150375 100644
--- a/Makefile
+++ b/Makefile
@@ -699,6 +699,7 @@ fomantic:
cd $(FOMANTIC_WORK_DIR) && npm install --no-save
cp -f $(FOMANTIC_WORK_DIR)/theme.config.less $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/theme.config
cp -rf $(FOMANTIC_WORK_DIR)/_site $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/
+ cp -f web_src/js/vendor/dropdown.js $(FOMANTIC_WORK_DIR)/node_modules/fomantic-ui/src/definitions/modules
cd $(FOMANTIC_WORK_DIR) && npx gulp -f node_modules/fomantic-ui/gulpfile.js build
.PHONY: webpack
diff --git a/web_src/fomantic/build/semantic.css b/web_src/fomantic/build/semantic.css
index ee136e13ff..27ff073a56 100644
--- a/web_src/fomantic/build/semantic.css
+++ b/web_src/fomantic/build/semantic.css
@@ -30873,7 +30873,7 @@ ol.ui.suffixed.list li:before,
List
---------------*/
-/* Menu divider shouldn't apply */
+/* Menu divider shouldnt apply */
.ui.menu .list .item:before {
background: none !important;
@@ -31802,7 +31802,7 @@ Floated Menu / Item
opacity: 1;
}
-/* Icon Glyph */
+/* Icon Gylph */
.ui.icon.menu i.icon:before {
opacity: 1;
diff --git a/web_src/fomantic/build/semantic.js b/web_src/fomantic/build/semantic.js
index c68d266b0c..6514e99e33 100644
--- a/web_src/fomantic/build/semantic.js
+++ b/web_src/fomantic/build/semantic.js
@@ -142,7 +142,7 @@ $.api = $.fn.api = function(parameters) {
response = JSON.parse(response);
}
catch(e) {
- // isn't json string
+ // isnt json string
}
}
return response;
@@ -2220,7 +2220,7 @@ $.fn.dimmer = function(parameters) {
event: {
click: function(event) {
- module.verbose('Determining if event occurred on dimmer', event);
+ module.verbose('Determining if event occured on dimmer', event);
if( $dimmer.find(event.target).length === 0 || $(event.target).is(selector.content) ) {
module.hide();
event.stopImmediatePropagation();
@@ -2827,6 +2827,13 @@ $.fn.dimmer.settings = {
*
*/
+/*
+ * Copyright 2019 The Gitea Authors
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ * This version has been modified by Gitea to improve accessibility.
+ */
+
;(function ($, window, document, undefined) {
'use strict';
@@ -2860,6 +2867,7 @@ $.fn.dropdown = function(parameters) {
query = arguments[0],
methodInvoked = (typeof query == 'string'),
queryArguments = [].slice.call(arguments, 1),
+ lastAriaID = 1,
returnedValue
;
@@ -2952,6 +2960,8 @@ $.fn.dropdown = function(parameters) {
module.observeChanges();
module.instantiate();
+
+ module.aria.setup();
}
},
@@ -3152,6 +3162,86 @@ $.fn.dropdown = function(parameters) {
}
},
+ aria: {
+ setup: function() {
+ var role = module.aria.guessRole();
+ if( role !== 'menu' ) {
+ return;
+ }
+ $module.attr('aria-busy', 'true');
+ $module.attr('role', 'menu');
+ $module.attr('aria-haspopup', 'menu');
+ $module.attr('aria-expanded', 'false');
+ $menu.find('.divider').attr('role', 'separator');
+ $item.attr('role', 'menuitem');
+ $item.each(function (index, item) {
+ if( !item.id ) {
+ item.id = module.aria.nextID('menuitem');
+ }
+ });
+ $text = $module
+ .find('> .text')
+ .eq(0)
+ ;
+ if( $module.data('content') ) {
+ $text.attr('aria-hidden');
+ $module.attr('aria-label', $module.data('content'));
+ }
+ else {
+ $text.attr('id', module.aria.nextID('menutext'));
+ $module.attr('aria-labelledby', $text.attr('id'));
+ }
+ $module.attr('aria-busy', 'false');
+ },
+ nextID: function(prefix) {
+ var nextID;
+ do {
+ nextID = prefix + '_' + lastAriaID++;
+ } while( document.getElementById(nextID) );
+ return nextID;
+ },
+ setExpanded: function(expanded) {
+ if( $module.attr('aria-haspopup') ) {
+ $module.attr('aria-expanded', expanded);
+ }
+ },
+ refreshDescendant: function() {
+ if( $module.attr('aria-haspopup') !== 'menu' ) {
+ return;
+ }
+ var
+ $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
+ $activeItem = $menu.children('.' + className.active).eq(0),
+ $selectedItem = ($currentlySelected.length > 0)
+ ? $currentlySelected
+ : $activeItem
+ ;
+ if( $selectedItem ) {
+ $module.attr('aria-activedescendant', $selectedItem.attr('id'));
+ }
+ else {
+ module.aria.removeDescendant();
+ }
+ },
+ removeDescendant: function() {
+ if( $module.attr('aria-haspopup') == 'menu' ) {
+ $module.removeAttr('aria-activedescendant');
+ }
+ },
+ guessRole: function() {
+ var
+ isIcon = $module.hasClass('icon'),
+ hasSearch = module.has.search(),
+ hasInput = ($input.length > 0),
+ isMultiple = module.is.multiple()
+ ;
+ if ( !isIcon && !hasSearch && !hasInput && !isMultiple ) {
+ return 'menu';
+ }
+ return 'unknown';
+ }
+ },
+
setup: {
api: function() {
var
@@ -3198,6 +3288,7 @@ $.fn.dropdown = function(parameters) {
if(settings.allowTab) {
module.set.tabbable();
}
+ $item.attr('tabindex', '-1');
},
select: function() {
var
@@ -3344,6 +3435,8 @@ $.fn.dropdown = function(parameters) {
return true;
}
if(settings.onShow.call(element) !== false) {
+ module.aria.setExpanded(true);
+ module.aria.refreshDescendant();
module.animate.show(function() {
if( module.can.click() ) {
module.bind.intent();
@@ -3366,9 +3459,11 @@ $.fn.dropdown = function(parameters) {
if( module.is.active() && !module.is.animatingOutward() ) {
module.debug('Hiding dropdown');
if(settings.onHide.call(element) !== false) {
+ module.aria.setExpanded(false);
+ module.aria.removeDescendant();
module.animate.hide(function() {
module.remove.visible();
- // hiding search focus
+ // hidding search focus
if ( module.is.focusedOnSearch() && preventBlur !== true ) {
$search.blur();
}
@@ -4319,7 +4414,7 @@ $.fn.dropdown = function(parameters) {
// allow selection with menu closed
if(isAdditionWithoutMenu) {
module.verbose('Selecting item from keyboard shortcut', $selectedItem);
- module.event.item.click.call($selectedItem, event);
+ $selectedItem[0].click();
if(module.is.searchSelection()) {
module.remove.searchTerm();
}
@@ -4339,7 +4434,7 @@ $.fn.dropdown = function(parameters) {
}
else if(selectedIsSelectable) {
module.verbose('Selecting item from keyboard shortcut', $selectedItem);
- module.event.item.click.call($selectedItem, event);
+ $selectedItem[0].click();
if(module.is.searchSelection()) {
module.remove.searchTerm();
if(module.is.multiple()) {
@@ -4367,6 +4462,7 @@ $.fn.dropdown = function(parameters) {
.closest(selector.item)
.addClass(className.selected)
;
+ module.aria.refreshDescendant();
event.preventDefault();
}
}
@@ -4383,6 +4479,7 @@ $.fn.dropdown = function(parameters) {
.find(selector.item).eq(0)
.addClass(className.selected)
;
+ module.aria.refreshDescendant();
event.preventDefault();
}
}
@@ -4407,6 +4504,7 @@ $.fn.dropdown = function(parameters) {
$nextItem
.addClass(className.selected)
;
+ module.aria.refreshDescendant();
module.set.scrollPosition($nextItem);
if(settings.selectOnKeydown && module.is.single()) {
module.set.selectedItem($nextItem);
@@ -4434,6 +4532,7 @@ $.fn.dropdown = function(parameters) {
$nextItem
.addClass(className.selected)
;
+ module.aria.refreshDescendant();
module.set.scrollPosition($nextItem);
if(settings.selectOnKeydown && module.is.single()) {
module.set.selectedItem($nextItem);
@@ -5403,6 +5502,7 @@ $.fn.dropdown = function(parameters) {
module.set.scrollPosition($nextValue);
$selectedItem.removeClass(className.selected);
$nextValue.addClass(className.selected);
+ module.aria.refreshDescendant();
if(settings.selectOnKeydown && module.is.single()) {
module.set.selectedItem($nextValue);
}
@@ -11937,7 +12037,7 @@ $.fn.progress = function(parameters) {
*
* @param min A minimum value within multiple values
* @param total A total amount of multiple values
- * @returns {number} A precision. Could be 1, 10, 100, ... 1e+10.
+ * @returns {number} A precison. Could be 1, 10, 100, ... 1e+10.
*/
derivePrecision: function(min, total) {
var precisionPower = 0
@@ -12837,7 +12937,7 @@ $.fn.progress.settings = {
nonNumeric : 'Progress value is non numeric',
tooHigh : 'Value specified is above 100%',
tooLow : 'Value specified is below 0%',
- sumExceedsTotal : 'Sum of multiple values exceed total',
+ sumExceedsTotal : 'Sum of multple values exceed total',
},
regExp: {
@@ -18076,7 +18176,7 @@ $.fn.transition.settings = {
// possible errors
error: {
- noAnimation : 'Element is no longer attached to DOM. Unable to animate. Use silent setting to suppress this warning in production.',
+ noAnimation : 'Element is no longer attached to DOM. Unable to animate. Use silent setting to surpress this warning in production.',
repeated : 'That animation is already occurring, cancelling repeated animation',
method : 'The method you called is not defined',
support : 'This browser does not support CSS animations'
diff --git a/web_src/js/vendor/dropdown.js b/web_src/js/vendor/dropdown.js
new file mode 100644
index 0000000000..3d4cfec27a
--- /dev/null
+++ b/web_src/js/vendor/dropdown.js
@@ -0,0 +1,4338 @@
+/*!
+ * # Fomantic-UI - Dropdown
+ * http://github.com/fomantic/Fomantic-UI/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+/*
+ * Copyright 2019 The Gitea Authors
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ * This version has been modified by Gitea to improve accessibility.
+ */
+
+;(function ($, window, document, undefined) {
+
+'use strict';
+
+$.isFunction = $.isFunction || function(obj) {
+ return typeof obj === "function" && typeof obj.nodeType !== "number";
+};
+
+window = (typeof window != 'undefined' && window.Math == Math)
+ ? window
+ : (typeof self != 'undefined' && self.Math == Math)
+ ? self
+ : Function('return this')()
+;
+
+$.fn.dropdown = function(parameters) {
+ var
+ $allModules = $(this),
+ $document = $(document),
+
+ moduleSelector = $allModules.selector || '',
+
+ hasTouch = ('ontouchstart' in document.documentElement),
+ clickEvent = hasTouch
+ ? 'touchstart'
+ : 'click',
+
+ time = new Date().getTime(),
+ performance = [],
+
+ query = arguments[0],
+ methodInvoked = (typeof query == 'string'),
+ queryArguments = [].slice.call(arguments, 1),
+ lastAriaID = 1,
+ returnedValue
+ ;
+
+ $allModules
+ .each(function(elementIndex) {
+ var
+ settings = ( $.isPlainObject(parameters) )
+ ? $.extend(true, {}, $.fn.dropdown.settings, parameters)
+ : $.extend({}, $.fn.dropdown.settings),
+
+ className = settings.className,
+ message = settings.message,
+ fields = settings.fields,
+ keys = settings.keys,
+ metadata = settings.metadata,
+ namespace = settings.namespace,
+ regExp = settings.regExp,
+ selector = settings.selector,
+ error = settings.error,
+ templates = settings.templates,
+
+ eventNamespace = '.' + namespace,
+ moduleNamespace = 'module-' + namespace,
+
+ $module = $(this),
+ $context = $(settings.context),
+ $text = $module.find(selector.text),
+ $search = $module.find(selector.search),
+ $sizer = $module.find(selector.sizer),
+ $input = $module.find(selector.input),
+ $icon = $module.find(selector.icon),
+ $clear = $module.find(selector.clearIcon),
+
+ $combo = ($module.prev().find(selector.text).length > 0)
+ ? $module.prev().find(selector.text)
+ : $module.prev(),
+
+ $menu = $module.children(selector.menu),
+ $item = $menu.find(selector.item),
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $(),
+
+ activated = false,
+ itemActivated = false,
+ internalChange = false,
+ iconClicked = false,
+ element = this,
+ instance = $module.data(moduleNamespace),
+
+ selectActionActive,
+ initialLoad,
+ pageLostFocus,
+ willRefocus,
+ elementNamespace,
+ id,
+ selectObserver,
+ menuObserver,
+ classObserver,
+ module
+ ;
+
+ module = {
+
+ initialize: function() {
+ module.debug('Initializing dropdown', settings);
+
+ if( module.is.alreadySetup() ) {
+ module.setup.reference();
+ }
+ else {
+ if (settings.ignoreDiacritics && !String.prototype.normalize) {
+ settings.ignoreDiacritics = false;
+ module.error(error.noNormalize, element);
+ }
+
+ module.setup.layout();
+
+ if(settings.values) {
+ module.set.initialLoad();
+ module.change.values(settings.values);
+ module.remove.initialLoad();
+ }
+
+ module.refreshData();
+
+ module.save.defaults();
+ module.restore.selected();
+
+ module.create.id();
+ module.bind.events();
+
+ module.observeChanges();
+ module.instantiate();
+
+ module.aria.setup();
+ }
+
+ },
+
+ instantiate: function() {
+ module.verbose('Storing instance of dropdown', module);
+ instance = module;
+ $module
+ .data(moduleNamespace, module)
+ ;
+ },
+
+ destroy: function() {
+ module.verbose('Destroying previous dropdown', $module);
+ module.remove.tabbable();
+ module.remove.active();
+ $menu.transition('stop all');
+ $menu.removeClass(className.visible).addClass(className.hidden);
+ $module
+ .off(eventNamespace)
+ .removeData(moduleNamespace)
+ ;
+ $menu
+ .off(eventNamespace)
+ ;
+ $document
+ .off(elementNamespace)
+ ;
+ module.disconnect.menuObserver();
+ module.disconnect.selectObserver();
+ module.disconnect.classObserver();
+ },
+
+ observeChanges: function() {
+ if('MutationObserver' in window) {
+ selectObserver = new MutationObserver(module.event.select.mutation);
+ menuObserver = new MutationObserver(module.event.menu.mutation);
+ classObserver = new MutationObserver(module.event.class.mutation);
+ module.debug('Setting up mutation observer', selectObserver, menuObserver, classObserver);
+ module.observe.select();
+ module.observe.menu();
+ module.observe.class();
+ }
+ },
+
+ disconnect: {
+ menuObserver: function() {
+ if(menuObserver) {
+ menuObserver.disconnect();
+ }
+ },
+ selectObserver: function() {
+ if(selectObserver) {
+ selectObserver.disconnect();
+ }
+ },
+ classObserver: function() {
+ if(classObserver) {
+ classObserver.disconnect();
+ }
+ }
+ },
+ observe: {
+ select: function() {
+ if(module.has.input() && selectObserver) {
+ selectObserver.observe($module[0], {
+ childList : true,
+ subtree : true
+ });
+ }
+ },
+ menu: function() {
+ if(module.has.menu() && menuObserver) {
+ menuObserver.observe($menu[0], {
+ childList : true,
+ subtree : true
+ });
+ }
+ },
+ class: function() {
+ if(module.has.search() && classObserver) {
+ classObserver.observe($module[0], {
+ attributes : true
+ });
+ }
+ }
+ },
+
+ create: {
+ id: function() {
+ id = (Math.random().toString(16) + '000000000').substr(2, 8);
+ elementNamespace = '.' + id;
+ module.verbose('Creating unique id for element', id);
+ },
+ userChoice: function(values) {
+ var
+ $userChoices,
+ $userChoice,
+ isUserValue,
+ html
+ ;
+ values = values || module.get.userValues();
+ if(!values) {
+ return false;
+ }
+ values = Array.isArray(values)
+ ? values
+ : [values]
+ ;
+ $.each(values, function(index, value) {
+ if(module.get.item(value) === false) {
+ html = settings.templates.addition( module.add.variables(message.addResult, value) );
+ $userChoice = $('
')
+ .html(html)
+ .attr('data-' + metadata.value, value)
+ .attr('data-' + metadata.text, value)
+ .addClass(className.addition)
+ .addClass(className.item)
+ ;
+ if(settings.hideAdditions) {
+ $userChoice.addClass(className.hidden);
+ }
+ $userChoices = ($userChoices === undefined)
+ ? $userChoice
+ : $userChoices.add($userChoice)
+ ;
+ module.verbose('Creating user choices for value', value, $userChoice);
+ }
+ });
+ return $userChoices;
+ },
+ userLabels: function(value) {
+ var
+ userValues = module.get.userValues()
+ ;
+ if(userValues) {
+ module.debug('Adding user labels', userValues);
+ $.each(userValues, function(index, value) {
+ module.verbose('Adding custom user value');
+ module.add.label(value, value);
+ });
+ }
+ },
+ menu: function() {
+ $menu = $('')
+ .addClass(className.menu)
+ .appendTo($module)
+ ;
+ },
+ sizer: function() {
+ $sizer = $('')
+ .addClass(className.sizer)
+ .insertAfter($search)
+ ;
+ }
+ },
+
+ search: function(query) {
+ query = (query !== undefined)
+ ? query
+ : module.get.query()
+ ;
+ module.verbose('Searching for query', query);
+ if(module.has.minCharacters(query)) {
+ module.filter(query);
+ }
+ else {
+ module.hide(null,true);
+ }
+ },
+
+ select: {
+ firstUnfiltered: function() {
+ module.verbose('Selecting first non-filtered element');
+ module.remove.selectedItem();
+ $item
+ .not(selector.unselectable)
+ .not(selector.addition + selector.hidden)
+ .eq(0)
+ .addClass(className.selected)
+ ;
+ },
+ nextAvailable: function($selected) {
+ $selected = $selected.eq(0);
+ var
+ $nextAvailable = $selected.nextAll(selector.item).not(selector.unselectable).eq(0),
+ $prevAvailable = $selected.prevAll(selector.item).not(selector.unselectable).eq(0),
+ hasNext = ($nextAvailable.length > 0)
+ ;
+ if(hasNext) {
+ module.verbose('Moving selection to', $nextAvailable);
+ $nextAvailable.addClass(className.selected);
+ }
+ else {
+ module.verbose('Moving selection to', $prevAvailable);
+ $prevAvailable.addClass(className.selected);
+ }
+ }
+ },
+
+ aria: {
+ setup: function() {
+ var role = module.aria.guessRole();
+ if( role !== 'menu' ) {
+ return;
+ }
+ $module.attr('aria-busy', 'true');
+ $module.attr('role', 'menu');
+ $module.attr('aria-haspopup', 'menu');
+ $module.attr('aria-expanded', 'false');
+ $menu.find('.divider').attr('role', 'separator');
+ $item.attr('role', 'menuitem');
+ $item.each(function (index, item) {
+ if( !item.id ) {
+ item.id = module.aria.nextID('menuitem');
+ }
+ });
+ $text = $module
+ .find('> .text')
+ .eq(0)
+ ;
+ if( $module.data('content') ) {
+ $text.attr('aria-hidden');
+ $module.attr('aria-label', $module.data('content'));
+ }
+ else {
+ $text.attr('id', module.aria.nextID('menutext'));
+ $module.attr('aria-labelledby', $text.attr('id'));
+ }
+ $module.attr('aria-busy', 'false');
+ },
+ nextID: function(prefix) {
+ var nextID;
+ do {
+ nextID = prefix + '_' + lastAriaID++;
+ } while( document.getElementById(nextID) );
+ return nextID;
+ },
+ setExpanded: function(expanded) {
+ if( $module.attr('aria-haspopup') ) {
+ $module.attr('aria-expanded', expanded);
+ }
+ },
+ refreshDescendant: function() {
+ if( $module.attr('aria-haspopup') !== 'menu' ) {
+ return;
+ }
+ var
+ $currentlySelected = $item.not(selector.unselectable).filter('.' + className.selected).eq(0),
+ $activeItem = $menu.children('.' + className.active).eq(0),
+ $selectedItem = ($currentlySelected.length > 0)
+ ? $currentlySelected
+ : $activeItem
+ ;
+ if( $selectedItem ) {
+ $module.attr('aria-activedescendant', $selectedItem.attr('id'));
+ }
+ else {
+ module.aria.removeDescendant();
+ }
+ },
+ removeDescendant: function() {
+ if( $module.attr('aria-haspopup') == 'menu' ) {
+ $module.removeAttr('aria-activedescendant');
+ }
+ },
+ guessRole: function() {
+ var
+ isIcon = $module.hasClass('icon'),
+ hasSearch = module.has.search(),
+ hasInput = ($input.length > 0),
+ isMultiple = module.is.multiple()
+ ;
+ if ( !isIcon && !hasSearch && !hasInput && !isMultiple ) {
+ return 'menu';
+ }
+ return 'unknown';
+ }
+ },
+
+ setup: {
+ api: function() {
+ var
+ apiSettings = {
+ debug : settings.debug,
+ urlData : {
+ value : module.get.value(),
+ query : module.get.query()
+ },
+ on : false
+ }
+ ;
+ module.verbose('First request, initializing API');
+ $module
+ .api(apiSettings)
+ ;
+ },
+ layout: function() {
+ if( $module.is('select') ) {
+ module.setup.select();
+ module.setup.returnedObject();
+ }
+ if( !module.has.menu() ) {
+ module.create.menu();
+ }
+ if ( module.is.selection() && module.is.clearable() && !module.has.clearItem() ) {
+ module.verbose('Adding clear icon');
+ $clear = $('')
+ .addClass('remove icon')
+ .insertBefore($text)
+ ;
+ }
+ if( module.is.search() && !module.has.search() ) {
+ module.verbose('Adding search input');
+ $search = $('')
+ .addClass(className.search)
+ .prop('autocomplete', 'off')
+ .insertBefore($text)
+ ;
+ }
+ if( module.is.multiple() && module.is.searchSelection() && !module.has.sizer()) {
+ module.create.sizer();
+ }
+ if(settings.allowTab) {
+ module.set.tabbable();
+ }
+ $item.attr('tabindex', '-1');
+ },
+ select: function() {
+ var
+ selectValues = module.get.selectValues()
+ ;
+ module.debug('Dropdown initialized on a select', selectValues);
+ if( $module.is('select') ) {
+ $input = $module;
+ }
+ // see if select is placed correctly already
+ if($input.parent(selector.dropdown).length > 0) {
+ module.debug('UI dropdown already exists. Creating dropdown menu only');
+ $module = $input.closest(selector.dropdown);
+ if( !module.has.menu() ) {
+ module.create.menu();
+ }
+ $menu = $module.children(selector.menu);
+ module.setup.menu(selectValues);
+ }
+ else {
+ module.debug('Creating entire dropdown from select');
+ $module = $('')
+ .attr('class', $input.attr('class') )
+ .addClass(className.selection)
+ .addClass(className.dropdown)
+ .html( templates.dropdown(selectValues, fields, settings.preserveHTML, settings.className) )
+ .insertBefore($input)
+ ;
+ if($input.hasClass(className.multiple) && $input.prop('multiple') === false) {
+ module.error(error.missingMultiple);
+ $input.prop('multiple', true);
+ }
+ if($input.is('[multiple]')) {
+ module.set.multiple();
+ }
+ if ($input.prop('disabled')) {
+ module.debug('Disabling dropdown');
+ $module.addClass(className.disabled);
+ }
+ $input
+ .removeAttr('required')
+ .removeAttr('class')
+ .detach()
+ .prependTo($module)
+ ;
+ }
+ module.refresh();
+ },
+ menu: function(values) {
+ $menu.html( templates.menu(values, fields,settings.preserveHTML,settings.className));
+ $item = $menu.find(selector.item);
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
+ },
+ reference: function() {
+ module.debug('Dropdown behavior was called on select, replacing with closest dropdown');
+ // replace module reference
+ $module = $module.parent(selector.dropdown);
+ instance = $module.data(moduleNamespace);
+ element = $module.get(0);
+ module.refresh();
+ module.setup.returnedObject();
+ },
+ returnedObject: function() {
+ var
+ $firstModules = $allModules.slice(0, elementIndex),
+ $lastModules = $allModules.slice(elementIndex + 1)
+ ;
+ // adjust all modules to use correct reference
+ $allModules = $firstModules.add($module).add($lastModules);
+ }
+ },
+
+ refresh: function() {
+ module.refreshSelectors();
+ module.refreshData();
+ },
+
+ refreshItems: function() {
+ $item = $menu.find(selector.item);
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
+ },
+
+ refreshSelectors: function() {
+ module.verbose('Refreshing selector cache');
+ $text = $module.find(selector.text);
+ $search = $module.find(selector.search);
+ $input = $module.find(selector.input);
+ $icon = $module.find(selector.icon);
+ $combo = ($module.prev().find(selector.text).length > 0)
+ ? $module.prev().find(selector.text)
+ : $module.prev()
+ ;
+ $menu = $module.children(selector.menu);
+ $item = $menu.find(selector.item);
+ $divider = settings.hideDividers ? $item.parent().children(selector.divider) : $();
+ },
+
+ refreshData: function() {
+ module.verbose('Refreshing cached metadata');
+ $item
+ .removeData(metadata.text)
+ .removeData(metadata.value)
+ ;
+ },
+
+ clearData: function() {
+ module.verbose('Clearing metadata');
+ $item
+ .removeData(metadata.text)
+ .removeData(metadata.value)
+ ;
+ $module
+ .removeData(metadata.defaultText)
+ .removeData(metadata.defaultValue)
+ .removeData(metadata.placeholderText)
+ ;
+ },
+
+ toggle: function() {
+ module.verbose('Toggling menu visibility');
+ if( !module.is.active() ) {
+ module.show();
+ }
+ else {
+ module.hide();
+ }
+ },
+
+ show: function(callback, preventFocus) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if(!module.can.show() && module.is.remote()) {
+ module.debug('No API results retrieved, searching before show');
+ module.queryRemote(module.get.query(), module.show);
+ }
+ if( module.can.show() && !module.is.active() ) {
+ module.debug('Showing dropdown');
+ if(module.has.message() && !(module.has.maxSelections() || module.has.allResultsFiltered()) ) {
+ module.remove.message();
+ }
+ if(module.is.allFiltered()) {
+ return true;
+ }
+ if(settings.onShow.call(element) !== false) {
+ module.aria.setExpanded(true);
+ module.aria.refreshDescendant();
+ module.animate.show(function() {
+ if( module.can.click() ) {
+ module.bind.intent();
+ }
+ if(module.has.search() && !preventFocus) {
+ module.focusSearch();
+ }
+ module.set.visible();
+ callback.call(element);
+ });
+ }
+ }
+ },
+
+ hide: function(callback, preventBlur) {
+ callback = $.isFunction(callback)
+ ? callback
+ : function(){}
+ ;
+ if( module.is.active() && !module.is.animatingOutward() ) {
+ module.debug('Hiding dropdown');
+ if(settings.onHide.call(element) !== false) {
+ module.aria.setExpanded(false);
+ module.aria.removeDescendant();
+ module.animate.hide(function() {
+ module.remove.visible();
+ // hidding search focus
+ if ( module.is.focusedOnSearch() && preventBlur !== true ) {
+ $search.blur();
+ }
+ callback.call(element);
+ });
+ }
+ } else if( module.can.click() ) {
+ module.unbind.intent();
+ }
+ iconClicked = false;
+ },
+
+ hideOthers: function() {
+ module.verbose('Finding other dropdowns to hide');
+ $allModules
+ .not($module)
+ .has(selector.menu + '.' + className.visible)
+ .dropdown('hide')
+ ;
+ },
+
+ hideMenu: function() {
+ module.verbose('Hiding menu instantaneously');
+ module.remove.active();
+ module.remove.visible();
+ $menu.transition('hide');
+ },
+
+ hideSubMenus: function() {
+ var
+ $subMenus = $menu.children(selector.item).find(selector.menu)
+ ;
+ module.verbose('Hiding sub menus', $subMenus);
+ $subMenus.transition('hide');
+ },
+
+ bind: {
+ events: function() {
+ module.bind.keyboardEvents();
+ module.bind.inputEvents();
+ module.bind.mouseEvents();
+ },
+ keyboardEvents: function() {
+ module.verbose('Binding keyboard events');
+ $module
+ .on('keydown' + eventNamespace, module.event.keydown)
+ ;
+ if( module.has.search() ) {
+ $module
+ .on(module.get.inputEvent() + eventNamespace, selector.search, module.event.input)
+ ;
+ }
+ if( module.is.multiple() ) {
+ $document
+ .on('keydown' + elementNamespace, module.event.document.keydown)
+ ;
+ }
+ },
+ inputEvents: function() {
+ module.verbose('Binding input change events');
+ $module
+ .on('change' + eventNamespace, selector.input, module.event.change)
+ ;
+ },
+ mouseEvents: function() {
+ module.verbose('Binding mouse events');
+ if(module.is.multiple()) {
+ $module
+ .on(clickEvent + eventNamespace, selector.label, module.event.label.click)
+ .on(clickEvent + eventNamespace, selector.remove, module.event.remove.click)
+ ;
+ }
+ if( module.is.searchSelection() ) {
+ $module
+ .on('mousedown' + eventNamespace, module.event.mousedown)
+ .on('mouseup' + eventNamespace, module.event.mouseup)
+ .on('mousedown' + eventNamespace, selector.menu, module.event.menu.mousedown)
+ .on('mouseup' + eventNamespace, selector.menu, module.event.menu.mouseup)
+ .on(clickEvent + eventNamespace, selector.icon, module.event.icon.click)
+ .on(clickEvent + eventNamespace, selector.clearIcon, module.event.clearIcon.click)
+ .on('focus' + eventNamespace, selector.search, module.event.search.focus)
+ .on(clickEvent + eventNamespace, selector.search, module.event.search.focus)
+ .on('blur' + eventNamespace, selector.search, module.event.search.blur)
+ .on(clickEvent + eventNamespace, selector.text, module.event.text.focus)
+ ;
+ if(module.is.multiple()) {
+ $module
+ .on(clickEvent + eventNamespace, module.event.click)
+ ;
+ }
+ }
+ else {
+ if(settings.on == 'click') {
+ $module
+ .on(clickEvent + eventNamespace, selector.icon, module.event.icon.click)
+ .on(clickEvent + eventNamespace, module.event.test.toggle)
+ ;
+ }
+ else if(settings.on == 'hover') {
+ $module
+ .on('mouseenter' + eventNamespace, module.delay.show)
+ .on('mouseleave' + eventNamespace, module.delay.hide)
+ ;
+ }
+ else {
+ $module
+ .on(settings.on + eventNamespace, module.toggle)
+ ;
+ }
+ $module
+ .on('mousedown' + eventNamespace, module.event.mousedown)
+ .on('mouseup' + eventNamespace, module.event.mouseup)
+ .on('focus' + eventNamespace, module.event.focus)
+ .on(clickEvent + eventNamespace, selector.clearIcon, module.event.clearIcon.click)
+ ;
+ if(module.has.menuSearch() ) {
+ $module
+ .on('blur' + eventNamespace, selector.search, module.event.search.blur)
+ ;
+ }
+ else {
+ $module
+ .on('blur' + eventNamespace, module.event.blur)
+ ;
+ }
+ }
+ $menu
+ .on((hasTouch ? 'touchstart' : 'mouseenter') + eventNamespace, selector.item, module.event.item.mouseenter)
+ .on('mouseleave' + eventNamespace, selector.item, module.event.item.mouseleave)
+ .on('click' + eventNamespace, selector.item, module.event.item.click)
+ ;
+ },
+ intent: function() {
+ module.verbose('Binding hide intent event to document');
+ if(hasTouch) {
+ $document
+ .on('touchstart' + elementNamespace, module.event.test.touch)
+ .on('touchmove' + elementNamespace, module.event.test.touch)
+ ;
+ }
+ $document
+ .on(clickEvent + elementNamespace, module.event.test.hide)
+ ;
+ }
+ },
+
+ unbind: {
+ intent: function() {
+ module.verbose('Removing hide intent event from document');
+ if(hasTouch) {
+ $document
+ .off('touchstart' + elementNamespace)
+ .off('touchmove' + elementNamespace)
+ ;
+ }
+ $document
+ .off(clickEvent + elementNamespace)
+ ;
+ }
+ },
+
+ filter: function(query) {
+ var
+ searchTerm = (query !== undefined)
+ ? query
+ : module.get.query(),
+ afterFiltered = function() {
+ if(module.is.multiple()) {
+ module.filterActive();
+ }
+ if(query || (!query && module.get.activeItem().length == 0)) {
+ module.select.firstUnfiltered();
+ }
+ if( module.has.allResultsFiltered() ) {
+ if( settings.onNoResults.call(element, searchTerm) ) {
+ if(settings.allowAdditions) {
+ if(settings.hideAdditions) {
+ module.verbose('User addition with no menu, setting empty style');
+ module.set.empty();
+ module.hideMenu();
+ }
+ }
+ else {
+ module.verbose('All items filtered, showing message', searchTerm);
+ module.add.message(message.noResults);
+ }
+ }
+ else {
+ module.verbose('All items filtered, hiding dropdown', searchTerm);
+ module.hideMenu();
+ }
+ }
+ else {
+ module.remove.empty();
+ module.remove.message();
+ }
+ if(settings.allowAdditions) {
+ module.add.userSuggestion(module.escape.htmlEntities(query));
+ }
+ if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
+ module.show();
+ }
+ }
+ ;
+ if(settings.useLabels && module.has.maxSelections()) {
+ return;
+ }
+ if(settings.apiSettings) {
+ if( module.can.useAPI() ) {
+ module.queryRemote(searchTerm, function() {
+ if(settings.filterRemoteData) {
+ module.filterItems(searchTerm);
+ }
+ var preSelected = $input.val();
+ if(!Array.isArray(preSelected)) {
+ preSelected = preSelected && preSelected!=="" ? preSelected.split(settings.delimiter) : [];
+ }
+ $.each(preSelected,function(index,value){
+ $item.filter('[data-value="'+value+'"]')
+ .addClass(className.filtered)
+ ;
+ });
+ afterFiltered();
+ });
+ }
+ else {
+ module.error(error.noAPI);
+ }
+ }
+ else {
+ module.filterItems(searchTerm);
+ afterFiltered();
+ }
+ },
+
+ queryRemote: function(query, callback) {
+ var
+ apiSettings = {
+ errorDuration : false,
+ cache : 'local',
+ throttle : settings.throttle,
+ urlData : {
+ query: query
+ },
+ onError: function() {
+ module.add.message(message.serverError);
+ callback();
+ },
+ onFailure: function() {
+ module.add.message(message.serverError);
+ callback();
+ },
+ onSuccess : function(response) {
+ var
+ values = response[fields.remoteValues]
+ ;
+ if (!Array.isArray(values)){
+ values = [];
+ }
+ module.remove.message();
+ var menuConfig = {};
+ menuConfig[fields.values] = values;
+ module.setup.menu(menuConfig);
+
+ if(values.length===0 && !settings.allowAdditions) {
+ module.add.message(message.noResults);
+ }
+ callback();
+ }
+ }
+ ;
+ if( !$module.api('get request') ) {
+ module.setup.api();
+ }
+ apiSettings = $.extend(true, {}, apiSettings, settings.apiSettings);
+ $module
+ .api('setting', apiSettings)
+ .api('query')
+ ;
+ },
+
+ filterItems: function(query) {
+ var
+ searchTerm = module.remove.diacritics(query !== undefined
+ ? query
+ : module.get.query()
+ ),
+ results = null,
+ escapedTerm = module.escape.string(searchTerm),
+ regExpFlags = (settings.ignoreSearchCase ? 'i' : '') + 'gm',
+ beginsWithRegExp = new RegExp('^' + escapedTerm, regExpFlags)
+ ;
+ // avoid loop if we're matching nothing
+ if( module.has.query() ) {
+ results = [];
+
+ module.verbose('Searching for matching values', searchTerm);
+ $item
+ .each(function(){
+ var
+ $choice = $(this),
+ text,
+ value
+ ;
+ if($choice.hasClass(className.unfilterable)) {
+ results.push(this);
+ return true;
+ }
+ if(settings.match === 'both' || settings.match === 'text') {
+ text = module.remove.diacritics(String(module.get.choiceText($choice, false)));
+ if(text.search(beginsWithRegExp) !== -1) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, text)) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, text)) {
+ results.push(this);
+ return true;
+ }
+ }
+ if(settings.match === 'both' || settings.match === 'value') {
+ value = module.remove.diacritics(String(module.get.choiceValue($choice, text)));
+ if(value.search(beginsWithRegExp) !== -1) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === 'exact' && module.exactSearch(searchTerm, value)) {
+ results.push(this);
+ return true;
+ }
+ else if (settings.fullTextSearch === true && module.fuzzySearch(searchTerm, value)) {
+ results.push(this);
+ return true;
+ }
+ }
+ })
+ ;
+ }
+ module.debug('Showing only matched items', searchTerm);
+ module.remove.filteredItem();
+ if(results) {
+ $item
+ .not(results)
+ .addClass(className.filtered)
+ ;
+ }
+
+ if(!module.has.query()) {
+ $divider
+ .removeClass(className.hidden);
+ } else if(settings.hideDividers === true) {
+ $divider
+ .addClass(className.hidden);
+ } else if(settings.hideDividers === 'empty') {
+ $divider
+ .removeClass(className.hidden)
+ .filter(function() {
+ // First find the last divider in this divider group
+ // Dividers which are direct siblings are considered a group
+ var lastDivider = $(this).nextUntil(selector.item);
+
+ return (lastDivider.length ? lastDivider : $(this))
+ // Count all non-filtered items until the next divider (or end of the dropdown)
+ .nextUntil(selector.divider)
+ .filter(selector.item + ":not(." + className.filtered + ")")
+ // Hide divider if no items are found
+ .length === 0;
+ })
+ .addClass(className.hidden);
+ }
+ },
+
+ fuzzySearch: function(query, term) {
+ var
+ termLength = term.length,
+ queryLength = query.length
+ ;
+ query = (settings.ignoreSearchCase ? query.toLowerCase() : query);
+ term = (settings.ignoreSearchCase ? term.toLowerCase() : term);
+ if(queryLength > termLength) {
+ return false;
+ }
+ if(queryLength === termLength) {
+ return (query === term);
+ }
+ search: for (var characterIndex = 0, nextCharacterIndex = 0; characterIndex < queryLength; characterIndex++) {
+ var
+ queryCharacter = query.charCodeAt(characterIndex)
+ ;
+ while(nextCharacterIndex < termLength) {
+ if(term.charCodeAt(nextCharacterIndex++) === queryCharacter) {
+ continue search;
+ }
+ }
+ return false;
+ }
+ return true;
+ },
+ exactSearch: function (query, term) {
+ query = (settings.ignoreSearchCase ? query.toLowerCase() : query);
+ term = (settings.ignoreSearchCase ? term.toLowerCase() : term);
+ return term.indexOf(query) > -1;
+
+ },
+ filterActive: function() {
+ if(settings.useLabels) {
+ $item.filter('.' + className.active)
+ .addClass(className.filtered)
+ ;
+ }
+ },
+
+ focusSearch: function(skipHandler) {
+ if( module.has.search() && !module.is.focusedOnSearch() ) {
+ if(skipHandler) {
+ $module.off('focus' + eventNamespace, selector.search);
+ $search.focus();
+ $module.on('focus' + eventNamespace, selector.search, module.event.search.focus);
+ }
+ else {
+ $search.focus();
+ }
+ }
+ },
+
+ blurSearch: function() {
+ if( module.has.search() ) {
+ $search.blur();
+ }
+ },
+
+ forceSelection: function() {
+ var
+ $currentlySelected = $item.not(className.filtered).filter('.' + className.selected).eq(0),
+ $activeItem = $item.not(className.filtered).filter('.' + className.active).eq(0),
+ $selectedItem = ($currentlySelected.length > 0)
+ ? $currentlySelected
+ : $activeItem,
+ hasSelected = ($selectedItem.length > 0)
+ ;
+ if(settings.allowAdditions || (hasSelected && !module.is.multiple())) {
+ module.debug('Forcing partial selection to selected item', $selectedItem);
+ module.event.item.click.call($selectedItem, {}, true);
+ }
+ else {
+ module.remove.searchTerm();
+ }
+ },
+
+ change: {
+ values: function(values) {
+ if(!settings.allowAdditions) {
+ module.clear();
+ }
+ module.debug('Creating dropdown with specified values', values);
+ var menuConfig = {};
+ menuConfig[fields.values] = values;
+ module.setup.menu(menuConfig);
+ $.each(values, function(index, item) {
+ if(item.selected == true) {
+ module.debug('Setting initial selection to', item[fields.value]);
+ module.set.selected(item[fields.value]);
+ if(!module.is.multiple()) {
+ return false;
+ }
+ }
+ });
+
+ if(module.has.selectInput()) {
+ module.disconnect.selectObserver();
+ $input.html('');
+ $input.append('');
+ $.each(values, function(index, item) {
+ var
+ value = settings.templates.deQuote(item[fields.value]),
+ name = settings.templates.escape(
+ item[fields.name] || '',
+ settings.preserveHTML
+ )
+ ;
+ $input.append('');
+ });
+ module.observe.select();
+ }
+ }
+ },
+
+ event: {
+ change: function() {
+ if(!internalChange) {
+ module.debug('Input changed, updating selection');
+ module.set.selected();
+ }
+ },
+ focus: function() {
+ if(settings.showOnFocus && !activated && module.is.hidden() && !pageLostFocus) {
+ module.show();
+ }
+ },
+ blur: function(event) {
+ pageLostFocus = (document.activeElement === this);
+ if(!activated && !pageLostFocus) {
+ module.remove.activeLabel();
+ module.hide();
+ }
+ },
+ mousedown: function() {
+ if(module.is.searchSelection()) {
+ // prevent menu hiding on immediate re-focus
+ willRefocus = true;
+ }
+ else {
+ // prevents focus callback from occurring on mousedown
+ activated = true;
+ }
+ },
+ mouseup: function() {
+ if(module.is.searchSelection()) {
+ // prevent menu hiding on immediate re-focus
+ willRefocus = false;
+ }
+ else {
+ activated = false;
+ }
+ },
+ click: function(event) {
+ var
+ $target = $(event.target)
+ ;
+ // focus search
+ if($target.is($module)) {
+ if(!module.is.focusedOnSearch()) {
+ module.focusSearch();
+ }
+ else {
+ module.show();
+ }
+ }
+ },
+ search: {
+ focus: function(event) {
+ activated = true;
+ if(module.is.multiple()) {
+ module.remove.activeLabel();
+ }
+ if(settings.showOnFocus || (event.type !== 'focus' && event.type !== 'focusin')) {
+ module.search();
+ }
+ },
+ blur: function(event) {
+ pageLostFocus = (document.activeElement === this);
+ if(module.is.searchSelection() && !willRefocus) {
+ if(!itemActivated && !pageLostFocus) {
+ if(settings.forceSelection) {
+ module.forceSelection();
+ } else if(!settings.allowAdditions){
+ module.remove.searchTerm();
+ }
+ module.hide();
+ }
+ }
+ willRefocus = false;
+ }
+ },
+ clearIcon: {
+ click: function(event) {
+ module.clear();
+ if(module.is.searchSelection()) {
+ module.remove.searchTerm();
+ }
+ module.hide();
+ event.stopPropagation();
+ }
+ },
+ icon: {
+ click: function(event) {
+ iconClicked=true;
+ if(module.has.search()) {
+ if(!module.is.active()) {
+ if(settings.showOnFocus){
+ module.focusSearch();
+ } else {
+ module.toggle();
+ }
+ } else {
+ module.blurSearch();
+ }
+ } else {
+ module.toggle();
+ }
+ }
+ },
+ text: {
+ focus: function(event) {
+ activated = true;
+ module.focusSearch();
+ }
+ },
+ input: function(event) {
+ if(module.is.multiple() || module.is.searchSelection()) {
+ module.set.filtered();
+ }
+ clearTimeout(module.timer);
+ module.timer = setTimeout(module.search, settings.delay.search);
+ },
+ label: {
+ click: function(event) {
+ var
+ $label = $(this),
+ $labels = $module.find(selector.label),
+ $activeLabels = $labels.filter('.' + className.active),
+ $nextActive = $label.nextAll('.' + className.active),
+ $prevActive = $label.prevAll('.' + className.active),
+ $range = ($nextActive.length > 0)
+ ? $label.nextUntil($nextActive).add($activeLabels).add($label)
+ : $label.prevUntil($prevActive).add($activeLabels).add($label)
+ ;
+ if(event.shiftKey) {
+ $activeLabels.removeClass(className.active);
+ $range.addClass(className.active);
+ }
+ else if(event.ctrlKey) {
+ $label.toggleClass(className.active);
+ }
+ else {
+ $activeLabels.removeClass(className.active);
+ $label.addClass(className.active);
+ }
+ settings.onLabelSelect.apply(this, $labels.filter('.' + className.active));
+ }
+ },
+ remove: {
+ click: function() {
+ var
+ $label = $(this).parent()
+ ;
+ if( $label.hasClass(className.active) ) {
+ // remove all selected labels
+ module.remove.activeLabels();
+ }
+ else {
+ // remove this label only
+ module.remove.activeLabels( $label );
+ }
+ }
+ },
+ test: {
+ toggle: function(event) {
+ var
+ toggleBehavior = (module.is.multiple())
+ ? module.show
+ : module.toggle
+ ;
+ if(module.is.bubbledLabelClick(event) || module.is.bubbledIconClick(event)) {
+ return;
+ }
+ if( module.determine.eventOnElement(event, toggleBehavior) ) {
+ event.preventDefault();
+ }
+ },
+ touch: function(event) {
+ module.determine.eventOnElement(event, function() {
+ if(event.type == 'touchstart') {
+ module.timer = setTimeout(function() {
+ module.hide();
+ }, settings.delay.touch);
+ }
+ else if(event.type == 'touchmove') {
+ clearTimeout(module.timer);
+ }
+ });
+ event.stopPropagation();
+ },
+ hide: function(event) {
+ if(module.determine.eventInModule(event, module.hide)){
+ if(element.id && $(event.target).attr('for') === element.id){
+ event.preventDefault();
+ }
+ }
+ }
+ },
+ class: {
+ mutation: function(mutations) {
+ mutations.forEach(function(mutation) {
+ if(mutation.attributeName === "class") {
+ module.check.disabled();
+ }
+ });
+ }
+ },
+ select: {
+ mutation: function(mutations) {
+ module.debug('