Initializing with MagicSearch 0.1.5.9

First commit of MagicSearch code

Change-Id: Idc51127a72a0a80aab706690db26387cc9f72b81
This commit is contained in:
Randy Bertram 2015-02-18 08:29:16 -05:00
parent c5a716cb22
commit 1e15e5bbb5
13 changed files with 749 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
*.pyc
*.sw?
*.sqlite3
.DS_STORE
*.egg-info
.venv
.tox
build
dist

8
MANIFEST.in Executable file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
__import__('pkg_resources').declare_namespace(__name__)

1
xstatic/pkg/__init__.py Executable file
View File

@ -0,0 +1 @@
__import__('pkg_resources').declare_namespace(__name__)

View 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.
}

View 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;
}

View 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 &gt; 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 &gt; 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 &gt; 0" title="{{ strings.cancel }}">
<i class="fi-x fa fa-times cancel"></i>
</a>
</div>
</div>

View 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();
}
};
})
;

View 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;
}
}

View 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 &gt; 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 &gt; 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 &gt; 0" title="{{ strings.cancel }}">
<i class="fi-x fa fa-times cancel"></i>
</a>
</div>
</div>

View 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();
});
}
};
});