From d9be82bd003d2dfd1c2bd31bf871d49c898b5c6b Mon Sep 17 00:00:00 2001 From: mrsdizzie Date: Fri, 8 Nov 2019 02:01:24 -0500 Subject: [PATCH 01/16] Fix edit content button on migrated issue content (#8877) Typo on a closing span tag caused edit button not to work properly on the original issue content for a migrated issue. Fixes #8876 --- templates/repo/issue/view_content.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl index 29d48d70891d..9599930d7127 100644 --- a/templates/repo/issue/view_content.tmpl +++ b/templates/repo/issue/view_content.tmpl @@ -22,7 +22,7 @@
{{if .Issue.OriginalAuthor }} - {{ .Issue.OriginalAuthor }} {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}} {{if .Repository.OriginalURL}} ({{$.i18n.Tr "repo.migrated_from" .Repository.OriginalURL .Repository.GetOriginalURLHostname | Safe }}){{end}} + {{ .Issue.OriginalAuthor }} {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}} {{if .Repository.OriginalURL}} ({{$.i18n.Tr "repo.migrated_from" .Repository.OriginalURL .Repository.GetOriginalURLHostname | Safe }}){{end}} {{else}} {{.Issue.Poster.GetDisplayName}} {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}} {{end}} From 1274ad864e59b857344f3e11cc867d3146ddd64b Mon Sep 17 00:00:00 2001 From: Jookia <166291@gmail.com> Date: Fri, 8 Nov 2019 17:33:21 +0000 Subject: [PATCH 02/16] a11y: Improve accessibility of dropdown menus (#8638) * js: Import Semantic-UI's dropdown.js (version 2.3.1) * js: Set tabindex=-1 on dropdown items Setting tabindex=-1 on focusable elements within dropdown menus allows the user to treat dropdown menus as a single focusable item with its own internal navigation using arrow keys. * js: Don't use jQuery to click menu items Menu items are often elements, which jQuery refuses to trigger click events on. Instead it just bubbles up to the menu. Using HTMLElement's click method fixes this and makes menu items clickable from the keyboard using dropdown menus. * js: Set correct ARIA 1.1 roles on dropdown menus Setting role= makes assistive technology aware there is a widget here. In this case, Orca will now exit browse mode and allow us to capture keydown events when focused on a dropdown menu. It will also inform the user that there's a menu focused. Since dropdowns can be used in multiple elements each with different ARIA roles, a guessRole method is used to find the correct role. All roles I consider possible are listed, but only menu is implemented. * js: Set aria-expanded when dropdown menus show and hide This is deliberately done before the transition finishes so that screen readers get immediate feedback. * js: Set aria-label or aria-labelledby on dropdown menus This makes dropdown menu buttons screen reader accessible. aria-labelledby refers to an element using an ID, so the chosen labels are now assigned a unique ID- This ID is not stable, do not refer to it with user scripts. * js: Set aria-activedescendant on dropdown menus As the menus grab focus and navigate by tracking a 'selected' div class, assistive technology has no idea that what the current selection is. Assign IDs to each menu item and set aria-activedescendant to the ID of the currently selected menu item. When the menu is unfocused, remove aria-activedescendant- This isn't neccessary but in my experience it triggers Orca to remind the user of their current selection when re-focusing the menu. * Makefile: Make eslint ignore semantic.dropdown.js This file is taken from Semantic UI which isn't linted upstream. Ignore it as we won't fix these issues. * js: Add version note to semantic.dropdown.js * Add Md5 AppVer to templates/base/footer.tmpl Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Add Md5 AppVer to templates/pwa/serviceworker_js.tmpl Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * semantic.dropdown.js -> semantic.dropdown.custom.js * Use eslintignore * remove bogus submodule --- .eslintignore | 1 + public/js/semantic.dropdown.custom.js | 4023 +++++++++++++++++++++++++ public/vendor/librejs.html | 5 + templates/base/footer.tmpl | 1 + templates/pwa/serviceworker_js.tmpl | 1 + 5 files changed, 4031 insertions(+) create mode 100644 .eslintignore create mode 100644 public/js/semantic.dropdown.custom.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000000..125f92a2538a --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/public/js/semantic.dropdown.custom.js diff --git a/public/js/semantic.dropdown.custom.js b/public/js/semantic.dropdown.custom.js new file mode 100644 index 000000000000..1745869fbfd0 --- /dev/null +++ b/public/js/semantic.dropdown.custom.js @@ -0,0 +1,4023 @@ +/*! + * # Semantic UI 2.3.1 - Dropdown + * http://github.com/semantic-org/semantic-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'; + +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), + 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), + + $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), + + activated = false, + itemActivated = false, + internalChange = false, + element = this, + instance = $module.data(moduleNamespace), + + initialLoad, + pageLostFocus, + willRefocus, + elementNamespace, + id, + selectObserver, + menuObserver, + module + ; + + module = { + + initialize: function() { + module.debug('Initializing dropdown', settings); + + if( module.is.alreadySetup() ) { + module.setup.reference(); + } + else { + + module.setup.layout(); + + if(settings.values) { + module.change.values(settings.values); + } + + 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 + .off(eventNamespace) + .removeData(moduleNamespace) + ; + $menu + .off(eventNamespace) + ; + $document + .off(elementNamespace) + ; + module.disconnect.menuObserver(); + module.disconnect.selectObserver(); + }, + + observeChanges: function() { + if('MutationObserver' in window) { + selectObserver = new MutationObserver(module.event.select.mutation); + menuObserver = new MutationObserver(module.event.menu.mutation); + module.debug('Setting up mutation observer', selectObserver, menuObserver); + module.observe.select(); + module.observe.menu(); + } + }, + + disconnect: { + menuObserver: function() { + if(menuObserver) { + menuObserver.disconnect(); + } + }, + selectObserver: function() { + if(selectObserver) { + selectObserver.disconnect(); + } + } + }, + observe: { + select: function() { + if(module.has.input()) { + selectObserver.observe($module[0], { + childList : true, + subtree : true + }); + } + }, + menu: function() { + if(module.has.menu()) { + menuObserver.observe($menu[0], { + childList : true, + subtree : 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 = $.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(); + } + }, + + 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.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) ) + .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('class') + .detach() + .prependTo($module) + ; + } + module.refresh(); + }, + menu: function(values) { + $menu.html( templates.menu(values, fields)); + $item = $menu.find(selector.item); + }, + 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); + }, + + 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); + }, + + 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) { + 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.menuSearch()) { + module.focusSearch(); + } + module.set.visible(); + callback.call(element); + }); + } + } + }, + + hide: function(callback) { + 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(); + callback.call(element); + }); + } + } + }, + + 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() { + if(hasTouch) { + module.bind.touchEvents(); + } + module.bind.keyboardEvents(); + module.bind.inputEvents(); + module.bind.mouseEvents(); + }, + touchEvents: function() { + module.debug('Touch device detected binding additional touch events'); + if( module.is.searchSelection() ) { + // do nothing special yet + } + else if( module.is.single() ) { + $module + .on('touchstart' + eventNamespace, module.event.test.toggle) + ; + } + $menu + .on('touchstart' + eventNamespace, selector.item, module.event.item.mouseenter) + ; + }, + 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('click' + eventNamespace, selector.label, module.event.label.click) + .on('click' + 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('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('focus' + eventNamespace, selector.search, module.event.search.focus) + .on('click' + eventNamespace, selector.search, module.event.search.focus) + .on('blur' + eventNamespace, selector.search, module.event.search.blur) + .on('click' + eventNamespace, selector.text, module.event.text.focus) + ; + if(module.is.multiple()) { + $module + .on('click' + eventNamespace, module.event.click) + ; + } + } + else { + if(settings.on == 'click') { + $module + .on('click' + eventNamespace, selector.icon, module.event.icon.click) + .on('click' + 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) + ; + if(module.has.menuSearch() ) { + $module + .on('blur' + eventNamespace, selector.search, module.event.search.blur) + ; + } + else { + $module + .on('blur' + eventNamespace, module.event.blur) + ; + } + } + $menu + .on('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('click' + 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('click' + 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(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); + } + 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) { + module.remove.message(); + module.setup.menu({ + values: response[fields.remoteValues] + }); + 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 = (query !== undefined) + ? query + : module.get.query(), + results = null, + escapedTerm = module.escape.string(searchTerm), + beginsWithRegExp = new RegExp('^' + escapedTerm, 'igm') + ; + // 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(settings.match == 'both' || settings.match == 'text') { + text = 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 = 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) + ; + } + }, + + fuzzySearch: function(query, term) { + var + termLength = term.length, + queryLength = query.length + ; + query = query.toLowerCase(); + term = term.toLowerCase(); + 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 = query.toLowerCase(); + term = term.toLowerCase(); + if(term.indexOf(query) > -1) { + return true; + } + return false; + }, + 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(); + } + } + }, + + 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(hasSelected && !module.is.multiple()) { + module.debug('Forcing partial selection to selected item', $selectedItem); + $selectedItem[0].click(); + return; + } + else { + if(settings.allowAdditions) { + module.set.selected(module.get.query()); + module.remove.searchTerm(); + } + else { + module.remove.searchTerm(); + } + } + }, + + change: { + values: function(values) { + if(!settings.allowAdditions) { + module.clear(); + } + module.debug('Creating dropdown with specified values', values); + module.setup.menu({values: values}); + $.each(values, function(index, item) { + if(item.selected == true) { + module.debug('Setting initial selection to', item.value); + module.set.selected(item.value); + return true; + } + }); + } + }, + + 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() { + activated = true; + if(module.is.multiple()) { + module.remove.activeLabel(); + } + if(settings.showOnFocus) { + module.search(); + } + }, + blur: function(event) { + pageLostFocus = (document.activeElement === this); + if(module.is.searchSelection() && !willRefocus) { + if(!itemActivated && !pageLostFocus) { + if(settings.forceSelection) { + module.forceSelection(); + } + module.hide(); + } + } + willRefocus = false; + } + }, + icon: { + click: function(event) { + 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) { + module.determine.eventInModule(event, module.hide); + } + }, + select: { + mutation: function(mutations) { + module.debug(' removing selected option', removedValue); + newValue = module.remove.arrayValue(removedValue, values); + module.remove.optionValue(removedValue); + } + else { + module.verbose('Removing from delimited values', removedValue); + newValue = module.remove.arrayValue(removedValue, values); + newValue = newValue.join(settings.delimiter); + } + if(settings.fireOnInit === false && module.is.initialLoad()) { + module.verbose('No callback on initial load', settings.onRemove); + } + else { + settings.onRemove.call(element, removedValue, removedText, $removedItem); + } + module.set.value(newValue, removedText, $removedItem); + module.check.maxSelections(); + }, + arrayValue: function(removedValue, values) { + if( !$.isArray(values) ) { + values = [values]; + } + values = $.grep(values, function(value){ + return (removedValue != value); + }); + module.verbose('Removed value from delimited string', removedValue, values); + return values; + }, + label: function(value, shouldAnimate) { + var + $labels = $module.find(selector.label), + $removedLabel = $labels.filter('[data-' + metadata.value + '="' + module.escape.string(value) +'"]') + ; + module.verbose('Removing label', $removedLabel); + $removedLabel.remove(); + }, + activeLabels: function($activeLabels) { + $activeLabels = $activeLabels || $module.find(selector.label).filter('.' + className.active); + module.verbose('Removing active label selections', $activeLabels); + module.remove.labels($activeLabels); + }, + labels: function($labels) { + $labels = $labels || $module.find(selector.label); + module.verbose('Removing labels', $labels); + $labels + .each(function(){ + var + $label = $(this), + value = $label.data(metadata.value), + stringValue = (value !== undefined) + ? String(value) + : value, + isUserValue = module.is.userValue(stringValue) + ; + if(settings.onLabelRemove.call($label, value) === false) { + module.debug('Label remove callback cancelled removal'); + return; + } + module.remove.message(); + if(isUserValue) { + module.remove.value(stringValue); + module.remove.label(stringValue); + } + else { + // selected will also remove label + module.remove.selected(stringValue); + } + }) + ; + }, + tabbable: function() { + if( module.is.searchSelection() ) { + module.debug('Searchable dropdown initialized'); + $search + .removeAttr('tabindex') + ; + $menu + .removeAttr('tabindex') + ; + } + else { + module.debug('Simple selection dropdown initialized'); + $module + .removeAttr('tabindex') + ; + $menu + .removeAttr('tabindex') + ; + } + } + }, + + has: { + menuSearch: function() { + return (module.has.search() && $search.closest($menu).length > 0); + }, + search: function() { + return ($search.length > 0); + }, + sizer: function() { + return ($sizer.length > 0); + }, + selectInput: function() { + return ( $input.is('select') ); + }, + minCharacters: function(searchTerm) { + if(settings.minCharacters) { + searchTerm = (searchTerm !== undefined) + ? String(searchTerm) + : String(module.get.query()) + ; + return (searchTerm.length >= settings.minCharacters); + } + return true; + }, + firstLetter: function($item, letter) { + var + text, + firstLetter + ; + if(!$item || $item.length === 0 || typeof letter !== 'string') { + return false; + } + text = module.get.choiceText($item, false); + letter = letter.toLowerCase(); + firstLetter = String(text).charAt(0).toLowerCase(); + return (letter == firstLetter); + }, + input: function() { + return ($input.length > 0); + }, + items: function() { + return ($item.length > 0); + }, + menu: function() { + return ($menu.length > 0); + }, + message: function() { + return ($menu.children(selector.message).length !== 0); + }, + label: function(value) { + var + escapedValue = module.escape.value(value), + $labels = $module.find(selector.label) + ; + if(settings.ignoreCase) { + escapedValue = escapedValue.toLowerCase(); + } + return ($labels.filter('[data-' + metadata.value + '="' + module.escape.string(escapedValue) +'"]').length > 0); + }, + maxSelections: function() { + return (settings.maxSelections && module.get.selectionCount() >= settings.maxSelections); + }, + allResultsFiltered: function() { + var + $normalResults = $item.not(selector.addition) + ; + return ($normalResults.filter(selector.unselectable).length === $normalResults.length); + }, + userSuggestion: function() { + return ($menu.children(selector.addition).length > 0); + }, + query: function() { + return (module.get.query() !== ''); + }, + value: function(value) { + return (settings.ignoreCase) + ? module.has.valueIgnoringCase(value) + : module.has.valueMatchingCase(value) + ; + }, + valueMatchingCase: function(value) { + var + values = module.get.values(), + hasValue = $.isArray(values) + ? values && ($.inArray(value, values) !== -1) + : (values == value) + ; + return (hasValue) + ? true + : false + ; + }, + valueIgnoringCase: function(value) { + var + values = module.get.values(), + hasValue = false + ; + if(!$.isArray(values)) { + values = [values]; + } + $.each(values, function(index, existingValue) { + if(String(value).toLowerCase() == String(existingValue).toLowerCase()) { + hasValue = true; + return false; + } + }); + return hasValue; + } + }, + + is: { + active: function() { + return $module.hasClass(className.active); + }, + animatingInward: function() { + return $menu.transition('is inward'); + }, + animatingOutward: function() { + return $menu.transition('is outward'); + }, + bubbledLabelClick: function(event) { + return $(event.target).is('select, input') && $module.closest('label').length > 0; + }, + bubbledIconClick: function(event) { + return $(event.target).closest($icon).length > 0; + }, + alreadySetup: function() { + return ($module.is('select') && $module.parent(selector.dropdown).data(moduleNamespace) !== undefined && $module.prev().length === 0); + }, + animating: function($subMenu) { + return ($subMenu) + ? $subMenu.transition && $subMenu.transition('is animating') + : $menu.transition && $menu.transition('is animating') + ; + }, + leftward: function($subMenu) { + var $selectedMenu = $subMenu || $menu; + return $selectedMenu.hasClass(className.leftward); + }, + disabled: function() { + return $module.hasClass(className.disabled); + }, + focused: function() { + return (document.activeElement === $module[0]); + }, + focusedOnSearch: function() { + return (document.activeElement === $search[0]); + }, + allFiltered: function() { + return( (module.is.multiple() || module.has.search()) && !(settings.hideAdditions == false && module.has.userSuggestion()) && !module.has.message() && module.has.allResultsFiltered() ); + }, + hidden: function($subMenu) { + return !module.is.visible($subMenu); + }, + initialLoad: function() { + return initialLoad; + }, + inObject: function(needle, object) { + var + found = false + ; + $.each(object, function(index, property) { + if(property == needle) { + found = true; + return true; + } + }); + return found; + }, + multiple: function() { + return $module.hasClass(className.multiple); + }, + remote: function() { + return settings.apiSettings && module.can.useAPI(); + }, + single: function() { + return !module.is.multiple(); + }, + selectMutation: function(mutations) { + var + selectChanged = false + ; + $.each(mutations, function(index, mutation) { + if(mutation.target && $(mutation.target).is('select')) { + selectChanged = true; + return true; + } + }); + return selectChanged; + }, + search: function() { + return $module.hasClass(className.search); + }, + searchSelection: function() { + return ( module.has.search() && $search.parent(selector.dropdown).length === 1 ); + }, + selection: function() { + return $module.hasClass(className.selection); + }, + userValue: function(value) { + return ($.inArray(value, module.get.userValues()) !== -1); + }, + upward: function($menu) { + var $element = $menu || $module; + return $element.hasClass(className.upward); + }, + visible: function($subMenu) { + return ($subMenu) + ? $subMenu.hasClass(className.visible) + : $menu.hasClass(className.visible) + ; + }, + verticallyScrollableContext: function() { + var + overflowY = ($context.get(0) !== window) + ? $context.css('overflow-y') + : false + ; + return (overflowY == 'auto' || overflowY == 'scroll'); + }, + horizontallyScrollableContext: function() { + var + overflowX = ($context.get(0) !== window) + ? $context.css('overflow-X') + : false + ; + return (overflowX == 'auto' || overflowX == 'scroll'); + } + }, + + can: { + activate: function($item) { + if(settings.useLabels) { + return true; + } + if(!module.has.maxSelections()) { + return true; + } + if(module.has.maxSelections() && $item.hasClass(className.active)) { + return true; + } + return false; + }, + openDownward: function($subMenu) { + var + $currentMenu = $subMenu || $menu, + canOpenDownward = true, + onScreen = {}, + calculations + ; + $currentMenu + .addClass(className.loading) + ; + calculations = { + context: { + offset : ($context.get(0) === window) + ? { top: 0, left: 0} + : $context.offset(), + scrollTop : $context.scrollTop(), + height : $context.outerHeight() + }, + menu : { + offset: $currentMenu.offset(), + height: $currentMenu.outerHeight() + } + }; + if(module.is.verticallyScrollableContext()) { + calculations.menu.offset.top += calculations.context.scrollTop; + } + onScreen = { + above : (calculations.context.scrollTop) <= calculations.menu.offset.top - calculations.context.offset.top - calculations.menu.height, + below : (calculations.context.scrollTop + calculations.context.height) >= calculations.menu.offset.top - calculations.context.offset.top + calculations.menu.height + }; + if(onScreen.below) { + module.verbose('Dropdown can fit in context downward', onScreen); + canOpenDownward = true; + } + else if(!onScreen.below && !onScreen.above) { + module.verbose('Dropdown cannot fit in either direction, favoring downward', onScreen); + canOpenDownward = true; + } + else { + module.verbose('Dropdown cannot fit below, opening upward', onScreen); + canOpenDownward = false; + } + $currentMenu.removeClass(className.loading); + return canOpenDownward; + }, + openRightward: function($subMenu) { + var + $currentMenu = $subMenu || $menu, + canOpenRightward = true, + isOffscreenRight = false, + calculations + ; + $currentMenu + .addClass(className.loading) + ; + calculations = { + context: { + offset : ($context.get(0) === window) + ? { top: 0, left: 0} + : $context.offset(), + scrollLeft : $context.scrollLeft(), + width : $context.outerWidth() + }, + menu: { + offset : $currentMenu.offset(), + width : $currentMenu.outerWidth() + } + }; + if(module.is.horizontallyScrollableContext()) { + calculations.menu.offset.left += calculations.context.scrollLeft; + } + isOffscreenRight = (calculations.menu.offset.left - calculations.context.offset.left + calculations.menu.width >= calculations.context.scrollLeft + calculations.context.width); + if(isOffscreenRight) { + module.verbose('Dropdown cannot fit in context rightward', isOffscreenRight); + canOpenRightward = false; + } + $currentMenu.removeClass(className.loading); + return canOpenRightward; + }, + click: function() { + return (hasTouch || settings.on == 'click'); + }, + extendSelect: function() { + return settings.allowAdditions || settings.apiSettings; + }, + show: function() { + return !module.is.disabled() && (module.has.items() || module.has.message()); + }, + useAPI: function() { + return $.fn.api !== undefined; + } + }, + + animate: { + show: function(callback, $subMenu) { + var + $currentMenu = $subMenu || $menu, + start = ($subMenu) + ? function() {} + : function() { + module.hideSubMenus(); + module.hideOthers(); + module.set.active(); + }, + transition + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + module.verbose('Doing menu show animation', $currentMenu); + module.set.direction($subMenu); + transition = module.get.transition($subMenu); + if( module.is.selection() ) { + module.set.scrollPosition(module.get.selectedItem(), true); + } + if( module.is.hidden($currentMenu) || module.is.animating($currentMenu) ) { + if(transition == 'none') { + start(); + $currentMenu.transition('show'); + callback.call(element); + } + else if($.fn.transition !== undefined && $module.transition('is supported')) { + $currentMenu + .transition({ + animation : transition + ' in', + debug : settings.debug, + verbose : settings.verbose, + duration : settings.duration, + queue : true, + onStart : start, + onComplete : function() { + callback.call(element); + } + }) + ; + } + else { + module.error(error.noTransition, transition); + } + } + }, + hide: function(callback, $subMenu) { + var + $currentMenu = $subMenu || $menu, + duration = ($subMenu) + ? (settings.duration * 0.9) + : settings.duration, + start = ($subMenu) + ? function() {} + : function() { + if( module.can.click() ) { + module.unbind.intent(); + } + module.remove.active(); + }, + transition = module.get.transition($subMenu) + ; + callback = $.isFunction(callback) + ? callback + : function(){} + ; + if( module.is.visible($currentMenu) || module.is.animating($currentMenu) ) { + module.verbose('Doing menu hide animation', $currentMenu); + + if(transition == 'none') { + start(); + $currentMenu.transition('hide'); + callback.call(element); + } + else if($.fn.transition !== undefined && $module.transition('is supported')) { + $currentMenu + .transition({ + animation : transition + ' out', + duration : settings.duration, + debug : settings.debug, + verbose : settings.verbose, + queue : false, + onStart : start, + onComplete : function() { + callback.call(element); + } + }) + ; + } + else { + module.error(error.transition); + } + } + } + }, + + hideAndClear: function() { + module.remove.searchTerm(); + if( module.has.maxSelections() ) { + return; + } + if(module.has.search()) { + module.hide(function() { + module.remove.filteredItem(); + }); + } + else { + module.hide(); + } + }, + + delay: { + show: function() { + module.verbose('Delaying show event to ensure user intent'); + clearTimeout(module.timer); + module.timer = setTimeout(module.show, settings.delay.show); + }, + hide: function() { + module.verbose('Delaying hide event to ensure user intent'); + clearTimeout(module.timer); + module.timer = setTimeout(module.hide, settings.delay.hide); + } + }, + + escape: { + value: function(value) { + var + multipleValues = $.isArray(value), + stringValue = (typeof value === 'string'), + isUnparsable = (!stringValue && !multipleValues), + hasQuotes = (stringValue && value.search(regExp.quote) !== -1), + values = [] + ; + if(isUnparsable || !hasQuotes) { + return value; + } + module.debug('Encoding quote values for use in select', value); + if(multipleValues) { + $.each(value, function(index, value){ + values.push(value.replace(regExp.quote, '"')); + }); + return values; + } + return value.replace(regExp.quote, '"'); + }, + string: function(text) { + text = String(text); + return text.replace(regExp.escape, '\\$&'); + } + }, + + setting: function(name, value) { + module.debug('Changing setting', name, value); + if( $.isPlainObject(name) ) { + $.extend(true, settings, name); + } + else if(value !== undefined) { + if($.isPlainObject(settings[name])) { + $.extend(true, settings[name], value); + } + else { + settings[name] = value; + } + } + else { + return settings[name]; + } + }, + internal: function(name, value) { + if( $.isPlainObject(name) ) { + $.extend(true, module, name); + } + else if(value !== undefined) { + module[name] = value; + } + else { + return module[name]; + } + }, + debug: function() { + if(!settings.silent && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.debug.apply(console, arguments); + } + } + }, + verbose: function() { + if(!settings.silent && settings.verbose && settings.debug) { + if(settings.performance) { + module.performance.log(arguments); + } + else { + module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':'); + module.verbose.apply(console, arguments); + } + } + }, + error: function() { + if(!settings.silent) { + module.error = Function.prototype.bind.call(console.error, console, settings.name + ':'); + module.error.apply(console, arguments); + } + }, + performance: { + log: function(message) { + var + currentTime, + executionTime, + previousTime + ; + if(settings.performance) { + currentTime = new Date().getTime(); + previousTime = time || currentTime; + executionTime = currentTime - previousTime; + time = currentTime; + performance.push({ + 'Name' : message[0], + 'Arguments' : [].slice.call(message, 1) || '', + 'Element' : element, + 'Execution Time' : executionTime + }); + } + clearTimeout(module.performance.timer); + module.performance.timer = setTimeout(module.performance.display, 500); + }, + display: function() { + var + title = settings.name + ':', + totalTime = 0 + ; + time = false; + clearTimeout(module.performance.timer); + $.each(performance, function(index, data) { + totalTime += data['Execution Time']; + }); + title += ' ' + totalTime + 'ms'; + if(moduleSelector) { + title += ' \'' + moduleSelector + '\''; + } + if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) { + console.groupCollapsed(title); + if(console.table) { + console.table(performance); + } + else { + $.each(performance, function(index, data) { + console.log(data['Name'] + ': ' + data['Execution Time']+'ms'); + }); + } + console.groupEnd(); + } + performance = []; + } + }, + invoke: function(query, passedArguments, context) { + var + object = instance, + maxDepth, + found, + response + ; + passedArguments = passedArguments || queryArguments; + context = element || context; + if(typeof query == 'string' && object !== undefined) { + query = query.split(/[\. ]/); + maxDepth = query.length - 1; + $.each(query, function(depth, value) { + var camelCaseValue = (depth != maxDepth) + ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1) + : query + ; + if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) { + object = object[camelCaseValue]; + } + else if( object[camelCaseValue] !== undefined ) { + found = object[camelCaseValue]; + return false; + } + else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) { + object = object[value]; + } + else if( object[value] !== undefined ) { + found = object[value]; + return false; + } + else { + module.error(error.method, query); + return false; + } + }); + } + if ( $.isFunction( found ) ) { + response = found.apply(context, passedArguments); + } + else if(found !== undefined) { + response = found; + } + if($.isArray(returnedValue)) { + returnedValue.push(response); + } + else if(returnedValue !== undefined) { + returnedValue = [returnedValue, response]; + } + else if(response !== undefined) { + returnedValue = response; + } + return found; + } + }; + + if(methodInvoked) { + if(instance === undefined) { + module.initialize(); + } + module.invoke(query); + } + else { + if(instance !== undefined) { + instance.invoke('destroy'); + } + module.initialize(); + } + }) + ; + return (returnedValue !== undefined) + ? returnedValue + : $allModules + ; +}; + +$.fn.dropdown.settings = { + + silent : false, + debug : false, + verbose : false, + performance : true, + + on : 'click', // what event should show menu action on item selection + action : 'activate', // action on item selection (nothing, activate, select, combo, hide, function(){}) + + values : false, // specify values to use for dropdown + + apiSettings : false, + selectOnKeydown : true, // Whether selection should occur automatically when keyboard shortcuts used + minCharacters : 0, // Minimum characters required to trigger API call + + filterRemoteData : false, // Whether API results should be filtered after being returned for query term + saveRemoteData : true, // Whether remote name/value pairs should be stored in sessionStorage to allow remote data to be restored on page refresh + + throttle : 200, // How long to wait after last user input to search remotely + + context : window, // Context to use when determining if on screen + direction : 'auto', // Whether dropdown should always open in one direction + keepOnScreen : true, // Whether dropdown should check whether it is on screen before showing + + match : 'both', // what to match against with search selection (both, text, or label) + fullTextSearch : false, // search anywhere in value (set to 'exact' to require exact matches) + + placeholder : 'auto', // whether to convert blank the values will be delimited with this character + + showOnFocus : true, // show menu on focus + allowReselection : false, // whether current value should trigger callbacks when reselected + allowTab : true, // add tabindex to element + allowCategorySelection : false, // allow elements with sub-menus to be selected + + fireOnInit : false, // Whether callbacks should fire when initializing dropdown values + + transition : 'auto', // auto transition will slide down or up based on direction + duration : 200, // duration of transition + + glyphWidth : 1.037, // widest glyph width in em (W is 1.037 em) used to calculate multiselect input width + + // label settings on multi-select + label: { + transition : 'scale', + duration : 200, + variation : false + }, + + // delay before event + delay : { + hide : 300, + show : 200, + search : 20, + touch : 50 + }, + + /* Callbacks */ + onChange : function(value, text, $selected){}, + onAdd : function(value, text, $selected){}, + onRemove : function(value, text, $selected){}, + + onLabelSelect : function($selectedLabels){}, + onLabelCreate : function(value, text) { return $(this); }, + onLabelRemove : function(value) { return true; }, + onNoResults : function(searchTerm) { return true; }, + onShow : function(){}, + onHide : function(){}, + + /* Component */ + name : 'Dropdown', + namespace : 'dropdown', + + message: { + addResult : 'Add {term}', + count : '{count} selected', + maxSelections : 'Max {maxCount} selections', + noResults : 'No results found.', + serverError : 'There was an error contacting the server' + }, + + error : { + action : 'You called a dropdown action that was not defined', + alreadySetup : 'Once a select has been initialized behaviors must be called on the created ui dropdown', + labels : 'Allowing user additions currently requires the use of labels.', + missingMultiple : ' +
+
+
+ {{.CsrfTokenHtml}} +
+
-
- - + + +
+
+
+ + +
+
{{end}}
+ + + + + {{template "base/footer" .}} From 6e1912c73ac3b350593efbe4e3ac8f37b052e008 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Sat, 9 Nov 2019 00:40:37 -0300 Subject: [PATCH 09/16] Fix password complexity check on registration (#8887) * Fix registration password complexity * Fix integration to use a complex password ;) --- integrations/signup_test.go | 4 ++-- routers/user/auth.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/integrations/signup_test.go b/integrations/signup_test.go index 325c906326bf..e122efa39c5b 100644 --- a/integrations/signup_test.go +++ b/integrations/signup_test.go @@ -19,8 +19,8 @@ func TestSignup(t *testing.T) { req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ "user_name": "exampleUser", "email": "exampleUser@example.com", - "password": "examplePassword", - "retype": "examplePassword", + "password": "examplePassword!1", + "retype": "examplePassword!1", }) MakeRequest(t, req, http.StatusFound) diff --git a/routers/user/auth.go b/routers/user/auth.go index 82a508e4dc84..b328ac094efa 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -1070,6 +1070,11 @@ func SignUpPost(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterFo ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form) return } + if !password.IsComplexEnough(form.Password) { + ctx.Data["Err_Password"] = true + ctx.RenderWithErr(ctx.Tr("form.password_complexity"), tplSignUp, &form) + return + } u := &models.User{ Name: form.UserName, From c15d371939330f3b7d0e0aeaca3116a7b1c15065 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 9 Nov 2019 03:44:04 +0000 Subject: [PATCH 10/16] [skip ci] Updated translations via Crowdin --- options/locale/locale_bg-BG.ini | 2 -- options/locale/locale_cs-CZ.ini | 2 -- options/locale/locale_de-DE.ini | 2 -- options/locale/locale_es-ES.ini | 2 -- options/locale/locale_fa-IR.ini | 2 -- options/locale/locale_fi-FI.ini | 2 -- options/locale/locale_fr-FR.ini | 2 -- options/locale/locale_hu-HU.ini | 2 -- options/locale/locale_id-ID.ini | 2 -- options/locale/locale_it-IT.ini | 2 -- options/locale/locale_ja-JP.ini | 2 -- options/locale/locale_ko-KR.ini | 2 -- options/locale/locale_lv-LV.ini | 2 -- options/locale/locale_nl-NL.ini | 2 -- options/locale/locale_pl-PL.ini | 2 -- options/locale/locale_pt-BR.ini | 2 -- options/locale/locale_ru-RU.ini | 2 -- options/locale/locale_sr-SP.ini | 2 -- options/locale/locale_sv-SE.ini | 2 -- options/locale/locale_tr-TR.ini | 2 -- options/locale/locale_uk-UA.ini | 2 -- options/locale/locale_zh-CN.ini | 2 -- options/locale/locale_zh-HK.ini | 2 -- options/locale/locale_zh-TW.ini | 2 -- 24 files changed, 48 deletions(-) diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index 40dabfae9f38..37adbbf9fc6c 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -550,8 +550,6 @@ teams.members=Участници в екипа teams.update_settings=Запази настройките teams.add_team_member=Добави участник в екипа teams.repositories=Хранилища на екипа -teams.add_team_repository=Добави хранилище на екипа -teams.remove_repo=Премахни teams.add_nonexistent_repo=Хранилището, което се опитвате да добавите не съществува. Моля първо го създайте! [admin] diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 24680cea5b3b..e95d786120af 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1527,8 +1527,6 @@ teams.write_permission_desc=Členství v tom týmu poskytuje právo záp teams.admin_permission_desc=Členství v tom týmu poskytuje právo správce: členové mohou číst z, nahrávat do a přidávat spolupracovníky do repozitářů týmu. teams.repositories=Repozitáře týmu teams.search_repo_placeholder=Hledat repozitář… -teams.add_team_repository=Přidat repozitář týmu -teams.remove_repo=Smazat teams.add_nonexistent_repo=Repozitář, který se snažíte přidat, neexistuje. Prosím, nejdříve jej vytvořte. teams.add_duplicate_users=Uživatel je již členem týmu. teams.repos.none=Tento tým nemůže přistoupit k žádným repozitářům. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index dc32a9f154e0..9897696fb4fd 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1582,8 +1582,6 @@ teams.write_permission_desc=Dieses Team hat Schreibzugriff: Mit teams.admin_permission_desc=Dieses Team hat Adminzugriff: Mitglieder dieses Teams können Team-Repositories ansehen, auf sie pushen und Mitarbeiter hinzufügen. teams.repositories=Team-Repositories teams.search_repo_placeholder=Repository durchsuchen… -teams.add_team_repository=Team-Repository hinzufügen -teams.remove_repo=Entfernen teams.add_nonexistent_repo=Das Repository, das du hinzufügen möchten, existiert nicht. Bitte erstelle es zuerst. teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied. teams.repos.none=Dieses Team hat Zugang zu keinem Repository. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index a33b78f71b92..593843c51e50 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1561,8 +1561,6 @@ teams.write_permission_desc=Este equipo tiene permisos de EscrituraAdministración: los miembros pueden ver, hacer push y añadir colaboradores a los repositorios del equipo. teams.repositories=Repositorios del equipo teams.search_repo_placeholder=Buscar repositorio… -teams.add_team_repository=Añadir repositorio al equipo -teams.remove_repo=Eliminar teams.add_nonexistent_repo=El repositorio que estás intentando añadir no existe, por favor, créalo primero. teams.add_duplicate_users=El usuario ya es miembro del equipo. teams.repos.none=Este equipo no tiene repositorios accesibles. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index a0551ed71d61..7f14f280d98b 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1563,8 +1563,6 @@ teams.write_permission_desc=این تیم دسترسی نوشتننوشتن خواهد داشت: اعضا خواهند توانست مخازن تیم را خوانده ، تغییراتی در آنها اعمال کرده و یا همکارانشان را به مخازن اضافه نمایند. teams.repositories=مخازن تیم teams.search_repo_placeholder=جستجوی مخزن... -teams.add_team_repository=افزودن مخزن تیمی -teams.remove_repo=حذف teams.add_nonexistent_repo=مخزنی را که شما قصد افزودن آن را دارید موجود نیست، لطفا ابتدا آن را ایجاد کنید. teams.add_duplicate_users=این کاربر پیش از این عضو تیم بوده است. teams.repos.none=این تیم به هیچ مخزنی دسترسی ندارد. diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 75a80de52806..fc79584d1a20 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -609,8 +609,6 @@ teams.members=Ryhmän jäsenet teams.update_settings=Päivitä asetukset teams.add_team_member=Lisää tiimin jäsen teams.repositories=Tiimin repot -teams.add_team_repository=Lisää tiimirepo -teams.remove_repo=Poista teams.add_nonexistent_repo=Repo jota yrität lisätä ei ole vielä olemassa, ole hyvä ja luo se ensin. [admin] diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index c1509b108994..1f1496700232 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1550,8 +1550,6 @@ teams.write_permission_desc=Cette équipe permet l'accès en écritureadministrateur : les membres peuvent voir, participer et ajouter des collaborateurs à ses dépôts. teams.repositories=Dépôts de l'Équipe teams.search_repo_placeholder=Rechercher dans le dépôt… -teams.add_team_repository=Ajouter un Dépôt à l'Équipe -teams.remove_repo=Supprimer teams.add_nonexistent_repo=Dépôt inexistant, veuillez d'abord le créer. teams.add_duplicate_users=L’utilisateur est déjà un membre de l’équipe. teams.repos.none=Aucun dépôt n'est accessible par cette équipe. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 2d90909d1e7e..4992dbbef777 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -699,8 +699,6 @@ teams.add_team_member=Csapattag hozzáadása teams.delete_team_success=A csoport törölve lett. teams.repositories=Csoport tárolói teams.search_repo_placeholder=Tároló keresése… -teams.add_team_repository=Új csoport tároló -teams.remove_repo=Eltávolítás teams.add_nonexistent_repo=A tároló, melybe feltölteni szeretne, még nem létezik; először hozza létre. [admin] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index bd7d0eeb6345..bbe4870cbb82 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -745,8 +745,6 @@ teams.add_team_member=Tambahkan Anggota Tim teams.delete_team_success=Tim sudah di hapus. teams.repositories=Tim repositori teams.search_repo_placeholder=Cari repositori… -teams.add_team_repository=Tambahkan Tim Repositori -teams.remove_repo=Menghapus teams.add_nonexistent_repo=Repositori yang ingin Anda tambahkan tidak ada; Silahkan buat terlebih dahulu. [admin] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 782940629736..00b6f3b37880 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1195,8 +1195,6 @@ teams.write_permission_desc=Questo team concede l'accesso di ScritturaAmministratore: i membri possono leggere da, pushare su e aggiungere collaboratori ai repository del team. teams.repositories=Repository di Squadra teams.search_repo_placeholder=Ricerca repository… -teams.add_team_repository=Aggiungere Repository di Squadra -teams.remove_repo=Rimuovi teams.add_nonexistent_repo=Il repository che stai tentando di aggiungere non esiste, crealo prima. [admin] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index a006e17ccc79..319aad730c93 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1581,8 +1581,6 @@ teams.write_permission_desc=このチームは書き込みア teams.admin_permission_desc=このチームは管理者アクセス権を持ちます: メンバーはチームリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。 teams.repositories=チームのリポジトリ teams.search_repo_placeholder=リポジトリを検索… -teams.add_team_repository=チームのリポジトリを追加 -teams.remove_repo=削除 teams.add_nonexistent_repo=追加しようとしているリポジトリは存在しません。 先にリポジトリを作成してください。 teams.add_duplicate_users=ユーザーは既にチームのメンバーです。 teams.repos.none=このチームがアクセスできるリポジトリはありません。 diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index f0ce4cdb30e5..dbc74b22ceb3 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -570,8 +570,6 @@ teams.update_settings=설정 업데이트 teams.add_team_member=팀 구성원 추가 teams.delete_team_success=팀이 삭제되었습니다. teams.repositories=팀 저장소 -teams.add_team_repository=팀 저장소 추가 -teams.remove_repo=삭제 teams.add_nonexistent_repo=추가하려는 저장소를 존재하지 않습니다. 먼저 생성해주세요. [admin] diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 96fe30a7a4a8..f7491f1a89ff 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1539,8 +1539,6 @@ teams.write_permission_desc=Šai komandai ir rakstīšanas ties teams.admin_permission_desc=Šai komandai ir administratora tiesības: dalībnieki var lasīt, rakstīt un pievienot citus dalībniekus komandas repozitorijiem. teams.repositories=Komandas repozitoriji teams.search_repo_placeholder=Meklēt repozitorijā… -teams.add_team_repository=Pievienot komandas repozitoriju -teams.remove_repo=Noņemt teams.add_nonexistent_repo=Repozitorijs, kuram Jūs mēģinat pievienot neeksistē, sākumā izveidojiet to. teams.add_duplicate_users=Lietotājs jau ir šajā komandā. teams.repos.none=Šai komandai nav piekļuves nevienam repozitorijam. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 66f8efe03111..5aac7b0f1a38 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1156,8 +1156,6 @@ teams.delete_team_title=Verwijder team teams.delete_team_success=Het team is verwijderd. teams.repositories=Teamrepositories teams.search_repo_placeholder=Repository zoeken… -teams.add_team_repository=Nieuwe teamrepositorie aanmaken -teams.remove_repo=Verwijder teams.add_nonexistent_repo=De opslagplaats die u probeert toe te voegen bestaat niet: maak deze eerst aan. teams.add_duplicate_users=Gebruiker is al een teamlid. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 71c9522921f5..0fdbaced6b05 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1468,8 +1468,6 @@ teams.write_permission_desc=Ten zespół udziela dostępu z zapisemadministratora: członkowie mogą wyświetlać i wypychać zmiany oraz dodawać współpracowników do repozytoriów zespołu. teams.repositories=Repozytoria zespołu teams.search_repo_placeholder=Szukaj repozytorium… -teams.add_team_repository=Dodaj repozytorium zespołu -teams.remove_repo=Usuń teams.add_nonexistent_repo=Repozytorium, które próbujesz dodać, nie istnieje. Proszę je najpierw utworzyć. teams.add_duplicate_users=Użytkownik jest już członkiem zespołu. teams.repos.none=Ten zespół nie ma dostępu do żadnego repozytorium. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 33da0ed0c411..e937d7091ae4 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1582,8 +1582,6 @@ teams.write_permission_desc=Esta equipe concede acesso para escritaAdministrador: Membros podem ler, fazer push e adicionar outros colaboradores para os repositórios da equipe. teams.repositories=Repositórios da equipe teams.search_repo_placeholder=Pesquisar repositório... -teams.add_team_repository=Adicionar repositório da equipe -teams.remove_repo=Remover teams.add_nonexistent_repo=O repositório que você está tentando adicionar não existe, por favor, crie-o primeiro. teams.add_duplicate_users=Usuário já é um membro da equipe. teams.repos.none=Nenhum repositório pode ser acessado por essa equipe. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 9102603425a0..341ee4719aa2 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1423,8 +1423,6 @@ teams.write_permission_desc=Эта команда предоставляет д teams.admin_permission_desc=Эта команда дает административный доступ: участники могут читать, пушить и добавлять соавторов к ее репозиториям. teams.repositories=Репозитории группы разработки teams.search_repo_placeholder=Поиск репозитория… -teams.add_team_repository=Добавить репозиторий группы разработки -teams.remove_repo=Удалить teams.add_nonexistent_repo=Вы добавляете в отсутствующий репозиторий, пожалуйста сначала его создайте. teams.add_duplicate_users=Пользователь уже состоит в команде. teams.repos.none=Для этой команды нет доступных репозиториев. diff --git a/options/locale/locale_sr-SP.ini b/options/locale/locale_sr-SP.ini index a4072b7dfaf7..f8eed4333ccf 100644 --- a/options/locale/locale_sr-SP.ini +++ b/options/locale/locale_sr-SP.ini @@ -488,8 +488,6 @@ teams.members=Чланови тима teams.update_settings=Примени промене teams.add_team_member=Додај члан тиму teams.repositories=Тимска спремишта -teams.add_team_repository=Додај тимско спремиште -teams.remove_repo=Уклони teams.add_nonexistent_repo=Овакво спремиште не постоји, молим вас прво да га направите. [admin] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 37d6621642bd..7b737bded6b7 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -1231,8 +1231,6 @@ teams.write_permission_desc=Medlemskap i detta team ger skrivrättighete teams.admin_permission_desc=Medlemskap i detta team ger administratörsrättigheter: medlemmar kan läsa, pusha och lägga till medarbetare till teamets utvecklingskataloger. teams.repositories=Teamförråd teams.search_repo_placeholder=Sök utvecklingskatalog… -teams.add_team_repository=Lägg till teamförråd -teams.remove_repo=Ta bort teams.add_nonexistent_repo=Förrådet du försöka lägga till finns inte, vänligen skapa det först. [admin] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 5a6af8def48c..bfd42b40d807 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1517,8 +1517,6 @@ teams.write_permission_desc=Bu takım Yazma erişimi veriyor. teams.admin_permission_desc=Bu takım Yönetici erişimi veriyor. Üyeler takım depolarını okuyabilir, itebilir ve katkıcı ekleyebilir. teams.repositories=Ekip Depoları teams.search_repo_placeholder=Depo ara… -teams.add_team_repository=Ekip Deposu Ekle -teams.remove_repo=Kaldır teams.add_nonexistent_repo=Eklemeye çalıştığınz depo mevcut değil. Lütfen önce oluşturun. teams.add_duplicate_users=Kullanıcı zaten takımın üyesi. teams.repos.none=Bu takım tarafından hiçbir depoya erişilemedi. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 51e52b27852b..2c57b08aff41 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1294,8 +1294,6 @@ teams.write_permission_desc=Ця команда надає доступ на адміністраторський доступ: учасники можуть читати, виконувати push команди та додавати співробітників до репозиторію. teams.repositories=Репозиторії команди teams.search_repo_placeholder=Пошук репозиторію… -teams.add_team_repository=Додати репозиторій команди -teams.remove_repo=Видалити teams.add_nonexistent_repo=Ви намагаєтеся додати у репозиторій якого не існує. Будь ласка, спочатку створіть його. teams.add_duplicate_users=Користувач уже є членом команди. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 594231262fe2..edcd99f5251d 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1577,8 +1577,6 @@ teams.write_permission_desc=该团队拥有对所属仓库的 读取管理 权限,团队成员可以读取、克隆、推送以及添加其它仓库协作者。 teams.repositories=团队仓库 teams.search_repo_placeholder=搜索仓库... -teams.add_team_repository=添加团队仓库 -teams.remove_repo=移除仓库 teams.add_nonexistent_repo=您尝试添加到团队的仓库不存在,请先创建仓库! teams.add_duplicate_users=用户已经是团队成员。 teams.repos.none=此团队无法访问任何仓库。 diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 43e9b3eea525..f6bfebabc9e3 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -588,8 +588,6 @@ teams.update_settings=更新團隊設定 teams.add_team_member=新增團隊成員 teams.delete_team_success=該團隊已被刪除。 teams.repositories=團隊儲存庫 -teams.add_team_repository=新增團隊儲存庫 -teams.remove_repo=移除儲存庫 teams.add_nonexistent_repo=您嘗試新增到團隊的儲存庫不存在,請先建立儲存庫! [admin] diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index de787b6b4793..41b174ebdb0a 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1037,8 +1037,6 @@ teams.delete_team_title=刪除團隊 teams.delete_team_success=該團隊已被刪除。 teams.repositories=團隊儲存庫 teams.search_repo_placeholder=搜尋儲存庫... -teams.add_team_repository=新增團隊儲存庫 -teams.remove_repo=移除儲存庫 teams.add_nonexistent_repo=您嘗試新增到團隊的儲存庫不存在,請先建立儲存庫! [admin] From f4937879b0f85e102bfe658ff75f09e6a5103ff9 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Sat, 9 Nov 2019 06:42:34 -0300 Subject: [PATCH 11/16] Fix require external registration password (#8885) * Fix require external registration password * Fix ctx on error condition by @jolheiser --- routers/user/auth.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/routers/user/auth.go b/routers/user/auth.go index b328ac094efa..cb5611e0459b 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -707,7 +707,7 @@ func oAuth2UserLoginCallback(loginSource *models.LoginSource, request *http.Requ // LinkAccount shows the page where the user can decide to login or create a new account func LinkAccount(ctx *context.Context) { - ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationCaptcha || setting.Service.AllowOnlyExternalRegistration + ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha @@ -757,7 +757,7 @@ func LinkAccount(ctx *context.Context) { // LinkAccountPostSignIn handle the coupling of external account with another account using signIn func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) { - ctx.Data["DisablePassword"] = setting.Service.AllowOnlyExternalRegistration + ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true ctx.Data["LinkAccountModeSignIn"] = true @@ -840,7 +840,7 @@ func LinkAccountPostSignIn(ctx *context.Context, signInForm auth.SignInForm) { func LinkAccountPostRegister(ctx *context.Context, cpt *captcha.Captcha, form auth.RegisterForm) { // TODO Make insecure passwords optional for local accounts also, // once email-based Second-Factor Auth is available - ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationCaptcha || setting.Service.AllowOnlyExternalRegistration + ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true ctx.Data["LinkAccountModeRegister"] = true From 1f3ba6919d35cc37d1d2575a70927986392f12b5 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 9 Nov 2019 09:45:09 +0000 Subject: [PATCH 12/16] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.ini | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 319aad730c93..1802108a50d9 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -68,6 +68,10 @@ pull_requests=プルリクエスト issues=課題 cancel=キャンセル +add=追加 +add_all=すべて追加 +remove=除去 +remove_all=すべて除去 write=書き込み preview=プレビュー @@ -1514,6 +1518,7 @@ team_name=チーム名 team_desc=説明 team_name_helper=チーム名は短く覚えやすいものにしましょう。 team_desc_helper=チームの目的や役割を説明します。 +team_access_desc=リポジトリアクセス team_permission_desc=権限 team_unit_desc=リポジトリのセクションへのアクセスを許可 @@ -1581,10 +1586,21 @@ teams.write_permission_desc=このチームは書き込みア teams.admin_permission_desc=このチームは管理者アクセス権を持ちます: メンバーはチームリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。 teams.repositories=チームのリポジトリ teams.search_repo_placeholder=リポジトリを検索… +teams.remove_all_repos_title=チームリポジトリをすべて除去 +teams.remove_all_repos_desc=チームからすべてのリポジトリを除去します。 +teams.add_all_repos_title=すべてのリポジトリを追加 +teams.add_all_repos_desc=組織のすべてのリポジトリをチームに追加します。 teams.add_nonexistent_repo=追加しようとしているリポジトリは存在しません。 先にリポジトリを作成してください。 teams.add_duplicate_users=ユーザーは既にチームのメンバーです。 teams.repos.none=このチームがアクセスできるリポジトリはありません。 teams.members.none=このチームにはメンバーがいません。 +teams.specific_repositories=指定したリポジトリ +teams.specific_repositories_helper=メンバーは、明示的にチームへ追加したリポジトリにのみアクセスできます。 これを選択しても、すでにすべてのリポジトリで追加されたリポジトリは自動的に除去されません。 +teams.all_repositories=すべてのリポジトリ +teams.all_repositories_helper=チームはすべてのリポジトリにアクセスできます。 これを選択すると、既存のすべてのリポジトリをチームに追加します。 +teams.all_repositories_read_permission_desc=このチームはすべてのリポジトリ読み取りアクセス権を持ちます: メンバーはリポジトリの閲覧とクローンが可能です。 +teams.all_repositories_write_permission_desc=このチームはすべてのリポジトリ書き込みアクセス権を持ちます: メンバーはリポジトリの読み取りとプッシュが可能です。 +teams.all_repositories_admin_permission_desc=このチームはすべてのリポジトリ管理者アクセス権を持ちます: メンバーはリポジトリの読み取り、プッシュ、共同作業者の追加が可能です。 [admin] dashboard=ダッシュボード From a647a54a08fc78286ea6ded008ea368f43e7c2ca Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Sat, 9 Nov 2019 10:09:01 -0300 Subject: [PATCH 13/16] Leave non-dated issues for last (#8871) --- models/issue.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/issue.go b/models/issue.go index 1e9d9731861f..389304853380 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1075,7 +1075,8 @@ func sortIssuesSession(sess *xorm.Session, sortType string, priorityRepoID int64 case "priority": sess.Desc("issue.priority") case "nearduedate": - sess.Asc("issue.deadline_unix") + // 253370764800 is 01/01/9999 @ 12:00am (UTC) + sess.OrderBy("CASE WHEN issue.deadline_unix = 0 THEN 253370764800 ELSE issue.deadline_unix END ASC") case "farduedate": sess.Desc("issue.deadline_unix") case "priorityrepo": From c54145174fba9d43b75b9f0d3f580bb02e69e6ee Mon Sep 17 00:00:00 2001 From: mrsdizzie Date: Sat, 9 Nov 2019 15:13:35 -0500 Subject: [PATCH 14/16] Update Github migration test (#8893) * Update Github migration test Earlier today #716 was reopened which updated the modification time for an old milestone (1.6.0) that we use in testing with the assumption that it is old and won't change. This breaks all builds now, so remove this test since we have others that test the same code and this milestone will likely be updated again as that issue changes etc... * ci --- modules/migrations/github_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index 2a0a4edf3268..d9976d11326a 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -118,12 +118,6 @@ func TestGitHubDownloadRepo(t *testing.T) { "2018-09-05 16:34:22 +0000 UTC", "2018-08-11 08:45:01 +0000 UTC", "closed", milestone) - case "1.6.0": - assertMilestoneEqual(t, "1.6.0", "2018-09-25 07:00:00 +0000 UTC", - "2018-05-11 05:37:01 +0000 UTC", - "2019-01-27 19:21:22 +0000 UTC", - "2018-11-23 13:23:16 +0000 UTC", - "closed", milestone) case "1.7.0": assertMilestoneEqual(t, "1.7.0", "2018-12-25 08:00:00 +0000 UTC", "2018-08-28 14:20:14 +0000 UTC", From bb04fb55d75242c71a131998565a567e193a3d8c Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Sat, 9 Nov 2019 19:12:05 -0300 Subject: [PATCH 15/16] Enable punctuations ending mentions (#8889) * Enable punctuations ending mentions * Improve tests --- modules/references/references.go | 2 +- modules/references/references_test.go | 47 ++++++++++++++++++++------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/modules/references/references.go b/modules/references/references.go index 58a8da28957b..af0fe1aa0df3 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -27,7 +27,7 @@ var ( // TODO: fix invalid linking issue // mentionPattern matches all mentions in the form of "@user" - mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_\.]+)(?:\s|$|\)|\])`) + mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`) // issueNumericPattern matches string that references to a numeric issue, e.g. #1287 issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(#[0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`) // issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234 diff --git a/modules/references/references_test.go b/modules/references/references_test.go index 52e9b4ff524e..d46c5e85d72a 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -208,14 +208,32 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) { } func TestRegExp_mentionPattern(t *testing.T) { - trueTestCases := []string{ - "@Unknwon", - "@ANT_123", - "@xxx-DiN0-z-A..uru..s-xxx", - " @lol ", - " @Te-st", - "(@gitea)", - "[@gitea]", + trueTestCases := []struct { + pat string + exp string + }{ + {"@Unknwon", "@Unknwon"}, + {"@ANT_123", "@ANT_123"}, + {"@xxx-DiN0-z-A..uru..s-xxx", "@xxx-DiN0-z-A..uru..s-xxx"}, + {" @lol ", "@lol"}, + {" @Te-st", "@Te-st"}, + {"(@gitea)", "@gitea"}, + {"[@gitea]", "@gitea"}, + {"@gitea! this", "@gitea"}, + {"@gitea? this", "@gitea"}, + {"@gitea. this", "@gitea"}, + {"@gitea, this", "@gitea"}, + {"@gitea; this", "@gitea"}, + {"@gitea!\nthis", "@gitea"}, + {"\n@gitea?\nthis", "@gitea"}, + {"\t@gitea.\nthis", "@gitea"}, + {"@gitea,\nthis", "@gitea"}, + {"@gitea;\nthis", "@gitea"}, + {"@gitea!", "@gitea"}, + {"@gitea?", "@gitea"}, + {"@gitea.", "@gitea"}, + {"@gitea,", "@gitea"}, + {"@gitea;", "@gitea"}, } falseTestCases := []string{ "@ 0", @@ -223,17 +241,24 @@ func TestRegExp_mentionPattern(t *testing.T) { "@", "", "ABC", + "@.ABC", "/home/gitea/@gitea", "\"@gitea\"", + "@@gitea", + "@gitea!this", + "@gitea?this", + "@gitea,this", + "@gitea;this", } for _, testCase := range trueTestCases { - res := mentionPattern.MatchString(testCase) - assert.True(t, res) + found := mentionPattern.FindStringSubmatch(testCase.pat) + assert.Len(t, found, 2) + assert.Equal(t, testCase.exp, found[1]) } for _, testCase := range falseTestCases { res := mentionPattern.MatchString(testCase) - assert.False(t, res) + assert.False(t, res, "[%s] should be false", testCase) } } From 1b7402bd1dfcf7435c2c2345aab1c43d6b0186ca Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 9 Nov 2019 23:21:53 +0000 Subject: [PATCH 16/16] Fix issue with user.fullname (#8902) --- templates/base/footer.tmpl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 5a3d24ab5865..bed8eea01953 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -63,11 +63,13 @@ noMatchTemplate: function () { return null }, menuItemTemplate: function (item) { var user = item.original; - var itemStr = '' + user.name + ''; + var item = $('
') + item.append($('', {'src': user.avatar})) + item.append($('', {'class': 'name'}).text(user.name)) if (user.fullname && user.fullname != '') { - itemStr += '' + user.fullname + ''; + item.append($('', {'class': 'fullname'}).text(user.fullname)) } - return itemStr; + return item.html(); } }); var content = document.getElementById('content');