提交 2b8457d6 编写于 作者: J Jacob Schatz

initial dropdown integration with gitlab

上级 6f4ea679
//= 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';
var defaults = {
data: [],
title: '',
tip: '',
name: 'multi-awesome',
header: [],
multiple: false,
alwaysPrefixWithSearch: false,
placeholder: 'Filter...',
onChange: function(){},
always: [],
dataObject: {
label: 'label',
data: 'data',
category: 'category',
subtitle: 'subtitle',
image: 'image'
},
minSearchLength: 2
};
$.fn.multiawesome = function( options ) {
var MultiAwesome = {
extraMenuContainerTemplate: '<li data-extra-menu-container class="dropdown-multi-menu-extras-container"></li>',
searchTemplate: '<li class="dropdown-multi-menu-search-container"><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" data-item-selectable tabIndex="-1"><input type="checkbox" name="{{name}}" value="{{data}}"/>{{label}}</a></li>',
seperatorTemplate: '<li role="separator" class="divider"></li>',
headerTemplate: '<li class="dropdown-multi-menu-header"><div class="dropdown-multi-menu-header-area"><ul class="dropdown-multi-menu-header-list">{{header}}</ul></div></li>',
headerItemTemplate: '<li class="dropdown-multi-menu-header-item"><a href="#" data-header-selectable class="header-item" tabIndex="-1"><input name="{{name}}" type="checkbox" value="{{data}}" />{{headeritem}}</a></li>',
titleTemplate: '<li class="dropdown-multi-menu-title"><div class="dropdown-multi-menu-title-area"><a href="#" data-back-button class="dropdown-multi-menu-back-button"></a><h3 class="dropdown-multi-menu-title-text">{{title}}</h3></div></li>',
categoryContainerTemplate: '<li class="dropdown-multi-menu-category"><ul></ul></li>',
categoryItemTemplate: '<li><a href="#" class="category" data-category-selectable 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>',
subtitleTemplate: '<p class="dropdown-multi-menu-subtitle">{{subtitle}}</p>',
imageTemplate: '<div class="dropdown-multi-menu-image" style="background-image:url(\'{{image}}\');"></div>'
};
return this.each(function() {
var self = this,
categories = [],
$self = $(self),
$form = $(self).closest('form'),
$itemContainer,
$itemContainerUL,
$extraMenuContainer,
categoriesAppended = false,
categoriesSet = false,
selectedItems = [],
selectedCategories = [],
$searchInput,
$extraMenus,
$addedMenu,
$backButton,
$currentMenu,
toHideForExtraMenus = [],
sifter,
// Merge the options and defaults into the settings.
// No need to check for undefined
settings = $.extend(true, {}, defaults, options, $self.data());
if( self.tagName !== 'UL') {
return;
}
var prepareDropdown = function() {
var $searchTemplate = $(MultiAwesome.searchTemplate);
toHideForExtraMenus.push($searchTemplate);
$self.prepend($searchTemplate);
$searchInput = $searchTemplate.find('input');
if( settings.placeholder ) {
$searchInput.prop( 'placeholder', settings.placeholder );
}
};
var attachListeners = function() {
$self.on( 'click', '[data-category-selectable]' , dropdownCategoryLinkClicked );
$self.on( 'click',
'[data-item-selectable], [data-header-selectable]', dropdownSelectionLinkClicked );
$self.on( 'click', dropdownClickedAnywhere );
$self.on( 'click', $backButton, backButtonClicked );
$searchInput.on( 'keydown keyup update', inputSearched );
};
var parseSearchResults = function(results) {
var finalData = [];
results.forEach( function( result ) {
finalData.push(settings.data[result.id]);
});
renderData( finalData );
};
var addCategories = function() {
var $categoryContainer = $(MultiAwesome.categoryContainerTemplate);
var $categoryContainerUL = $categoryContainer.find('ul');
if( categories.length ) {
var $seperatorTemplate = $(MultiAwesome.seperatorTemplate);
toHideForExtraMenus.push($seperatorTemplate);
$self.prepend($seperatorTemplate);
categories.forEach(function( category ) {
$categoryContainerUL.prepend( MultiAwesome.categoryItemTemplate
.replace( /\{\{category\}\}/g, category ) );
});
$self.prepend($categoryContainer);
}
};
var addTitle = function() {
var titleTemplate;
if( settings.title ) {
var $seperatorTemplate = $(MultiAwesome.seperatorTemplate);
var $titleTemplate = $(MultiAwesome.titleTemplate);
$self.prepend($seperatorTemplate);
$self.prepend(MultiAwesome.titleTemplate
.replace(/\{\{title\}\}/g, settings.title)
);
$backButton = $self.find('[data-back-button]');
}
};
var addExtrasContainer = function() {
$extraMenuContainer = $(MultiAwesome.extraMenuContainerTemplate);
$self.prepend($extraMenuContainer);
};
var addHeader = function() {
var $headerTemplate,
headerList = [];
// if we have some header data
if( settings.header.length ) {
settings.header.forEach( function(item) {
headerList.push(
MultiAwesome.headerItemTemplate
.replace(/\{\{headeritem\}\}/g, item[settings.dataObject.label])
.replace(/\{\{data\}\}/g, item[settings.dataObject.data])
.replace(/\{\{name\}\}/g, '_' + settings.name)
);
});
var $seperatorTemplate = $(MultiAwesome.seperatorTemplate);
toHideForExtraMenus.push($seperatorTemplate);
$self.prepend($seperatorTemplate);
$headerTemplate = $(MultiAwesome.headerTemplate.replace(/\{\{header\}\}/g, headerList.join('')));
toHideForExtraMenus.push($headerTemplate);
$self.prepend($headerTemplate);
}
};
var addData = function(callback) {
function parseDataWhenReady() {
$itemContainer = $(MultiAwesome.itemContainerTemplate);
toHideForExtraMenus.push($itemContainer);
$itemContainerUL = $itemContainer.find('ul');
sifter = new Sifter(settings.data);
renderData( settings.data,callback );
}
if ( settings.data ) {
if ( typeof settings.data === 'string') {
$.getJSON(settings.data, function(data) {
settings.data = data;
parseDataWhenReady();
});
} else if ( typeof settings.data === 'object' ) {
parseDataWhenReady();
} else {
$.error('Data must be a string or array');
}
}
};
var renderData = function( data, callback ) {
selectedItems = [];
$itemContainerUL.empty();
var emptyObj = {},
skipMatch = false,
searchInputVal = [],
o,
tempAlwaysData = [],
tempItemTemplate,
$itemTemplate;
if( !data.length ) {
emptyObj[settings.dataObject.label] = 'No matches found';
emptyObj[settings.dataObject.data] = 'dropdown-multi-menu-selectable:false';
emptyObj[settings.dataObject.category] = '';
emptyObj.selectable = false;
data.push(emptyObj);
skipMatch = true;
}
if( settings.alwaysPrefixWithSearch && $searchInput ) {
searchInputVal = $searchInput.val();
tempAlwaysData = settings.always.map(function(item){
// copy, don't alter real object.
o = $.extend({}, item);
if(searchInputVal.length){
o.label = '<strong>"' + searchInputVal + '"</strong>' + ' ' + o.label;
}
o.always = true;
return o;
});
} else {
tempAlwaysData = settings.always;
}
data = data.concat(tempAlwaysData);
data.forEach( function( item ) {
tempItemTemplate = MultiAwesome.itemTemplate;
if( !categoriesSet && !skipMatch ) {
var addCategory = item[settings.dataObject.category];
if( item.hasOwnProperty( settings.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[settings.dataObject.category]) === -1 &&
!skipMatch) {
return;
}
}
$itemTemplate = $(MultiAwesome.itemTemplate);
if( item.hasOwnProperty('selectable') && !item.selectable ) {
tempItemTemplate = $itemTemplate
.find('a')
.addClass('disabled')
.find('input[type="checkbox"]')
.prop('disabled','disabled')
// back to the anchor tag
.end()
// back to the li
.end()
.get(0)
.outerHTML;
}
if( item.hasOwnProperty( settings.dataObject.subtitle ) &&
item[settings.dataObject.subtitle].length ) {
tempItemTemplate = $(tempItemTemplate)
.find('a')
.addClass('dropdown-multi-menu-item-with-subtitle')
.append(
MultiAwesome.subtitleTemplate
.replace(/\{\{subtitle\}\}/g, item[settings.dataObject.subtitle])
)
.end()
.get(0)
.outerHTML;
}
if( item.hasOwnProperty( settings.dataObject.image ) &&
item[settings.dataObject.image].length ) {
tempItemTemplate = $(tempItemTemplate)
.find('a')
.addClass('dropdown-multi-menu-with-image')
.prepend(
MultiAwesome.imageTemplate
.replace(/\{\{image\}\}/g, item[settings.dataObject.image])
)
.end()
.get(0)
.outerHTML;
}
$itemContainerUL.append(
tempItemTemplate
.replace(/\{\{data\}\}/g,item[settings.dataObject.data])
.replace(/\{\{label\}\}/g,item[settings.dataObject.label])
.replace(/\{\{name\}\}/g, '_' + settings.name)
);
});
if( !categoriesSet && !categoriesAppended ) {
$self.append($itemContainer);
categoriesAppended = true;
}
if( categories.length ){
categoriesSet = true;
}
if( callback ) {
callback();
}
};
var addToForm = function( val ) {
$form.prepend('<input type="hidden" name="' + settings.name + '" value="' + val + '" />');
};
var removeFromForm = function( val ) {
$form
.find('input[name="' + settings.name + '"][value="' + val + '"]')
.remove();
};
var getExtraMenus = function() {
$extraMenus = $self
// get parent button group
.closest('div.button-group')
// get the extra menu divs
.find('[data-extra-menu]');
};
/* * * * * * * * * * * * * * * */
/* listeners
/* * * * * * * * * * * * * * * */
var backButtonClicked = function() {
toHideForExtraMenus.forEach(function(menuSection){
menuSection.show();
});
$extraMenuContainer.empty();
$backButton.hide();
return false;
};
var inputSearched = function() {
//remove current hidden inputs
$('input[type="hidden"][name="' + settings.name + '"]').remove();
if( $searchInput.val().length > settings.minSearchLength ) {
var results = sifter.search($searchInput.val(), {
fields: [settings.dataObject.label],
sort: [{field: settings.dataObject.label, direction: 'asc'}]
});
parseSearchResults(results.items);
} else {
renderData( settings.data );
}
};
var moveToExtraMenu = function($menu) {
var $cloneMenu = $menu.clone();
toHideForExtraMenus.forEach(function(menuSection){
menuSection.hide();
});
$currentMenu = $menu;
$extraMenuContainer.append($cloneMenu);
$cloneMenu.show();
$backButton.show();
};
var isExtraMenuValue = function( val ) {
var isMatch = false;
$extraMenus.each( function() {
var $this = $(this);
if(val === $this.data('menu-target-value')) {
isMatch = true;
moveToExtraMenu($this);
return;
}
});
return isMatch;
};
var dropdownClickedAnywhere = function( e ) {};
var dropdownCategoryLinkClicked = function ( e ) {
var $target = $( e.currentTarget ),
$inp = $target.find( 'input' ),
val = $inp.val(),
i = selectedCategories.indexOf( val );
e.preventDefault();
// if the checkbox is disabled.
if( $inp.prop('disabled') ) {
return false;
}
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(settings.dataObject.data) &&
item[settings.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( $inp.prop('disabled') ) {
return false;
}
if( isExtraMenuValue( val ) ) {
return false;
}
findItemInData = findItemWithData(settings.data, val);
if( typeof findItemInData === 'undefined' ) {
findItemInData = findItemWithData(settings.always, val);
}
if( typeof findItemInData === 'undefined' ) {
findItemInData = findItemWithData(settings.header, val);
}
if( typeof findItemInData === 'undefined' &&
val === 'dropdown-multi-menu-selectable:false' ) {
// don't close the dropdown.
return false;
}
if ( typeof findItemInData !== 'undefined' &&
findItemInData.hasOwnProperty('selectable') &&
findItemInData.selectable === false ) {
// don't close the dropdown
return false;
}
if( findItemInData.hasOwnProperty( 'href' ) ) {
window.location.href = findItemInData.href;
return;
}
if( findItemInData.hasOwnProperty('selectable') &&
!findItemWithData.selectable ) {
return;
}
if ( i > -1 ) {
var spliced = selectedItems.splice( i, 1 );
if( settings.multiple ) {
removeFromForm( spliced );
} else {
$form
.find('input[name="' + settings.name + '"]')
.remove();
}
$target.removeClass('selected');
setTimeout( function() {
$inp.prop( 'checked', false );
}, 0);
} else {
if( !settings.multiple ) {
selectedItems = [];
$form
.find('input[name="' + settings.name + '"]')
.remove();
$form
.find('input[name="_' + settings.name + '"]')
.parent()
.removeClass('selected');
}
selectedItems.push( val );
addToForm( val );
$target.addClass('selected');
setTimeout( function() {
$inp.prop( 'checked', true );
}, 0);
}
settings.onChange({"changed":findItemInData, "selected":selectedItems});
// close the dropdown if single selection
// otherwise don't close the dropdown
if( settings.multiple ) {
$( e.target ).blur();
return false;
} else {
var button = $self.siblings('.dropdown-toggle').first();
button.contents()
.each(
function(){
if ( this.nodeType === 3 && this.nodeValue.trim() ) {
this.textContent = $target.text();
}
});
}
};
var addTip = function() {
var $tipTemplate;
if( settings.tip ) {
var $seperatorTemplate = $(MultiAwesome.seperatorTemplate);
toHideForExtraMenus.push($seperatorTemplate);
$self.append($seperatorTemplate);
$tipTemplate = $(MultiAwesome.tipTemplate.replace(/\{\{tip\}\}/g, settings.tip));
toHideForExtraMenus.push($tipTemplate);
$self.append($tipTemplate);
}
};
/* * * * * * * * * * * * * * * */
/* setup
/* * * * * * * * * * * * * * * */
var shouldInit = function() {
if(!$self.hasClass('initialized')) {
$self.addClass('initialized');
return true;
}
return false;
};
var setup = function() {
if(!shouldInit()){
return;
}
addData(function(){
addHeader();
addCategories();
prepareDropdown();
addExtrasContainer();
addTitle();
attachListeners();
addTip();
getExtraMenus();
});
};
setup();
});
};
$(function(){
$('[data-multi-awesome]').each(function(){
$(this).multiawesome();
});
});
})( jQuery );
\ No newline at end of file
.open>.dropdown-menu {
max-width: 320px;
border-radius: 0px;
}
.dropdown-menu input[type='text'] {
margin: 0 5px;
width: 309px;
height: 35px;
padding-left: 6px;
}
.dropdown-menu input[type='checkbox'] {
margin: 0 5px;
display: none;
}
.dropdown-menu>li>a {
padding-left: 0;
}
.dropdown-menu .dropdown-multi-menu-title-area h3 {
font-size: 15px;
text-align: center;
margin-top: 5px;
}
.dropdown-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-menu .dropdown-multi-menu-selections {
max-height: 150px;
overflow-y: scroll;
margin-top: 15px;
}
.dropdown-menu .dropdown-multi-menu-category ul{
padding: 0 5px;
list-style: none;
max-height: 65px;
overflow-y: scroll;
}
.dropdown-menu ul.dropdown-multi-menu-header-list {
padding: 0 5px;
list-style: none;
max-height: 75px;
overflow-y: scroll;
}
.dropdown-menu .dropdown-multi-menu-selections ul{
padding: 0 5px;
list-style: none;
}
.dropdown-menu .dropdown-multi-menu-selections li,
.dropdown-menu .dropdown-multi-menu-header-list li{
padding: 7px 5px;
}
.dropdown-menu .dropdown-multi-menu-selections
li.dropdown-multi-menu-list-striped:nth-child(odd){
background: #F5F5F5;
}
.dropdown-menu .dropdown-multi-menu-selections
li a.disabled{
cursor: default;
}
.dropdown-menu .dropdown-multi-menu-selections .dropdown-multi-menu-subtitle {
font-size: 12px;
color: #9E9E9E;
}
[data-extra-menu] {
display: none;
}
.dropdown-multi-menu-back-button {
width: 12px;
height: 12px;
display: none;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAACDUlEQVRIibWWMZLaMBSGn2QBGS0MMJMUTCq7CdAEF9kLqNsqJ8gFlgvQUS0XIOcgF3CXagvMVvY2UKXF9srIYCQ5FQxkIdhL8kqN/H/vvV9PMsqyDPKG73ksA4BOp+Pk/Ybk3Thz3UGSJF8QQuB7HrRzQi4C5vN5LYqivhCCKSlZiRCnSNUXAVEU9eM4ftBaAyHEoTc3k063e32LDjPXWgPGGCilzude73te8b8CXmVOqdNoNMZFxE8CFotFLQzDw8wfKaWObdujouInAWEY7jMvlUoTSqnTK9iWk4A/MzcMA64VPwIEQdBfrVYPSqlr9F4Fevb92yAI7tM0bSutb7MsA4QQIISAEOKUy2WfEPIrj9huPjDGL5Vy2e90uw7hnH/lnH/DGAPCeL8xyzJI05RtNhuWd7B2+wzDgHeVyuTZ9zl+S9lFgtRqtR9Syo//o0Wf2u1HtFucTqeDQ5MJIVCtVvv/7BQ1m80xQgiEEExKya4RPYy9B6Zpctu2R5RSB2MMSikQQrDZbHZ/DcAYDodHC+v1+klrzZVSIKW8226375fL5YdWq/XzLQB07gi6rjs4ddmZpsmLAM7epvV6fQwAe0/iON75UujSOzsHlmUdeaK1BiEEeyroycUX7bASJSUTQrx4nufnffgvAizL4gAwmrkuJEnygjHmCKHcPpw1+VR4nscQQO4/CgCA3yx5RbfFRth/AAAAAElFTkSuQmCC');
float: left;
background-size: 12px;
background-repeat: no-repeat;
margin-top: 3px;
margin-left: 10px;
margin-right: -10px;
}
.dropdown-menu .dropdown-multi-menu-selections ul a,
.dropdown-menu .dropdown-multi-menu-category ul a,
.dropdown-menu .dropdown-multi-menu-header-item a{
text-decoration: none;
color: #333;
display: inline-block;
width: 285px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
padding-left: 30px;
vertical-align: middle;
}
.dropdown-menu .dropdown-multi-menu-selections ul a:focus,
.dropdown-menu .dropdown-multi-menu-category ul a:focus,
.dropdown-menu .dropdown-multi-menu-header-item a:focus {
border: none;
outline: none;
}
.dropdown-menu .dropdown-multi-menu-selections ul a.selected,
.dropdown-menu .dropdown-multi-menu-category ul a.selected,
.dropdown-menu .dropdown-multi-menu-header 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-menu .dropdown-multi-menu-selections ul a.selected.dropdown-multi-menu-item-with-subtitle,
.dropdown-menu .dropdown-multi-menu-selections ul a.selected.dropdown-multi-menu-with-image {
background-position: 2px 12px;
}
.dropdown-menu .dropdown-multi-menu-image {
background-size: cover;
/*crop from the center*/
background-position: center;
width: 30px;
height: 30px;
display: inline-block;
position: relative;
border-radius: 15px;
float: left;
margin-top: 3px;
margin-right: 11px;
}
.dropdown-menu .dropdown-multi-menu-tip-area{
margin: 10px;
text-align: left;
max-height: 40px;
overflow-y: scroll;
}
.dropdown-menu .dropdown-multi-menu-header-area {
}
.dropdown-menu .dropdown-multi-menu-tip-area p {
}
.dropdown-menu .input-with-icon {
position: relative;
margin: 10px 0;
}
.dropdown-menu .input-with-icon i {
position: absolute;
right: 0;
padding: 10px 12px;
color: #817F7F;
pointer-events: none;
}
......@@ -19,6 +19,10 @@ class Projects::MilestonesController < Projects::ApplicationController
@milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html # show.html.erb
format.json { render :json => @milestones }
end
end
def new
......
......@@ -20,7 +20,7 @@ module MilestonesHelper
end
end
def projects_milestones_options
def projects_milestones_data_options
milestones =
if @project
@project.milestones
......@@ -28,12 +28,11 @@ module MilestonesHelper
Milestone.where(project_id: @projects)
end.active
epoch = DateTime.parse('1970-01-01')
grouped_milestones = GlobalMilestone.build_collection(milestones)
grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
milestones.as_json only: [:title, :id]
end
options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title])
def projects_milestones_header_options
grouped_milestones = [Milestone::Any, Milestone::None].as_json
end
end
......@@ -36,6 +36,41 @@ module SelectsHelper
hidden_field_tag(id, value, html)
end
def multi_select_tag(name, opts = {})
css_class = "dropdown-menu "
css_class << (opts[:class] || '')
header = opts[:header] || ''
ul_html = {
class: css_class,
data: {
header: header,
data: opts[:header_url],
"multi-awesome" => '',
"data-object" => {
label: "title",
data: "id"
}
}
}
button_html = {
class: ["btn", "btn-default", "dropdown-toggle"],
type: "button",
data: {
toggle: "dropdown"
}
}
button_class = "btn btn-default dropdown-toggle"
content_tag :div, :class => "button-group" do
content_tag(:button, content_tag(:span, name) +
content_tag(:span,nil ,:class => "caret"),
button_html) +
content_tag(:ul, nil, ul_html)
end
end
def groups_select_tag(id, opts = {})
opts[:class] ||= ''
opts[:class] << ' ajax-groups-select'
......
......@@ -38,9 +38,7 @@
placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
.filter-item.inline.milestone-filter
= select_tag('milestone_title', projects_milestones_options,
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Milestone'})
= multi_select_tag('Milestone', class: 'milestone-filter', header: projects_milestones_header_options)
.filter-item.inline.labels-filter
= select_tag('label_name', projects_labels_options,
......
/**
* 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;
}));
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册