diff options
-rw-r--r-- | app/assets/javascripts/issues.js.coffee | 12 | ||||
-rw-r--r-- | app/assets/javascripts/jquery.multiawesome.js | 354 | ||||
-rw-r--r-- | app/assets/javascripts/multiawesome.js | 390 | ||||
-rw-r--r-- | app/assets/stylesheets/application.scss | 5 | ||||
-rw-r--r-- | app/assets/stylesheets/multiawesome.css | 105 | ||||
-rw-r--r-- | app/views/shared/issuable/_filter.html.haml | 13 | ||||
-rw-r--r-- | db/schema.rb | 897 | ||||
-rw-r--r-- | vendor/assets/javascripts/sifter.js | 471 |
8 files changed, 1344 insertions, 903 deletions
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index ac9e022e727..da22cf2a851 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -16,12 +16,12 @@ else $(this).html totalIssues - 1 $("body").on "click", ".issues-other-filters .dropdown-menu a", -> - $('.issues-list').block( - message: null, - overlayCSS: - backgroundColor: '#DDD' - opacity: .4 - ) + # $('.issues-list').block( + # message: null, + # overlayCSS: + # backgroundColor: '#DDD' + # opacity: .4 + # ) reload: -> Issues.initSelects() diff --git a/app/assets/javascripts/jquery.multiawesome.js b/app/assets/javascripts/jquery.multiawesome.js new file mode 100644 index 00000000000..f2253cdd0ad --- /dev/null +++ b/app/assets/javascripts/jquery.multiawesome.js @@ -0,0 +1,354 @@ +//= require sifter.js + +/** + * multiawesome.js (v0.12.1) + * Copyright (c) 2015 Jacob Schatz & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Jacob Schatz <jschatz@gitlab.com> + */ + +/*jshint curly:false */ +/*jshint browser:true */ +(function ( $ ) { + $.fn.multiawesome = function( settings ) { + settings = settings || {}; + var MultiAwesome = { + searchTemplate: '<li><div class="input-with-icon"><i class="fa fa-search"></i><input type="text" id="multiawesome-search-input" /></div></li>', + itemContainerTemplate: '<li class="dropdown-multi-menu-selections"><ul></ul></li>', + itemTemplate: '<li><a href="#" class="item" tabIndex="-1"><input type="checkbox" name="{{name}}" value="{{data}}"/>{{label}}</a></li>', + seperatorTemplate: '<li role="separator" class="divider"></li>', + titleTemplate: '<li class=dropdown-multi-menu-title><div class=dropdown-multi-menu-title-area><h3>{{title}}</h3></div></li>', + categoryContainerTemplate: '<li class=dropdown-multi-menu-category><ul></ul></li>', + categoryItemTemplate: '<li><a href="#" class="category" tabIndex="-1"><input type="checkbox" value="{{category}}"/>{{category}}</a></li>', + tipTemplate: '<li class=dropdown-multi-menu-tip><div class=dropdown-multi-menu-tip-area><p>{{tip}}</p></div></li>', + minSearchLength: 2, + }; + + + return this.each(function() { + var self = this, + categories = [], + $self = $(self), + $form = $(self).closest('form'), + $itemContainer, + $itemContainerUL, + itemTemplate, + itemContainerTemplate, + categoriesSet = false, + selectedItems = [], + selectedCategories = [], + $searchInput, + multiple = typeof $self.attr('data-multiple') !== 'undefined', + currentData = settings.data || $self.data('data'), + inputName = $(self).data('name'), + sifter, + dataObject = { + label: 'label', + data: 'data', + category: 'category' + }; + + if( self.tagName !== 'UL') { + return; + } + + var prepareDropdown = function() { + var placeholder = $self.data('placeholder'); + if( typeof settings.searchTemplate !== 'undefined' ) { + MultiAwesome.searchTemplate = settings.searchTemplate; + } + + $self.prepend(MultiAwesome.searchTemplate); + if( placeholder ) { + $('#multiawesome-search-input').prop( 'placeholder', placeholder ); + } + }; + + var attachBtnListeners = function() { + $( 'ul.dropdown-multi-menu' ).on( 'click', 'li.dropdown-multi-menu-category a' , dropdownCategoryLinkClicked ); + $( 'ul.dropdown-multi-menu' ).on( 'click', 'li.dropdown-multi-menu-selections a', dropdownSelectionLinkClicked ); + $searchInput = $('#multiawesome-search-input'); + $searchInput.on( 'keydown keyup update', inputSearched ); + }; + + var parseSearchResults = function(results) { + var finalData = []; + results.forEach( function( result ) { + finalData.push(currentData[result.id]); + }); + renderData( finalData ); + }; + + var inputSearched = function() { + var minSearchLength = settings.minSearchLength || MultiAwesome.minSearchLength; + + //remove current hidden inputs + $('input[type="hidden"][name="' + inputName + '"]').remove(); + + if( $searchInput.val().length > minSearchLength ) { + var results = sifter.search($searchInput.val(), { + fields: [dataObject.label], + sort: [{field: dataObject.label, direction: 'asc'}] + }); + parseSearchResults(results.items); + } else { + renderData( currentData ); + } + }; + + var configureData = function() { + if ( typeof settings.itemLabelTitle !== 'undefined' ) { + dataObject.label = settings.itemLabelTitle; + } + + if ( typeof settings.itemDataTitle !== 'undefined' ) { + dataObject.data = settings.itemDataTitle; + } + + if ( typeof settings.itemCategoryTitle !== 'undefined' ) { + dataObject.category = settings.itemCategoryTitle; + } + }; + + var addCategories = function() { + var $categoryContainer; + var $categoryContainerUL; + if( categories.length ) { + if( typeof settings.categoryContainerTemplate !== 'undefined' ) { + $categoryContainer = $(settings.categoryContainerTemplate); + } else { + $categoryContainer = $(MultiAwesome.categoryContainerTemplate); + } + $categoryContainerUL = $categoryContainer.find('ul'); + $self.prepend(MultiAwesome.seperatorTemplate); + categories.forEach(function( category ) { + $categoryContainerUL.prepend( MultiAwesome.categoryItemTemplate + .replace( /{{category}}/g, category ) ); + }); + $self.prepend($categoryContainer); + } + }; + + var addTitle = function() { + var titleTemplate; + var titleData = $self.data('title') || settings.title; + if( typeof titleData !== 'undefined') { + if(typeof settings.titleTemplate !== 'undefined') { + titleTemplate = settings.titleTemplate; + } else { + titleTemplate = MultiAwesome.titleTemplate; + } + $self.prepend(MultiAwesome.seperatorTemplate); + $self.prepend(titleTemplate + .replace(/{{title}}/g, titleData) + ); + } + }; + + var addData = function(callback) { + function parseDataWhenReady() { + itemTemplate = MultiAwesome.itemTemplate; + itemContainerTemplate = MultiAwesome.itemContainerTemplate; + if( typeof settings.itemTemplate !== 'undefined' ) { + itemTemplate = settings.itemTemplate; + } + if( typeof settings.itemContainerTemplate !== 'undefined' ) { + itemContainerTemplate = settings.itemContainerTemplate; + } + $itemContainer = $(itemContainerTemplate); + $itemContainerUL = $itemContainer.find('ul'); + + sifter = new Sifter(currentData); + renderData( currentData,callback ); + } + configureData(); + if ( typeof currentData !== 'undefined' ) { + if ( typeof currentData === 'string') { + $.getJSON(currentData, function(data) { + currentData = data; + parseDataWhenReady(); + }); + } else if ( typeof currentData === 'object' ) { + parseDataWhenReady(); + } + } + }; + + var renderData = function( data, callback ) { + selectedItems = []; + $itemContainerUL.empty(); + var emptyObj = {}; + var skipMatch = false; + if( !data.length ) { + emptyObj[dataObject.label] = 'No matches found'; + emptyObj[dataObject.data] = ''; + emptyObj[dataObject.category] = ''; + data.push(emptyObj); + skipMatch = true; + } + data.forEach( function( item ) { + if( !categoriesSet && !skipMatch ) { + var addCategory = item[dataObject.category]; + if( item.hasOwnProperty( dataObject.category ) && categories.indexOf( addCategory ) === -1 ) { + categories.push( addCategory ); + } + } else { + // only do this if the categories are already set... they won't search categories on the first time. + if( selectedCategories.length && + selectedCategories.indexOf(item[dataObject.category]) === -1 && + !skipMatch) { + return; + } + } + + $itemContainerUL.append( + itemTemplate + .replace(/{{data}}/g,item[dataObject.data]) + .replace(/{{label}}/g,item[dataObject.label]) + .replace(/{{name}}/g, '_' + inputName) + ); + }); + if( !categoriesSet ) { + $self.append($itemContainer); + } + + if( categories.length ){ + categoriesSet = true; + } + + if( callback ) { + callback(); + } + }; + + var addToForm = function( val ) { + $form.prepend('<input type="hidden" name="' + inputName + '" value="' + val + '" />'); + }; + + var removeFromForm = function( val ) { + $form + .find('input[name="' + inputName + '"][value="' + val + '"]') + .remove(); + }; + + /* * * * * * * * * * * * * * * */ + /* listeners + /* * * * * * * * * * * * * * * */ + var dropdownInputClicked = function( e ) { + return false; + }; + + var dropdownCategoryLinkClicked = function ( e ) { + var $target = $( e.currentTarget ), + $inp = $target.find( 'input' ), + val = $inp.val(), + i = selectedCategories.indexOf( val ); + + if ( i > -1 ) { + var spliced = selectedCategories.splice( i, 1 ); + $target.removeClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', false ); + }, 0); + } else { + selectedCategories.push( val ); + $target.addClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', true ); + }, 0); + } + + inputSearched(); + + $( e.target ).blur(); + return false; + }; + + var dropdownSelectionLinkClicked = function ( e ) { + var $target = $( e.currentTarget ), + $inp = $target.find( 'input' ), + val = $inp.val(), + i = selectedItems.indexOf( val ); + + if ( i > -1 ) { + var spliced = selectedItems.splice( i, 1 ); + if( multiple ) { + removeFromForm( spliced ); + } else { + $form + .find('input[name="' + inputName + '"]') + .remove(); + } + + $target.removeClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', false ); + }, 0); + } else { + if( !multiple ) { + selectedItems = []; + $form + .find('input[name="' + inputName + '"]') + .remove(); + $form + .find('input[name="_' + inputName + '"]') + .parent() + .removeClass('selected'); + } + + selectedItems.push( val ); + addToForm( val ); + $target.addClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', true ); + }, 0); + } + $( e.target ).blur(); + return false; + + }; + + var addTip = function() { + var tipTemplate; + var tipData = $self.data('tip') || settings.tip; + if( typeof tipData !== 'undefined') { + if(typeof settings.tipTemplate !== 'undefined') { + tipTemplate = settings.tipTemplate; + } else { + tipTemplate = MultiAwesome.tipTemplate; + } + $self.append(MultiAwesome.seperatorTemplate); + $self.append(tipTemplate + .replace(/{{tip}}/g, tipData) + ); + } + }; + + /* * * * * * * * * * * * * * * */ + /* setup + /* * * * * * * * * * * * * * * */ + + var setup = function() { + addData(function(){ + addCategories(); + prepareDropdown(); + addTitle(); + attachBtnListeners(); + addTip(); + }); + }; + + setup(); + + }); + }; +})( jQuery );
\ No newline at end of file diff --git a/app/assets/javascripts/multiawesome.js b/app/assets/javascripts/multiawesome.js new file mode 100644 index 00000000000..7c078e2f8e2 --- /dev/null +++ b/app/assets/javascripts/multiawesome.js @@ -0,0 +1,390 @@ +//= require sifter +/** + * multiawesome.js (v0.12.1) + * Copyright (c) 2015 Jacob Schatz & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Jacob Schatz <jschatz@gitlab.com> + */ + +/*jshint curly:false */ +/*jshint browser:true */ +(function ( $ ) { + 'use strict'; + $.fn.multiawesome = function( settings ) { + settings = settings || {}; + var MultiAwesome = { + searchTemplate: '<li><div class="input-with-icon"><i class="fa fa-search"></i><input type="text" id="multiawesome-search-input" /></div></li>', + itemContainerTemplate: '<li class="dropdown-multi-menu-selections"><ul></ul></li>', + itemTemplate: '<li><a href="#" class="item" tabIndex="-1"><input type="checkbox" name="{{name}}" value="{{data}}"/>{{label}}</a></li>', + seperatorTemplate: '<li role="separator" class="divider"></li>', + titleTemplate: '<li class=dropdown-multi-menu-title><div class=dropdown-multi-menu-title-area><h3>{{title}}</h3></div></li>', + categoryContainerTemplate: '<li class=dropdown-multi-menu-category><ul></ul></li>', + categoryItemTemplate: '<li><a href="#" class="category" tabIndex="-1"><input type="checkbox" value="{{category}}"/>{{category}}</a></li>', + tipTemplate: '<li class=dropdown-multi-menu-tip><div class=dropdown-multi-menu-tip-area><p>{{tip}}</p></div></li>', + minSearchLength: 2 + }; + + + return this.each(function() { + var self = this, + categories = [], + $self = $(self), + $form = $(self).closest('form'), + $itemContainer, + $itemContainerUL, + itemTemplate, + itemContainerTemplate, + categoriesSet = false, + selectedItems = [], + selectedCategories = [], + $searchInput, + onChange = settings.onChange || function(){}, + multiple = typeof $self.attr('data-multiple') !== 'undefined', + currentData = settings.data || $self.data('data'), + inputName = $(self).data('name'), + sifter, + alwaysData = settings.always || [], + dataObject = { + label: 'label', + data: 'data', + category: 'category' + }; + + if( self.tagName !== 'UL') { + return; + } + + var prepareDropdown = function() { + var placeholder = $self.data('placeholder'); + if( typeof settings.searchTemplate !== 'undefined' ) { + MultiAwesome.searchTemplate = settings.searchTemplate; + } + + $self.prepend(MultiAwesome.searchTemplate); + if( placeholder ) { + $('#multiawesome-search-input').prop( 'placeholder', placeholder ); + } + }; + + var attachBtnListeners = function() { + $( 'ul.dropdown-multi-menu' ).on( 'click', 'li.dropdown-multi-menu-category a' , dropdownCategoryLinkClicked ); + $( 'ul.dropdown-multi-menu' ).on( 'click', 'li.dropdown-multi-menu-selections a', dropdownSelectionLinkClicked ); + $searchInput = $('#multiawesome-search-input'); + $searchInput.on( 'keydown keyup update', inputSearched ); + }; + + var parseSearchResults = function(results) { + var finalData = []; + results.forEach( function( result ) { + finalData.push(currentData[result.id]); + }); + renderData( finalData ); + }; + + var inputSearched = function() { + var minSearchLength = settings.minSearchLength || MultiAwesome.minSearchLength; + + //remove current hidden inputs + $('input[type="hidden"][name="' + inputName + '"]').remove(); + + if( $searchInput.val().length > minSearchLength ) { + var results = sifter.search($searchInput.val(), { + fields: [dataObject.label], + sort: [{field: dataObject.label, direction: 'asc'}] + }); + parseSearchResults(results.items); + } else { + renderData( currentData ); + } + }; + + var configureData = function() { + if ( typeof settings.itemLabelTitle !== 'undefined' ) { + dataObject.label = settings.itemLabelTitle; + } + + if ( typeof settings.itemDataTitle !== 'undefined' ) { + dataObject.data = settings.itemDataTitle; + } + + if ( typeof settings.itemCategoryTitle !== 'undefined' ) { + dataObject.category = settings.itemCategoryTitle; + } + }; + + var addCategories = function() { + var $categoryContainer; + var $categoryContainerUL; + if( categories.length ) { + if( typeof settings.categoryContainerTemplate !== 'undefined' ) { + $categoryContainer = $(settings.categoryContainerTemplate); + } else { + $categoryContainer = $(MultiAwesome.categoryContainerTemplate); + } + $categoryContainerUL = $categoryContainer.find('ul'); + $self.prepend(MultiAwesome.seperatorTemplate); + categories.forEach(function( category ) { + $categoryContainerUL.prepend( MultiAwesome.categoryItemTemplate + .replace( /\{\{category\}\}/g, category ) ); + }); + $self.prepend($categoryContainer); + } + }; + + var addTitle = function() { + var titleTemplate; + var titleData = $self.data('title') || settings.title; + if( typeof titleData !== 'undefined') { + if(typeof settings.titleTemplate !== 'undefined') { + titleTemplate = settings.titleTemplate; + } else { + titleTemplate = MultiAwesome.titleTemplate; + } + $self.prepend(MultiAwesome.seperatorTemplate); + $self.prepend(titleTemplate + .replace(/\{\{title\}\}/g, titleData) + ); + } + }; + + var addData = function(callback) { + function parseDataWhenReady() { + itemTemplate = MultiAwesome.itemTemplate; + itemContainerTemplate = MultiAwesome.itemContainerTemplate; + if( typeof settings.itemTemplate !== 'undefined' ) { + itemTemplate = settings.itemTemplate; + } + if( typeof settings.itemContainerTemplate !== 'undefined' ) { + itemContainerTemplate = settings.itemContainerTemplate; + } + $itemContainer = $(itemContainerTemplate); + $itemContainerUL = $itemContainer.find('ul'); + + sifter = new Sifter(currentData); + renderData( currentData,callback ); + } + configureData(); + if ( typeof currentData !== 'undefined' ) { + if ( typeof currentData === 'string') { + $.getJSON(currentData, function(data) { + currentData = data; + parseDataWhenReady(); + }); + } else if ( typeof currentData === 'object' ) { + parseDataWhenReady(); + } + } + }; + + var renderData = function( data, callback ) { + selectedItems = []; + $itemContainerUL.empty(); + var emptyObj = {}; + var skipMatch = false; + if( !data.length ) { + emptyObj[dataObject.label] = 'No matches found'; + emptyObj[dataObject.data] = ''; + emptyObj[dataObject.category] = ''; + data.push(emptyObj); + skipMatch = true; + } + data = data.concat(alwaysData); + data.forEach( function( item ) { + if( !categoriesSet && !skipMatch ) { + var addCategory = item[dataObject.category]; + if( item.hasOwnProperty( dataObject.category ) && categories.indexOf( addCategory ) === -1 ) { + categories.push( addCategory ); + } + } else { + // only do this if the categories are already set... they won't search categories on the first time. + if( selectedCategories.length && + selectedCategories.indexOf(item[dataObject.category]) === -1 && + !skipMatch) { + return; + } + } + + $itemContainerUL.append( + itemTemplate + .replace(/\{\{data\}\}/g,item[dataObject.data]) + .replace(/\{\{label\}\}/g,item[dataObject.label]) + .replace(/\{\{name\}\}/g, '_' + inputName) + ); + }); + if( !categoriesSet ) { + $self.append($itemContainer); + } + + if( categories.length ){ + categoriesSet = true; + } + + if( callback ) { + callback(); + } + }; + + var addToForm = function( val ) { + $form.prepend('<input type="hidden" name="' + inputName + '" value="' + val + '" />'); + }; + + var removeFromForm = function( val ) { + $form + .find('input[name="' + inputName + '"][value="' + val + '"]') + .remove(); + }; + + /* * * * * * * * * * * * * * * */ + /* listeners + /* * * * * * * * * * * * * * * */ + var dropdownInputClicked = function( e ) { + return false; + }; + + var dropdownCategoryLinkClicked = function ( e ) { + var $target = $( e.currentTarget ), + $inp = $target.find( 'input' ), + val = $inp.val(), + i = selectedCategories.indexOf( val ); + + e.preventDefault(); + + if ( i > -1 ) { + var spliced = selectedCategories.splice( i, 1 ); + $target.removeClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', false ); + }, 0); + } else { + selectedCategories.push( val ); + $target.addClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', true ); + }, 0); + } + + inputSearched(); + + $( e.target ).blur(); + return false; + }; + + var findItemWithData = function(searchData, id) { + var item = {}; + for (var i = searchData.length - 1; i >= 0; i--) { + item = searchData[i]; + if( item.hasOwnProperty(dataObject.data) && item[dataObject.data] == id ) { + return item; + } + }; + return undefined; + }; + + var dropdownSelectionLinkClicked = function ( e ) { + var $target = $( e.currentTarget ), + $inp = $target.find( 'input' ), + findItemInData, + val = $inp.val(), + i = selectedItems.indexOf( val ); + + e.preventDefault(); + + if ( i > -1 ) { + var spliced = selectedItems.splice( i, 1 ); + if( multiple ) { + removeFromForm( spliced ); + } else { + $form + .find('input[name="' + inputName + '"]') + .remove(); + } + + $target.removeClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', false ); + }, 0); + } else { + if( !multiple ) { + selectedItems = []; + $form + .find('input[name="' + inputName + '"]') + .remove(); + $form + .find('input[name="_' + inputName + '"]') + .parent() + .removeClass('selected'); + } + findItemInData = findItemWithData(currentData, val); + if( typeof findItemInData === 'undefined' ) { + findItemInData = findItemWithData(alwaysData, val) + } + onChange(findItemInData); + + selectedItems.push( val ); + addToForm( val ); + $target.addClass('selected'); + setTimeout( function() { + $inp.prop( 'checked', true ); + }, 0); + } + if( multiple ) { + // close the dropdown if single selection + $( e.target ).blur(); + return false; + } else { + var button = $self.prevAll('.dropdown-toggle'); + $self.prevAll('.dropdown-toggle') + .contents() + .each( + function(){ + if ( this.nodeType === 3 && this.nodeValue.trim() ) { + this.textContent = $target.text(); + } + }); + } + }; + + var addTip = function() { + var tipTemplate; + var tipData = $self.data('tip') || settings.tip; + if( typeof tipData !== 'undefined') { + if(typeof settings.tipTemplate !== 'undefined') { + tipTemplate = settings.tipTemplate; + } else { + tipTemplate = MultiAwesome.tipTemplate; + } + $self.append(MultiAwesome.seperatorTemplate); + $self.append(tipTemplate + .replace(/\{\{tip\}\}/g, tipData) + ); + } + }; + + /* * * * * * * * * * * * * * * */ + /* setup + /* * * * * * * * * * * * * * * */ + + var setup = function() { + addData(function(){ + addCategories(); + prepareDropdown(); + addTitle(); + attachBtnListeners(); + addTip(); + }); + }; + + setup(); + + }); + }; +})( jQuery );
\ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 0c0451fe4dd..2400782ecf0 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -49,3 +49,8 @@ * Styles for JS behaviors. */ @import "behaviors.scss"; + +/* + * Styles for multiawesome + */ +@import "multiawesome";
\ No newline at end of file diff --git a/app/assets/stylesheets/multiawesome.css b/app/assets/stylesheets/multiawesome.css new file mode 100644 index 00000000000..a0c77a85bfd --- /dev/null +++ b/app/assets/stylesheets/multiawesome.css @@ -0,0 +1,105 @@ +.open>.dropdown-multi-menu { + max-width: 320px; + border-radius: 0px; +} + +.dropdown-multi-menu input[type='text'] { + margin: 0 5px; + width: 309px; + height: 35px; + padding-left: 6px; +} + +.dropdown-multi-menu input[type='checkbox'] { + margin: 0 5px; + display: none; +} + +.dropdown-multi-menu>li>a { + padding-left: 0; +} + +.dropdown-multi-menu .dropdown-multi-menu-title-area h3 { + font-size: 15px; + text-align: center; + margin-top: 5px; +} + +.dropdown-multi-menu .dropdown-multi-menu-title-area h3::after { + content: ""; + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAACGUlEQVQ4jZ2VMXbiMBCGf83KKcxzntOmI1VMEyj2BLpD9gRbwQVygeUCpMsdcgKfQaYJqeIu1RYGC/stNjNbJOaBY8hu/lrz69PoH0mJCBaLhVEArqMoxhf0vFgYARBFUawWT08my7KxiMD3/fhmOLz/H7PE2ruyLL8rpRCG4b0WEQjzeVXXxq3X59ba8zAMZ/1+Pz9l9PLyEiyXy0lRFGZb18bTOhYR6GgwiOdJcu3Wa9R1bZxz5r1mespwuVxOnHO/mBla69jv9R6jwSAmALgZDu9934+JCMyMoiiMtfYuTdOgbZSmaWCtvSuKwjAziOigVbpZGIbhDACKojCnSLMsOyTz/bipPTB879nUWgvnnNkjxdnZ2TMAbDab6zbZaDQ62FCjpS5SIgIAMDOOkTVSItLZ9CRJxnmez5gZzRqlFIgIQRBMhkfiRZ1uADzPeyUiiMiOTERARPA87/VY3VHDr+pDDxtVVXXJzLtjAm9HZmZUVXV5rO5DD9M0DbIsmzSXQkQ4dSntifpA2JWzdmxO5XRnuE92KmddOd0n3Rl+NgGNPpsoAoB5koy7yLpenH6/n49Go2l79udJMgaAbz9ub02+Wv2s69poreNer/cQhuHs4uJi0zbbV1mWcxHJt9sttnVtmPmPc+63VkpBEa205z36vh8fm4C2rq6ucgDTxFqUZbkiolwplavmCwDenvB/MWtr/wv5CwCanfXE6iK0AAAAAElFTkSuQmCC'); + background-repeat: no-repeat; + background-size: 11px; + width: 12px; + height: 12px; + display: inline-block; + position: absolute; + right: 13px; + top: 13px; + cursor: pointer; +} + +.dropdown-multi-menu .dropdown-multi-menu-selections { + max-height: 150px; + overflow-y: scroll; + margin-top: 15px; +} + +.dropdown-multi-menu .dropdown-multi-menu-category ul{ + padding-left: 15px; + list-style: none; + max-height: 65px; + overflow-y: scroll; +} + +.dropdown-multi-menu .dropdown-multi-menu-selections ul{ + padding-left: 15px; + list-style: none; +} + +.dropdown-multi-menu .dropdown-multi-menu-selections li{ + margin-bottom: 10px; +} + +.dropdown-multi-menu .dropdown-multi-menu-selections ul a, .dropdown-multi-menu .dropdown-multi-menu-category ul a{ + text-decoration: none; + color: #333; + display: inline-block; + width: 285px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + height: 30px; + padding-left: 30px; +} + +.dropdown-multi-menu .dropdown-multi-menu-selections ul a.selected, .dropdown-multi-menu .dropdown-multi-menu-category ul a.selected { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAYCAMAAADat72NAAABtlBMVEX////b29vc29vc3NzZ2dna2dnb2trY19fZ2NjV1dXW1tbT09PU09PV1NTR0dHS0dHS0tLPz8/Qz8/Q0NDNzc3Ozc3Ly8vMy8vMzMzJycnKycnKysrIyMjJyMjKyMjFxcXGxcXGxsbHxcXHxsbEw8PExMTFxMTBwcHBwsLCwcG/v7+9vb2+vr6/vr67u7u8vLy5ubm6ubm7ubm3t7e4t7e4uLi1tbW3tra0s7OysbGysrKzsbGvr6+wr6+urq6vrq6sq6utrKypqamrqqqsqqqop6epp6eqqKimpaWnpaWnpqako6OkpKSlo6OioaGjoqKkoqKgn5+goKChn5+hoKCenp6fnp6cm5ucnJydm5udnJyenJx+fX1/fHx/fX2Afn6Af3+Bf3+BgICCgICCgYGDgYGDgoKEgoKFgoKFg4OGhISGhYWHhYWIh4eJh4eJiIiJiYmKiIiLiYmLioqMioqMi4uNi4uNjIyOjIyPjY2Qjo6Qj4+SkJCSkZGTkZGTkpKUk5OVk5OVlJSWlZWXlZWYlpaYl5eYmJiZmJiZmZmamJibmZmbmpqcm5uenp6fn5+op6erqqouFWZ5AAAAXHRSTlMADw8PFhYWHR0kJCwsLDMzMzo6OkJCSUlJUFBQV1dXX19fX19mZmZtbW11fHx8g4OKioqSkpKZmaCoqKivr7a2vb3FxcXMzMzT09Pb29vi4uLp6enp8PD4+Pj4+E8lS9YAAAEjSURBVCjPY2DACYRtrDhwywralTZL45RldymLb5LEJctkWBOXHiSAS9q8OSOzxRiXrGJPWmqdNTcOWY3ggtQyN15cXvIpTCnxVcIhK+pYnZwZLYdDls22Nj2rywiHLKNeW2p8oxaMK6vGgiKt05edVGXPDOWJB7S6KyPJSgVnxZRb8sC4Co2JJV4icFkJl6KYXB8hOJ8/KiW5wl8TyuN0KonP81BEMs24Iye10A8S+Kwm1bGZESoobtHszkwtDtUGMS0aUjM7TdG8YhRZmJIdLc/IpNqWktpoxonuVeWw3OTC3gkT+/NTK525MINCN6QsNSuvND+93FUGW1CJeZekJiSn5wSKYw9KNc/q9IS8cH1cCYDPoT63XR13ypQwczbAEAQAnkJCZAp/V+QAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-size: 16px; + background-position: 2px 4px; +} + +.dropdown-multi-menu .dropdown-multi-menu-tip-area { + margin: 10px; + text-align: left; + max-height: 40px; + overflow-y: scroll; +} + +.dropdown-multi-menu .dropdown-multi-menu-tip-area p { + margin-bottom: 0; +} + +.dropdown-multi-menu .input-with-icon { + position: relative; + margin: 10px 0; +} + +.dropdown-multi-menu .input-with-icon i { + position: absolute; + right: 0; + padding: 10px 12px; + color: #817F7F; + pointer-events: none; +} diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index be06738eac9..2acb319b927 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -42,6 +42,11 @@ class: 'select2 trigger-submit', include_blank: true, data: {placeholder: 'Milestone'}) + .filter-item.inline.people-filter.btn-group + %button.btn.btn-default.dropdown-toggle{ "data-toggle" => 'dropdown' } + People + %span.caret + %ul.dropdown-menu.dropdown-multi-menu .filter-item.inline.labels-filter = select_tag('label_name', projects_labels_options, class: 'select2 trigger-submit', include_blank: true, @@ -70,3 +75,11 @@ event.preventDefault(); Turbolinks.visit(this.action + '&' + $(this).serialize()); }); + + $('ul.dropdown-multi-menu').multiawesome({ + data: [{label:'milestone',data:1},{label:'no milestone',data:1}], + title: "Milestones", + onChange: function(item) { + console.log("changed",item) + } + }); diff --git a/db/schema.rb b/db/schema.rb deleted file mode 100644 index df7f72d5ad4..00000000000 --- a/db/schema.rb +++ /dev/null @@ -1,897 +0,0 @@ -# encoding: UTF-8 -# This file is auto-generated from the current state of the database. Instead -# of editing this file, please use the migrations feature of Active Record to -# incrementally modify your database, and then regenerate this schema definition. -# -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). -# -# It's strongly recommended that you check this file into your version control system. - -ActiveRecord::Schema.define(version: 20151229112614) do - - # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" - - create_table "abuse_reports", force: :cascade do |t| - t.integer "reporter_id" - t.integer "user_id" - t.text "message" - t.datetime "created_at" - t.datetime "updated_at" - end - - create_table "application_settings", force: :cascade do |t| - t.integer "default_projects_limit" - t.boolean "signup_enabled" - t.boolean "signin_enabled" - t.boolean "gravatar_enabled" - t.text "sign_in_text" - t.datetime "created_at" - t.datetime "updated_at" - t.string "home_page_url" - t.integer "default_branch_protection", default: 2 - t.boolean "twitter_sharing_enabled", default: true - t.text "restricted_visibility_levels" - t.boolean "version_check_enabled", default: true - t.integer "max_attachment_size", default: 10, null: false - t.integer "default_project_visibility" - t.integer "default_snippet_visibility" - t.text "restricted_signup_domains" - t.boolean "user_oauth_applications", default: true - t.string "after_sign_out_path" - t.integer "session_expire_delay", default: 10080, null: false - t.text "import_sources" - t.text "help_page_text" - t.string "admin_notification_email" - t.boolean "shared_runners_enabled", default: true, null: false - t.integer "max_artifacts_size", default: 100, null: false - t.string "runners_registration_token" - t.boolean "require_two_factor_authentication", default: false - t.integer "two_factor_grace_period", default: 48 - t.boolean "metrics_enabled", default: false - t.string "metrics_host", default: "localhost" - t.string "metrics_username" - t.string "metrics_password" - t.integer "metrics_pool_size", default: 16 - t.integer "metrics_timeout", default: 10 - t.integer "metrics_method_call_threshold", default: 10 - t.boolean "recaptcha_enabled", default: false - t.string "recaptcha_site_key" - t.string "recaptcha_private_key" - t.integer "metrics_port", default: 8089 - end - - create_table "audit_events", force: :cascade do |t| - t.integer "author_id", null: false - t.string "type", null: false - t.integer "entity_id", null: false - t.string "entity_type", null: false - t.text "details" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree - add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree - add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree - - create_table "broadcast_messages", force: :cascade do |t| - t.text "message", null: false - t.datetime "starts_at" - t.datetime "ends_at" - t.integer "alert_type" - t.datetime "created_at" - t.datetime "updated_at" - t.string "color" - t.string "font" - end - - create_table "ci_application_settings", force: :cascade do |t| - t.boolean "all_broken_builds" - t.boolean "add_pusher" - t.datetime "created_at" - t.datetime "updated_at" - end - - create_table "ci_builds", force: :cascade do |t| - t.integer "project_id" - t.string "status" - t.datetime "finished_at" - t.text "trace" - t.datetime "created_at" - t.datetime "updated_at" - t.datetime "started_at" - t.integer "runner_id" - t.float "coverage" - t.integer "commit_id" - t.text "commands" - t.integer "job_id" - t.string "name" - t.boolean "deploy", default: false - t.text "options" - t.boolean "allow_failure", default: false, null: false - t.string "stage" - t.integer "trigger_request_id" - t.integer "stage_idx" - t.boolean "tag" - t.string "ref" - t.integer "user_id" - t.string "type" - t.string "target_url" - t.string "description" - t.text "artifacts_file" - t.integer "gl_project_id" - end - - add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree - add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree - add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree - add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree - add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree - add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree - add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree - add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree - add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree - add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree - add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree - - create_table "ci_commits", force: :cascade do |t| - t.integer "project_id" - t.string "ref" - t.string "sha" - t.string "before_sha" - t.text "push_data" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "tag", default: false - t.text "yaml_errors" - t.datetime "committed_at" - t.integer "gl_project_id" - end - - add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree - add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree - add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree - add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree - add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree - add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree - - create_table "ci_events", force: :cascade do |t| - t.integer "project_id" - t.integer "user_id" - t.integer "is_admin" - t.text "description" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree - add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree - add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree - - create_table "ci_jobs", force: :cascade do |t| - t.integer "project_id", null: false - t.text "commands" - t.boolean "active", default: true, null: false - t.datetime "created_at" - t.datetime "updated_at" - t.string "name" - t.boolean "build_branches", default: true, null: false - t.boolean "build_tags", default: false, null: false - t.string "job_type", default: "parallel" - t.string "refs" - t.datetime "deleted_at" - end - - add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree - add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree - - create_table "ci_projects", force: :cascade do |t| - t.string "name" - t.integer "timeout", default: 3600, null: false - t.datetime "created_at" - t.datetime "updated_at" - t.string "token" - t.string "default_ref" - t.string "path" - t.boolean "always_build", default: false, null: false - t.integer "polling_interval" - t.boolean "public", default: false, null: false - t.string "ssh_url_to_repo" - t.integer "gitlab_id" - t.boolean "allow_git_fetch", default: true, null: false - t.string "email_recipients", default: "", null: false - t.boolean "email_add_pusher", default: true, null: false - t.boolean "email_only_broken_builds", default: true, null: false - t.string "skip_refs" - t.string "coverage_regex" - t.boolean "shared_runners_enabled", default: false - t.text "generated_yaml_config" - end - - add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree - add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree - - create_table "ci_runner_projects", force: :cascade do |t| - t.integer "runner_id", null: false - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "gl_project_id" - end - - add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree - add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree - - create_table "ci_runners", force: :cascade do |t| - t.string "token" - t.datetime "created_at" - t.datetime "updated_at" - t.string "description" - t.datetime "contacted_at" - t.boolean "active", default: true, null: false - t.boolean "is_shared", default: false - t.string "name" - t.string "version" - t.string "revision" - t.string "platform" - t.string "architecture" - end - - create_table "ci_services", force: :cascade do |t| - t.string "type" - t.string "title" - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false - t.text "properties" - end - - add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree - - create_table "ci_sessions", force: :cascade do |t| - t.string "session_id", null: false - t.text "data" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree - add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree - - create_table "ci_taggings", force: :cascade do |t| - t.integer "tag_id" - t.integer "taggable_id" - t.string "taggable_type" - t.integer "tagger_id" - t.string "tagger_type" - t.string "context", limit: 128 - t.datetime "created_at" - end - - add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree - add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree - - create_table "ci_tags", force: :cascade do |t| - t.string "name" - t.integer "taggings_count", default: 0 - end - - add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree - - create_table "ci_trigger_requests", force: :cascade do |t| - t.integer "trigger_id", null: false - t.text "variables" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "commit_id" - end - - create_table "ci_triggers", force: :cascade do |t| - t.string "token" - t.integer "project_id" - t.datetime "deleted_at" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "gl_project_id" - end - - add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree - add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree - - create_table "ci_variables", force: :cascade do |t| - t.integer "project_id" - t.string "key" - t.text "value" - t.text "encrypted_value" - t.string "encrypted_value_salt" - t.string "encrypted_value_iv" - t.integer "gl_project_id" - end - - add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree - - create_table "ci_web_hooks", force: :cascade do |t| - t.string "url", null: false - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - create_table "deploy_keys_projects", force: :cascade do |t| - t.integer "deploy_key_id", null: false - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree - - create_table "emails", force: :cascade do |t| - t.integer "user_id", null: false - t.string "email", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree - add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree - - create_table "events", force: :cascade do |t| - t.string "target_type" - t.integer "target_id" - t.string "title" - t.text "data" - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "action" - t.integer "author_id" - end - - add_index "events", ["action"], name: "index_events_on_action", using: :btree - add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree - add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree - add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree - add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree - add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree - - create_table "forked_project_links", force: :cascade do |t| - t.integer "forked_to_project_id", null: false - t.integer "forked_from_project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree - - create_table "identities", force: :cascade do |t| - t.string "extern_uid" - t.string "provider" - t.integer "user_id" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree - add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree - - create_table "issues", force: :cascade do |t| - t.string "title" - t.integer "assignee_id" - t.integer "author_id" - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "position", default: 0 - t.string "branch_name" - t.text "description" - t.integer "milestone_id" - t.string "state" - t.integer "iid" - t.integer "updated_by_id" - end - - add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree - add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree - add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree - add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree - add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree - add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree - add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree - add_index "issues", ["state"], name: "index_issues_on_state", using: :btree - add_index "issues", ["title"], name: "index_issues_on_title", using: :btree - - create_table "keys", force: :cascade do |t| - t.integer "user_id" - t.datetime "created_at" - t.datetime "updated_at" - t.text "key" - t.string "title" - t.string "type" - t.string "fingerprint" - t.boolean "public", default: false, null: false - end - - add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree - add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree - - create_table "label_links", force: :cascade do |t| - t.integer "label_id" - t.integer "target_id" - t.string "target_type" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree - add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree - - create_table "labels", force: :cascade do |t| - t.string "title" - t.string "color" - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "template", default: false - end - - add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree - - create_table "lfs_objects", force: :cascade do |t| - t.string "oid", null: false - t.integer "size", null: false - t.datetime "created_at" - t.datetime "updated_at" - t.string "file" - end - - add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree - - create_table "lfs_objects_projects", force: :cascade do |t| - t.integer "lfs_object_id", null: false - t.integer "project_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree - - create_table "members", force: :cascade do |t| - t.integer "access_level", null: false - t.integer "source_id", null: false - t.string "source_type", null: false - t.integer "user_id" - t.integer "notification_level", null: false - t.string "type" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "created_by_id" - t.string "invite_email" - t.string "invite_token" - t.datetime "invite_accepted_at" - end - - add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree - add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree - add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree - add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree - add_index "members", ["type"], name: "index_members_on_type", using: :btree - add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree - - create_table "merge_request_diffs", force: :cascade do |t| - t.string "state" - t.text "st_commits" - t.text "st_diffs" - t.integer "merge_request_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree - - create_table "merge_requests", force: :cascade do |t| - t.string "target_branch", null: false - t.string "source_branch", null: false - t.integer "source_project_id", null: false - t.integer "author_id" - t.integer "assignee_id" - t.string "title" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "milestone_id" - t.string "state" - t.string "merge_status" - t.integer "target_project_id", null: false - t.integer "iid" - t.text "description" - t.integer "position", default: 0 - t.datetime "locked_at" - t.integer "updated_by_id" - t.string "merge_error" - t.text "merge_params" - t.boolean "merge_when_build_succeeds", default: false, null: false - t.integer "merge_user_id" - end - - add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree - add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree - add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree - add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree - add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree - add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree - add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree - add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree - add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree - add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree - - create_table "milestones", force: :cascade do |t| - t.string "title", null: false - t.integer "project_id", null: false - t.text "description" - t.date "due_date" - t.datetime "created_at" - t.datetime "updated_at" - t.string "state" - t.integer "iid" - end - - add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree - add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree - add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree - add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree - - create_table "namespaces", force: :cascade do |t| - t.string "name", null: false - t.string "path", null: false - t.integer "owner_id" - t.datetime "created_at" - t.datetime "updated_at" - t.string "type" - t.string "description", default: "", null: false - t.string "avatar" - t.boolean "public", default: false - end - - add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree - add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree - add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree - add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree - add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree - add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree - - create_table "notes", force: :cascade do |t| - t.text "note" - t.string "noteable_type" - t.integer "author_id" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "project_id" - t.string "attachment" - t.string "line_code" - t.string "commit_id" - t.integer "noteable_id" - t.boolean "system", default: false, null: false - t.text "st_diff" - t.integer "updated_by_id" - t.boolean "is_award", default: false, null: false - end - - add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree - add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree - add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree - add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree - add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree - add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree - add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree - add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree - add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree - add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree - add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree - - create_table "oauth_access_grants", force: :cascade do |t| - t.integer "resource_owner_id", null: false - t.integer "application_id", null: false - t.string "token", null: false - t.integer "expires_in", null: false - t.text "redirect_uri", null: false - t.datetime "created_at", null: false - t.datetime "revoked_at" - t.string "scopes" - end - - add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree - - create_table "oauth_access_tokens", force: :cascade do |t| - t.integer "resource_owner_id" - t.integer "application_id" - t.string "token", null: false - t.string "refresh_token" - t.integer "expires_in" - t.datetime "revoked_at" - t.datetime "created_at", null: false - t.string "scopes" - end - - add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree - add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree - add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree - - create_table "oauth_applications", force: :cascade do |t| - t.string "name", null: false - t.string "uid", null: false - t.string "secret", null: false - t.text "redirect_uri", null: false - t.string "scopes", default: "", null: false - t.datetime "created_at" - t.datetime "updated_at" - t.integer "owner_id" - t.string "owner_type" - end - - add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree - add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree - - create_table "project_import_data", force: :cascade do |t| - t.integer "project_id" - t.text "data" - end - - create_table "projects", force: :cascade do |t| - t.string "name" - t.string "path" - t.text "description" - t.datetime "created_at" - t.datetime "updated_at" - t.integer "creator_id" - t.boolean "issues_enabled", default: true, null: false - t.boolean "wall_enabled", default: true, null: false - t.boolean "merge_requests_enabled", default: true, null: false - t.boolean "wiki_enabled", default: true, null: false - t.integer "namespace_id" - t.string "issues_tracker", default: "gitlab", null: false - t.string "issues_tracker_id" - t.boolean "snippets_enabled", default: true, null: false - t.datetime "last_activity_at" - t.string "import_url" - t.integer "visibility_level", default: 0, null: false - t.boolean "archived", default: false, null: false - t.string "avatar" - t.string "import_status" - t.float "repository_size", default: 0.0 - t.integer "star_count", default: 0, null: false - t.string "import_type" - t.string "import_source" - t.integer "commit_count", default: 0 - t.text "import_error" - t.integer "ci_id" - t.boolean "builds_enabled", default: true, null: false - t.boolean "shared_runners_enabled", default: true, null: false - t.string "runners_token" - t.string "build_coverage_regex" - t.boolean "build_allow_git_fetch", default: true, null: false - t.integer "build_timeout", default: 3600, null: false - end - - add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree - add_index "projects", ["builds_enabled"], name: "index_projects_on_builds_enabled", using: :btree - add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree - add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree - add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree - add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree - add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree - add_index "projects", ["path"], name: "index_projects_on_path", using: :btree - add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree - add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree - add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree - - create_table "protected_branches", force: :cascade do |t| - t.integer "project_id", null: false - t.string "name", null: false - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "developers_can_push", default: false, null: false - end - - add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree - - create_table "releases", force: :cascade do |t| - t.string "tag" - t.text "description" - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree - add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree - - create_table "sent_notifications", force: :cascade do |t| - t.integer "project_id" - t.integer "noteable_id" - t.string "noteable_type" - t.integer "recipient_id" - t.string "commit_id" - t.string "reply_key", null: false - t.string "line_code" - end - - add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree - - create_table "services", force: :cascade do |t| - t.string "type" - t.string "title" - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.boolean "active", default: false, null: false - t.text "properties" - t.boolean "template", default: false - t.boolean "push_events", default: true - t.boolean "issues_events", default: true - t.boolean "merge_requests_events", default: true - t.boolean "tag_push_events", default: true - t.boolean "note_events", default: true, null: false - t.boolean "build_events", default: false, null: false - end - - add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree - add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree - add_index "services", ["template"], name: "index_services_on_template", using: :btree - - create_table "snippets", force: :cascade do |t| - t.string "title" - t.text "content" - t.integer "author_id", null: false - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.string "file_name" - t.datetime "expires_at" - t.string "type" - t.integer "visibility_level", default: 0, null: false - end - - add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree - add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree - add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree - add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree - add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree - add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree - - create_table "subscriptions", force: :cascade do |t| - t.integer "user_id" - t.integer "subscribable_id" - t.string "subscribable_type" - t.boolean "subscribed" - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree - - create_table "taggings", force: :cascade do |t| - t.integer "tag_id" - t.integer "taggable_id" - t.string "taggable_type" - t.integer "tagger_id" - t.string "tagger_type" - t.string "context" - t.datetime "created_at" - end - - add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree - add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree - - create_table "tags", force: :cascade do |t| - t.string "name" - t.integer "taggings_count", default: 0 - end - - add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree - - create_table "users", force: :cascade do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0 - t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.datetime "created_at" - t.datetime "updated_at" - t.string "name" - t.boolean "admin", default: false, null: false - t.integer "projects_limit", default: 10 - t.string "skype", default: "", null: false - t.string "linkedin", default: "", null: false - t.string "twitter", default: "", null: false - t.string "authentication_token" - t.integer "theme_id", default: 1, null: false - t.string "bio" - t.integer "failed_attempts", default: 0 - t.datetime "locked_at" - t.string "username" - t.boolean "can_create_group", default: true, null: false - t.boolean "can_create_team", default: true, null: false - t.string "state" - t.integer "color_scheme_id", default: 1, null: false - t.integer "notification_level", default: 1, null: false - t.datetime "password_expires_at" - t.integer "created_by_id" - t.datetime "last_credential_check_at" - t.string "avatar" - t.string "confirmation_token" - t.datetime "confirmed_at" - t.datetime "confirmation_sent_at" - t.string "unconfirmed_email" - t.boolean "hide_no_ssh_key", default: false - t.string "website_url", default: "", null: false - t.string "notification_email" - t.boolean "hide_no_password", default: false - t.boolean "password_automatically_set", default: false - t.string "location" - t.string "encrypted_otp_secret" - t.string "encrypted_otp_secret_iv" - t.string "encrypted_otp_secret_salt" - t.boolean "otp_required_for_login", default: false, null: false - t.text "otp_backup_codes" - t.string "public_email", default: "", null: false - t.integer "dashboard", default: 0 - t.integer "project_view", default: 0 - t.integer "consumed_timestep" - t.integer "layout", default: 0 - t.boolean "hide_project_limit", default: false - t.string "unlock_token" - t.datetime "otp_grace_period_started_at" - end - - add_index "users", ["admin"], name: "index_users_on_admin", using: :btree - add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree - add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree - add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree - add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree - add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree - add_index "users", ["name"], name: "index_users_on_name", using: :btree - add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree - add_index "users", ["username"], name: "index_users_on_username", using: :btree - - create_table "users_star_projects", force: :cascade do |t| - t.integer "project_id", null: false - t.integer "user_id", null: false - t.datetime "created_at" - t.datetime "updated_at" - end - - add_index "users_star_projects", ["project_id"], name: "index_users_star_projects_on_project_id", using: :btree - add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree - add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree - - create_table "web_hooks", force: :cascade do |t| - t.string "url" - t.integer "project_id" - t.datetime "created_at" - t.datetime "updated_at" - t.string "type", default: "ProjectHook" - t.integer "service_id" - t.boolean "push_events", default: true, null: false - t.boolean "issues_events", default: false, null: false - t.boolean "merge_requests_events", default: false, null: false - t.boolean "tag_push_events", default: false - t.boolean "note_events", default: false, null: false - t.boolean "enable_ssl_verification", default: true - t.boolean "build_events", default: false, null: false - end - - add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree - add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree - -end diff --git a/vendor/assets/javascripts/sifter.js b/vendor/assets/javascripts/sifter.js new file mode 100644 index 00000000000..c80aec11c36 --- /dev/null +++ b/vendor/assets/javascripts/sifter.js @@ -0,0 +1,471 @@ +/** + * sifter.js + * Copyright (c) 2013 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis <brian@thirdroute.com> + */ + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.Sifter = factory(); + } +}(this, function() { + + /** + * Textually searches arrays and hashes of objects + * by property (or multiple properties). Designed + * specifically for autocomplete. + * + * @constructor + * @param {array|object} items + * @param {object} items + */ + var Sifter = function(items, settings) { + this.items = items; + this.settings = settings || {diacritics: true}; + }; + + /** + * Splits a search string into an array of individual + * regexps to be used to match results. + * + * @param {string} query + * @returns {array} + */ + Sifter.prototype.tokenize = function(query) { + query = trim(String(query || '').toLowerCase()); + if (!query || !query.length) return []; + + var i, n, regex, letter; + var tokens = []; + var words = query.split(/ +/); + + for (i = 0, n = words.length; i < n; i++) { + regex = escape_regex(words[i]); + if (this.settings.diacritics) { + for (letter in DIACRITICS) { + if (DIACRITICS.hasOwnProperty(letter)) { + regex = regex.replace(new RegExp(letter, 'g'), DIACRITICS[letter]); + } + } + } + tokens.push({ + string : words[i], + regex : new RegExp(regex, 'i') + }); + } + + return tokens; + }; + + /** + * Iterates over arrays and hashes. + * + * ``` + * this.iterator(this.items, function(item, id) { + * // invoked for each item + * }); + * ``` + * + * @param {array|object} object + */ + Sifter.prototype.iterator = function(object, callback) { + var iterator; + if (is_array(object)) { + iterator = Array.prototype.forEach || function(callback) { + for (var i = 0, n = this.length; i < n; i++) { + callback(this[i], i, this); + } + }; + } else { + iterator = function(callback) { + for (var key in this) { + if (this.hasOwnProperty(key)) { + callback(this[key], key, this); + } + } + }; + } + + iterator.apply(object, [callback]); + }; + + /** + * Returns a function to be used to score individual results. + * + * Good matches will have a higher score than poor matches. + * If an item is not a match, 0 will be returned by the function. + * + * @param {object|string} search + * @param {object} options (optional) + * @returns {function} + */ + Sifter.prototype.getScoreFunction = function(search, options) { + var self, fields, tokens, token_count; + + self = this; + search = self.prepareSearch(search, options); + tokens = search.tokens; + fields = search.options.fields; + token_count = tokens.length; + + /** + * Calculates how close of a match the + * given value is against a search token. + * + * @param {mixed} value + * @param {object} token + * @return {number} + */ + var scoreValue = function(value, token) { + var score, pos; + + if (!value) return 0; + value = String(value || ''); + pos = value.search(token.regex); + if (pos === -1) return 0; + score = token.string.length / value.length; + if (pos === 0) score += 0.5; + return score; + }; + + /** + * Calculates the score of an object + * against the search query. + * + * @param {object} token + * @param {object} data + * @return {number} + */ + var scoreObject = (function() { + var field_count = fields.length; + if (!field_count) { + return function() { return 0; }; + } + if (field_count === 1) { + return function(token, data) { + return scoreValue(data[fields[0]], token); + }; + } + return function(token, data) { + for (var i = 0, sum = 0; i < field_count; i++) { + sum += scoreValue(data[fields[i]], token); + } + return sum / field_count; + }; + })(); + + if (!token_count) { + return function() { return 0; }; + } + if (token_count === 1) { + return function(data) { + return scoreObject(tokens[0], data); + }; + } + + if (search.options.conjunction === 'and') { + return function(data) { + var score; + for (var i = 0, sum = 0; i < token_count; i++) { + score = scoreObject(tokens[i], data); + if (score <= 0) return 0; + sum += score; + } + return sum / token_count; + }; + } else { + return function(data) { + for (var i = 0, sum = 0; i < token_count; i++) { + sum += scoreObject(tokens[i], data); + } + return sum / token_count; + }; + } + }; + + /** + * Returns a function that can be used to compare two + * results, for sorting purposes. If no sorting should + * be performed, `null` will be returned. + * + * @param {string|object} search + * @param {object} options + * @return function(a,b) + */ + Sifter.prototype.getSortFunction = function(search, options) { + var i, n, self, field, fields, fields_count, multiplier, multipliers, get_field, implicit_score, sort; + + self = this; + search = self.prepareSearch(search, options); + sort = (!search.query && options.sort_empty) || options.sort; + + /** + * Fetches the specified sort field value + * from a search result item. + * + * @param {string} name + * @param {object} result + * @return {mixed} + */ + get_field = function(name, result) { + if (name === '$score') return result.score; + return self.items[result.id][name]; + }; + + // parse options + fields = []; + if (sort) { + for (i = 0, n = sort.length; i < n; i++) { + if (search.query || sort[i].field !== '$score') { + fields.push(sort[i]); + } + } + } + + // the "$score" field is implied to be the primary + // sort field, unless it's manually specified + if (search.query) { + implicit_score = true; + for (i = 0, n = fields.length; i < n; i++) { + if (fields[i].field === '$score') { + implicit_score = false; + break; + } + } + if (implicit_score) { + fields.unshift({field: '$score', direction: 'desc'}); + } + } else { + for (i = 0, n = fields.length; i < n; i++) { + if (fields[i].field === '$score') { + fields.splice(i, 1); + break; + } + } + } + + multipliers = []; + for (i = 0, n = fields.length; i < n; i++) { + multipliers.push(fields[i].direction === 'desc' ? -1 : 1); + } + + // build function + fields_count = fields.length; + if (!fields_count) { + return null; + } else if (fields_count === 1) { + field = fields[0].field; + multiplier = multipliers[0]; + return function(a, b) { + return multiplier * cmp( + get_field(field, a), + get_field(field, b) + ); + }; + } else { + return function(a, b) { + var i, result, a_value, b_value, field; + for (i = 0; i < fields_count; i++) { + field = fields[i].field; + result = multipliers[i] * cmp( + get_field(field, a), + get_field(field, b) + ); + if (result) return result; + } + return 0; + }; + } + }; + + /** + * Parses a search query and returns an object + * with tokens and fields ready to be populated + * with results. + * + * @param {string} query + * @param {object} options + * @returns {object} + */ + Sifter.prototype.prepareSearch = function(query, options) { + if (typeof query === 'object') return query; + + options = extend({}, options); + + var option_fields = options.fields; + var option_sort = options.sort; + var option_sort_empty = options.sort_empty; + + if (option_fields && !is_array(option_fields)) options.fields = [option_fields]; + if (option_sort && !is_array(option_sort)) options.sort = [option_sort]; + if (option_sort_empty && !is_array(option_sort_empty)) options.sort_empty = [option_sort_empty]; + + return { + options : options, + query : String(query || '').toLowerCase(), + tokens : this.tokenize(query), + total : 0, + items : [] + }; + }; + + /** + * Searches through all items and returns a sorted array of matches. + * + * The `options` parameter can contain: + * + * - fields {string|array} + * - sort {array} + * - score {function} + * - filter {bool} + * - limit {integer} + * + * Returns an object containing: + * + * - options {object} + * - query {string} + * - tokens {array} + * - total {int} + * - items {array} + * + * @param {string} query + * @param {object} options + * @returns {object} + */ + Sifter.prototype.search = function(query, options) { + var self = this, value, score, search, calculateScore; + var fn_sort; + var fn_score; + + search = this.prepareSearch(query, options); + options = search.options; + query = search.query; + + // generate result scoring function + fn_score = options.score || self.getScoreFunction(search); + + // perform search and sort + if (query.length) { + self.iterator(self.items, function(item, id) { + score = fn_score(item); + if (options.filter === false || score > 0) { + search.items.push({'score': score, 'id': id}); + } + }); + } else { + self.iterator(self.items, function(item, id) { + search.items.push({'score': 1, 'id': id}); + }); + } + + fn_sort = self.getSortFunction(search, options); + if (fn_sort) search.items.sort(fn_sort); + + // apply limits + search.total = search.items.length; + if (typeof options.limit === 'number') { + search.items = search.items.slice(0, options.limit); + } + + return search; + }; + + // utilities + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + var cmp = function(a, b) { + if (typeof a === 'number' && typeof b === 'number') { + return a > b ? 1 : (a < b ? -1 : 0); + } + a = asciifold(String(a || '')); + b = asciifold(String(b || '')); + if (a > b) return 1; + if (b > a) return -1; + return 0; + }; + + var extend = function(a, b) { + var i, n, k, object; + for (i = 1, n = arguments.length; i < n; i++) { + object = arguments[i]; + if (!object) continue; + for (k in object) { + if (object.hasOwnProperty(k)) { + a[k] = object[k]; + } + } + } + return a; + }; + + var trim = function(str) { + return (str + '').replace(/^\s+|\s+$|/g, ''); + }; + + var escape_regex = function(str) { + return (str + '').replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); + }; + + var is_array = Array.isArray || (typeof $ !== 'undefined' && $.isArray) || function(object) { + return Object.prototype.toString.call(object) === '[object Array]'; + }; + + var DIACRITICS = { + 'a': '[aÀÁÂÃÄÅàáâãäåĀāąĄ]', + 'c': '[cÇçćĆčČ]', + 'd': '[dđĐďĎð]', + 'e': '[eÈÉÊËèéêëěĚĒēęĘ]', + 'i': '[iÌÍÎÏìíîïĪī]', + 'l': '[lłŁ]', + 'n': '[nÑñňŇńŃ]', + 'o': '[oÒÓÔÕÕÖØòóôõöøŌō]', + 'r': '[rřŘ]', + 's': '[sŠšśŚ]', + 't': '[tťŤ]', + 'u': '[uÙÚÛÜùúûüůŮŪū]', + 'y': '[yŸÿýÝ]', + 'z': '[zŽžżŻźŹ]' + }; + + var asciifold = (function() { + var i, n, k, chunk; + var foreignletters = ''; + var lookup = {}; + for (k in DIACRITICS) { + if (DIACRITICS.hasOwnProperty(k)) { + chunk = DIACRITICS[k].substring(2, DIACRITICS[k].length - 1); + foreignletters += chunk; + for (i = 0, n = chunk.length; i < n; i++) { + lookup[chunk.charAt(i)] = k; + } + } + } + var regexp = new RegExp('[' + foreignletters + ']', 'g'); + return function(str) { + return str.replace(regexp, function(foreignletter) { + return lookup[foreignletter]; + }).toLowerCase(); + }; + })(); + + + // export + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + return Sifter; +})); + |