Initializing with MagicSearch 0.1.5.9
First commit of MagicSearch code Change-Id: Idc51127a72a0a80aab706690db26387cc9f72b81
This commit is contained in:
parent
c5a716cb22
commit
1e15e5bbb5
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
*.pyc
|
||||||
|
*.sw?
|
||||||
|
*.sqlite3
|
||||||
|
.DS_STORE
|
||||||
|
*.egg-info
|
||||||
|
.venv
|
||||||
|
.tox
|
||||||
|
build
|
||||||
|
dist
|
8
MANIFEST.in
Executable file
8
MANIFEST.in
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
include README.txt
|
||||||
|
recursive-include xstatic/pkg/magic_search *
|
||||||
|
|
||||||
|
global-exclude *.pyc
|
||||||
|
global-exclude *.pyo
|
||||||
|
global-exclude *.orig
|
||||||
|
global-exclude *.rej
|
||||||
|
|
21
README.txt
Executable file
21
README.txt
Executable file
@ -0,0 +1,21 @@
|
|||||||
|
XStatic-MagicSearch
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
MagicSearch is an AngularJS directive that provides a UI for both faceted
|
||||||
|
filtering and as-you-type filtering. It is intended for filtering tables,
|
||||||
|
such as an AngularJS smart-table, but it can be used in any situation
|
||||||
|
where you can provide it with facets/options and consume its events.
|
||||||
|
|
||||||
|
MagicSearch was initially developed by David Kavanagh for Eucalyptus.
|
||||||
|
|
||||||
|
|
||||||
|
MagicSearch javascript library packaged for setuptools (easy_install) / pip.
|
||||||
|
|
||||||
|
This package is intended to be used by **any** project that needs these files.
|
||||||
|
|
||||||
|
It intentionally does **not** provide any extra code except some metadata
|
||||||
|
**nor** has any extra requirements. You MAY use some minimal support code from
|
||||||
|
the XStatic base package, if you like.
|
||||||
|
|
||||||
|
You can find more info about the xstatic packaging way in the package `XStatic`.
|
||||||
|
|
27
setup.py
Executable file
27
setup.py
Executable file
@ -0,0 +1,27 @@
|
|||||||
|
from xstatic.pkg import magic_search as xs
|
||||||
|
|
||||||
|
# The README.txt file should be written in reST so that PyPI can use
|
||||||
|
# it to generate your project's PyPI page.
|
||||||
|
long_description = open('README.txt').read()
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name=xs.PACKAGE_NAME,
|
||||||
|
version=xs.PACKAGE_VERSION,
|
||||||
|
description=xs.DESCRIPTION,
|
||||||
|
long_description=long_description,
|
||||||
|
classifiers=xs.CLASSIFIERS,
|
||||||
|
keywords=xs.KEYWORDS,
|
||||||
|
maintainer=xs.MAINTAINER,
|
||||||
|
maintainer_email=xs.MAINTAINER_EMAIL,
|
||||||
|
license=xs.LICENSE,
|
||||||
|
url=xs.HOMEPAGE,
|
||||||
|
platforms=xs.PLATFORMS,
|
||||||
|
packages=find_packages(),
|
||||||
|
namespace_packages=['xstatic', 'xstatic.pkg', ],
|
||||||
|
include_package_data=True,
|
||||||
|
zip_safe=False,
|
||||||
|
install_requires=[], # nothing! :)
|
||||||
|
# if you like, you MAY use the 'XStatic' package.
|
||||||
|
)
|
1
xstatic/__init__.py
Executable file
1
xstatic/__init__.py
Executable file
@ -0,0 +1 @@
|
|||||||
|
__import__('pkg_resources').declare_namespace(__name__)
|
1
xstatic/pkg/__init__.py
Executable file
1
xstatic/pkg/__init__.py
Executable file
@ -0,0 +1 @@
|
|||||||
|
__import__('pkg_resources').declare_namespace(__name__)
|
49
xstatic/pkg/magic_search/__init__.py
Executable file
49
xstatic/pkg/magic_search/__init__.py
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
XStatic resource package
|
||||||
|
|
||||||
|
See package 'XStatic' for documentation and basic tools.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DISPLAY_NAME = 'Magic-Search' # official name, upper/lowercase allowed, no spaces
|
||||||
|
PACKAGE_NAME = 'XStatic-%s' % DISPLAY_NAME # name used for PyPi
|
||||||
|
|
||||||
|
NAME = __name__.split('.')[-1] # package name (e.g. 'foo' or 'foo_bar')
|
||||||
|
# please use a all-lowercase valid python
|
||||||
|
# package name
|
||||||
|
|
||||||
|
VERSION = '0.1.5' # version of the packaged files, please use the upstream
|
||||||
|
# version number
|
||||||
|
BUILD = '9' # our package build number, so we can release new builds
|
||||||
|
# with fixes for xstatic stuff.
|
||||||
|
PACKAGE_VERSION = VERSION + '.' + BUILD # version used for PyPi
|
||||||
|
|
||||||
|
DESCRIPTION = "%s %s (XStatic packaging standard)" % (DISPLAY_NAME, VERSION)
|
||||||
|
|
||||||
|
PLATFORMS = 'any'
|
||||||
|
CLASSIFIERS = []
|
||||||
|
KEYWORDS = '%s xstatic' % NAME
|
||||||
|
|
||||||
|
# XStatic-* package maintainer:
|
||||||
|
MAINTAINER = 'Randy Bertram'
|
||||||
|
MAINTAINER_EMAIL = 'rbertram@us.ibm.com'
|
||||||
|
|
||||||
|
# this refers to the project homepage of the stuff we packaged:
|
||||||
|
HOMEPAGE = 'https://github.com/eucalyptus/magic-search'
|
||||||
|
|
||||||
|
# this refers to all files:
|
||||||
|
LICENSE = '(same as %s)' % DISPLAY_NAME
|
||||||
|
|
||||||
|
from os.path import join, dirname
|
||||||
|
BASE_DIR = join(dirname(__file__), 'data')
|
||||||
|
# linux package maintainers just can point to their file locations like this:
|
||||||
|
#BASE_DIR = '/usr/share/javascript/jquery'
|
||||||
|
|
||||||
|
LOCATIONS = {
|
||||||
|
# CDN locations (if no public CDN exists, use an empty dict)
|
||||||
|
# if value is a string, it is a base location, just append relative
|
||||||
|
# path/filename. if value is a dict, do another lookup using the
|
||||||
|
# relative path/filename you want.
|
||||||
|
# your relative path/filenames should usually be without version
|
||||||
|
# information, because either the base dir/url is exactly for this
|
||||||
|
# version or the mapping will care for accessing this version.
|
||||||
|
}
|
85
xstatic/pkg/magic_search/data/magic_search.css
Executable file
85
xstatic/pkg/magic_search/data/magic_search.css
Executable file
@ -0,0 +1,85 @@
|
|||||||
|
/* Copyright 2014-2015 Eucalyptus Systems, Inc. */
|
||||||
|
/*-----------------------------------------
|
||||||
|
Colors
|
||||||
|
----------------------------------------- */
|
||||||
|
/*-----------------------------------------
|
||||||
|
Item list
|
||||||
|
----------------------------------------- */
|
||||||
|
/*-----------------------------------------
|
||||||
|
Magic Search bar
|
||||||
|
----------------------------------------- */
|
||||||
|
/* line 30, ../src/magic_search.scss */
|
||||||
|
.search-bar {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: white;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding: 4px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
/* line 37, ../src/magic_search.scss */
|
||||||
|
.search-bar i.fi-filter {
|
||||||
|
color: #444444;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
left: 0.65rem;
|
||||||
|
}
|
||||||
|
/* line 46, ../src/magic_search.scss */
|
||||||
|
.search-bar #search-main-area {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 1.65rem;
|
||||||
|
margin-right: 1.65rem;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
/* line 14, ../src/magic_search.scss */
|
||||||
|
.search-bar .item-list {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
/* line 16, ../src/magic_search.scss */
|
||||||
|
.search-bar .item-list .item {
|
||||||
|
color: #333;
|
||||||
|
background-color: #e6e7e8;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
/* line 20, ../src/magic_search.scss */
|
||||||
|
.search-bar .item-list .item a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
/* line 53, ../src/magic_search.scss */
|
||||||
|
.search-bar .item-list {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
/* line 56, ../src/magic_search.scss */
|
||||||
|
.search-bar #search-selected {
|
||||||
|
background-color: white;
|
||||||
|
color: #444444;
|
||||||
|
}
|
||||||
|
/* line 60, ../src/magic_search.scss */
|
||||||
|
.search-bar #search-entry {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
/* line 64, ../src/magic_search.scss */
|
||||||
|
.search-bar #search-input {
|
||||||
|
width: 220px;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 3px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
/* line 75, ../src/magic_search.scss */
|
||||||
|
.search-bar .match {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
/* line 78, ../src/magic_search.scss */
|
||||||
|
.search-bar i.cancel {
|
||||||
|
color: #444444;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.65rem;
|
||||||
|
}
|
||||||
|
/* line 80, ../src/magic_search.scss */
|
||||||
|
.search-bar i.cancel:hover {
|
||||||
|
color: darkred;
|
||||||
|
}
|
49
xstatic/pkg/magic_search/data/magic_search.html
Executable file
49
xstatic/pkg/magic_search/data/magic_search.html
Executable file
@ -0,0 +1,49 @@
|
|||||||
|
<!--! Magic Searchbar -->
|
||||||
|
<div id="magic-search" magic-overrides>
|
||||||
|
<div class="search-bar">
|
||||||
|
<i class="fi-filter fa fa-filter go" ng-class="{'has-items': currentSearch.length > 0}"></i>
|
||||||
|
<div id="search-main-area">
|
||||||
|
<span class="item-list">
|
||||||
|
<span class="label radius secondary item"
|
||||||
|
ng-repeat="facet in currentSearch" ng-cloak="cloak">
|
||||||
|
<span>
|
||||||
|
{{ facet.label[0] }}:<b>{{ facet.label[1] }}</b>
|
||||||
|
</span>
|
||||||
|
<a class="remove" ng-click="removeFacet($index, $event)" title="{{ strings.remove }}">
|
||||||
|
<i class="fi-x fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span id="search-selected" class="label" ng-cloak="" ng-show="facetSelected">
|
||||||
|
{{ facetSelected.label[0] }}:
|
||||||
|
</span>
|
||||||
|
<div id="search-entry" is-open="isMenuOpen">
|
||||||
|
<input id="search-input" type="text" data-dropdown="facet-drop" dropdown-toggle
|
||||||
|
placeholder="{{ strings.prompt }}" autocomplete="off"
|
||||||
|
ng-class="{'has-items': currentSearch.length > 0}" />
|
||||||
|
<ul id="facet-drop" class="f-dropdown dropdown-menu" data-dropdown-content="">
|
||||||
|
<li ng-repeat="facet in filteredObj" ng-show="!facetSelected">
|
||||||
|
<a ng-click="facetClicked($index, $event, facet.name)"
|
||||||
|
ng-show="!isMatchLabel(facet.label)">{{ facet.label }}</a>
|
||||||
|
<a ng-click="facetClicked($index, $event, facet.name)"
|
||||||
|
ng-show="isMatchLabel(facet.label)">
|
||||||
|
{{ facet.label[0] }}<span class="match">{{ facet.label[1] }}</span>{{ facet.label[2] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li ng-repeat="option in filteredOptions" ng-show="facetSelected">
|
||||||
|
<a ng-click="optionClicked($index, $event, option.key)"
|
||||||
|
ng-show="!isMatchLabel(option.label)">
|
||||||
|
{{ option.label }}
|
||||||
|
</a>
|
||||||
|
<a ng-click="optionClicked($index, $event, option.key)"
|
||||||
|
ng-show="isMatchLabel(option.label)">
|
||||||
|
{{ option.label[0] }}<span class="match">{{ option.label[1] }}</span>{{ option.label[2] }}
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a ng-click="clearSearch()" ng-show="currentSearch.length > 0" title="{{ strings.cancel }}">
|
||||||
|
<i class="fi-x fa fa-times cancel"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
334
xstatic/pkg/magic_search/data/magic_search.js
Executable file
334
xstatic/pkg/magic_search/data/magic_search.js
Executable file
@ -0,0 +1,334 @@
|
|||||||
|
/**
|
||||||
|
* @fileOverview Magic Search JS
|
||||||
|
* @requires AngularJS
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Allow the module to be pre-defined with additional dependencies
|
||||||
|
try{
|
||||||
|
angular.module('MagicSearch');
|
||||||
|
} catch (exception) {
|
||||||
|
angular.module('MagicSearch', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module('MagicSearch')
|
||||||
|
.directive('magicSearch', function($compile) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
scope: {
|
||||||
|
facets_json: '@facets',
|
||||||
|
filter_keys: '=filterKeys',
|
||||||
|
strings: '=strings'
|
||||||
|
},
|
||||||
|
templateUrl: function (scope, elem) {
|
||||||
|
return elem.template;
|
||||||
|
},
|
||||||
|
controller: function ($scope, $timeout) {
|
||||||
|
$scope.currentSearch = [];
|
||||||
|
$scope.initSearch = function() {
|
||||||
|
// Parse facets JSON and convert to a list of facets.
|
||||||
|
$scope.facetsJson = $scope.facets_json.replace(/__apos__/g, "\'").replace(/__dquote__/g, '\\"').replace(/__bslash__/g, "\\");
|
||||||
|
$scope.facetsObj = JSON.parse($scope.facetsJson);
|
||||||
|
// set facets selected and remove them from facetsObj
|
||||||
|
var initialFacets = window.location.search;
|
||||||
|
if (initialFacets.indexOf('?') === 0) {
|
||||||
|
initialFacets = initialFacets.slice(1);
|
||||||
|
}
|
||||||
|
initialFacets = initialFacets.split('&');
|
||||||
|
if (initialFacets.length > 1 || initialFacets[0].length > 0) {
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.strings['prompt'] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
angular.forEach(initialFacets, function(facet, idx) {
|
||||||
|
var facetParts = facet.split('=');
|
||||||
|
angular.forEach($scope.facetsObj, function(value, idx) {
|
||||||
|
if (value.name == facetParts[0]) {
|
||||||
|
if (value.options === undefined) {
|
||||||
|
$scope.currentSearch.push({'name':facet, 'label':[value.label, facetParts[1]]});
|
||||||
|
// allow free-form facets to remain
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
angular.forEach(value.options, function(option, idx) {
|
||||||
|
if (option.key == facetParts[1]) {
|
||||||
|
$scope.currentSearch.push({'name':facet, 'label':[value.label, option.label]});
|
||||||
|
$scope.deleteFacetSelection(facetParts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$scope.filteredObj = $scope.facetsObj;
|
||||||
|
};
|
||||||
|
// removes a facet from the menu
|
||||||
|
$scope.deleteFacetSelection = function(facet_parts) {
|
||||||
|
angular.forEach($scope.facetsObj.slice(), function(facet, idx) {
|
||||||
|
if (facet.name == facet_parts[0]) {
|
||||||
|
if (facet.options === undefined) {
|
||||||
|
return; // allow free-form facets to remain
|
||||||
|
}
|
||||||
|
for (var i=0; i<facet.options.length; i++) {
|
||||||
|
var option = facet.options[i];
|
||||||
|
if (option.key == facet_parts[1]) {
|
||||||
|
$scope.facetsObj[idx].options.splice($scope.facetsObj[idx].options.indexOf(option), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (facet.options.length === 0) {
|
||||||
|
$scope.facetsObj.splice($scope.facetsObj.indexOf(facet), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$('#search-input').on('keydown', function($event) { // handle ctrl-char input
|
||||||
|
var key = $event.keyCode || $event.charCode;
|
||||||
|
if (key == 9) { // prevent default when we can.
|
||||||
|
$event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#search-input').on('keyup', function($event) { // handle ctrl-char input
|
||||||
|
if ($event.metaKey == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var search_val = $('#search-input').val();
|
||||||
|
var key = $event.keyCode || $event.charCode;
|
||||||
|
if (key == 9) { // tab, so select facet if narrowed down to 1
|
||||||
|
if ($scope.facetSelected === undefined) {
|
||||||
|
if ($scope.filteredObj.length != 1) return;
|
||||||
|
$scope.facetClicked(0, '', $scope.filteredObj[0].name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($scope.filteredOptions.length != 1) return;
|
||||||
|
$scope.optionClicked(0, '', $scope.filteredOptions[0].key);
|
||||||
|
$scope.resetState();
|
||||||
|
}
|
||||||
|
$timeout(function() {
|
||||||
|
$('#search-input').val('');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key == 27) { // esc, so cancel and reset everthing
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.hideMenu();
|
||||||
|
$('#search-input').val('');
|
||||||
|
});
|
||||||
|
$scope.resetState();
|
||||||
|
$scope.$emit('textSearch', '', $scope.filter_keys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key == 13) { // enter, so accept value
|
||||||
|
// if tag search, treat as regular facet
|
||||||
|
if ($scope.facetSelected && $scope.facetSelected.options === undefined) {
|
||||||
|
var curr = $scope.facetSelected;
|
||||||
|
curr.name = curr.name + '=' + search_val;
|
||||||
|
curr.label[1] = search_val;
|
||||||
|
$scope.currentSearch.push(curr);
|
||||||
|
$scope.resetState();
|
||||||
|
$scope.emitQuery();
|
||||||
|
$scope.showMenu();
|
||||||
|
}
|
||||||
|
// if text search treat as search
|
||||||
|
else {
|
||||||
|
for (i=0; i<$scope.currentSearch.length; i++) {
|
||||||
|
if ($scope.currentSearch[i].name.indexOf('text') === 0) {
|
||||||
|
$scope.currentSearch.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.currentSearch.push({'name':'text='+search_val, 'label':[$scope.strings['text'], search_val]});
|
||||||
|
$scope.$apply();
|
||||||
|
$scope.hideMenu();
|
||||||
|
$('#search-input').val('');
|
||||||
|
$scope.$emit('textSearch', search_val, $scope.filter_keys);
|
||||||
|
}
|
||||||
|
$scope.filteredObj = $scope.facetsObj;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (search_val === '') {
|
||||||
|
$scope.filteredObj = $scope.facetsObj;
|
||||||
|
$scope.$emit('textSearch', '', $scope.filter_keys);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.filterFacets(search_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('#search-input').on('keypress', function($event) { // handle character input
|
||||||
|
var search_val = $('#search-input').val();
|
||||||
|
var key = $event.which || $event.keyCode || $event.charCode;
|
||||||
|
if (key != 8 && key != 46 && key != 13 && key != 9 && key != 27) {
|
||||||
|
search_val = search_val + String.fromCharCode(key).toLowerCase();
|
||||||
|
}
|
||||||
|
if (search_val == ' ') { // space and field is empty, show menu
|
||||||
|
$scope.showMenu();
|
||||||
|
$timeout(function() {
|
||||||
|
$('#search-input').val('');
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (search_val === '') {
|
||||||
|
$scope.filteredObj = $scope.facetsObj;
|
||||||
|
$scope.$emit('textSearch', '', $scope.filter_keys);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key != 8 && key != 46) {
|
||||||
|
$scope.filterFacets(search_val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$scope.filterFacets = function(search_val) {
|
||||||
|
// try filtering facets/options.. if no facets match, do text search
|
||||||
|
var i, idx, label;
|
||||||
|
var filtered = [];
|
||||||
|
if ($scope.facetSelected === undefined) {
|
||||||
|
$scope.filteredObj = $scope.facetsObj;
|
||||||
|
for (i=0; i<$scope.filteredObj.length; i++) {
|
||||||
|
var facet = $scope.filteredObj[i];
|
||||||
|
idx = facet.label.toLowerCase().indexOf(search_val);
|
||||||
|
if (idx > -1) {
|
||||||
|
label = [facet.label.substring(0, idx), facet.label.substring(idx, idx + search_val.length), facet.label.substring(idx + search_val.length)];
|
||||||
|
filtered.push({'name':facet.name, 'label':label, 'options':facet.options});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
$scope.showMenu();
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.filteredObj = filtered;
|
||||||
|
}, 0.1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.$emit('textSearch', search_val, $scope.filter_keys);
|
||||||
|
$scope.hideMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // assume option search
|
||||||
|
$scope.filteredOptions = $scope.facetOptions;
|
||||||
|
if ($scope.facetOptions === undefined) { // no options, assume free form text facet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (i=0; i<$scope.filteredOptions.length; i++) {
|
||||||
|
var option = $scope.filteredOptions[i];
|
||||||
|
idx = option.label.toLowerCase().indexOf(search_val);
|
||||||
|
if (idx > -1) {
|
||||||
|
label = [option.label.substring(0, idx), option.label.substring(idx, idx + search_val.length), option.label.substring(idx + search_val.length)];
|
||||||
|
filtered.push({'key':option.key, 'label':label});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
$scope.showMenu();
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.filteredOptions = filtered;
|
||||||
|
}, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// enable text entry when mouse clicked anywhere in search box
|
||||||
|
$('#search-main-area').on("click", function($event) {
|
||||||
|
$('#search-input').trigger("focus");
|
||||||
|
if ($scope.facetSelected === undefined) {
|
||||||
|
$scope.showMenu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// when facet clicked, add 1st part of facet and set up options
|
||||||
|
$scope.facetClicked = function($index, $event, name) {
|
||||||
|
$scope.hideMenu();
|
||||||
|
var facet = $scope.filteredObj[$index];
|
||||||
|
var label = facet.label;
|
||||||
|
if (Array.isArray(label)) {
|
||||||
|
label = label.join('');
|
||||||
|
}
|
||||||
|
$scope.facetSelected = {'name':facet.name, 'label':[label, '']};
|
||||||
|
if (facet.options !== undefined) {
|
||||||
|
$scope.filteredOptions = $scope.facetOptions = facet.options;
|
||||||
|
$scope.showMenu();
|
||||||
|
}
|
||||||
|
$timeout(function() {
|
||||||
|
$('#search-input').val('');
|
||||||
|
});
|
||||||
|
$scope.strings['prompt'] = '';
|
||||||
|
$timeout(function() {
|
||||||
|
$('#search-input').focus();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// when option clicked, complete facet and send event
|
||||||
|
$scope.optionClicked = function($index, $event, name) {
|
||||||
|
$scope.hideMenu();
|
||||||
|
var curr = $scope.facetSelected;
|
||||||
|
curr.name = curr.name + '=' + name;
|
||||||
|
curr.label[1] = $scope.filteredOptions[$index].label;
|
||||||
|
if (Array.isArray(curr.label[1])) {
|
||||||
|
curr.label[1] = curr.label[1].join('');
|
||||||
|
}
|
||||||
|
$scope.currentSearch.push(curr);
|
||||||
|
$scope.resetState();
|
||||||
|
$scope.emitQuery();
|
||||||
|
$scope.showMenu();
|
||||||
|
};
|
||||||
|
// send event with new query string
|
||||||
|
$scope.emitQuery = function(removed) {
|
||||||
|
var query = '';
|
||||||
|
for (var i=0; i<$scope.currentSearch.length; i++) {
|
||||||
|
if ($scope.currentSearch[i].name.indexOf('text') !== 0) {
|
||||||
|
if (query.length > 0) query = query + "&";
|
||||||
|
query = query + $scope.currentSearch[i].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removed !== undefined && removed.indexOf('text') === 0) {
|
||||||
|
$scope.$emit('textSearch', '', $scope.filter_keys);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.$emit('searchUpdated', query);
|
||||||
|
if ($scope.currentSearch.length > 0) {
|
||||||
|
var newFacet = $scope.currentSearch[$scope.currentSearch.length-1].name;
|
||||||
|
$scope.deleteFacetSelection(newFacet.split('='));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// remove facet and either update filter or search
|
||||||
|
$scope.removeFacet = function($index, $event) {
|
||||||
|
var removed = $scope.currentSearch[$index].name;
|
||||||
|
$scope.currentSearch.splice($index, 1);
|
||||||
|
if ($scope.facetSelected === undefined) {
|
||||||
|
$scope.emitQuery(removed);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.resetState();
|
||||||
|
$('#search-input').val('');
|
||||||
|
}
|
||||||
|
// facet re-enabled by reload
|
||||||
|
};
|
||||||
|
// clear entire searchbar
|
||||||
|
$scope.clearSearch = function() {
|
||||||
|
if ($scope.currentSearch.length > 0) {
|
||||||
|
$scope.currentSearch = [];
|
||||||
|
$scope.facetsObj = JSON.parse($scope.facetsJson);
|
||||||
|
$scope.resetState();
|
||||||
|
$scope.$emit('searchUpdated', '');
|
||||||
|
$scope.$emit('textSearch', '', $scope.filter_keys);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$scope.isMatchLabel = function(label) {
|
||||||
|
return Array.isArray(label);
|
||||||
|
};
|
||||||
|
$scope.resetState = function() {
|
||||||
|
$('#search-input').val('');
|
||||||
|
$scope.filteredObj = $scope.facetsObj;
|
||||||
|
$scope.facetSelected = undefined;
|
||||||
|
$scope.facetOptions = undefined;
|
||||||
|
$scope.filteredOptions = undefined
|
||||||
|
};
|
||||||
|
// showMenu and hideMenu depend on foundation's dropdown. They need
|
||||||
|
// to be modified to work with another dropdown implemenation (i.e. bootstrap)
|
||||||
|
$scope.showMenu = function() {
|
||||||
|
$timeout(function() {
|
||||||
|
if ($('#facet-drop').hasClass('open') === false) {
|
||||||
|
$('#search-input').trigger('click');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.hideMenu = function() {
|
||||||
|
$(document).foundation('dropdown', 'closeall');
|
||||||
|
};
|
||||||
|
$scope.initSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
;
|
87
xstatic/pkg/magic_search/data/magic_search.scss
Executable file
87
xstatic/pkg/magic_search/data/magic_search.scss
Executable file
@ -0,0 +1,87 @@
|
|||||||
|
/* Copyright 2014-2015 Eucalyptus Systems, Inc. */
|
||||||
|
|
||||||
|
/*-----------------------------------------
|
||||||
|
Colors
|
||||||
|
----------------------------------------- */
|
||||||
|
$textcolor: #444;
|
||||||
|
$background: white;
|
||||||
|
$itembackground: #e6e7e8;
|
||||||
|
|
||||||
|
/*-----------------------------------------
|
||||||
|
Item list
|
||||||
|
----------------------------------------- */
|
||||||
|
@mixin item-list {
|
||||||
|
.item-list {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
.item {
|
||||||
|
color: #333;
|
||||||
|
background-color: $itembackground;
|
||||||
|
margin-right: 8px;
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*-----------------------------------------
|
||||||
|
Magic Search bar
|
||||||
|
----------------------------------------- */
|
||||||
|
.search-bar {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: $background;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding: 4px;
|
||||||
|
height: auto;
|
||||||
|
i.fi-filter {
|
||||||
|
color: $textcolor;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
left: 0.65rem;
|
||||||
|
//&.has-items {
|
||||||
|
// margin-top: 6px;
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
#search-main-area {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 1.65rem;
|
||||||
|
margin-right: 1.65rem;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
@include item-list;
|
||||||
|
.item-list {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
#search-selected {
|
||||||
|
background-color: $background;
|
||||||
|
color: $textcolor;
|
||||||
|
}
|
||||||
|
#search-entry {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
#search-input {
|
||||||
|
width: 220px;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 3px;
|
||||||
|
background-color: $background;
|
||||||
|
//&.has-items {
|
||||||
|
// margin-top: 6px;
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
.match {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
i.cancel {
|
||||||
|
color: $textcolor;
|
||||||
|
&:hover {
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.65rem;
|
||||||
|
}
|
||||||
|
}
|
50
xstatic/pkg/magic_search/data/magic_search_bootstrap.html
Executable file
50
xstatic/pkg/magic_search/data/magic_search_bootstrap.html
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
<!--! Magic Searchbar -->
|
||||||
|
<div id="magic-search" magic-overrides>
|
||||||
|
<div class="search-bar">
|
||||||
|
<i class="fi-filter fa fa-filter go" ng-class="{'has-items': currentSearch.length > 0}"></i>
|
||||||
|
<div id="search-main-area">
|
||||||
|
<span class="item-list">
|
||||||
|
<span class="label radius secondary item"
|
||||||
|
ng-repeat="facet in currentSearch" ng-cloak="cloak">
|
||||||
|
<span>
|
||||||
|
{{ facet.label[0] }}:<b>{{ facet.label[1] }}</b>
|
||||||
|
</span>
|
||||||
|
<a class="remove" ng-click="removeFacet($index, $event)" title="{{ strings.remove }}">
|
||||||
|
<i class="fi-x fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span id="search-selected" class="label" ng-cloak="" ng-show="facetSelected">
|
||||||
|
{{ facetSelected.label[0] }}:
|
||||||
|
</span>
|
||||||
|
<!-- For bootstrap, the dropdown attribute is moved from input up to div. -->
|
||||||
|
<div id="search-entry" dropdown is-open="isMenuOpen">
|
||||||
|
<input id="search-input" type="text" dropdown-toggle
|
||||||
|
placeholder="{{ strings.prompt }}" autocomplete="off"
|
||||||
|
ng-class="{'has-items': currentSearch.length > 0}" />
|
||||||
|
<ul id="facet-drop" class="f-dropdown dropdown-menu" data-dropdown-content="">
|
||||||
|
<li ng-repeat="facet in filteredObj" ng-show="!facetSelected">
|
||||||
|
<a ng-click="facetClicked($index, $event, facet.name)"
|
||||||
|
ng-show="!isMatchLabel(facet.label)">{{ facet.label }}</a>
|
||||||
|
<a ng-click="facetClicked($index, $event, facet.name)"
|
||||||
|
ng-show="isMatchLabel(facet.label)">
|
||||||
|
{{ facet.label[0] }}<span class="match">{{ facet.label[1] }}</span>{{ facet.label[2] }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li ng-repeat="option in filteredOptions" ng-show="facetSelected">
|
||||||
|
<a ng-click="optionClicked($index, $event, option.key)"
|
||||||
|
ng-show="!isMatchLabel(option.label)">
|
||||||
|
{{ option.label }}
|
||||||
|
</a>
|
||||||
|
<a ng-click="optionClicked($index, $event, option.key)"
|
||||||
|
ng-show="isMatchLabel(option.label)">
|
||||||
|
{{ option.label[0] }}<span class="match">{{ option.label[1] }}</span>{{ option.label[2] }}
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a ng-click="clearSearch()" ng-show="currentSearch.length > 0" title="{{ strings.cancel }}">
|
||||||
|
<i class="fi-x fa fa-times cancel"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
28
xstatic/pkg/magic_search/data/magic_search_bootstrap.js
Executable file
28
xstatic/pkg/magic_search/data/magic_search_bootstrap.js
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
angular.module('MagicSearch', ['ui.bootstrap'])
|
||||||
|
.directive('magicOverrides', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
controller: function($scope) {
|
||||||
|
// showMenu and hideMenu depend on foundation's dropdown. They need
|
||||||
|
// to be modified to work with another dropdown implemenation.
|
||||||
|
// For bootstrap, they are not needed at all.
|
||||||
|
$scope.showMenu = function() {
|
||||||
|
$scope.isMenuOpen = true;
|
||||||
|
};
|
||||||
|
$scope.hideMenu = function() {
|
||||||
|
$scope.isMenuOpen = false;
|
||||||
|
};
|
||||||
|
$scope.isMenuOpen = false;
|
||||||
|
|
||||||
|
// remove the following when magic_search.js handles changing the facets/options
|
||||||
|
$scope.$watch('facets_json', function(newVal, oldVal) {
|
||||||
|
if (newVal === oldVal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$scope.currentSearch = [];
|
||||||
|
$scope.initSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user