├── .gitignore ├── lib ├── js │ ├── templates │ │ ├── search_input.jst │ │ ├── search_box.jst │ │ ├── search_facet.jst │ │ └── templates.js │ ├── utils │ │ ├── inflector.js │ │ ├── backbone_extensions.js │ │ ├── search_parser.js │ │ ├── hotkeys.js │ │ └── jquery_extensions.js │ ├── models │ │ ├── search_facets.js │ │ └── search_query.js │ ├── visualsearch.js │ └── views │ │ ├── search_input.js │ │ ├── search_box.js │ │ └── search_facet.js ├── images │ └── embed │ │ └── icons │ │ ├── cancel_search.png │ │ └── search_glyph.png └── css │ ├── icons.css │ ├── reset.css │ └── workspace.css ├── docs ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff │ └── stylesheets │ │ └── normalize.css ├── assets │ ├── javascript.js │ ├── github.css │ └── xml.js ├── templates.html ├── backbone_extensions.html ├── inflector.html ├── visualsearch.html ├── search_facets.html ├── docco.css ├── search_query.html ├── search_parser.html └── hotkeys.html ├── package.json ├── bower.json ├── assets.yml ├── Rakefile ├── README ├── LICENSE └── vendor ├── jquery.ui.core.js └── jquery.ui.widget.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bower_components 3 | node_modules -------------------------------------------------------------------------------- /lib/js/templates/search_input.jst: -------------------------------------------------------------------------------- 1 | disabled="disabled"<% } %> /> -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /lib/images/embed/icons/cancel_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/lib/images/embed/icons/cancel_search.png -------------------------------------------------------------------------------- /lib/images/embed/icons/search_glyph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/documentcloud/visualsearch/HEAD/lib/images/embed/icons/search_glyph.png -------------------------------------------------------------------------------- /lib/js/templates/search_box.jst: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/js/templates/search_facet.jst: -------------------------------------------------------------------------------- 1 | <% if (model.has('category')) { %> 2 |
<%- model.get('category') %>:
3 | <% } %> 4 | 5 |
6 | disabled="disabled"<% } %> /> 7 |
8 | 9 |
-------------------------------------------------------------------------------- /lib/css/icons.css: -------------------------------------------------------------------------------- 1 | .VS-search .VS-icon { 2 | background-repeat: no-repeat; 3 | background-position: center center; 4 | vertical-align: middle; 5 | width: 16px; height: 16px; 6 | } 7 | .VS-search .VS-icon-cancel { 8 | width: 11px; height: 11px; 9 | background-position: center 0; 10 | background-image: url('../images/embed/icons/cancel_search.png'); 11 | cursor: pointer; 12 | } 13 | .VS-search .VS-icon-cancel:hover { 14 | background-position: center -11px; 15 | } 16 | .VS-search .VS-icon-search { 17 | width: 12px; height: 12px; 18 | background-image: url('../images/embed/icons/search_glyph.png'); 19 | } 20 | -------------------------------------------------------------------------------- /lib/js/utils/inflector.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // Naive English transformations on words. Only used for a few transformations 6 | // in VisualSearch.js. 7 | VS.utils.inflector = { 8 | 9 | // Delegate to the ECMA5 String.prototype.trim function, if available. 10 | trim : function(s) { 11 | return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, ''); 12 | }, 13 | 14 | // Escape strings that are going to be used in a regex. Escapes punctuation 15 | // that would be incorrect in a regex. 16 | escapeRegExp : function(s) { 17 | return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); 18 | } 19 | }; 20 | 21 | })(); -------------------------------------------------------------------------------- /lib/js/utils/backbone_extensions.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // Makes the view enter a mode. Modes have both a 'mode' and a 'group', 6 | // and are mutually exclusive with any other modes in the same group. 7 | // Setting will update the view's modes hash, as well as set an HTML class 8 | // of *[mode]_[group]* on the view's element. Convenient way to swap styles 9 | // and behavior. 10 | Backbone.View.prototype.setMode = function(mode, group) { 11 | this.modes || (this.modes = {}); 12 | if (this.modes[group] === mode) return; 13 | $(this.el).setMode(mode, group); 14 | this.modes[group] = mode; 15 | }; 16 | 17 | })(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visualsearch", 3 | "version": "1.0.0", 4 | "description": "A rich search box for real data", 5 | "directories": { 6 | "doc": "docs", 7 | "lib": "lib" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/documentcloud/visualsearch.git" 12 | }, 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/documentcloud/visualsearch/issues" 16 | }, 17 | "homepage": "https://github.com/documentcloud/visualsearch#readme", 18 | "dependencies": { 19 | "backbone": "1.3.3", 20 | "jquery": "1.11.0", 21 | "jquery-ui": "1.12.1", 22 | "underscore": "1.4.3" 23 | }, 24 | "engines": { 25 | "yarn": ">= 1.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visualsearch", 3 | "homepage": "http://documentcloud.github.io/visualsearch/", 4 | "authors": [ 5 | "Samuel Clay", 6 | "@samuelclay" 7 | ], 8 | "description": "A Rich Search Box for Real Data", 9 | "main": ["build-min/visualsearch-datauri.css", "build-min/visualsearch.css", "build-min/visualsearch.js"], 10 | "license": "MIT", 11 | "dependencies": { 12 | "jquery": ">=1.4", 13 | "jquery-ui": ">=1.8", 14 | "backbone": ">=0.9.10", 15 | "underscore":">=1.4.3" 16 | }, 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests", 23 | "Rakefile", 24 | "vendor", 25 | "*.html", 26 | "build", 27 | "docs", 28 | "*.yml" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /lib/css/reset.css: -------------------------------------------------------------------------------- 1 | /*------------------------------ RESET + DEFAULT STYLES ---------------------------------*/ 2 | 3 | /* 4 | Eric Meyer's final reset.css 5 | Source: http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/ 6 | */ 7 | .VS-search div, .VS-search span, .VS-search a, .VS-search img, 8 | .VS-search ul, .VS-search li, .VS-search form, .VS-search label, 9 | .VS-interface ul, .VS-interface li, .VS-interface { 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | outline: 0; 14 | font-weight: inherit; 15 | font-style: inherit; 16 | font-size: 100%; 17 | font-family: inherit; 18 | vertical-align: baseline; 19 | } 20 | 21 | .VS-search :focus { 22 | outline: 0; 23 | } 24 | .VS-search { 25 | line-height: 1; 26 | color: black; 27 | } 28 | .VS-search ol, .VS-search ul { 29 | list-style: none; 30 | } 31 | -------------------------------------------------------------------------------- /assets.yml: -------------------------------------------------------------------------------- 1 | embed_assets: datauri 2 | javascript_compressor: closure 3 | template_function: _.template 4 | gzip_assets: <% if $VS_MIN %>on<% else %>off<% end %> 5 | compress_assets: <% if $VS_MIN %>on<% else %>off<% end %> 6 | 7 | javascripts: 8 | dependencies: 9 | - vendor/jquery-*.js 10 | - vendor/jquery.ui.core.js 11 | - vendor/jquery.ui.widget.js 12 | - vendor/jquery.ui.position.js 13 | - vendor/jquery.ui.*.js 14 | - vendor/underscore-*.js 15 | - vendor/backbone-*.js 16 | visualsearch: 17 | - lib/js/visualsearch.js 18 | - lib/js/views/*.js 19 | - lib/js/utils/*.js 20 | - lib/js/models/*.js 21 | - lib/js/templates/*.jst 22 | <% if not $VS_MIN %>visualsearch_templates: 23 | - lib/js/templates/*.jst 24 | <% end %> 25 | 26 | stylesheets: 27 | visualsearch: 28 | - lib/css/*.css -------------------------------------------------------------------------------- /lib/js/templates/templates.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | window.JST = window.JST || {}; 3 | 4 | window.JST['search_box'] = _.template(''); 5 | window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n
<%- model.get(\'category\') %>:
\n<% } %>\n\n
\n disabled="disabled"<% } %> />\n
\n\n
'); 6 | window.JST['search_input'] = _.template('disabled="disabled"<% } %> />'); 7 | })(); -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'jammit' 3 | require 'fileutils' 4 | 5 | desc "Use Jammit to compile the multiple versions of Visual Search" 6 | task :build do 7 | $VS_MIN = false 8 | Jammit.package!({ 9 | :config_path => "assets.yml", 10 | :output_folder => "build" 11 | }) 12 | 13 | $VS_MIN = true 14 | Jammit.package!({ 15 | :config_path => "assets.yml", 16 | :output_folder => "build-min" 17 | }) 18 | 19 | # Move the JSTs back to lib to accomodate the demo page. 20 | FileUtils.mv("build/visualsearch_templates.js", "lib/js/templates/templates.js") 21 | 22 | # Fix image url paths. 23 | ['build', 'build-min'].each do |build| 24 | File.open("#{build}/visualsearch.css", 'r+') do |file| 25 | css = file.read 26 | css.gsub!(/url\((.*?)images\/embed\/icons/, 'url(../images/embed/icons') 27 | file.rewind 28 | file.write(css) 29 | file.truncate(css.length) 30 | end 31 | end 32 | end 33 | 34 | desc "Build the docco documentation" 35 | task :docs do 36 | sh "docco lib/js/*.js lib/js/**/*.js" 37 | end 38 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | __ ___ _ _____ _ _ 2 | \ \ / (_) | |/ ____| | | (_) 3 | \ \ / / _ ___ _ _ __ _| | (___ ___ __ _ _ __ ___| |__ _ ___ 4 | \ \/ / | / __| | | |/ _` | |\___ \ / _ \/ _` | '__/ __| '_ \ | / __| 5 | \ / | \__ \ |_| | (_| | |____) | __/ (_| | | | (__| | | |_| \__ \ 6 | \/ |_|___/\__,_|\__,_|_|_____/ \___|\__,_|_| \___|_| |_(_) |___/ 7 | _/ | 8 | |__/ 9 | 10 | VisualSearch.js enhances ordinary search boxes with the ability to autocomplete 11 | faceted search queries. Specify the facets for completion, along with the 12 | completable values for any facet. You can retrieve the search query as a 13 | structured object, so you don't have to parse the query string yourself. 14 | 15 | For documentation, pre-packed downloads, demos, and tests, see: 16 | http://documentcloud.github.com/visualsearch 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Samuel Clay, @samuelclay, DocumentCloud 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /docs/assets/javascript.js: -------------------------------------------------------------------------------- 1 | /* 2 | Language: Javascript 3 | Category: common 4 | */ 5 | 6 | hljs.LANGUAGES.javascript = { 7 | defaultMode: { 8 | keywords: { 9 | 'keyword': {'in': 1, 'if': 1, 'for': 1, 'while': 1, 'finally': 1, 'var': 1, 'new': 1, 'function': 1, 'do': 1, 'return': 1, 'void': 1, 'else': 1, 'break': 1, 'catch': 1, 'instanceof': 1, 'with': 1, 'throw': 1, 'case': 1, 'default': 1, 'try': 1, 'this': 1, 'switch': 1, 'continue': 1, 'typeof': 1, 'delete': 1}, 10 | 'literal': {'true': 1, 'false': 1, 'null': 1} 11 | }, 12 | contains: [ 13 | hljs.APOS_STRING_MODE, 14 | hljs.QUOTE_STRING_MODE, 15 | hljs.C_LINE_COMMENT_MODE, 16 | hljs.C_BLOCK_COMMENT_MODE, 17 | hljs.C_NUMBER_MODE, 18 | { // regexp container 19 | begin: '(' + hljs.RE_STARTERS_RE + '|case|return|throw)\\s*', 20 | keywords: {'return': 1, 'throw': 1, 'case': 1}, 21 | contains: [ 22 | hljs.C_LINE_COMMENT_MODE, 23 | hljs.C_BLOCK_COMMENT_MODE, 24 | { 25 | className: 'regexp', 26 | begin: '/.*?[^\\\\/]/[gim]*' 27 | } 28 | ], 29 | relevance: 0 30 | }, 31 | { 32 | className: 'function', 33 | begin: '\\bfunction\\b', end: '{', 34 | keywords: {'function': 1}, 35 | contains: [ 36 | { 37 | className: 'title', begin: '[A-Za-z$_][0-9A-Za-z$_]*' 38 | }, 39 | { 40 | className: 'params', 41 | begin: '\\(', end: '\\)', 42 | contains: [ 43 | hljs.APOS_STRING_MODE, 44 | hljs.QUOTE_STRING_MODE, 45 | hljs.C_LINE_COMMENT_MODE, 46 | hljs.C_BLOCK_COMMENT_MODE 47 | ] 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /docs/assets/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | pre code { 8 | display: block; padding: 0.5em; 9 | color: #000; 10 | background: #f8f8ff 11 | } 12 | 13 | pre .comment, 14 | pre .template_comment, 15 | pre .diff .header, 16 | pre .javadoc { 17 | color: #998; 18 | font-style: italic 19 | } 20 | 21 | pre .keyword, 22 | pre .css .rule .keyword, 23 | pre .winutils, 24 | pre .javascript .title, 25 | pre .lisp .title, 26 | pre .subst { 27 | color: #000; 28 | font-weight: bold 29 | } 30 | 31 | pre .number, 32 | pre .hexcolor { 33 | color: #40a070 34 | } 35 | 36 | pre .string, 37 | pre .tag .value, 38 | pre .phpdoc, 39 | pre .tex .formula { 40 | color: #d14 41 | } 42 | 43 | pre .title, 44 | pre .id { 45 | color: #900; 46 | font-weight: bold 47 | } 48 | 49 | pre .javascript .title, 50 | pre .lisp .title, 51 | pre .subst { 52 | font-weight: normal 53 | } 54 | 55 | pre .class .title, 56 | pre .haskell .label, 57 | pre .tex .command { 58 | color: #458; 59 | font-weight: bold 60 | } 61 | 62 | pre .tag, 63 | pre .tag .title, 64 | pre .rules .property, 65 | pre .django .tag .keyword { 66 | color: #000080; 67 | font-weight: normal 68 | } 69 | 70 | pre .attribute, 71 | pre .variable, 72 | pre .instancevar, 73 | pre .lisp .body { 74 | color: #008080 75 | } 76 | 77 | pre .regexp { 78 | color: #009926 79 | } 80 | 81 | pre .class { 82 | color: #458; 83 | font-weight: bold 84 | } 85 | 86 | pre .symbol, 87 | pre .ruby .symbol .string, 88 | pre .ruby .symbol .keyword, 89 | pre .ruby .symbol .keymethods, 90 | pre .lisp .keyword, 91 | pre .tex .special, 92 | pre .input_number { 93 | color: #990073 94 | } 95 | 96 | pre .builtin, 97 | pre .built_in, 98 | pre .lisp .title { 99 | color: #0086b3 100 | } 101 | 102 | pre .preprocessor, 103 | pre .pi, 104 | pre .doctype, 105 | pre .shebang, 106 | pre .cdata { 107 | color: #999; 108 | font-weight: bold 109 | } 110 | 111 | pre .deletion { 112 | background: #fdd 113 | } 114 | 115 | pre .addition { 116 | background: #dfd 117 | } 118 | 119 | pre .diff .change { 120 | background: #0086b3 121 | } 122 | 123 | pre .chunk { 124 | color: #aaa 125 | } 126 | 127 | pre .tex .formula { 128 | opacity: 0.5; 129 | } 130 | -------------------------------------------------------------------------------- /lib/js/models/search_facets.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // The model that holds individual search facets and their categories. 6 | // Held in a collection by `VS.app.searchQuery`. 7 | VS.model.SearchFacet = Backbone.Model.extend({ 8 | 9 | // Extract the category and value and serialize it in preparation for 10 | // turning the entire searchBox into a search query that can be sent 11 | // to the server for parsing and searching. 12 | serialize : function() { 13 | var category = this.quoteCategory(this.get('category')); 14 | var value = VS.utils.inflector.trim(this.get('value')); 15 | var remainder = this.get("app").options.remainder; 16 | 17 | if (!value) return ''; 18 | 19 | if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) { 20 | value = this.quoteValue(value); 21 | } 22 | 23 | if (category != remainder) { 24 | category = category + ': '; 25 | } else { 26 | category = ""; 27 | } 28 | return category + value; 29 | }, 30 | 31 | // Wrap categories that have spaces or any kind of quote with opposite matching 32 | // quotes to preserve the complex category during serialization. 33 | quoteCategory : function(category) { 34 | var hasDoubleQuote = (/"/).test(category); 35 | var hasSingleQuote = (/'/).test(category); 36 | var hasSpace = (/\s/).test(category); 37 | 38 | if (hasDoubleQuote && !hasSingleQuote) { 39 | return "'" + category + "'"; 40 | } else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) { 41 | return '"' + category + '"'; 42 | } else { 43 | return category; 44 | } 45 | }, 46 | 47 | // Wrap values that have quotes in opposite matching quotes. If a value has 48 | // both single and double quotes, just use the double quotes. 49 | quoteValue : function(value) { 50 | var hasDoubleQuote = (/"/).test(value); 51 | var hasSingleQuote = (/'/).test(value); 52 | 53 | if (hasDoubleQuote && !hasSingleQuote) { 54 | return "'" + value + "'"; 55 | } else { 56 | return '"' + value + '"'; 57 | } 58 | }, 59 | 60 | // If provided, use a custom label instead of the raw value. 61 | label : function() { 62 | return this.get('label') || this.get('value'); 63 | } 64 | 65 | }); 66 | 67 | })(); -------------------------------------------------------------------------------- /lib/js/models/search_query.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // Collection which holds all of the individual facets (category: value). 6 | // Used for finding and removing specific facets. 7 | VS.model.SearchQuery = Backbone.Collection.extend({ 8 | 9 | // Model holds the category and value of the facet. 10 | model : VS.model.SearchFacet, 11 | 12 | // Turns all of the facets into a single serialized string. 13 | serialize : function() { 14 | return this.map(function(facet){ return facet.serialize(); }).join(' '); 15 | }, 16 | 17 | facets : function() { 18 | return this.map(function(facet) { 19 | var value = {}; 20 | value[facet.get('category')] = facet.get('value'); 21 | return value; 22 | }); 23 | }, 24 | 25 | // Find a facet by its category. Multiple facets with the same category 26 | // is fine, but only the first is returned. 27 | find : function(category) { 28 | var facet = this.detect(function(facet) { 29 | return facet.get('category').toLowerCase() == category.toLowerCase(); 30 | }); 31 | return facet && facet.get('value'); 32 | }, 33 | 34 | // Counts the number of times a specific category is in the search query. 35 | count : function(category) { 36 | return this.select(function(facet) { 37 | return facet.get('category').toLowerCase() == category.toLowerCase(); 38 | }).length; 39 | }, 40 | 41 | // Returns an array of extracted values from each facet in a category. 42 | values : function(category) { 43 | var facets = this.select(function(facet) { 44 | return facet.get('category').toLowerCase() == category.toLowerCase(); 45 | }); 46 | return _.map(facets, function(facet) { return facet.get('value'); }); 47 | }, 48 | 49 | // Checks all facets for matches of either a category or both category and value. 50 | has : function(category, value) { 51 | return this.any(function(facet) { 52 | var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase(); 53 | if (!value) return categoryMatched; 54 | return categoryMatched && facet.get('value') == value; 55 | }); 56 | }, 57 | 58 | // Used to temporarily hide specific categories and serialize the search query. 59 | withoutCategory : function() { 60 | var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); }); 61 | return this.map(function(facet) { 62 | if (!_.include(categories, facet.get('category').toLowerCase())) { 63 | return facet.serialize(); 64 | }; 65 | }).join(' '); 66 | } 67 | 68 | }); 69 | 70 | })(); 71 | -------------------------------------------------------------------------------- /docs/assets/xml.js: -------------------------------------------------------------------------------- 1 | /* 2 | Language: HTML, XML 3 | Category: common 4 | */ 5 | 6 | hljs.LANGUAGES.xml = function(){ 7 | var XML_IDENT_RE = '[A-Za-z0-9\\._:-]+'; 8 | var TAG_INTERNALS = { 9 | endsWithParent: true, 10 | contains: [ 11 | { 12 | className: 'attribute', 13 | begin: XML_IDENT_RE, 14 | relevance: 0 15 | }, 16 | { 17 | begin: '="', returnBegin: true, end: '"', 18 | contains: [{ 19 | className: 'value', 20 | begin: '"', endsWithParent: true 21 | }] 22 | }, 23 | { 24 | begin: '=\'', returnBegin: true, end: '\'', 25 | contains: [{ 26 | className: 'value', 27 | begin: '\'', endsWithParent: true 28 | }] 29 | }, 30 | { 31 | begin: '=', 32 | contains: [{ 33 | className: 'value', 34 | begin: '[^\\s/>]+' 35 | }] 36 | } 37 | ] 38 | }; 39 | return { 40 | case_insensitive: true, 41 | defaultMode: { 42 | contains: [ 43 | { 44 | className: 'pi', 45 | begin: '<\\?', end: '\\?>', 46 | relevance: 10 47 | }, 48 | { 49 | className: 'doctype', 50 | begin: '', 51 | relevance: 10 52 | }, 53 | { 54 | className: 'comment', 55 | begin: '', 56 | relevance: 10 57 | }, 58 | { 59 | className: 'cdata', 60 | begin: '<\\!\\[CDATA\\[', end: '\\]\\]>', 61 | relevance: 10 62 | }, 63 | { 64 | className: 'tag', 65 | begin: '', 66 | keywords: {'title': {'style': 1}}, 67 | contains: [TAG_INTERNALS], 68 | starts: { 69 | className: 'css', 70 | end: '', returnEnd: true, 71 | subLanguage: 'css' 72 | } 73 | }, 74 | { 75 | className: 'tag', 76 | begin: '', 77 | keywords: {'title': {'script': 1}}, 78 | contains: [TAG_INTERNALS], 79 | starts: { 80 | className: 'javascript', 81 | end: '', returnEnd: true, 82 | subLanguage: 'javascript' 83 | } 84 | }, 85 | { 86 | className: 'vbscript', 87 | begin: '<%', end: '%>', 88 | subLanguage: 'vbscript' 89 | }, 90 | { 91 | className: 'tag', 92 | begin: '', 93 | contains: [ 94 | { 95 | className: 'title', begin: '[^ />]+' 96 | }, 97 | TAG_INTERNALS 98 | ] 99 | } 100 | ] 101 | } 102 | }; 103 | }(); 104 | -------------------------------------------------------------------------------- /lib/js/visualsearch.js: -------------------------------------------------------------------------------- 1 | // This is the annotated source code for 2 | // [VisualSearch.js](http://documentcloud.github.com/visualsearch/), 3 | // a rich search box for real data. 4 | // 5 | // The annotated source HTML is generated by 6 | // [Docco](http://jashkenas.github.com/docco/). 7 | 8 | /** @license VisualSearch.js 0.5.1 9 | * (c) 2011 Samuel Clay, @samuelclay, DocumentCloud Inc., Investigative Reporters & Editors 10 | * VisualSearch.js may be freely distributed under the MIT license. 11 | * For all details and documentation: 12 | * http://documentcloud.github.com/visualsearch 13 | */ 14 | 15 | (function() { 16 | 17 | var $ = jQuery; // Handle namespaced jQuery 18 | 19 | // Setting up VisualSearch globals. These will eventually be made instance-based. 20 | if (!window.VS) window.VS = {}; 21 | if (!VS.app) VS.app = {}; 22 | if (!VS.ui) VS.ui = {}; 23 | if (!VS.model) VS.model = {}; 24 | if (!VS.utils) VS.utils = {}; 25 | 26 | // Sets the version for VisualSearch to be used programatically elsewhere. 27 | VS.VERSION = '0.5.1'; 28 | 29 | VS.VisualSearch = function(options) { 30 | var defaults = { 31 | container : '', 32 | query : '', 33 | autosearch : true, 34 | unquotable : [], 35 | remainder : 'text', 36 | showFacets : true, 37 | readOnly : false, 38 | callbacks : { 39 | search : $.noop, 40 | focus : $.noop, 41 | blur : $.noop, 42 | facetMatches : $.noop, 43 | valueMatches : $.noop, 44 | clearSearch : $.noop, 45 | removedFacet : $.noop 46 | } 47 | }; 48 | this.options = _.extend({}, defaults, options); 49 | this.options.callbacks = _.extend({}, defaults.callbacks, options.callbacks); 50 | 51 | VS.app.hotkeys.initialize(); 52 | this.searchQuery = new VS.model.SearchQuery(); 53 | this.searchBox = new VS.ui.SearchBox({ 54 | app: this, 55 | showFacets: this.options.showFacets 56 | }); 57 | 58 | if (options.container) { 59 | var searchBox = this.searchBox.render().el; 60 | $(this.options.container).html(searchBox); 61 | } 62 | this.searchBox.value(this.options.query || ''); 63 | 64 | // Disable page caching for browsers that incorrectly cache the visual search inputs. 65 | // This forces the browser to re-render the page when it is retrieved in its history. 66 | $(window).bind('unload', function(e) {}); 67 | 68 | // Gives the user back a reference to the `searchBox` so they 69 | // can use public methods. 70 | return this; 71 | }; 72 | 73 | // Entry-point used to tie all parts of VisualSearch together. It will either attach 74 | // itself to `options.container`, or pass back the `searchBox` so it can be rendered 75 | // at will. 76 | VS.init = function(options) { 77 | return new VS.VisualSearch(options); 78 | }; 79 | 80 | })(); 81 | -------------------------------------------------------------------------------- /lib/js/utils/search_parser.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // Used to extract keywords and facets from the free text search. 6 | var QUOTES_RE = "('[^']+'|\"[^\"]+\")"; 7 | var FREETEXT_RE = "('[^']+'|\"[^\"]+\"|[^'\"\\s]\\S*)"; 8 | var CATEGORY_RE = FREETEXT_RE + ':\\s*'; 9 | VS.app.SearchParser = { 10 | 11 | // Matches `category: "free text"`, with and without quotes. 12 | ALL_FIELDS : new RegExp(CATEGORY_RE + FREETEXT_RE, 'g'), 13 | 14 | // Matches a single category without the text. Used to correctly extract facets. 15 | CATEGORY : new RegExp(CATEGORY_RE), 16 | 17 | // Called to parse a query into a collection of `SearchFacet` models. 18 | parse : function(instance, query) { 19 | var searchFacets = this._extractAllFacets(instance, query); 20 | instance.searchQuery.reset(searchFacets); 21 | return searchFacets; 22 | }, 23 | 24 | // Walks the query and extracts facets, categories, and free text. 25 | _extractAllFacets : function(instance, query) { 26 | var facets = []; 27 | var originalQuery = query; 28 | while (query) { 29 | var category, value; 30 | originalQuery = query; 31 | var field = this._extractNextField(query); 32 | if (!field) { 33 | category = instance.options.remainder; 34 | value = this._extractSearchText(query); 35 | query = VS.utils.inflector.trim(query.replace(value, '')); 36 | } else if (field.indexOf(':') != -1) { 37 | category = field.match(this.CATEGORY)[1].replace(/(^['"]|['"]$)/g, ''); 38 | value = field.replace(this.CATEGORY, '').replace(/(^['"]|['"]$)/g, ''); 39 | query = VS.utils.inflector.trim(query.replace(field, '')); 40 | } else if (field.indexOf(':') == -1) { 41 | category = instance.options.remainder; 42 | value = field; 43 | query = VS.utils.inflector.trim(query.replace(value, '')); 44 | } 45 | 46 | if (category && value) { 47 | var searchFacet = new VS.model.SearchFacet({ 48 | category : category, 49 | value : VS.utils.inflector.trim(value), 50 | app : instance 51 | }); 52 | facets.push(searchFacet); 53 | } 54 | if (originalQuery == query) break; 55 | } 56 | 57 | return facets; 58 | }, 59 | 60 | // Extracts the first field found, capturing any free text that comes 61 | // before the category. 62 | _extractNextField : function(query) { 63 | var textRe = new RegExp('^\\s*(\\S+)\\s+(?=' + QUOTES_RE + FREETEXT_RE + ')'); 64 | var textMatch = query.match(textRe); 65 | if (textMatch && textMatch.length >= 1) { 66 | return textMatch[1]; 67 | } else { 68 | return this._extractFirstField(query); 69 | } 70 | }, 71 | 72 | // If there is no free text before the facet, extract the category and value. 73 | _extractFirstField : function(query) { 74 | var fields = query.match(this.ALL_FIELDS); 75 | return fields && fields.length && fields[0]; 76 | }, 77 | 78 | // If the found match is not a category and facet, extract the trimmed free text. 79 | _extractSearchText : function(query) { 80 | query = query || ''; 81 | var text = VS.utils.inflector.trim(query.replace(this.ALL_FIELDS, '')); 82 | return text; 83 | } 84 | 85 | }; 86 | 87 | })(); 88 | -------------------------------------------------------------------------------- /lib/js/utils/hotkeys.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // DocumentCloud workspace hotkeys. To tell if a key is currently being pressed, 6 | // just ask `VS.app.hotkeys.[key]` on `keypress`, or ask `VS.app.hotkeys.key(e)` 7 | // on `keydown`. 8 | // 9 | // For the most headache-free way to use this utility, check modifier keys, 10 | // like shift and command, with `VS.app.hotkeys.shift`, and check every other 11 | // key with `VS.app.hotkeys.key(e) == 'key_name'`. 12 | VS.app.hotkeys = { 13 | 14 | // Keys that will be mapped to the `hotkeys` namespace. 15 | KEYS: { 16 | '16': 'shift', 17 | '17': 'command', 18 | '91': 'command', 19 | '93': 'command', 20 | '224': 'command', 21 | '13': 'enter', 22 | '37': 'left', 23 | '38': 'upArrow', 24 | '39': 'right', 25 | '40': 'downArrow', 26 | '46': 'delete', 27 | '8': 'backspace', 28 | '35': 'end', 29 | '36': 'home', 30 | '9': 'tab', 31 | '188': 'comma' 32 | }, 33 | 34 | // Binds global keydown and keyup events to listen for keys that match `this.KEYS`. 35 | initialize : function() { 36 | _.bindAll(this, 'down', 'up', 'blur'); 37 | $(document).bind('keydown', this.down); 38 | $(document).bind('keyup', this.up); 39 | $(window).bind('blur', this.blur); 40 | }, 41 | 42 | // On `keydown`, turn on all keys that match. 43 | down : function(e) { 44 | var key = this.KEYS[e.which]; 45 | if (key) this[key] = true; 46 | }, 47 | 48 | // On `keyup`, turn off all keys that match. 49 | up : function(e) { 50 | var key = this.KEYS[e.which]; 51 | if (key) this[key] = false; 52 | }, 53 | 54 | // If an input is blurred, all keys need to be turned off, since they are no longer 55 | // able to modify the document. 56 | blur : function(e) { 57 | for (var key in this.KEYS) this[this.KEYS[key]] = false; 58 | }, 59 | 60 | // Check a key from an event and return the common english name. 61 | key : function(e) { 62 | return this.KEYS[e.which]; 63 | }, 64 | 65 | // Colon is special, since the value is different between browsers. 66 | colon : function(e) { 67 | var charCode = e.which; 68 | return charCode && String.fromCharCode(charCode) == ":"; 69 | }, 70 | 71 | // Check a key from an event and match it against any known characters. 72 | // The `keyCode` is different depending on the event type: `keydown` vs. `keypress`. 73 | // 74 | // These were determined by looping through every `keyCode` and `charCode` that 75 | // resulted from `keydown` and `keypress` events and counting what was printable. 76 | printable : function(e) { 77 | var code = e.which; 78 | if (e.type == 'keydown') { 79 | if (code == 32 || // space 80 | (code >= 48 && code <= 90) || // 0-1a-z 81 | (code >= 96 && code <= 111) || // 0-9+-/*. 82 | (code >= 186 && code <= 192) || // ;=,-./^ 83 | (code >= 219 && code <= 222)) { // (\)' 84 | return true; 85 | } 86 | } else { 87 | // [space]!"#$%&'()*+,-.0-9:;<=>?@A-Z[\]^_`a-z{|} and unicode characters 88 | if ((code >= 32 && code <= 126) || 89 | (code >= 160 && code <= 500) || 90 | (String.fromCharCode(code) == ":")) { 91 | return true; 92 | } 93 | } 94 | return false; 95 | } 96 | 97 | }; 98 | 99 | })(); -------------------------------------------------------------------------------- /docs/templates.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | templates.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 84 | 85 |
    86 | 87 |
  • 88 |
    89 |

    templates.js

    90 |
    91 |
  • 92 | 93 | 94 | 95 |
  • 96 |
    97 | 98 |
    99 | 100 |
    101 | 102 |
    103 | 104 |
    (function(){
    105 | window.JST = window.JST || {};
    106 | 
    107 | window.JST['search_box'] = _.template('<div class="VS-search <% if (readOnly) { %>VS-readonly<% } %>">\n  <div class="VS-search-box-wrapper VS-search-box">\n    <div class="VS-icon VS-icon-search"></div>\n    <div class="VS-placeholder"></div>\n    <div class="VS-search-inner"></div>\n    <div class="VS-icon VS-icon-cancel VS-cancel-search-box" title="clear search"></div>\n  </div>\n</div>');
    108 | window.JST['search_facet'] = _.template('<% if (model.has(\'category\')) { %>\n  <div class="category"><%= model.get(\'category\') %>:</div>\n<% } %>\n\n<div class="search_facet_input_container">\n  <input type="text" class="search_facet_input ui-menu VS-interface" value="" <% if (readOnly) { %>disabled="disabled"<% } %> />\n</div>\n\n<div class="search_facet_remove VS-icon VS-icon-cancel"></div>');
    109 | window.JST['search_input'] = _.template('<input type="text" class="ui-menu" <% if (readOnly) { %>disabled="disabled"<% } %> />');
    110 | })();
    111 | 112 |
  • 113 | 114 |
115 |
116 | 117 | 118 | -------------------------------------------------------------------------------- /docs/backbone_extensions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backbone_extensions.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 84 | 85 |
    86 | 87 |
  • 88 |
    89 |

    backbone_extensions.js

    90 |
    91 |
  • 92 | 93 | 94 | 95 |
  • 96 |
    97 | 98 |
    99 | 100 |
    101 | 102 |
    103 | 104 |
    (function(){
    105 | 
    106 |   var $ = jQuery; // Handle namespaced jQuery
    107 | 108 |
  • 109 | 110 | 111 |
  • 112 |
    113 | 114 |
    115 | 116 |
    117 |

    Makes the view enter a mode. Modes have both a ‘mode’ and a ‘group’, 118 | and are mutually exclusive with any other modes in the same group. 119 | Setting will update the view’s modes hash, as well as set an HTML class 120 | of [mode]_[group] on the view’s element. Convenient way to swap styles 121 | and behavior.

    122 | 123 |
    124 | 125 |
      Backbone.View.prototype.setMode = function(mode, group) {
    126 |     this.modes || (this.modes = {});
    127 |     if (this.modes[group] === mode) return;
    128 |     $(this.el).setMode(mode, group);
    129 |     this.modes[group] = mode;
    130 |   };
    131 | 
    132 | })();
    133 | 134 |
  • 135 | 136 |
137 |
138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/inflector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | inflector.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 84 | 85 |
    86 | 87 |
  • 88 |
    89 |

    inflector.js

    90 |
    91 |
  • 92 | 93 | 94 | 95 |
  • 96 |
    97 | 98 |
    99 | 100 |
    101 | 102 |
    103 | 104 |
    (function() {
    105 | 
    106 | var $ = jQuery; // Handle namespaced jQuery
    107 | 108 |
  • 109 | 110 | 111 |
  • 112 |
    113 | 114 |
    115 | 116 |
    117 |

    Naive English transformations on words. Only used for a few transformations 118 | in VisualSearch.js.

    119 | 120 |
    121 | 122 |
    VS.utils.inflector = {
    123 | 124 |
  • 125 | 126 | 127 |
  • 128 |
    129 | 130 |
    131 | 132 |
    133 |

    Delegate to the ECMA5 String.prototype.trim function, if available.

    134 | 135 |
    136 | 137 |
      trim : function(s) {
    138 |     return s.trim ? s.trim() : s.replace(/^\s+|\s+$/g, '');
    139 |   },
    140 | 141 |
  • 142 | 143 | 144 |
  • 145 |
    146 | 147 |
    148 | 149 |
    150 |

    Escape strings that are going to be used in a regex. Escapes punctuation 151 | that would be incorrect in a regex.

    152 | 153 |
    154 | 155 |
      escapeRegExp : function(s) {
    156 |     return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
    157 |   }
    158 | };
    159 | 
    160 | })();
    161 | 162 |
  • 163 | 164 |
165 |
166 | 167 | 168 | -------------------------------------------------------------------------------- /lib/js/utils/jquery_extensions.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | $.fn.extend({ 6 | 7 | // Makes the selector enter a mode. Modes have both a 'mode' and a 'group', 8 | // and are mutually exclusive with any other modes in the same group. 9 | // Setting will update the view's modes hash, as well as set an HTML class 10 | // of *[mode]_[group]* on the view's element. Convenient way to swap styles 11 | // and behavior. 12 | setMode : function(state, group) { 13 | group = group || 'mode'; 14 | var re = new RegExp("\\w+_" + group + "(\\s|$)", 'g'); 15 | var mode = (state === null) ? "" : state + "_" + group; 16 | this.each(function() { 17 | this.className = (this.className.replace(re, '')+' '+mode) 18 | .replace(/\s\s/g, ' '); 19 | }); 20 | return mode; 21 | }, 22 | 23 | // When attached to an input element, this will cause the width of the input 24 | // to match its contents. This calculates the width of the contents of the input 25 | // by measuring a hidden shadow div that should match the styling of the input. 26 | autoGrowInput: function() { 27 | return this.each(function() { 28 | var $input = $(this); 29 | var $tester = $('
').css({ 30 | opacity : 0, 31 | top : -9999, 32 | left : -9999, 33 | position : 'absolute', 34 | whiteSpace : 'nowrap' 35 | }).addClass('VS-input-width-tester').addClass('VS-interface'); 36 | 37 | // Watch for input value changes on all of these events. `resize` 38 | // event is called explicitly when the input has been changed without 39 | // a single keypress. 40 | var events = 'keydown.autogrow keypress.autogrow ' + 41 | 'resize.autogrow change.autogrow'; 42 | $input.next('.VS-input-width-tester').remove(); 43 | $input.after($tester); 44 | $input.unbind(events).bind(events, function(e, realEvent) { 45 | if (realEvent) e = realEvent; 46 | var value = $input.val(); 47 | 48 | // Watching for the backspace key is tricky because it may not 49 | // actually be deleting the character, but instead the key gets 50 | // redirected to move the cursor from facet to facet. 51 | if (VS.app.hotkeys.key(e) == 'backspace') { 52 | var position = $input.getCursorPosition(); 53 | if (position > 0) value = value.slice(0, position-1) + 54 | value.slice(position, value.length); 55 | } else if (VS.app.hotkeys.printable(e) && 56 | !VS.app.hotkeys.command) { 57 | value += String.fromCharCode(e.which); 58 | } 59 | value = value.replace(/&/g, '&') 60 | .replace(/\s/g,' ') 61 | .replace(//g, '>'); 63 | 64 | $tester.html(value); 65 | 66 | $input.width($tester.width() + 3 + parseInt($input.css('min-width'))); 67 | $input.trigger('updated.autogrow'); 68 | }); 69 | 70 | // Sets the width of the input on initialization. 71 | $input.trigger('resize.autogrow'); 72 | }); 73 | }, 74 | 75 | 76 | // Cross-browser method used for calculating where the cursor is in an 77 | // input field. 78 | getCursorPosition: function() { 79 | var position = 0; 80 | var input = this.get(0); 81 | 82 | if (document.selection) { // IE 83 | input.focus(); 84 | var sel = document.selection.createRange(); 85 | var selLen = document.selection.createRange().text.length; 86 | sel.moveStart('character', -input.value.length); 87 | position = sel.text.length - selLen; 88 | } else if (input && $(input).is(':visible') && 89 | input.selectionStart != null) { // Firefox/Safari 90 | position = input.selectionStart; 91 | } 92 | 93 | return position; 94 | }, 95 | 96 | // A simple proxy for `selectRange` that sets the cursor position in an 97 | // input field. 98 | setCursorPosition: function(position) { 99 | return this.each(function() { 100 | return $(this).selectRange(position, position); 101 | }); 102 | }, 103 | 104 | // Cross-browser way to select text in an input field. 105 | selectRange: function(start, end) { 106 | return this.filter(':visible').each(function() { 107 | if (this.setSelectionRange) { // FF/Webkit 108 | this.focus(); 109 | this.setSelectionRange(start, end); 110 | } else if (this.createTextRange) { // IE 111 | var range = this.createTextRange(); 112 | range.collapse(true); 113 | range.moveEnd('character', end); 114 | range.moveStart('character', start); 115 | if (end - start >= 0) range.select(); 116 | } 117 | }); 118 | }, 119 | 120 | // Returns an object that contains the text selection range values for 121 | // an input field. 122 | getSelection: function() { 123 | var input = this[0]; 124 | 125 | if (input.selectionStart != null) { // FF/Webkit 126 | var start = input.selectionStart; 127 | var end = input.selectionEnd; 128 | return { 129 | start : start, 130 | end : end, 131 | length : end-start, 132 | text : input.value.substr(start, end-start) 133 | }; 134 | } else if (document.selection) { // IE 135 | var range = document.selection.createRange(); 136 | if (range) { 137 | var textRange = input.createTextRange(); 138 | var copyRange = textRange.duplicate(); 139 | textRange.moveToBookmark(range.getBookmark()); 140 | copyRange.setEndPoint('EndToStart', textRange); 141 | var start = copyRange.text.length; 142 | var end = start + range.text.length; 143 | return { 144 | start : start, 145 | end : end, 146 | length : end-start, 147 | text : range.text 148 | }; 149 | } 150 | } 151 | return {start: 0, end: 0, length: 0}; 152 | } 153 | 154 | }); 155 | 156 | // Debugging in Internet Explorer. This allows you to use 157 | // `console.log(['message', var1, var2, ...])`. Just remove the `false` and 158 | // add your console.logs. This will automatically stringify objects using 159 | // `JSON.stringify', so you can read what's going out. Think of this as a 160 | // *Diet Firebug Lite Zero with Lemon*. 161 | if (false) { 162 | window.console = {}; 163 | var _$ied; 164 | window.console.log = function(msg) { 165 | if (_.isArray(msg)) { 166 | var message = msg[0]; 167 | var vars = _.map(msg.slice(1), function(arg) { 168 | return JSON.stringify(arg); 169 | }).join(' - '); 170 | } 171 | if(!_$ied){ 172 | _$ied = $('
    ').css({ 173 | 'position': 'fixed', 174 | 'bottom': 10, 175 | 'left': 10, 176 | 'zIndex': 20000, 177 | 'width': $('body').width() - 80, 178 | 'border': '1px solid #000', 179 | 'padding': '10px', 180 | 'backgroundColor': '#fff', 181 | 'fontFamily': 'arial,helvetica,sans-serif', 182 | 'fontSize': '11px' 183 | }); 184 | $('body').append(_$ied); 185 | } 186 | var $message = $('
  1. '+message+' - '+vars+'
  2. ').css({ 187 | 'borderBottom': '1px solid #999999' 188 | }); 189 | _$ied.find('ol').append($message); 190 | _.delay(function() { 191 | $message.fadeOut(500); 192 | }, 5000); 193 | }; 194 | 195 | } 196 | 197 | })(); 198 | -------------------------------------------------------------------------------- /docs/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /lib/css/workspace.css: -------------------------------------------------------------------------------- 1 | /* ===================== */ 2 | /* = General and Reset = */ 3 | /* ===================== */ 4 | 5 | .VS-search { 6 | font-family: Arial, sans-serif; 7 | color: #373737; 8 | font-size: 12px; 9 | } 10 | .VS-search input { 11 | display: block; 12 | border: none; 13 | -moz-box-shadow: none; 14 | -webkit-box-shadow: none; 15 | box-shadow: none; 16 | outline: none; 17 | margin: 0; padding: 4px; 18 | background: transparent; 19 | font-size: 16px; 20 | line-height: 20px; 21 | width: 100%; 22 | } 23 | .VS-interface, .VS-search .dialog, .VS-search input { 24 | font-family: "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif !important; 25 | line-height: 1.1em; 26 | } 27 | 28 | /* ========== */ 29 | /* = Layout = */ 30 | /* ========== */ 31 | 32 | .VS-search .VS-search-box { 33 | cursor: text; 34 | position: relative; 35 | background: transparent; 36 | border: 2px solid #ccc; 37 | border-radius: 16px; -webkit-border-radius: 16px; -moz-border-radius: 16px; 38 | background-color: #fafafa; 39 | -webkit-box-shadow: inset 0px 0px 3px #ccc; 40 | -moz-box-shadow: inset 0px 0px 3px #ccc; 41 | box-shadow: inset 0px 0px 3px #ccc; 42 | min-height: 28px; 43 | height: auto; 44 | } 45 | .VS-search.VS-readonly .VS-search-box { 46 | cursor: default; 47 | } 48 | .VS-search .VS-search-box.VS-focus { 49 | border-color: #acf; 50 | -webkit-box-shadow: inset 0px 0px 3px #acf; 51 | -moz-box-shadow: inset 0px 0px 3px #acf; 52 | box-shadow: inset 0px 0px 3px #acf; 53 | } 54 | .VS-search .VS-placeholder { 55 | position: absolute; 56 | top: 7px; 57 | left: 4px; 58 | margin: 0 20px 0 22px; 59 | color: #808080; 60 | font-size: 14px; 61 | } 62 | .VS-search .VS-search-box.VS-focus .VS-placeholder, 63 | .VS-search .VS-search-box .VS-placeholder.VS-hidden { 64 | display: none; 65 | } 66 | .VS-search .VS-search-inner { 67 | position: relative; 68 | margin: 0 20px 0 22px; 69 | overflow: hidden; 70 | } 71 | .VS-search input { 72 | width: 100px; 73 | } 74 | .VS-search input, 75 | .VS-search .VS-input-width-tester { 76 | padding: 6px 0; 77 | float: left; 78 | color: #808080; 79 | font: 13px/17px Helvetica, Arial; 80 | } 81 | .VS-search.VS-focus input { 82 | color: #606060; 83 | } 84 | .VS-search .VS-icon-search { 85 | position: absolute; 86 | left: 9px; top: 8px; 87 | } 88 | .VS-search .VS-icon-cancel { 89 | position: absolute; 90 | right: 9px; top: 8px; 91 | } 92 | .VS-search.VS-readonly .VS-icon-cancel { 93 | display: none; 94 | } 95 | 96 | /* ================ */ 97 | /* = Search Facet = */ 98 | /* ================ */ 99 | 100 | .VS-search .search_facet { 101 | float: left; 102 | margin: 0; 103 | padding: 0 0 0 14px; 104 | position: relative; 105 | border: 1px solid transparent; 106 | height: 20px; 107 | margin: 3px -3px 3px 0; 108 | } 109 | .VS-search.VS-readonly .search_facet { 110 | padding-left: 0; 111 | } 112 | .VS-search .search_facet.is_selected { 113 | margin-left: -3px; 114 | -webkit-border-radius: 16px; 115 | -moz-border-radius: 16px; 116 | border-radius: 16px; 117 | background-color: #d2e6fd; 118 | background-image: -moz-linear-gradient(top, #d2e6fd, #b0d1f9); /* FF3.6 */ 119 | background-image: -webkit-gradient(linear, left top, left bottom, from(#d2e6fd), to(#b0d1f9)); /* Saf4+, Chrome */ 120 | background-image: linear-gradient(top, #d2e6fd, #b0d1f9); 121 | border: 1px solid #6eadf5; 122 | } 123 | .VS-search .search_facet .category { 124 | float: left; 125 | text-transform: uppercase; 126 | font-weight: bold; 127 | font-size: 10px; 128 | color: #808080; 129 | padding: 8px 0 5px; 130 | line-height: 13px; 131 | cursor: pointer; 132 | padding: 4px 0 0; 133 | } 134 | .VS-search.VS-readonly .search_facet .category { 135 | cursor: default; 136 | } 137 | .VS-search .search_facet.is_selected .category { 138 | margin-left: 3px; 139 | } 140 | .VS-search .search_facet .search_facet_input_container { 141 | float: left; 142 | } 143 | .VS-search .search_facet input { 144 | margin: 0; 145 | padding: 0; 146 | color: #000; 147 | font-size: 13px; 148 | line-height: 16px; 149 | padding: 5px 0 5px 4px; 150 | height: 16px; 151 | width: auto; 152 | z-index: 100; 153 | position: relative; 154 | padding-top: 1px; 155 | padding-bottom: 2px; 156 | padding-right: 3px; 157 | 158 | } 159 | .VS-search .search_facet.is_editing input, 160 | .VS-search .search_facet.is_selected input { 161 | color: #000; 162 | } 163 | .VS-search.VS-readonly .search_facet .search_facet_remove { 164 | display: none; 165 | } 166 | .VS-search .search_facet .search_facet_remove { 167 | position: absolute; 168 | left: 0; 169 | top: 4px; 170 | } 171 | .VS-search .search_facet.is_selected .search_facet_remove { 172 | opacity: 0.4; 173 | left: 3px; 174 | filter: alpha(opacity=40); 175 | background-position: center -11px; 176 | } 177 | .VS-search .search_facet .search_facet_remove:hover { 178 | opacity: 1; 179 | } 180 | .VS-search .search_facet.is_editing .category, 181 | .VS-search .search_facet.is_selected .category { 182 | color: #000; 183 | } 184 | .VS-search .search_facet.search_facet_maybe_delete .category, 185 | .VS-search .search_facet.search_facet_maybe_delete input { 186 | color: darkred; 187 | } 188 | 189 | /* ================ */ 190 | /* = Search Input = */ 191 | /* ================ */ 192 | 193 | .VS-search .search_input { 194 | height: 28px; 195 | float: left; 196 | margin-left: -1px; 197 | } 198 | .VS-search .search_input input { 199 | padding: 6px 3px 6px 2px; 200 | line-height: 10px; 201 | height: 22px; 202 | margin-top: -4px; 203 | width: 10px; 204 | z-index: 100; 205 | min-width: 4px; 206 | position: relative; 207 | } 208 | .VS-search .search_input.is_editing input { 209 | color: #202020; 210 | } 211 | 212 | /* ================ */ 213 | /* = Autocomplete = */ 214 | /* ================ */ 215 | 216 | .ui-helper-hidden-accessible { 217 | display: none; 218 | } 219 | 220 | .VS-interface.ui-autocomplete { 221 | position: absolute; 222 | border: 1px solid #C0C0C0; 223 | border-top: 1px solid #D9D9D9; 224 | background-color: #F6F6F6; 225 | cursor: pointer; 226 | z-index: 10000; 227 | padding: 0; 228 | margin: 0; 229 | width: auto; 230 | min-width: 80px; 231 | max-width: 220px; 232 | max-height: 240px; 233 | overflow-y: auto; 234 | overflow-x: hidden; 235 | font-size: 13px; 236 | top: 5px; 237 | opacity: 0.97; 238 | box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5); -webkit-box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5); -moz-box-shadow: 3px 4px 5px -2px rgba(0, 0, 0, 0.5); 239 | } 240 | .VS-interface.ui-autocomplete .ui-autocomplete-category { 241 | text-transform: capitalize; 242 | font-size: 11px; 243 | padding: 4px 4px 4px; 244 | border-top: 1px solid #A2A2A2; 245 | border-bottom: 1px solid #A2A2A2; 246 | background-color: #B7B7B7; 247 | text-shadow: 0 -1px 0 #999; 248 | font-weight: bold; 249 | color: white; 250 | cursor: default; 251 | } 252 | .VS-interface.ui-autocomplete .ui-menu-item { 253 | float: none; 254 | } 255 | .VS-interface.ui-autocomplete .ui-menu-item a { 256 | color: #000; 257 | outline: none; 258 | display: block; 259 | padding: 3px 4px 5px; 260 | border-radius: none; 261 | line-height: 1; 262 | background-color: #F8F8F8; 263 | background-image: -moz-linear-gradient(top, #F8F8F8, #F3F3F3); /* FF3.6 */ 264 | background-image: -webkit-gradient(linear, left top, left bottom, from(#F8F8F8), to(#F3F3F3)); /* Saf4+, Chrome */ 265 | background-image: linear-gradient(top, #F8F8F8, #F3F3F3); 266 | border-top: 1px solid #FAFAFA; 267 | border-bottom: 1px solid #f0f0f0; 268 | } 269 | .VS-interface.ui-autocomplete .ui-menu-item a:active { 270 | outline: none; 271 | } 272 | .VS-interface.ui-autocomplete .ui-menu-item .ui-state-hover, .VS-interface.ui-autocomplete .ui-menu-item .ui-state-focus { 273 | background-color: #6483F7; 274 | background-image: -moz-linear-gradient(top, #648bF5, #2465f3); /* FF3.6 */ 275 | background-image: -webkit-gradient(linear, left top, left bottom, from(#648bF5), to(#2465f3)); /* Saf4+, Chrome */ 276 | background-image: linear-gradient(top, #648bF5, #2465f3); 277 | border-top: 1px solid #5b83ec; 278 | border-bottom: 1px solid #1459e9; 279 | border-left: none; 280 | border-right: none; 281 | color: white; 282 | margin: 0; 283 | } 284 | .VS-interface.ui-autocomplete .ui-corner-all { 285 | border-radius: 0; 286 | } 287 | .VS-interface.ui-autocomplete li { 288 | list-style: none; 289 | width: auto; 290 | } 291 | -------------------------------------------------------------------------------- /vendor/jquery.ui.core.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Core 1.10.4 3 | * http://jqueryui.com 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | * 9 | * http://api.jqueryui.com/category/ui-core/ 10 | */ 11 | (function( $, undefined ) { 12 | 13 | var uuid = 0, 14 | runiqueId = /^ui-id-\d+$/; 15 | 16 | // $.ui might exist from components with no dependencies, e.g., $.ui.position 17 | $.ui = $.ui || {}; 18 | 19 | $.extend( $.ui, { 20 | version: "1.10.4", 21 | 22 | keyCode: { 23 | BACKSPACE: 8, 24 | COMMA: 188, 25 | DELETE: 46, 26 | DOWN: 40, 27 | END: 35, 28 | ENTER: 13, 29 | ESCAPE: 27, 30 | HOME: 36, 31 | LEFT: 37, 32 | NUMPAD_ADD: 107, 33 | NUMPAD_DECIMAL: 110, 34 | NUMPAD_DIVIDE: 111, 35 | NUMPAD_ENTER: 108, 36 | NUMPAD_MULTIPLY: 106, 37 | NUMPAD_SUBTRACT: 109, 38 | PAGE_DOWN: 34, 39 | PAGE_UP: 33, 40 | PERIOD: 190, 41 | RIGHT: 39, 42 | SPACE: 32, 43 | TAB: 9, 44 | UP: 38 45 | } 46 | }); 47 | 48 | // plugins 49 | $.fn.extend({ 50 | focus: (function( orig ) { 51 | return function( delay, fn ) { 52 | return typeof delay === "number" ? 53 | this.each(function() { 54 | var elem = this; 55 | setTimeout(function() { 56 | $( elem ).focus(); 57 | if ( fn ) { 58 | fn.call( elem ); 59 | } 60 | }, delay ); 61 | }) : 62 | orig.apply( this, arguments ); 63 | }; 64 | })( $.fn.focus ), 65 | 66 | scrollParent: function() { 67 | var scrollParent; 68 | if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { 69 | scrollParent = this.parents().filter(function() { 70 | return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); 71 | }).eq(0); 72 | } else { 73 | scrollParent = this.parents().filter(function() { 74 | return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); 75 | }).eq(0); 76 | } 77 | 78 | return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; 79 | }, 80 | 81 | zIndex: function( zIndex ) { 82 | if ( zIndex !== undefined ) { 83 | return this.css( "zIndex", zIndex ); 84 | } 85 | 86 | if ( this.length ) { 87 | var elem = $( this[ 0 ] ), position, value; 88 | while ( elem.length && elem[ 0 ] !== document ) { 89 | // Ignore z-index if position is set to a value where z-index is ignored by the browser 90 | // This makes behavior of this function consistent across browsers 91 | // WebKit always returns auto if the element is positioned 92 | position = elem.css( "position" ); 93 | if ( position === "absolute" || position === "relative" || position === "fixed" ) { 94 | // IE returns 0 when zIndex is not specified 95 | // other browsers return a string 96 | // we ignore the case of nested elements with an explicit value of 0 97 | //
    98 | value = parseInt( elem.css( "zIndex" ), 10 ); 99 | if ( !isNaN( value ) && value !== 0 ) { 100 | return value; 101 | } 102 | } 103 | elem = elem.parent(); 104 | } 105 | } 106 | 107 | return 0; 108 | }, 109 | 110 | uniqueId: function() { 111 | return this.each(function() { 112 | if ( !this.id ) { 113 | this.id = "ui-id-" + (++uuid); 114 | } 115 | }); 116 | }, 117 | 118 | removeUniqueId: function() { 119 | return this.each(function() { 120 | if ( runiqueId.test( this.id ) ) { 121 | $( this ).removeAttr( "id" ); 122 | } 123 | }); 124 | } 125 | }); 126 | 127 | // selectors 128 | function focusable( element, isTabIndexNotNaN ) { 129 | var map, mapName, img, 130 | nodeName = element.nodeName.toLowerCase(); 131 | if ( "area" === nodeName ) { 132 | map = element.parentNode; 133 | mapName = map.name; 134 | if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { 135 | return false; 136 | } 137 | img = $( "img[usemap=#" + mapName + "]" )[0]; 138 | return !!img && visible( img ); 139 | } 140 | return ( /input|select|textarea|button|object/.test( nodeName ) ? 141 | !element.disabled : 142 | "a" === nodeName ? 143 | element.href || isTabIndexNotNaN : 144 | isTabIndexNotNaN) && 145 | // the element and all of its ancestors must be visible 146 | visible( element ); 147 | } 148 | 149 | function visible( element ) { 150 | return $.expr.filters.visible( element ) && 151 | !$( element ).parents().addBack().filter(function() { 152 | return $.css( this, "visibility" ) === "hidden"; 153 | }).length; 154 | } 155 | 156 | $.extend( $.expr[ ":" ], { 157 | data: $.expr.createPseudo ? 158 | $.expr.createPseudo(function( dataName ) { 159 | return function( elem ) { 160 | return !!$.data( elem, dataName ); 161 | }; 162 | }) : 163 | // support: jQuery <1.8 164 | function( elem, i, match ) { 165 | return !!$.data( elem, match[ 3 ] ); 166 | }, 167 | 168 | focusable: function( element ) { 169 | return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); 170 | }, 171 | 172 | tabbable: function( element ) { 173 | var tabIndex = $.attr( element, "tabindex" ), 174 | isTabIndexNaN = isNaN( tabIndex ); 175 | return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); 176 | } 177 | }); 178 | 179 | // support: jQuery <1.8 180 | if ( !$( "" ).outerWidth( 1 ).jquery ) { 181 | $.each( [ "Width", "Height" ], function( i, name ) { 182 | var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], 183 | type = name.toLowerCase(), 184 | orig = { 185 | innerWidth: $.fn.innerWidth, 186 | innerHeight: $.fn.innerHeight, 187 | outerWidth: $.fn.outerWidth, 188 | outerHeight: $.fn.outerHeight 189 | }; 190 | 191 | function reduce( elem, size, border, margin ) { 192 | $.each( side, function() { 193 | size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; 194 | if ( border ) { 195 | size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; 196 | } 197 | if ( margin ) { 198 | size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; 199 | } 200 | }); 201 | return size; 202 | } 203 | 204 | $.fn[ "inner" + name ] = function( size ) { 205 | if ( size === undefined ) { 206 | return orig[ "inner" + name ].call( this ); 207 | } 208 | 209 | return this.each(function() { 210 | $( this ).css( type, reduce( this, size ) + "px" ); 211 | }); 212 | }; 213 | 214 | $.fn[ "outer" + name] = function( size, margin ) { 215 | if ( typeof size !== "number" ) { 216 | return orig[ "outer" + name ].call( this, size ); 217 | } 218 | 219 | return this.each(function() { 220 | $( this).css( type, reduce( this, size, true, margin ) + "px" ); 221 | }); 222 | }; 223 | }); 224 | } 225 | 226 | // support: jQuery <1.8 227 | if ( !$.fn.addBack ) { 228 | $.fn.addBack = function( selector ) { 229 | return this.add( selector == null ? 230 | this.prevObject : this.prevObject.filter( selector ) 231 | ); 232 | }; 233 | } 234 | 235 | // support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) 236 | if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { 237 | $.fn.removeData = (function( removeData ) { 238 | return function( key ) { 239 | if ( arguments.length ) { 240 | return removeData.call( this, $.camelCase( key ) ); 241 | } else { 242 | return removeData.call( this ); 243 | } 244 | }; 245 | })( $.fn.removeData ); 246 | } 247 | 248 | 249 | 250 | 251 | 252 | // deprecated 253 | $.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); 254 | 255 | $.support.selectstart = "onselectstart" in document.createElement( "div" ); 256 | $.fn.extend({ 257 | disableSelection: function() { 258 | return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + 259 | ".ui-disableSelection", function( event ) { 260 | event.preventDefault(); 261 | }); 262 | }, 263 | 264 | enableSelection: function() { 265 | return this.unbind( ".ui-disableSelection" ); 266 | } 267 | }); 268 | 269 | $.extend( $.ui, { 270 | // $.ui.plugin is deprecated. Use $.widget() extensions instead. 271 | plugin: { 272 | add: function( module, option, set ) { 273 | var i, 274 | proto = $.ui[ module ].prototype; 275 | for ( i in set ) { 276 | proto.plugins[ i ] = proto.plugins[ i ] || []; 277 | proto.plugins[ i ].push( [ option, set[ i ] ] ); 278 | } 279 | }, 280 | call: function( instance, name, args ) { 281 | var i, 282 | set = instance.plugins[ name ]; 283 | if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { 284 | return; 285 | } 286 | 287 | for ( i = 0; i < set.length; i++ ) { 288 | if ( instance.options[ set[ i ][ 0 ] ] ) { 289 | set[ i ][ 1 ].apply( instance.element, args ); 290 | } 291 | } 292 | } 293 | }, 294 | 295 | // only used by resizable 296 | hasScroll: function( el, a ) { 297 | 298 | //If overflow is hidden, the element might have extra content, but the user wants to hide it 299 | if ( $( el ).css( "overflow" ) === "hidden") { 300 | return false; 301 | } 302 | 303 | var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", 304 | has = false; 305 | 306 | if ( el[ scroll ] > 0 ) { 307 | return true; 308 | } 309 | 310 | // TODO: determine which cases actually cause this to happen 311 | // if the element doesn't have the scroll set, see if it's possible to 312 | // set the scroll 313 | el[ scroll ] = 1; 314 | has = ( el[ scroll ] > 0 ); 315 | el[ scroll ] = 0; 316 | return has; 317 | } 318 | }); 319 | 320 | })( jQuery ); 321 | -------------------------------------------------------------------------------- /docs/visualsearch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | visualsearch.js 6 | 7 | 8 | 9 | 10 | 11 |
    12 |
    13 | 14 |
    84 | 85 |
      86 | 87 |
    • 88 |
      89 |

      visualsearch.js

      90 |
      91 |
    • 92 | 93 | 94 | 95 |
    • 96 |
      97 | 98 |
      99 | 100 |
      101 |

      This is the annotated source code for 102 | VisualSearch.js, 103 | a rich search box for real data.

      104 |

      The annotated source HTML is generated by 105 | Docco.

      106 | 107 |
      108 | 109 |
      110 | /** @license VisualSearch.js 0.4.0
      111 |  *  (c) 2011 Samuel Clay, @samuelclay, DocumentCloud Inc.
      112 |  *  VisualSearch.js may be freely distributed under the MIT license.
      113 |  *  For all details and documentation:
      114 |  *  http://documentcloud.github.com/visualsearch
      115 |  */
      116 | 
      117 | (function() {
      118 | 
      119 |   var $ = jQuery; // Handle namespaced jQuery
      120 | 121 |
    • 122 | 123 | 124 |
    • 125 |
      126 | 127 |
      128 | 129 |
      130 |

      Setting up VisualSearch globals. These will eventually be made instance-based.

      131 | 132 |
      133 | 134 |
        if (!window.VS) window.VS = {};
      135 |   if (!VS.app)    VS.app    = {};
      136 |   if (!VS.ui)     VS.ui     = {};
      137 |   if (!VS.model)  VS.model  = {};
      138 |   if (!VS.utils)  VS.utils  = {};
      139 | 140 |
    • 141 | 142 | 143 |
    • 144 |
      145 | 146 |
      147 | 148 |
      149 |

      Sets the version for VisualSearch to be used programatically elsewhere.

      150 | 151 |
      152 | 153 |
        VS.VERSION = '0.5.0';
      154 | 
      155 |   VS.VisualSearch = function(options) {
      156 |     var defaults = {
      157 |       container   : '',
      158 |       query       : '',
      159 |       autosearch  : true,
      160 |       unquotable  : [],
      161 |       remainder   : 'text',
      162 |       showFacets  : true,
      163 |       readOnly    : false,
      164 |       callbacks   : {
      165 |         search          : $.noop,
      166 |         focus           : $.noop,
      167 |         blur            : $.noop,
      168 |         facetMatches    : $.noop,
      169 |         valueMatches    : $.noop,
      170 |         clearSearch     : $.noop,
      171 |         removedFacet    : $.noop
      172 |       }
      173 |     };
      174 |     this.options           = _.extend({}, defaults, options);
      175 |     this.options.callbacks = _.extend({}, defaults.callbacks, options.callbacks);
      176 |     
      177 |     VS.app.hotkeys.initialize();
      178 |     this.searchQuery   = new VS.model.SearchQuery();
      179 |     this.searchBox     = new VS.ui.SearchBox({
      180 |         app: this, 
      181 |         showFacets: this.options.showFacets
      182 |     });
      183 | 
      184 |     if (options.container) {
      185 |       var searchBox = this.searchBox.render().el;
      186 |       $(this.options.container).html(searchBox);
      187 |     }
      188 |     this.searchBox.value(this.options.query || '');
      189 | 190 |
    • 191 | 192 | 193 |
    • 194 |
      195 | 196 |
      197 | 198 |
      199 |

      Disable page caching for browsers that incorrectly cache the visual search inputs. 200 | This forces the browser to re-render the page when it is retrieved in its history.

      201 | 202 |
      203 | 204 |
          $(window).bind('unload', function(e) {});
      205 | 206 |
    • 207 | 208 | 209 |
    • 210 |
      211 | 212 |
      213 | 214 |
      215 |

      Gives the user back a reference to the searchBox so they 216 | can use public methods.

      217 | 218 |
      219 | 220 |
          return this;
      221 |   };
      222 | 223 |
    • 224 | 225 | 226 |
    • 227 |
      228 | 229 |
      230 | 231 |
      232 |

      Entry-point used to tie all parts of VisualSearch together. It will either attach 233 | itself to options.container, or pass back the searchBox so it can be rendered 234 | at will.

      235 | 236 |
      237 | 238 |
        VS.init = function(options) {
      239 |     return new VS.VisualSearch(options);
      240 |   };
      241 | 
      242 | })();
      243 | 244 |
    • 245 | 246 |
    247 |
    248 | 249 | 250 | -------------------------------------------------------------------------------- /docs/search_facets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | search_facets.js 6 | 7 | 8 | 9 | 10 | 11 |
    12 |
    13 | 14 | 84 | 85 |
      86 | 87 |
    • 88 |
      89 |

      search_facets.js

      90 |
      91 |
    • 92 | 93 | 94 | 95 |
    • 96 |
      97 | 98 |
      99 | 100 |
      101 | 102 |
      103 | 104 |
      (function() {
      105 | 
      106 | var $ = jQuery; // Handle namespaced jQuery
      107 | 108 |
    • 109 | 110 | 111 |
    • 112 |
      113 | 114 |
      115 | 116 |
      117 |

      The model that holds individual search facets and their categories. 118 | Held in a collection by VS.app.searchQuery.

      119 | 120 |
      121 | 122 |
      VS.model.SearchFacet = Backbone.Model.extend({
      123 | 124 |
    • 125 | 126 | 127 |
    • 128 |
      129 | 130 |
      131 | 132 |
      133 |

      Extract the category and value and serialize it in preparation for 134 | turning the entire searchBox into a search query that can be sent 135 | to the server for parsing and searching.

      136 | 137 |
      138 | 139 |
        serialize : function() {
      140 |     var category = this.quoteCategory(this.get('category'));
      141 |     var value    = VS.utils.inflector.trim(this.get('value'));
      142 |     var remainder = this.get("app").options.remainder;
      143 | 
      144 |     if (!value) return '';
      145 | 
      146 |     if (!_.contains(this.get("app").options.unquotable || [], category) && category != remainder) {
      147 |       value = this.quoteValue(value);
      148 |     }
      149 | 
      150 |     if (category != remainder) {
      151 |       category = category + ': ';
      152 |     } else {
      153 |       category = "";
      154 |     }
      155 |     return category + value;
      156 |   },
      157 | 158 |
    • 159 | 160 | 161 |
    • 162 |
      163 | 164 |
      165 | 166 |
      167 |

      Wrap categories that have spaces or any kind of quote with opposite matching 168 | quotes to preserve the complex category during serialization.

      169 | 170 |
      171 | 172 |
        quoteCategory : function(category) {
      173 |     var hasDoubleQuote = (/"/).test(category);
      174 |     var hasSingleQuote = (/'/).test(category);
      175 |     var hasSpace       = (/\s/).test(category);
      176 |     
      177 |     if (hasDoubleQuote && !hasSingleQuote) {
      178 |       return "'" + category + "'";
      179 |     } else if (hasSpace || (hasSingleQuote && !hasDoubleQuote)) {
      180 |       return '"' + category + '"';
      181 |     } else {
      182 |       return category;
      183 |     }
      184 |   },
      185 | 186 |
    • 187 | 188 | 189 |
    • 190 |
      191 | 192 |
      193 | 194 |
      195 |

      Wrap values that have quotes in opposite matching quotes. If a value has 196 | both single and double quotes, just use the double quotes.

      197 | 198 |
      199 | 200 |
        quoteValue : function(value) {
      201 |     var hasDoubleQuote = (/"/).test(value);
      202 |     var hasSingleQuote = (/'/).test(value);
      203 |     
      204 |     if (hasDoubleQuote && !hasSingleQuote) {
      205 |       return "'" + value + "'";
      206 |     } else {
      207 |       return '"' + value + '"';
      208 |     }
      209 |   },
      210 | 211 |
    • 212 | 213 | 214 |
    • 215 |
      216 | 217 |
      218 | 219 |
      220 |

      If provided, use a custom label instead of the raw value.

      221 | 222 |
      223 | 224 |
        label : function() {
      225 |       return this.get('label') || this.get('value');
      226 |   }
      227 | 
      228 | });
      229 | 
      230 | })();
      231 | 232 |
    • 233 | 234 |
    235 |
    236 | 237 | 238 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Typography ----------------------------*/ 2 | 3 | @font-face { 4 | font-family: 'aller-light'; 5 | src: url('public/fonts/aller-light.eot'); 6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), 7 | url('public/fonts/aller-light.woff') format('woff'), 8 | url('public/fonts/aller-light.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'aller-bold'; 15 | src: url('public/fonts/aller-bold.eot'); 16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), 17 | url('public/fonts/aller-bold.woff') format('woff'), 18 | url('public/fonts/aller-bold.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'novecento-bold'; 25 | src: url('public/fonts/novecento-bold.eot'); 26 | src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'), 27 | url('public/fonts/novecento-bold.woff') format('woff'), 28 | url('public/fonts/novecento-bold.ttf') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | 33 | /*--------------------- Layout ----------------------------*/ 34 | html { height: 100%; } 35 | body { 36 | font-family: "aller-light"; 37 | font-size: 14px; 38 | line-height: 18px; 39 | color: #30404f; 40 | margin: 0; padding: 0; 41 | height:100%; 42 | } 43 | #container { min-height: 100%; } 44 | 45 | a { 46 | color: #000; 47 | } 48 | 49 | b, strong { 50 | font-weight: normal; 51 | font-family: "aller-bold"; 52 | } 53 | 54 | p { 55 | margin: 15px 0 0px; 56 | } 57 | .annotation ul, .annotation ol { 58 | margin: 25px 0; 59 | } 60 | .annotation ul li, .annotation ol li { 61 | font-size: 14px; 62 | line-height: 18px; 63 | margin: 10px 0; 64 | } 65 | 66 | h1, h2, h3, h4, h5, h6 { 67 | color: #112233; 68 | line-height: 1em; 69 | font-weight: normal; 70 | font-family: "novecento-bold"; 71 | text-transform: uppercase; 72 | margin: 30px 0 15px 0; 73 | } 74 | 75 | h1 { 76 | margin-top: 40px; 77 | } 78 | 79 | hr { 80 | border: 0; 81 | background: 1px #ddd; 82 | height: 1px; 83 | margin: 20px 0; 84 | } 85 | 86 | pre, tt, code { 87 | font-size: 12px; line-height: 16px; 88 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 89 | margin: 0; padding: 0; 90 | } 91 | .annotation pre { 92 | display: block; 93 | margin: 0; 94 | padding: 7px 10px; 95 | background: #fcfcfc; 96 | -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 97 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 98 | box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 99 | overflow-x: auto; 100 | } 101 | .annotation pre code { 102 | border: 0; 103 | padding: 0; 104 | background: transparent; 105 | } 106 | 107 | 108 | blockquote { 109 | border-left: 5px solid #ccc; 110 | margin: 0; 111 | padding: 1px 0 1px 1em; 112 | } 113 | .sections blockquote p { 114 | font-family: Menlo, Consolas, Monaco, monospace; 115 | font-size: 12px; line-height: 16px; 116 | color: #999; 117 | margin: 10px 0 0; 118 | white-space: pre-wrap; 119 | } 120 | 121 | ul.sections { 122 | list-style: none; 123 | padding:0 0 5px 0;; 124 | margin:0; 125 | } 126 | 127 | /* 128 | Force border-box so that % widths fit the parent 129 | container without overlap because of margin/padding. 130 | 131 | More Info : http://www.quirksmode.org/css/box.html 132 | */ 133 | ul.sections > li > div { 134 | -moz-box-sizing: border-box; /* firefox */ 135 | -ms-box-sizing: border-box; /* ie */ 136 | -webkit-box-sizing: border-box; /* webkit */ 137 | -khtml-box-sizing: border-box; /* konqueror */ 138 | box-sizing: border-box; /* css3 */ 139 | } 140 | 141 | 142 | /*---------------------- Jump Page -----------------------------*/ 143 | #jump_to, #jump_page { 144 | margin: 0; 145 | background: white; 146 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 147 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 148 | font: 16px Arial; 149 | cursor: pointer; 150 | text-align: right; 151 | list-style: none; 152 | } 153 | 154 | #jump_to a { 155 | text-decoration: none; 156 | } 157 | 158 | #jump_to a.large { 159 | display: none; 160 | } 161 | #jump_to a.small { 162 | font-size: 22px; 163 | font-weight: bold; 164 | color: #676767; 165 | } 166 | 167 | #jump_to, #jump_wrapper { 168 | position: fixed; 169 | right: 0; top: 0; 170 | padding: 10px 15px; 171 | margin:0; 172 | } 173 | 174 | #jump_wrapper { 175 | display: none; 176 | padding:0; 177 | } 178 | 179 | #jump_to:hover #jump_wrapper { 180 | display: block; 181 | } 182 | 183 | #jump_page { 184 | padding: 5px 0 3px; 185 | margin: 0 0 25px 25px; 186 | } 187 | 188 | #jump_page .source { 189 | display: block; 190 | padding: 15px; 191 | text-decoration: none; 192 | border-top: 1px solid #eee; 193 | } 194 | 195 | #jump_page .source:hover { 196 | background: #f5f5ff; 197 | } 198 | 199 | #jump_page .source:first-child { 200 | } 201 | 202 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 203 | @media only screen and (min-width: 320px) { 204 | .pilwrap { display: none; } 205 | 206 | ul.sections > li > div { 207 | display: block; 208 | padding:5px 10px 0 10px; 209 | } 210 | 211 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 212 | padding-left: 30px; 213 | } 214 | 215 | ul.sections > li > div.content { 216 | overflow-x:auto; 217 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 218 | box-shadow: inset 0 0 5px #e5e5ee; 219 | border: 1px solid #dedede; 220 | margin:5px 10px 5px 10px; 221 | padding-bottom: 5px; 222 | } 223 | 224 | ul.sections > li > div.annotation pre { 225 | margin: 7px 0 7px; 226 | padding-left: 15px; 227 | } 228 | 229 | ul.sections > li > div.annotation p tt, .annotation code { 230 | background: #f8f8ff; 231 | border: 1px solid #dedede; 232 | font-size: 12px; 233 | padding: 0 0.2em; 234 | } 235 | } 236 | 237 | /*---------------------- (> 481px) ---------------------*/ 238 | @media only screen and (min-width: 481px) { 239 | #container { 240 | position: relative; 241 | } 242 | body { 243 | background-color: #F5F5FF; 244 | font-size: 15px; 245 | line-height: 21px; 246 | } 247 | pre, tt, code { 248 | line-height: 18px; 249 | } 250 | p, ul, ol { 251 | margin: 0 0 15px; 252 | } 253 | 254 | 255 | #jump_to { 256 | padding: 5px 10px; 257 | } 258 | #jump_wrapper { 259 | padding: 0; 260 | } 261 | #jump_to, #jump_page { 262 | font: 10px Arial; 263 | text-transform: uppercase; 264 | } 265 | #jump_page .source { 266 | padding: 5px 10px; 267 | } 268 | #jump_to a.large { 269 | display: inline-block; 270 | } 271 | #jump_to a.small { 272 | display: none; 273 | } 274 | 275 | 276 | 277 | #background { 278 | position: absolute; 279 | top: 0; bottom: 0; 280 | width: 350px; 281 | background: #fff; 282 | border-right: 1px solid #e5e5ee; 283 | z-index: -1; 284 | } 285 | 286 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 287 | padding-left: 40px; 288 | } 289 | 290 | ul.sections > li { 291 | white-space: nowrap; 292 | } 293 | 294 | ul.sections > li > div { 295 | display: inline-block; 296 | } 297 | 298 | ul.sections > li > div.annotation { 299 | max-width: 350px; 300 | min-width: 350px; 301 | min-height: 5px; 302 | padding: 13px; 303 | overflow-x: hidden; 304 | white-space: normal; 305 | vertical-align: top; 306 | text-align: left; 307 | } 308 | ul.sections > li > div.annotation pre { 309 | margin: 15px 0 15px; 310 | padding-left: 15px; 311 | } 312 | 313 | ul.sections > li > div.content { 314 | padding: 13px; 315 | vertical-align: top; 316 | border: none; 317 | -webkit-box-shadow: none; 318 | box-shadow: none; 319 | } 320 | 321 | .pilwrap { 322 | position: relative; 323 | display: inline; 324 | } 325 | 326 | .pilcrow { 327 | font: 12px Arial; 328 | text-decoration: none; 329 | color: #454545; 330 | position: absolute; 331 | top: 3px; left: -20px; 332 | padding: 1px 2px; 333 | opacity: 0; 334 | -webkit-transition: opacity 0.2s linear; 335 | } 336 | .for-h1 .pilcrow { 337 | top: 47px; 338 | } 339 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 340 | top: 35px; 341 | } 342 | 343 | ul.sections > li > div.annotation:hover .pilcrow { 344 | opacity: 1; 345 | } 346 | } 347 | 348 | /*---------------------- (> 1025px) ---------------------*/ 349 | @media only screen and (min-width: 1025px) { 350 | 351 | body { 352 | font-size: 16px; 353 | line-height: 24px; 354 | } 355 | 356 | #background { 357 | width: 525px; 358 | } 359 | ul.sections > li > div.annotation { 360 | max-width: 525px; 361 | min-width: 525px; 362 | padding: 10px 25px 1px 50px; 363 | } 364 | ul.sections > li > div.content { 365 | padding: 9px 15px 16px 25px; 366 | } 367 | } 368 | 369 | /*---------------------- Syntax Highlighting -----------------------------*/ 370 | 371 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 372 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 373 | /* 374 | 375 | github.com style (c) Vasily Polovnyov 376 | 377 | */ 378 | 379 | pre code { 380 | display: block; padding: 0.5em; 381 | color: #000; 382 | background: #f8f8ff 383 | } 384 | 385 | pre .hljs-comment, 386 | pre .hljs-template_comment, 387 | pre .hljs-diff .hljs-header, 388 | pre .hljs-javadoc { 389 | color: #408080; 390 | font-style: italic 391 | } 392 | 393 | pre .hljs-keyword, 394 | pre .hljs-assignment, 395 | pre .hljs-literal, 396 | pre .hljs-css .hljs-rule .hljs-keyword, 397 | pre .hljs-winutils, 398 | pre .hljs-javascript .hljs-title, 399 | pre .hljs-lisp .hljs-title, 400 | pre .hljs-subst { 401 | color: #954121; 402 | /*font-weight: bold*/ 403 | } 404 | 405 | pre .hljs-number, 406 | pre .hljs-hexcolor { 407 | color: #40a070 408 | } 409 | 410 | pre .hljs-string, 411 | pre .hljs-tag .hljs-value, 412 | pre .hljs-phpdoc, 413 | pre .hljs-tex .hljs-formula { 414 | color: #219161; 415 | } 416 | 417 | pre .hljs-title, 418 | pre .hljs-id { 419 | color: #19469D; 420 | } 421 | pre .hljs-params { 422 | color: #00F; 423 | } 424 | 425 | pre .hljs-javascript .hljs-title, 426 | pre .hljs-lisp .hljs-title, 427 | pre .hljs-subst { 428 | font-weight: normal 429 | } 430 | 431 | pre .hljs-class .hljs-title, 432 | pre .hljs-haskell .hljs-label, 433 | pre .hljs-tex .hljs-command { 434 | color: #458; 435 | font-weight: bold 436 | } 437 | 438 | pre .hljs-tag, 439 | pre .hljs-tag .hljs-title, 440 | pre .hljs-rules .hljs-property, 441 | pre .hljs-django .hljs-tag .hljs-keyword { 442 | color: #000080; 443 | font-weight: normal 444 | } 445 | 446 | pre .hljs-attribute, 447 | pre .hljs-variable, 448 | pre .hljs-instancevar, 449 | pre .hljs-lisp .hljs-body { 450 | color: #008080 451 | } 452 | 453 | pre .hljs-regexp { 454 | color: #B68 455 | } 456 | 457 | pre .hljs-class { 458 | color: #458; 459 | font-weight: bold 460 | } 461 | 462 | pre .hljs-symbol, 463 | pre .hljs-ruby .hljs-symbol .hljs-string, 464 | pre .hljs-ruby .hljs-symbol .hljs-keyword, 465 | pre .hljs-ruby .hljs-symbol .hljs-keymethods, 466 | pre .hljs-lisp .hljs-keyword, 467 | pre .hljs-tex .hljs-special, 468 | pre .hljs-input_number { 469 | color: #990073 470 | } 471 | 472 | pre .hljs-builtin, 473 | pre .hljs-constructor, 474 | pre .hljs-built_in, 475 | pre .hljs-lisp .hljs-title { 476 | color: #0086b3 477 | } 478 | 479 | pre .hljs-preprocessor, 480 | pre .hljs-pi, 481 | pre .hljs-doctype, 482 | pre .hljs-shebang, 483 | pre .hljs-cdata { 484 | color: #999; 485 | font-weight: bold 486 | } 487 | 488 | pre .hljs-deletion { 489 | background: #fdd 490 | } 491 | 492 | pre .hljs-addition { 493 | background: #dfd 494 | } 495 | 496 | pre .hljs-diff .hljs-change { 497 | background: #0086b3 498 | } 499 | 500 | pre .hljs-chunk { 501 | color: #aaa 502 | } 503 | 504 | pre .hljs-tex .hljs-formula { 505 | opacity: 0.5; 506 | } 507 | -------------------------------------------------------------------------------- /docs/search_query.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | search_query.js 6 | 7 | 8 | 9 | 10 | 11 |
    12 |
    13 | 14 | 84 | 85 |
      86 | 87 |
    • 88 |
      89 |

      search_query.js

      90 |
      91 |
    • 92 | 93 | 94 | 95 |
    • 96 |
      97 | 98 |
      99 | 100 |
      101 | 102 |
      103 | 104 |
      (function() {
      105 | 
      106 | var $ = jQuery; // Handle namespaced jQuery
      107 | 108 |
    • 109 | 110 | 111 |
    • 112 |
      113 | 114 |
      115 | 116 |
      117 |

      Collection which holds all of the individual facets (category: value). 118 | Used for finding and removing specific facets.

      119 | 120 |
      121 | 122 |
      VS.model.SearchQuery = Backbone.Collection.extend({
      123 | 124 |
    • 125 | 126 | 127 |
    • 128 |
      129 | 130 |
      131 | 132 |
      133 |

      Model holds the category and value of the facet.

      134 | 135 |
      136 | 137 |
        model : VS.model.SearchFacet,
      138 | 139 |
    • 140 | 141 | 142 |
    • 143 |
      144 | 145 |
      146 | 147 |
      148 |

      Turns all of the facets into a single serialized string.

      149 | 150 |
      151 | 152 |
        serialize : function() {
      153 |     return this.map(function(facet){ return facet.serialize(); }).join(' ');
      154 |   },
      155 |   
      156 |   facets : function() {
      157 |     return this.map(function(facet) {
      158 |       var value = {};
      159 |       value[facet.get('category')] = facet.get('value');
      160 |       return value;
      161 |     });
      162 |   },
      163 | 164 |
    • 165 | 166 | 167 |
    • 168 |
      169 | 170 |
      171 | 172 |
      173 |

      Find a facet by its category. Multiple facets with the same category 174 | is fine, but only the first is returned.

      175 | 176 |
      177 | 178 |
        find : function(category) {
      179 |     var facet = this.detect(function(facet) {
      180 |       return facet.get('category').toLowerCase() == category.toLowerCase();
      181 |     });
      182 |     return facet && facet.get('value');
      183 |   },
      184 | 185 |
    • 186 | 187 | 188 |
    • 189 |
      190 | 191 |
      192 | 193 |
      194 |

      Counts the number of times a specific category is in the search query.

      195 | 196 |
      197 | 198 |
        count : function(category) {
      199 |     return this.select(function(facet) {
      200 |       return facet.get('category').toLowerCase() == category.toLowerCase();
      201 |     }).length;
      202 |   },
      203 | 204 |
    • 205 | 206 | 207 |
    • 208 |
      209 | 210 |
      211 | 212 |
      213 |

      Returns an array of extracted values from each facet in a category.

      214 | 215 |
      216 | 217 |
        values : function(category) {
      218 |     var facets = this.select(function(facet) {
      219 |       return facet.get('category').toLowerCase() == category.toLowerCase();
      220 |     });
      221 |     return _.map(facets, function(facet) { return facet.get('value'); });
      222 |   },
      223 | 224 |
    • 225 | 226 | 227 |
    • 228 |
      229 | 230 |
      231 | 232 |
      233 |

      Checks all facets for matches of either a category or both category and value.

      234 | 235 |
      236 | 237 |
        has : function(category, value) {
      238 |     return this.any(function(facet) {
      239 |       var categoryMatched = facet.get('category').toLowerCase() == category.toLowerCase();
      240 |       if (!value) return categoryMatched;
      241 |       return categoryMatched && facet.get('value') == value;
      242 |     });
      243 |   },
      244 | 245 |
    • 246 | 247 | 248 |
    • 249 |
      250 | 251 |
      252 | 253 |
      254 |

      Used to temporarily hide specific categories and serialize the search query.

      255 | 256 |
      257 | 258 |
        withoutCategory : function() {
      259 |     var categories = _.map(_.toArray(arguments), function(cat) { return cat.toLowerCase(); });
      260 |     return this.map(function(facet) {
      261 |       if (!_.include(categories, facet.get('category').toLowerCase())) { 
      262 |         return facet.serialize();
      263 |       };
      264 |     }).join(' ');
      265 |   }
      266 | 
      267 | });
      268 | 
      269 | })();
      270 | 271 |
    • 272 | 273 |
    274 |
    275 | 276 | 277 | -------------------------------------------------------------------------------- /docs/search_parser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | search_parser.js 6 | 7 | 8 | 9 | 10 | 11 |
    12 |
    13 | 14 | 84 | 85 |
      86 | 87 |
    • 88 |
      89 |

      search_parser.js

      90 |
      91 |
    • 92 | 93 | 94 | 95 |
    • 96 |
      97 | 98 |
      99 | 100 |
      101 | 102 |
      103 | 104 |
      (function() {
      105 | 
      106 | var $ = jQuery; // Handle namespaced jQuery
      107 | 108 |
    • 109 | 110 | 111 |
    • 112 |
      113 | 114 |
      115 | 116 |
      117 |

      Used to extract keywords and facets from the free text search.

      118 | 119 |
      120 | 121 |
      var QUOTES_RE   = "('[^']+'|\"[^\"]+\")";
      122 | var FREETEXT_RE = "('[^']+'|\"[^\"]+\"|[^'\"\\s]\\S*)";
      123 | var CATEGORY_RE = FREETEXT_RE +                     ':\\s*';
      124 | VS.app.SearchParser = {
      125 | 126 |
    • 127 | 128 | 129 |
    • 130 |
      131 | 132 |
      133 | 134 |
      135 |

      Matches category: "free text", with and without quotes.

      136 | 137 |
      138 | 139 |
        ALL_FIELDS : new RegExp(CATEGORY_RE + FREETEXT_RE, 'g'),
      140 | 141 |
    • 142 | 143 | 144 |
    • 145 |
      146 | 147 |
      148 | 149 |
      150 |

      Matches a single category without the text. Used to correctly extract facets.

      151 | 152 |
      153 | 154 |
        CATEGORY   : new RegExp(CATEGORY_RE),
      155 | 156 |
    • 157 | 158 | 159 |
    • 160 |
      161 | 162 |
      163 | 164 |
      165 |

      Called to parse a query into a collection of SearchFacet models.

      166 | 167 |
      168 | 169 |
        parse : function(instance, query) {
      170 |     var searchFacets = this._extractAllFacets(instance, query);
      171 |     instance.searchQuery.reset(searchFacets);
      172 |     return searchFacets;
      173 |   },
      174 | 175 |
    • 176 | 177 | 178 |
    • 179 |
      180 | 181 |
      182 | 183 |
      184 |

      Walks the query and extracts facets, categories, and free text.

      185 | 186 |
      187 | 188 |
        _extractAllFacets : function(instance, query) {
      189 |     var facets = [];
      190 |     var originalQuery = query;
      191 |     while (query) {
      192 |       var category, value;
      193 |       originalQuery = query;
      194 |       var field = this._extractNextField(query);
      195 |       if (!field) {
      196 |         category = instance.options.remainder;
      197 |         value    = this._extractSearchText(query);
      198 |         query    = VS.utils.inflector.trim(query.replace(value, ''));
      199 |       } else if (field.indexOf(':') != -1) {
      200 |         category = field.match(this.CATEGORY)[1].replace(/(^['"]|['"]$)/g, '');
      201 |         value    = field.replace(this.CATEGORY, '').replace(/(^['"]|['"]$)/g, '');
      202 |         query    = VS.utils.inflector.trim(query.replace(field, ''));
      203 |       } else if (field.indexOf(':') == -1) {
      204 |         category = instance.options.remainder;
      205 |         value    = field;
      206 |         query    = VS.utils.inflector.trim(query.replace(value, ''));
      207 |       }
      208 | 
      209 |       if (category && value) {
      210 |           var searchFacet = new VS.model.SearchFacet({
      211 |             category : category,
      212 |             value    : VS.utils.inflector.trim(value),
      213 |             app      : instance
      214 |           });
      215 |           facets.push(searchFacet);
      216 |       }
      217 |       if (originalQuery == query) break;
      218 |     }
      219 | 
      220 |     return facets;
      221 |   },
      222 | 223 |
    • 224 | 225 | 226 |
    • 227 |
      228 | 229 |
      230 | 231 |
      232 |

      Extracts the first field found, capturing any free text that comes 233 | before the category.

      234 | 235 |
      236 | 237 |
        _extractNextField : function(query) {
      238 |     var textRe = new RegExp('^\\s*(\\S+)\\s+(?=' + QUOTES_RE + FREETEXT_RE + ')');
      239 |     var textMatch = query.match(textRe);
      240 |     if (textMatch && textMatch.length >= 1) {
      241 |       return textMatch[1];
      242 |     } else {
      243 |       return this._extractFirstField(query);
      244 |     }
      245 |   },
      246 | 247 |
    • 248 | 249 | 250 |
    • 251 |
      252 | 253 |
      254 | 255 |
      256 |

      If there is no free text before the facet, extract the category and value.

      257 | 258 |
      259 | 260 |
        _extractFirstField : function(query) {
      261 |     var fields = query.match(this.ALL_FIELDS);
      262 |     return fields && fields.length && fields[0];
      263 |   },
      264 | 265 |
    • 266 | 267 | 268 |
    • 269 |
      270 | 271 |
      272 | 273 |
      274 |

      If the found match is not a category and facet, extract the trimmed free text.

      275 | 276 |
      277 | 278 |
        _extractSearchText : function(query) {
      279 |     query = query || '';
      280 |     var text = VS.utils.inflector.trim(query.replace(this.ALL_FIELDS, ''));
      281 |     return text;
      282 |   }
      283 | 
      284 | };
      285 | 
      286 | })();
      287 | 288 |
    • 289 | 290 |
    291 |
    292 | 293 | 294 | -------------------------------------------------------------------------------- /docs/hotkeys.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hotkeys.js 6 | 7 | 8 | 9 | 10 | 11 |
    12 |
    13 | 14 | 84 | 85 |
      86 | 87 |
    • 88 |
      89 |

      hotkeys.js

      90 |
      91 |
    • 92 | 93 | 94 | 95 |
    • 96 |
      97 | 98 |
      99 | 100 |
      101 | 102 |
      103 | 104 |
      (function() {
      105 | 
      106 | var $ = jQuery; // Handle namespaced jQuery
      107 | 108 |
    • 109 | 110 | 111 |
    • 112 |
      113 | 114 |
      115 | 116 |
      117 |

      DocumentCloud workspace hotkeys. To tell if a key is currently being pressed, 118 | just ask VS.app.hotkeys.[key] on keypress, or ask VS.app.hotkeys.key(e) 119 | on keydown.

      120 |

      For the most headache-free way to use this utility, check modifier keys, 121 | like shift and command, with VS.app.hotkeys.shift, and check every other 122 | key with VS.app.hotkeys.key(e) == 'key_name'.

      123 | 124 |
      125 | 126 |
      VS.app.hotkeys = {
      127 | 128 |
    • 129 | 130 | 131 |
    • 132 |
      133 | 134 |
      135 | 136 |
      137 |

      Keys that will be mapped to the hotkeys namespace.

      138 | 139 |
      140 | 141 |
        KEYS: {
      142 |     '16':  'shift',
      143 |     '17':  'command',
      144 |     '91':  'command',
      145 |     '93':  'command',
      146 |     '224': 'command',
      147 |     '13':  'enter',
      148 |     '37':  'left',
      149 |     '38':  'upArrow',
      150 |     '39':  'right',
      151 |     '40':  'downArrow',
      152 |     '46':  'delete',
      153 |     '8':   'backspace',
      154 |     '35':  'end',
      155 |     '36':  'home',
      156 |     '9':   'tab',
      157 |     '188': 'comma'
      158 |   },
      159 | 160 |
    • 161 | 162 | 163 |
    • 164 |
      165 | 166 |
      167 | 168 |
      169 |

      Binds global keydown and keyup events to listen for keys that match this.KEYS.

      170 | 171 |
      172 | 173 |
        initialize : function() {
      174 |     _.bindAll(this, 'down', 'up', 'blur');
      175 |     $(document).bind('keydown', this.down);
      176 |     $(document).bind('keyup', this.up);
      177 |     $(window).bind('blur', this.blur);
      178 |   },
      179 | 180 |
    • 181 | 182 | 183 |
    • 184 |
      185 | 186 |
      187 | 188 |
      189 |

      On keydown, turn on all keys that match.

      190 | 191 |
      192 | 193 |
        down : function(e) {
      194 |     var key = this.KEYS[e.which];
      195 |     if (key) this[key] = true;
      196 |   },
      197 | 198 |
    • 199 | 200 | 201 |
    • 202 |
      203 | 204 |
      205 | 206 |
      207 |

      On keyup, turn off all keys that match.

      208 | 209 |
      210 | 211 |
        up : function(e) {
      212 |     var key = this.KEYS[e.which];
      213 |     if (key) this[key] = false;
      214 |   },
      215 | 216 |
    • 217 | 218 | 219 |
    • 220 |
      221 | 222 |
      223 | 224 |
      225 |

      If an input is blurred, all keys need to be turned off, since they are no longer 226 | able to modify the document.

      227 | 228 |
      229 | 230 |
        blur : function(e) {
      231 |     for (var key in this.KEYS) this[this.KEYS[key]] = false;
      232 |   },
      233 | 234 |
    • 235 | 236 | 237 |
    • 238 |
      239 | 240 |
      241 | 242 |
      243 |

      Check a key from an event and return the common english name.

      244 | 245 |
      246 | 247 |
        key : function(e) {
      248 |     return this.KEYS[e.which];
      249 |   },
      250 | 251 |
    • 252 | 253 | 254 |
    • 255 |
      256 | 257 |
      258 | 259 |
      260 |

      Colon is special, since the value is different between browsers.

      261 | 262 |
      263 | 264 |
        colon : function(e) {
      265 |     var charCode = e.which;
      266 |     return charCode && String.fromCharCode(charCode) == ":";
      267 |   },
      268 | 269 |
    • 270 | 271 | 272 |
    • 273 |
      274 | 275 |
      276 | 277 |
      278 |

      Check a key from an event and match it against any known characters. 279 | The keyCode is different depending on the event type: keydown vs. keypress.

      280 |

      These were determined by looping through every keyCode and charCode that 281 | resulted from keydown and keypress events and counting what was printable.

      282 | 283 |
      284 | 285 |
        printable : function(e) {
      286 |     var code = e.which;
      287 |     if (e.type == 'keydown') {
      288 |       if (code == 32 ||                      // space
      289 |           (code >= 48 && code <= 90) ||      // 0-1a-z
      290 |           (code >= 96 && code <= 111) ||     // 0-9+-/*.
      291 |           (code >= 186 && code <= 192) ||    // ;=,-./^
      292 |           (code >= 219 && code <= 222)) {    // (\)'
      293 |         return true;
      294 |       }
      295 |     } else {
      296 | 297 |
    • 298 | 299 | 300 |
    • 301 |
      302 | 303 |
      304 | 305 |
      306 |

      [space]!”#$%&’()*+,-.0-9:;<=>?@A-Z[]^_`a-z{|} and unicode characters

      307 | 308 |
      309 | 310 |
            if ((code >= 32 && code <= 126)  ||
      311 |           (code >= 160 && code <= 500) ||
      312 |           (String.fromCharCode(code) == ":")) {
      313 |         return true;
      314 |       }
      315 |     }
      316 |     return false;
      317 |   }
      318 | 
      319 | };
      320 | 
      321 | })();
      322 | 323 |
    • 324 | 325 |
    326 |
    327 | 328 | 329 | -------------------------------------------------------------------------------- /lib/js/views/search_input.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // This is the visual search input that is responsible for creating new facets. 6 | // There is one input placed in between all facets. 7 | VS.ui.SearchInput = Backbone.View.extend({ 8 | 9 | type : 'text', 10 | 11 | className : 'search_input ui-menu', 12 | 13 | events : { 14 | 'keypress input' : 'keypress', 15 | 'keydown input' : 'keydown', 16 | 'keyup input' : 'keyup', 17 | 'click input' : 'maybeTripleClick', 18 | 'dblclick input' : 'startTripleClickTimer' 19 | }, 20 | 21 | initialize : function(options) { 22 | this.options = _.extend({}, this.options, options); 23 | 24 | this.app = this.options.app; 25 | this.flags = { 26 | canClose : false 27 | }; 28 | _.bindAll(this, 'removeFocus', 'addFocus', 'moveAutocomplete', 'deferDisableEdit'); 29 | }, 30 | 31 | // Rendering the input sets up autocomplete, events on focusing and blurring 32 | // the input, and the auto-grow of the input. 33 | render : function() { 34 | $(this.el).html(JST['search_input']({ 35 | readOnly: this.app.options.readOnly 36 | })); 37 | 38 | this.setMode('not', 'editing'); 39 | this.setMode('not', 'selected'); 40 | this.box = this.$('input'); 41 | this.box.autoGrowInput(); 42 | this.box.bind('updated.autogrow', this.moveAutocomplete); 43 | this.box.bind('blur', this.deferDisableEdit); 44 | this.box.bind('focus', this.addFocus); 45 | this.setupAutocomplete(); 46 | 47 | return this; 48 | }, 49 | 50 | // Watches the input and presents an autocompleted menu, taking the 51 | // remainder of the input field and adding a separate facet for it. 52 | // 53 | // See `addTextFacetRemainder` for explanation on how the remainder works. 54 | setupAutocomplete : function() { 55 | this.box.autocomplete({ 56 | minLength : this.options.showFacets ? 0 : 1, 57 | delay : 50, 58 | autoFocus : true, 59 | position : {offset : "0 -1"}, 60 | source : _.bind(this.autocompleteValues, this), 61 | // Prevent changing the input value on focus of an option 62 | focus : function() { return false; }, 63 | create : _.bind(function(e, ui) { 64 | $(this.el).find('.ui-autocomplete-input').css('z-index','auto'); 65 | }, this), 66 | select : _.bind(function(e, ui) { 67 | e.preventDefault(); 68 | // stopPropogation does weird things in jquery-ui 1.9 69 | // e.stopPropagation(); 70 | var remainder = this.addTextFacetRemainder(ui.item.label || ui.item.value); 71 | var position = this.options.position + (remainder ? 1 : 0); 72 | this.app.searchBox.addFacet(ui.item instanceof String ? ui.item : ui.item.value, '', position); 73 | return false; 74 | }, this) 75 | }); 76 | 77 | // Renders the results grouped by the categories they belong to. 78 | this.box.data('ui-autocomplete')._renderMenu = function(ul, items) { 79 | var category = ''; 80 | _.each(items, _.bind(function(item, i) { 81 | if (item.category && item.category != category) { 82 | ul.append('
  3. '+item.category+'
  4. '); 83 | category = item.category; 84 | } 85 | 86 | if(this._renderItemData) { 87 | this._renderItemData(ul, item); 88 | } else { 89 | this._renderItem(ul, item); 90 | } 91 | 92 | }, this)); 93 | }; 94 | 95 | this.box.autocomplete('widget').addClass('VS-interface'); 96 | }, 97 | 98 | // Search terms used in the autocomplete menu. The values are matched on the 99 | // first letter of any word in matches, and finally sorted according to the 100 | // value's own category. You can pass `preserveOrder` as an option in the 101 | // `facetMatches` callback to skip any further ordering done client-side. 102 | autocompleteValues : function(req, resp) { 103 | var searchTerm = req.term; 104 | var lastWord = searchTerm.match(/\w+\*?$/); // Autocomplete only last word. 105 | var re = VS.utils.inflector.escapeRegExp(lastWord && lastWord[0] || ''); 106 | this.app.options.callbacks.facetMatches(function(prefixes, options) { 107 | options = options || {}; 108 | prefixes = prefixes || []; 109 | 110 | // Only match from the beginning of the word. 111 | var matcher = new RegExp('^' + re, 'i'); 112 | var matches = $.grep(prefixes, function(item) { 113 | return item && matcher.test(item.label || item); 114 | }); 115 | 116 | if (options.preserveOrder) { 117 | resp(matches); 118 | } else { 119 | resp(_.sortBy(matches, function(match) { 120 | if (match.label) return match.category + '-' + match.label; 121 | else return match; 122 | })); 123 | } 124 | }); 125 | 126 | }, 127 | 128 | // Closes the autocomplete menu. Called on disabling, selecting, deselecting, 129 | // and anything else that takes focus out of the facet's input field. 130 | closeAutocomplete : function() { 131 | var autocomplete = this.box.data('ui-autocomplete'); 132 | if (autocomplete) autocomplete.close(); 133 | }, 134 | 135 | // As the input field grows, it may move to the next line in the 136 | // search box. `autoGrowInput` triggers an `updated` event on the input 137 | // field, which is bound to this method to move the autocomplete menu. 138 | moveAutocomplete : function() { 139 | var autocomplete = this.box.data('ui-autocomplete'); 140 | if (autocomplete) { 141 | autocomplete.menu.element.position({ 142 | my : "left top", 143 | at : "left bottom", 144 | of : this.box.data('ui-autocomplete').element, 145 | collision : "none", 146 | offset : '0 -1' 147 | }); 148 | } 149 | }, 150 | 151 | // When a user enters a facet and it is being edited, immediately show 152 | // the autocomplete menu and size it to match the contents. 153 | searchAutocomplete : function(e) { 154 | var autocomplete = this.box.data('ui-autocomplete'); 155 | if (autocomplete) { 156 | var menu = autocomplete.menu.element; 157 | autocomplete.search(); 158 | 159 | // Resize the menu based on the correctly measured width of what's bigger: 160 | // the menu's original size or the menu items' new size. 161 | menu.outerWidth(Math.max( 162 | menu.width('').outerWidth(), 163 | autocomplete.element.outerWidth() 164 | )); 165 | } 166 | }, 167 | 168 | // If a user searches for "word word category", the category would be 169 | // matched and autocompleted, and when selected, the "word word" would 170 | // also be caught as the remainder and then added in its own facet. 171 | addTextFacetRemainder : function(facetValue) { 172 | var boxValue = this.box.val(); 173 | var lastWord = boxValue.match(/\b(\w+)$/); 174 | 175 | if (!lastWord) { 176 | return ''; 177 | } 178 | 179 | var matcher = new RegExp(lastWord[0], "i"); 180 | if (facetValue.search(matcher) == 0) { 181 | boxValue = boxValue.replace(/\b(\w+)$/, ''); 182 | } 183 | boxValue = boxValue.replace('^\s+|\s+$', ''); 184 | 185 | if (boxValue) { 186 | this.app.searchBox.addFacet(this.app.options.remainder, boxValue, this.options.position); 187 | } 188 | 189 | return boxValue; 190 | }, 191 | 192 | // Directly called to focus the input. This is different from `addFocus` 193 | // because this is not called by a focus event. This instead calls a 194 | // focus event causing the input to become focused. 195 | enableEdit : function(selectText) { 196 | this.addFocus(); 197 | if (selectText) { 198 | this.selectText(); 199 | } 200 | this.box.focus(); 201 | }, 202 | 203 | // Event called on user focus on the input. Tells all other input and facets 204 | // to give up focus, and starts revving the autocomplete. 205 | addFocus : function() { 206 | this.flags.canClose = false; 207 | if (!this.app.searchBox.allSelected()) { 208 | this.app.searchBox.disableFacets(this); 209 | } 210 | this.app.searchBox.addFocus(); 211 | this.setMode('is', 'editing'); 212 | this.setMode('not', 'selected'); 213 | if (!this.app.searchBox.allSelected()) { 214 | this.searchAutocomplete(); 215 | } 216 | }, 217 | 218 | // Directly called to blur the input. This is different from `removeFocus` 219 | // because this is not called by a blur event. 220 | disableEdit : function() { 221 | this.box.blur(); 222 | this.removeFocus(); 223 | }, 224 | 225 | // Event called when user blur's the input, either through the keyboard tabbing 226 | // away or the mouse clicking off. Cleans up 227 | removeFocus : function() { 228 | this.flags.canClose = false; 229 | this.app.searchBox.removeFocus(); 230 | this.setMode('not', 'editing'); 231 | this.setMode('not', 'selected'); 232 | this.closeAutocomplete(); 233 | }, 234 | 235 | // When the user blurs the input, they may either be going to another input 236 | // or off the search box entirely. If they go to another input, this facet 237 | // will be instantly disabled, and the canClose flag will be turned back off. 238 | // 239 | // However, if the user clicks elsewhere on the page, this method starts a timer 240 | // that checks if any of the other inputs are selected or are being edited. If 241 | // not, then it can finally close itself and its autocomplete menu. 242 | deferDisableEdit : function() { 243 | this.flags.canClose = true; 244 | _.delay(_.bind(function() { 245 | if (this.flags.canClose && 246 | !this.box.is(':focus') && 247 | this.modes.editing == 'is') { 248 | this.disableEdit(); 249 | } 250 | }, this), 250); 251 | }, 252 | 253 | // Starts a timer that will cause a triple-click, which highlights all facets. 254 | startTripleClickTimer : function() { 255 | this.tripleClickTimer = setTimeout(_.bind(function() { 256 | this.tripleClickTimer = null; 257 | }, this), 500); 258 | }, 259 | 260 | // Event on click that checks if a triple click is in play. The 261 | // `tripleClickTimer` is counting down, ready to be engaged and intercept 262 | // the click event to force a select all instead. 263 | maybeTripleClick : function(e) { 264 | if (this.app.options.readOnly) return; 265 | if (!!this.tripleClickTimer) { 266 | e.preventDefault(); 267 | this.app.searchBox.selectAllFacets(); 268 | return false; 269 | } 270 | }, 271 | 272 | // Is the user currently focused in the input field? 273 | isFocused : function() { 274 | return this.box.is(':focus'); 275 | }, 276 | 277 | // When serializing the facets, the inputs need to also have their values represented, 278 | // in case they contain text that is not yet faceted (but will be once the search is 279 | // completed). 280 | value : function() { 281 | return this.box.val(); 282 | }, 283 | 284 | // When switching between facets and inputs, depending on the direction the cursor 285 | // is coming from, the cursor in this facet's input field should match the original 286 | // direction. 287 | setCursorAtEnd : function(direction) { 288 | if (direction == -1) { 289 | this.box.setCursorPosition(this.box.val().length); 290 | } else { 291 | this.box.setCursorPosition(0); 292 | } 293 | }, 294 | 295 | // Selects the entire range of text in the input. Useful when tabbing between inputs 296 | // and facets. 297 | selectText : function() { 298 | this.box.selectRange(0, this.box.val().length); 299 | if (!this.app.searchBox.allSelected()) { 300 | this.box.focus(); 301 | } else { 302 | this.setMode('is', 'selected'); 303 | } 304 | }, 305 | 306 | // Before the searchBox performs a search, we need to close the 307 | // autocomplete menu. 308 | search : function(e, direction) { 309 | if (!direction) direction = 0; 310 | this.closeAutocomplete(); 311 | this.app.searchBox.searchEvent(e); 312 | _.defer(_.bind(function() { 313 | this.app.searchBox.focusNextFacet(this, direction); 314 | }, this)); 315 | }, 316 | 317 | // Callback fired on key press in the search box. We search when they hit return. 318 | keypress : function(e) { 319 | var key = VS.app.hotkeys.key(e); 320 | 321 | if (key == 'enter') { 322 | return this.search(e, 100); 323 | } else if (VS.app.hotkeys.colon(e)) { 324 | this.box.trigger('resize.autogrow', e); 325 | var query = this.box.val(); 326 | var prefixes = []; 327 | this.app.options.callbacks.facetMatches(function(p) { 328 | prefixes = p; 329 | }); 330 | var labels = _.map(prefixes, function(prefix) { 331 | if (prefix.label) return prefix.label; 332 | else return prefix; 333 | }); 334 | if (_.contains(labels, query)) { 335 | e.preventDefault(); 336 | var remainder = this.addTextFacetRemainder(query); 337 | var position = this.options.position + (remainder?1:0); 338 | this.app.searchBox.addFacet(query, '', position); 339 | return false; 340 | } 341 | } else if (key == 'backspace') { 342 | if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) { 343 | e.preventDefault(); 344 | e.stopPropagation(); 345 | e.stopImmediatePropagation(); 346 | this.app.searchBox.resizeFacets(); 347 | return false; 348 | } 349 | } 350 | }, 351 | 352 | // Handles all keyboard inputs when in the input field. This checks 353 | // for movement between facets and inputs, entering a new value that needs 354 | // to be autocompleted, as well as stepping between facets with backspace. 355 | keydown : function(e) { 356 | var key = VS.app.hotkeys.key(e); 357 | 358 | if (key == 'left') { 359 | if (this.box.getCursorPosition() == 0) { 360 | e.preventDefault(); 361 | this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1}); 362 | } 363 | } else if (key == 'right') { 364 | if (this.box.getCursorPosition() == this.box.val().length) { 365 | e.preventDefault(); 366 | this.app.searchBox.focusNextFacet(this, 1, {selectFacet: true}); 367 | } 368 | } else if (VS.app.hotkeys.shift && key == 'tab') { 369 | e.preventDefault(); 370 | this.app.searchBox.focusNextFacet(this, -1, {selectText: true}); 371 | } else if (key == 'tab') { 372 | var value = this.box.val(); 373 | if (value.length) { 374 | e.preventDefault(); 375 | var remainder = this.addTextFacetRemainder(value); 376 | var position = this.options.position + (remainder?1:0); 377 | if (value != remainder) { 378 | this.app.searchBox.addFacet(value, '', position); 379 | } 380 | } else { 381 | var foundFacet = this.app.searchBox.focusNextFacet(this, 0, { 382 | skipToFacet: true, 383 | selectText: true 384 | }); 385 | if (foundFacet) { 386 | e.preventDefault(); 387 | } 388 | } 389 | } else if (VS.app.hotkeys.command && 390 | String.fromCharCode(e.which).toLowerCase() == 'a') { 391 | e.preventDefault(); 392 | this.app.searchBox.selectAllFacets(); 393 | return false; 394 | } else if (key == 'backspace' && !this.app.searchBox.allSelected()) { 395 | if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) { 396 | e.preventDefault(); 397 | this.app.searchBox.focusNextFacet(this, -1, {backspace: true}); 398 | return false; 399 | } 400 | } else if (key == 'end') { 401 | var view = this.app.searchBox.inputViews[this.app.searchBox.inputViews.length-1]; 402 | view.setCursorAtEnd(-1); 403 | } else if (key == 'home') { 404 | var view = this.app.searchBox.inputViews[0]; 405 | view.setCursorAtEnd(-1); 406 | } 407 | 408 | }, 409 | 410 | // We should get the value of an input should be done 411 | // on keyup since keydown gets the previous value and not the current one 412 | keyup : function(e) { 413 | this.box.trigger('resize.autogrow', e); 414 | } 415 | 416 | }); 417 | 418 | })(); 419 | -------------------------------------------------------------------------------- /lib/js/views/search_box.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // The search box is responsible for managing the many facet views and input views. 6 | VS.ui.SearchBox = Backbone.View.extend({ 7 | 8 | id : 'search', 9 | 10 | events : { 11 | 'click .VS-cancel-search-box' : 'clearSearch', 12 | 'mousedown .VS-search-box' : 'maybeFocusSearch', 13 | 'dblclick .VS-search-box' : 'highlightSearch', 14 | 'click .VS-search-box' : 'maybeTripleClick' 15 | }, 16 | 17 | // Creating a new SearchBox registers handlers for re-rendering facets when necessary, 18 | // as well as handling typing when a facet is selected. 19 | initialize : function(options) { 20 | this.options = _.extend({}, this.options, options); 21 | 22 | this.app = this.options.app; 23 | this.flags = { 24 | allSelected : false 25 | }; 26 | this.facetViews = []; 27 | this.inputViews = []; 28 | _.bindAll(this, 'renderFacets', '_maybeDisableFacets', 'disableFacets', 29 | 'deselectAllFacets', 'addedFacet', 'removedFacet', 'changedFacet'); 30 | this.app.searchQuery 31 | .bind('reset', this.renderFacets) 32 | .bind('add', this.addedFacet) 33 | .bind('remove', this.removedFacet) 34 | .bind('change', this.changedFacet); 35 | $(document).bind('keydown', this._maybeDisableFacets); 36 | }, 37 | 38 | // Renders the search box, but requires placement on the page through `this.el`. 39 | render : function() { 40 | $(this.el).append(JST['search_box']({ 41 | readOnly: this.app.options.readOnly 42 | })); 43 | $(document.body).setMode('no', 'search'); 44 | 45 | return this; 46 | }, 47 | 48 | // # Querying Facets # 49 | 50 | // Either gets a serialized query string or sets the faceted query from a query string. 51 | value : function(query) { 52 | if (query == null) return this.serialize(); 53 | return this.setQuery(query); 54 | }, 55 | 56 | // Uses the VS.app.searchQuery collection to serialize the current query from the various 57 | // facets that are in the search box. 58 | serialize : function() { 59 | var query = []; 60 | var inputViewsCount = this.inputViews.length; 61 | 62 | this.app.searchQuery.each(_.bind(function(facet, i) { 63 | query.push(this.inputViews[i].value()); 64 | query.push(facet.serialize()); 65 | }, this)); 66 | 67 | if (inputViewsCount) { 68 | query.push(this.inputViews[inputViewsCount-1].value()); 69 | } 70 | 71 | return _.compact(query).join(' '); 72 | }, 73 | 74 | // Returns any facet views that are currently selected. Useful for changing the value 75 | // callbacks based on what else is in the search box and which facet is being edited. 76 | selected: function() { 77 | return _.select(this.facetViews, function(view) { 78 | return view.modes.editing == 'is' || view.modes.selected == 'is'; 79 | }); 80 | }, 81 | 82 | // Similar to `this.selected`, returns any facet models that are currently selected. 83 | selectedModels: function() { 84 | return _.pluck(this.selected(), 'model'); 85 | }, 86 | 87 | // Takes a query string and uses the SearchParser to parse and render it. Note that 88 | // `VS.app.SearchParser` refreshes the `VS.app.searchQuery` collection, which is bound 89 | // here to call `this.renderFacets`. 90 | setQuery : function(query) { 91 | this.currentQuery = query; 92 | VS.app.SearchParser.parse(this.app, query); 93 | }, 94 | 95 | // Returns the position of a facet/input view. Useful when moving between facets. 96 | viewPosition : function(view) { 97 | var views = view.type == 'facet' ? this.facetViews : this.inputViews; 98 | var position = _.indexOf(views, view); 99 | if (position == -1) position = 0; 100 | return position; 101 | }, 102 | 103 | // Used to launch a search. Hitting enter or clicking the search button. 104 | searchEvent : function(e) { 105 | var query = this.value(); 106 | this.focusSearch(e); 107 | this.value(query); 108 | this.app.options.callbacks.search(query, this.app.searchQuery); 109 | }, 110 | 111 | // # Rendering Facets # 112 | 113 | // Add a new facet. Facet will be focused and ready to accept a value. Can also 114 | // specify position, in the case of adding facets from an inbetween input. 115 | addFacet : function(category, initialQuery, position) { 116 | category = VS.utils.inflector.trim(category); 117 | initialQuery = VS.utils.inflector.trim(initialQuery || ''); 118 | if (!category) return; 119 | 120 | var model = new VS.model.SearchFacet({ 121 | category : category, 122 | value : initialQuery || '', 123 | app : this.app 124 | }); 125 | this.app.searchQuery.add(model, {at: position}); 126 | }, 127 | 128 | // Renders a newly added facet, and selects it. 129 | addedFacet : function (model) { 130 | this.renderFacets(); 131 | var facetView = _.detect(this.facetViews, function(view) { 132 | if (view.model == model) return true; 133 | }); 134 | 135 | _.defer(function() { 136 | facetView.enableEdit(); 137 | }); 138 | }, 139 | 140 | // Changing a facet programmatically re-renders it. 141 | changedFacet: function () { 142 | this.renderFacets(); 143 | }, 144 | 145 | // When removing a facet, potentially do something. For now, the adjacent 146 | // remaining facet is selected, but this is handled by the facet's view, 147 | // since its position is unknown by the time the collection triggers this 148 | // remove callback. 149 | removedFacet : function (facet, query, options) { 150 | this.app.options.callbacks.removedFacet(facet, query, options); 151 | }, 152 | 153 | // Renders each facet as a searchFacet view. 154 | renderFacets : function() { 155 | this.facetViews = []; 156 | this.inputViews = []; 157 | 158 | this.$('.VS-search-inner').empty(); 159 | 160 | this.app.searchQuery.each(_.bind(this.renderFacet, this)); 161 | 162 | // Add on an n+1 empty search input on the very end. 163 | this.renderSearchInput(); 164 | this.renderPlaceholder(); 165 | }, 166 | 167 | // Render a single facet, using its category and query value. 168 | renderFacet : function(facet, position) { 169 | var view = new VS.ui.SearchFacet({ 170 | app : this.app, 171 | model : facet, 172 | order : position 173 | }); 174 | 175 | // Input first, facet second. 176 | this.renderSearchInput(); 177 | this.facetViews.push(view); 178 | this.$('.VS-search-inner').children().eq(position*2).after(view.render().el); 179 | 180 | view.calculateSize(); 181 | _.defer(_.bind(view.calculateSize, view)); 182 | 183 | return view; 184 | }, 185 | 186 | // Render a single input, used to create and autocomplete facets 187 | renderSearchInput : function() { 188 | var input = new VS.ui.SearchInput({ 189 | position: this.inputViews.length, 190 | app: this.app, 191 | showFacets: this.options.showFacets 192 | }); 193 | this.$('.VS-search-inner').append(input.render().el); 194 | this.inputViews.push(input); 195 | }, 196 | 197 | // Handles showing/hiding the placeholder text 198 | renderPlaceholder : function() { 199 | var $placeholder = this.$('.VS-placeholder'); 200 | if (this.app.searchQuery.length) { 201 | $placeholder.addClass("VS-hidden"); 202 | } else { 203 | $placeholder.removeClass("VS-hidden") 204 | .text(this.app.options.placeholder); 205 | } 206 | }, 207 | 208 | // # Modifying Facets # 209 | 210 | // Clears out the search box. Command+A + delete can trigger this, as can a cancel button. 211 | // 212 | // If a `clearSearch` callback was provided, the callback is invoked and 213 | // provided with a function performs the actual removal of the data. This 214 | // allows third-party developers to either clear data asynchronously, or 215 | // prior to performing their custom "clear" logic. 216 | clearSearch : function(e) { 217 | if (this.app.options.readOnly) return; 218 | var actualClearSearch = _.bind(function() { 219 | this.disableFacets(); 220 | this.value(''); 221 | this.flags.allSelected = false; 222 | this.searchEvent(e); 223 | this.focusSearch(e); 224 | }, this); 225 | 226 | if (this.app.options.callbacks.clearSearch != $.noop) { 227 | this.app.options.callbacks.clearSearch(actualClearSearch); 228 | } else { 229 | actualClearSearch(); 230 | } 231 | }, 232 | 233 | // Command+A selects all facets. 234 | selectAllFacets : function() { 235 | this.flags.allSelected = true; 236 | 237 | $(document).one('click.selectAllFacets', this.deselectAllFacets); 238 | 239 | _.each(this.facetViews, function(facetView, i) { 240 | facetView.selectFacet(); 241 | }); 242 | _.each(this.inputViews, function(inputView, i) { 243 | inputView.selectText(); 244 | }); 245 | }, 246 | 247 | // Used by facets and input to see if all facets are currently selected. 248 | allSelected : function(deselect) { 249 | if (deselect) this.flags.allSelected = false; 250 | return this.flags.allSelected; 251 | }, 252 | 253 | // After `selectAllFacets` is engaged, this method is bound to the entire document. 254 | // This immediate disables and deselects all facets, but it also checks if the user 255 | // has clicked on either a facet or an input, and properly selects the view. 256 | deselectAllFacets : function(e) { 257 | this.disableFacets(); 258 | 259 | if (this.$(e.target).is('.category,input')) { 260 | var el = $(e.target).closest('.search_facet,.search_input'); 261 | var view = _.detect(this.facetViews.concat(this.inputViews), function(v) { 262 | return v.el == el[0]; 263 | }); 264 | if (view.type == 'facet') { 265 | view.selectFacet(); 266 | } else if (view.type == 'input') { 267 | _.defer(function() { 268 | view.enableEdit(true); 269 | }); 270 | } 271 | } 272 | }, 273 | 274 | // Disables all facets except for the passed in view. Used when switching between 275 | // facets, so as not to have to keep state of active facets. 276 | disableFacets : function(keepView) { 277 | _.each(this.inputViews, function(view) { 278 | if (view && view != keepView && 279 | (view.modes.editing == 'is' || view.modes.selected == 'is')) { 280 | view.disableEdit(); 281 | } 282 | }); 283 | _.each(this.facetViews, function(view) { 284 | if (view && view != keepView && 285 | (view.modes.editing == 'is' || view.modes.selected == 'is')) { 286 | view.disableEdit(); 287 | view.deselectFacet(); 288 | } 289 | }); 290 | 291 | this.flags.allSelected = false; 292 | this.removeFocus(); 293 | $(document).unbind('click.selectAllFacets'); 294 | }, 295 | 296 | // Resize all inputs to account for extra keystrokes which may be changing the facet 297 | // width incorrectly. This is a safety check to ensure inputs are correctly sized. 298 | resizeFacets : function(view) { 299 | _.each(this.facetViews, function(facetView, i) { 300 | if (!view || facetView == view) { 301 | facetView.resize(); 302 | } 303 | }); 304 | }, 305 | 306 | // Handles keydown events on the document. Used to complete the Cmd+A deletion, and 307 | // blurring focus. 308 | _maybeDisableFacets : function(e) { 309 | if (this.flags.allSelected && VS.app.hotkeys.key(e) == 'backspace') { 310 | e.preventDefault(); 311 | this.clearSearch(e); 312 | return false; 313 | } else if (this.flags.allSelected && VS.app.hotkeys.printable(e)) { 314 | this.clearSearch(e); 315 | } 316 | }, 317 | 318 | // # Focusing Facets # 319 | 320 | // Move focus between facets and inputs. Takes a direction as well as many options 321 | // for skipping over inputs and only to facets, placement of cursor position in facet 322 | // (i.e. at the end), and selecting the text in the input/facet. 323 | focusNextFacet : function(currentView, direction, options) { 324 | options = options || {}; 325 | var viewCount = this.facetViews.length; 326 | var viewPosition = options.viewPosition || this.viewPosition(currentView); 327 | 328 | if (!options.skipToFacet) { 329 | // Correct for bouncing between matching text and facet arrays. 330 | if (currentView.type == 'text' && direction > 0) direction -= 1; 331 | if (currentView.type == 'facet' && direction < 0) direction += 1; 332 | } else if (options.skipToFacet && currentView.type == 'text' && 333 | viewCount == viewPosition && direction >= 0) { 334 | // Special case of looping around to a facet from the last search input box. 335 | return false; 336 | } 337 | var view, next = Math.min(viewCount, viewPosition + direction); 338 | 339 | if (currentView.type == 'text') { 340 | if (next >= 0 && next < viewCount) { 341 | view = this.facetViews[next]; 342 | } else if (next == viewCount) { 343 | view = this.inputViews[this.inputViews.length-1]; 344 | } 345 | if (view && options.selectFacet && view.type == 'facet') { 346 | view.selectFacet(); 347 | } else if (view) { 348 | view.enableEdit(); 349 | view.setCursorAtEnd(direction || options.startAtEnd); 350 | } 351 | } else if (currentView.type == 'facet') { 352 | if (options.skipToFacet) { 353 | if (next >= viewCount || next < 0) { 354 | view = _.last(this.inputViews); 355 | view.enableEdit(); 356 | } else { 357 | view = this.facetViews[next]; 358 | view.enableEdit(); 359 | view.setCursorAtEnd(direction || options.startAtEnd); 360 | } 361 | } else { 362 | view = this.inputViews[next]; 363 | view.enableEdit(); 364 | } 365 | } 366 | if (options.selectText) view.selectText(); 367 | this.resizeFacets(); 368 | 369 | return true; 370 | }, 371 | 372 | maybeFocusSearch : function(e) { 373 | if (this.app.options.readOnly) return; 374 | if ($(e.target).is('.VS-search-box') || 375 | $(e.target).is('.VS-search-inner') || 376 | e.type == 'keydown') { 377 | this.focusSearch(e); 378 | } 379 | }, 380 | 381 | // Bring focus to last input field. 382 | focusSearch : function(e, selectText) { 383 | if (this.app.options.readOnly) return; 384 | var view = this.inputViews[this.inputViews.length-1]; 385 | view.enableEdit(selectText); 386 | if (!selectText) view.setCursorAtEnd(-1); 387 | if (e.type == 'keydown') { 388 | view.keydown(e); 389 | view.box.trigger('keydown'); 390 | } 391 | _.defer(_.bind(function() { 392 | if (!this.$('input:focus').length) { 393 | view.enableEdit(selectText); 394 | } 395 | }, this)); 396 | }, 397 | 398 | // Double-clicking on the search wrapper should select the existing text in 399 | // the last search input. Also start the triple-click timer. 400 | highlightSearch : function(e) { 401 | if (this.app.options.readOnly) return; 402 | if ($(e.target).is('.VS-search-box') || 403 | $(e.target).is('.VS-search-inner') || 404 | e.type == 'keydown') { 405 | var lastinput = this.inputViews[this.inputViews.length-1]; 406 | lastinput.startTripleClickTimer(); 407 | this.focusSearch(e, true); 408 | } 409 | }, 410 | 411 | maybeTripleClick : function(e) { 412 | var lastinput = this.inputViews[this.inputViews.length-1]; 413 | return lastinput.maybeTripleClick(e); 414 | }, 415 | 416 | // Used to show the user is focused on some input inside the search box. 417 | addFocus : function() { 418 | if (this.app.options.readOnly) return; 419 | this.app.options.callbacks.focus(); 420 | this.$('.VS-search-box').addClass('VS-focus'); 421 | }, 422 | 423 | // User is no longer focused on anything in the search box. 424 | removeFocus : function() { 425 | this.app.options.callbacks.blur(); 426 | var focus = _.any(this.facetViews.concat(this.inputViews), function(view) { 427 | return view.isFocused(); 428 | }); 429 | if (!focus) this.$('.VS-search-box').removeClass('VS-focus'); 430 | }, 431 | 432 | // Show a menu which adds pre-defined facets to the search box. This is unused for now. 433 | showFacetCategoryMenu : function(e) { 434 | e.preventDefault(); 435 | e.stopPropagation(); 436 | if (this.facetCategoryMenu && this.facetCategoryMenu.modes.open == 'is') { 437 | return this.facetCategoryMenu.close(); 438 | } 439 | 440 | var items = [ 441 | {title: 'Account', onClick: _.bind(this.addFacet, this, 'account', '')}, 442 | {title: 'Project', onClick: _.bind(this.addFacet, this, 'project', '')}, 443 | {title: 'Filter', onClick: _.bind(this.addFacet, this, 'filter', '')}, 444 | {title: 'Access', onClick: _.bind(this.addFacet, this, 'access', '')} 445 | ]; 446 | 447 | var menu = this.facetCategoryMenu || (this.facetCategoryMenu = new dc.ui.Menu({ 448 | items : items, 449 | standalone : true 450 | })); 451 | 452 | this.$('.VS-icon-search').after(menu.render().open().content); 453 | return false; 454 | } 455 | 456 | }); 457 | 458 | })(); 459 | -------------------------------------------------------------------------------- /lib/js/views/search_facet.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var $ = jQuery; // Handle namespaced jQuery 4 | 5 | // This is the visual search facet that holds the category and its autocompleted 6 | // input field. 7 | VS.ui.SearchFacet = Backbone.View.extend({ 8 | 9 | type : 'facet', 10 | 11 | className : 'search_facet', 12 | 13 | events : { 14 | 'click .category' : 'selectFacet', 15 | 'keydown input' : 'keydown', 16 | 'mousedown input' : 'enableEdit', 17 | 'mouseover .VS-icon-cancel' : 'showDelete', 18 | 'mouseout .VS-icon-cancel' : 'hideDelete', 19 | 'click .VS-icon-cancel' : 'remove' 20 | }, 21 | 22 | initialize : function(options) { 23 | this.options = _.extend({}, this.options, options); 24 | 25 | this.flags = { 26 | canClose : false 27 | }; 28 | _.bindAll(this, 'set', 'keydown', 'deselectFacet', 'deferDisableEdit'); 29 | this.app = this.options.app; 30 | }, 31 | 32 | // Rendering the facet sets up autocompletion, events on blur, and populates 33 | // the facet's input with its starting value. 34 | render : function() { 35 | $(this.el).html(JST['search_facet']({ 36 | model : this.model, 37 | readOnly: this.app.options.readOnly 38 | })); 39 | 40 | this.setMode('not', 'editing'); 41 | this.setMode('not', 'selected'); 42 | this.box = this.$('input'); 43 | this.box.val(this.model.label()); 44 | this.box.bind('blur', this.deferDisableEdit); 45 | // Handle paste events with `propertychange` 46 | this.box.bind('input propertychange', this.keydown); 47 | this.setupAutocomplete(); 48 | 49 | return this; 50 | }, 51 | 52 | // This method is used to setup the facet's input to auto-grow. 53 | // This is defered in the searchBox so it can be attached to the 54 | // DOM to get the correct font-size. 55 | calculateSize : function() { 56 | this.box.autoGrowInput(); 57 | this.box.unbind('updated.autogrow'); 58 | this.box.bind('updated.autogrow', _.bind(this.moveAutocomplete, this)); 59 | }, 60 | 61 | // Forces a recalculation of this facet's input field's value. Called when 62 | // the facet is focused, removed, or otherwise modified. 63 | resize : function(e) { 64 | this.box.trigger('resize.autogrow', e); 65 | }, 66 | 67 | // Watches the facet's input field to see if it matches the beginnings of 68 | // words in `autocompleteValues`, which is different for every category. 69 | // If the value, when selected from the autocompletion menu, is different 70 | // than what it was, commit the facet and search for it. 71 | setupAutocomplete : function() { 72 | this.box.autocomplete({ 73 | source : _.bind(this.autocompleteValues, this), 74 | minLength : 0, 75 | delay : 0, 76 | autoFocus : true, 77 | position : {offset : "0 5"}, 78 | create : _.bind(function(e, ui) { 79 | $(this.el).find('.ui-autocomplete-input').css('z-index','auto'); 80 | }, this), 81 | select : _.bind(function(e, ui) { 82 | e.preventDefault(); 83 | var originalValue = this.model.get('value'); 84 | this.set(ui.item.value); 85 | if (originalValue != ui.item.value || this.box.val() != ui.item.value) { 86 | if (this.app.options.autosearch) { 87 | this.search(e); 88 | } else { 89 | this.app.searchBox.renderFacets(); 90 | this.app.searchBox.focusNextFacet(this, 1, {viewPosition: this.options.order}); 91 | } 92 | } 93 | return false; 94 | }, this), 95 | open : _.bind(function(e, ui) { 96 | var box = this.box; 97 | this.box.autocomplete('widget').find('.ui-menu-item').each(function() { 98 | var $value = $(this), 99 | autoCompleteData = $value.data('item.autocomplete') || $value.data('ui-autocomplete-item'); 100 | 101 | if (autoCompleteData['value'] == box.val() && box.data('ui-autocomplete').menu.activate) { 102 | box.data('ui-autocomplete').menu.activate(new $.Event("mouseover"), $value); 103 | } 104 | }); 105 | }, this) 106 | }); 107 | 108 | this.box.autocomplete('widget').addClass('VS-interface'); 109 | }, 110 | 111 | // As the facet's input field grows, it may move to the next line in the 112 | // search box. `autoGrowInput` triggers an `updated` event on the input 113 | // field, which is bound to this method to move the autocomplete menu. 114 | moveAutocomplete : function() { 115 | var autocomplete = this.box.data('ui-autocomplete'); 116 | if (autocomplete) { 117 | autocomplete.menu.element.position({ 118 | my : "left top", 119 | at : "left bottom", 120 | of : this.box.data('ui-autocomplete').element, 121 | collision : "flip", 122 | offset : "0 5" 123 | }); 124 | } 125 | }, 126 | 127 | // When a user enters a facet and it is being edited, immediately show 128 | // the autocomplete menu and size it to match the contents. 129 | searchAutocomplete : function(e) { 130 | var autocomplete = this.box.data('ui-autocomplete'); 131 | if (autocomplete) { 132 | var menu = autocomplete.menu.element; 133 | autocomplete.search(); 134 | 135 | // Resize the menu based on the correctly measured width of what's bigger: 136 | // the menu's original size or the menu items' new size. 137 | menu.outerWidth(Math.max( 138 | menu.width('').outerWidth(), 139 | autocomplete.element.outerWidth() 140 | )); 141 | } 142 | }, 143 | 144 | // Closes the autocomplete menu. Called on disabling, selecting, deselecting, 145 | // and anything else that takes focus out of the facet's input field. 146 | closeAutocomplete : function() { 147 | var autocomplete = this.box.data('ui-autocomplete'); 148 | if (autocomplete) autocomplete.close(); 149 | }, 150 | 151 | // Search terms used in the autocomplete menu. These are specific to the facet, 152 | // and only match for the facet's category. The values are then matched on the 153 | // first letter of any word in matches, and finally sorted according to the 154 | // value's own category. You can pass `preserveOrder` as an option in the 155 | // `facetMatches` callback to skip any further ordering done client-side. 156 | autocompleteValues : function(req, resp) { 157 | var category = this.model.get('category'); 158 | var value = this.model.get('value'); 159 | var searchTerm = req.term; 160 | 161 | this.app.options.callbacks.valueMatches(category, searchTerm, function(matches, options) { 162 | options = options || {}; 163 | matches = matches || []; 164 | 165 | if (searchTerm && value != searchTerm) { 166 | if (options.preserveMatches) { 167 | resp(matches); 168 | } else { 169 | var re = VS.utils.inflector.escapeRegExp(searchTerm || ''); 170 | var matcher = new RegExp('\\b' + re, 'i'); 171 | matches = $.grep(matches, function(item) { 172 | return matcher.test(item) || 173 | matcher.test(item.value) || 174 | matcher.test(item.label); 175 | }); 176 | } 177 | } 178 | 179 | if (options.preserveOrder) { 180 | resp(matches); 181 | } else { 182 | resp(_.sortBy(matches, function(match) { 183 | if (match == value || match.value == value) return ''; 184 | else return match; 185 | })); 186 | } 187 | }); 188 | 189 | }, 190 | 191 | // Sets the facet's model's value. 192 | set : function(value) { 193 | if (!value) return; 194 | this.model.set({'value': value}); 195 | }, 196 | 197 | // Before the searchBox performs a search, we need to close the 198 | // autocomplete menu. 199 | search : function(e, direction) { 200 | if (!direction) direction = 1; 201 | this.closeAutocomplete(); 202 | this.app.searchBox.searchEvent(e); 203 | _.defer(_.bind(function() { 204 | this.app.searchBox.focusNextFacet(this, direction, {viewPosition: this.options.order}); 205 | }, this)); 206 | }, 207 | 208 | // Begin editing the facet's input. This is called when the user enters 209 | // the input either from another facet or directly clicking on it. 210 | // 211 | // This method tells all other facets and inputs to disable so it can have 212 | // the sole focus. It also prepares the autocompletion menu. 213 | enableEdit : function() { 214 | if (this.app.options.readOnly) return; 215 | if (this.modes.editing != 'is') { 216 | this.setMode('is', 'editing'); 217 | this.deselectFacet(); 218 | if (this.box.val() == '') { 219 | this.box.val(this.model.get('value')); 220 | } 221 | } 222 | 223 | this.flags.canClose = false; 224 | this.app.searchBox.disableFacets(this); 225 | this.app.searchBox.addFocus(); 226 | _.defer(_.bind(function() { 227 | this.app.searchBox.addFocus(); 228 | }, this)); 229 | this.resize(); 230 | this.searchAutocomplete(); 231 | this.box.focus(); 232 | }, 233 | 234 | // When the user blurs the input, they may either be going to another input 235 | // or off the search box entirely. If they go to another input, this facet 236 | // will be instantly disabled, and the canClose flag will be turned back off. 237 | // 238 | // However, if the user clicks elsewhere on the page, this method starts a timer 239 | // that checks if any of the other inputs are selected or are being edited. If 240 | // not, then it can finally close itself and its autocomplete menu. 241 | deferDisableEdit : function() { 242 | this.flags.canClose = true; 243 | _.delay(_.bind(function() { 244 | if (this.flags.canClose && !this.box.is(':focus') && 245 | this.modes.editing == 'is' && this.modes.selected != 'is') { 246 | this.disableEdit(); 247 | } 248 | }, this), 250); 249 | }, 250 | 251 | // Called either by other facets receiving focus or by the timer in `deferDisableEdit`, 252 | // this method will turn off the facet, remove any text selection, and close 253 | // the autocomplete menu. 254 | disableEdit : function() { 255 | var newFacetQuery = VS.utils.inflector.trim(this.box.val()); 256 | if (newFacetQuery != this.model.get('value')) { 257 | this.set(newFacetQuery); 258 | } 259 | this.flags.canClose = false; 260 | this.box.selectRange(0, 0); 261 | this.box.blur(); 262 | this.setMode('not', 'editing'); 263 | this.closeAutocomplete(); 264 | this.app.searchBox.removeFocus(); 265 | }, 266 | 267 | // Selects the facet, which blurs the facet's input and highlights the facet. 268 | // If this is the only facet being selected (and not part of a select all event), 269 | // we attach a mouse/keyboard watcher to check if the next action by the user 270 | // should delete this facet or just deselect it. 271 | selectFacet : function(e) { 272 | if (e) e.preventDefault(); 273 | if (this.app.options.readOnly) return; 274 | var allSelected = this.app.searchBox.allSelected(); 275 | if (this.modes.selected == 'is') return; 276 | 277 | if (this.box.is(':focus')) { 278 | this.box.setCursorPosition(0); 279 | this.box.blur(); 280 | } 281 | 282 | this.flags.canClose = false; 283 | this.closeAutocomplete(); 284 | this.setMode('is', 'selected'); 285 | this.setMode('not', 'editing'); 286 | if (!allSelected || e) { 287 | $(document).unbind('keydown.facet', this.keydown); 288 | $(document).unbind('click.facet', this.deselectFacet); 289 | _.defer(_.bind(function() { 290 | $(document).unbind('keydown.facet').bind('keydown.facet', this.keydown); 291 | $(document).unbind('click.facet').one('click.facet', this.deselectFacet); 292 | }, this)); 293 | this.app.searchBox.disableFacets(this); 294 | this.app.searchBox.addFocus(); 295 | } 296 | return false; 297 | }, 298 | 299 | // Turns off highlighting on the facet. Called in a variety of ways, this 300 | // only deselects the facet if it is selected, and then cleans up the 301 | // keyboard/mouse watchers that were created when the facet was first 302 | // selected. 303 | deselectFacet : function(e) { 304 | if (e) e.preventDefault(); 305 | if (this.modes.selected == 'is') { 306 | this.setMode('not', 'selected'); 307 | this.closeAutocomplete(); 308 | this.app.searchBox.removeFocus(); 309 | } 310 | $(document).unbind('keydown.facet', this.keydown); 311 | $(document).unbind('click.facet', this.deselectFacet); 312 | return false; 313 | }, 314 | 315 | // Is the user currently focused in this facet's input field? 316 | isFocused : function() { 317 | return this.box.is(':focus'); 318 | }, 319 | 320 | // Hovering over the delete button styles the facet so the user knows that 321 | // the delete button will kill the entire facet. 322 | showDelete : function() { 323 | $(this.el).addClass('search_facet_maybe_delete'); 324 | }, 325 | 326 | // On `mouseout`, the user is no longer hovering on the delete button. 327 | hideDelete : function() { 328 | $(this.el).removeClass('search_facet_maybe_delete'); 329 | }, 330 | 331 | // When switching between facets, depending on the direction the cursor is 332 | // coming from, the cursor in this facet's input field should match the original 333 | // direction. 334 | setCursorAtEnd : function(direction) { 335 | if (direction == -1) { 336 | this.box.setCursorPosition(this.box.val().length); 337 | } else { 338 | this.box.setCursorPosition(0); 339 | } 340 | }, 341 | 342 | // Deletes the facet and sends the cursor over to the nearest input field. 343 | remove : function(e) { 344 | var committed = this.model.get('value'); 345 | this.deselectFacet(); 346 | this.disableEdit(); 347 | this.app.searchQuery.remove(this.model); 348 | if (committed && this.app.options.autosearch) { 349 | this.search(e, -1); 350 | } else { 351 | this.app.searchBox.renderFacets(); 352 | this.app.searchBox.focusNextFacet(this, -1, {viewPosition: this.options.order}); 353 | } 354 | }, 355 | 356 | // Selects the text in the facet's input field. When the user tabs between 357 | // facets, convention is to highlight the entire field. 358 | selectText: function() { 359 | this.box.selectRange(0, this.box.val().length); 360 | }, 361 | 362 | // Handles all keyboard inputs when in the facet's input field. This checks 363 | // for movement between facets and inputs, entering a new value that needs 364 | // to be autocompleted, as well as the removal of this facet. 365 | keydown : function(e) { 366 | var key = VS.app.hotkeys.key(e); 367 | 368 | if (key == 'enter' && this.box.val()) { 369 | this.disableEdit(); 370 | this.search(e); 371 | } else if (key == 'left') { 372 | if (this.modes.selected == 'is') { 373 | this.deselectFacet(); 374 | this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1}); 375 | } else if (this.box.getCursorPosition() == 0 && !this.box.getSelection().length) { 376 | this.selectFacet(); 377 | } 378 | } else if (key == 'right') { 379 | if (this.modes.selected == 'is') { 380 | e.preventDefault(); 381 | this.deselectFacet(); 382 | this.setCursorAtEnd(0); 383 | this.enableEdit(); 384 | } else if (this.box.getCursorPosition() == this.box.val().length) { 385 | e.preventDefault(); 386 | this.disableEdit(); 387 | this.app.searchBox.focusNextFacet(this, 1); 388 | } 389 | } else if (VS.app.hotkeys.shift && key == 'tab') { 390 | e.preventDefault(); 391 | this.app.searchBox.focusNextFacet(this, -1, { 392 | startAtEnd : -1, 393 | skipToFacet : true, 394 | selectText : true 395 | }); 396 | } else if (key == 'tab') { 397 | e.preventDefault(); 398 | this.app.searchBox.focusNextFacet(this, 1, { 399 | skipToFacet : true, 400 | selectText : true 401 | }); 402 | } else if (VS.app.hotkeys.command && (e.which == 97 || e.which == 65)) { 403 | e.preventDefault(); 404 | this.app.searchBox.selectAllFacets(); 405 | return false; 406 | } else if (VS.app.hotkeys.printable(e) && this.modes.selected == 'is') { 407 | this.app.searchBox.focusNextFacet(this, -1, {startAtEnd: -1}); 408 | this.remove(e); 409 | } else if (key == 'backspace') { 410 | $(document).on('keydown.backspace', function(e) { 411 | if (VS.app.hotkeys.key(e) === 'backspace') { 412 | e.preventDefault(); 413 | } 414 | }); 415 | 416 | $(document).on('keyup.backspace', function(e) { 417 | $(document).off('.backspace'); 418 | }); 419 | 420 | if (this.modes.selected == 'is') { 421 | e.preventDefault(); 422 | this.remove(e); 423 | } else if (this.box.getCursorPosition() == 0 && 424 | !this.box.getSelection().length) { 425 | e.preventDefault(); 426 | this.selectFacet(); 427 | } 428 | e.stopPropagation(); 429 | } 430 | 431 | // Handle paste events 432 | if (e.which == null) { 433 | // this.searchAutocomplete(e); 434 | _.defer(_.bind(this.resize, this, e)); 435 | } else { 436 | this.resize(e); 437 | } 438 | } 439 | 440 | }); 441 | 442 | })(); 443 | -------------------------------------------------------------------------------- /vendor/jquery.ui.widget.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Widget 1.10.4 3 | * http://jqueryui.com 4 | * 5 | * Copyright 2014 jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | * 9 | * http://api.jqueryui.com/jQuery.widget/ 10 | */ 11 | (function( $, undefined ) { 12 | 13 | var uuid = 0, 14 | slice = Array.prototype.slice, 15 | _cleanData = $.cleanData; 16 | $.cleanData = function( elems ) { 17 | for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { 18 | try { 19 | $( elem ).triggerHandler( "remove" ); 20 | // http://bugs.jquery.com/ticket/8235 21 | } catch( e ) {} 22 | } 23 | _cleanData( elems ); 24 | }; 25 | 26 | $.widget = function( name, base, prototype ) { 27 | var fullName, existingConstructor, constructor, basePrototype, 28 | // proxiedPrototype allows the provided prototype to remain unmodified 29 | // so that it can be used as a mixin for multiple widgets (#8876) 30 | proxiedPrototype = {}, 31 | namespace = name.split( "." )[ 0 ]; 32 | 33 | name = name.split( "." )[ 1 ]; 34 | fullName = namespace + "-" + name; 35 | 36 | if ( !prototype ) { 37 | prototype = base; 38 | base = $.Widget; 39 | } 40 | 41 | // create selector for plugin 42 | $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { 43 | return !!$.data( elem, fullName ); 44 | }; 45 | 46 | $[ namespace ] = $[ namespace ] || {}; 47 | existingConstructor = $[ namespace ][ name ]; 48 | constructor = $[ namespace ][ name ] = function( options, element ) { 49 | // allow instantiation without "new" keyword 50 | if ( !this._createWidget ) { 51 | return new constructor( options, element ); 52 | } 53 | 54 | // allow instantiation without initializing for simple inheritance 55 | // must use "new" keyword (the code above always passes args) 56 | if ( arguments.length ) { 57 | this._createWidget( options, element ); 58 | } 59 | }; 60 | // extend with the existing constructor to carry over any static properties 61 | $.extend( constructor, existingConstructor, { 62 | version: prototype.version, 63 | // copy the object used to create the prototype in case we need to 64 | // redefine the widget later 65 | _proto: $.extend( {}, prototype ), 66 | // track widgets that inherit from this widget in case this widget is 67 | // redefined after a widget inherits from it 68 | _childConstructors: [] 69 | }); 70 | 71 | basePrototype = new base(); 72 | // we need to make the options hash a property directly on the new instance 73 | // otherwise we'll modify the options hash on the prototype that we're 74 | // inheriting from 75 | basePrototype.options = $.widget.extend( {}, basePrototype.options ); 76 | $.each( prototype, function( prop, value ) { 77 | if ( !$.isFunction( value ) ) { 78 | proxiedPrototype[ prop ] = value; 79 | return; 80 | } 81 | proxiedPrototype[ prop ] = (function() { 82 | var _super = function() { 83 | return base.prototype[ prop ].apply( this, arguments ); 84 | }, 85 | _superApply = function( args ) { 86 | return base.prototype[ prop ].apply( this, args ); 87 | }; 88 | return function() { 89 | var __super = this._super, 90 | __superApply = this._superApply, 91 | returnValue; 92 | 93 | this._super = _super; 94 | this._superApply = _superApply; 95 | 96 | returnValue = value.apply( this, arguments ); 97 | 98 | this._super = __super; 99 | this._superApply = __superApply; 100 | 101 | return returnValue; 102 | }; 103 | })(); 104 | }); 105 | constructor.prototype = $.widget.extend( basePrototype, { 106 | // TODO: remove support for widgetEventPrefix 107 | // always use the name + a colon as the prefix, e.g., draggable:start 108 | // don't prefix for widgets that aren't DOM-based 109 | widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name 110 | }, proxiedPrototype, { 111 | constructor: constructor, 112 | namespace: namespace, 113 | widgetName: name, 114 | widgetFullName: fullName 115 | }); 116 | 117 | // If this widget is being redefined then we need to find all widgets that 118 | // are inheriting from it and redefine all of them so that they inherit from 119 | // the new version of this widget. We're essentially trying to replace one 120 | // level in the prototype chain. 121 | if ( existingConstructor ) { 122 | $.each( existingConstructor._childConstructors, function( i, child ) { 123 | var childPrototype = child.prototype; 124 | 125 | // redefine the child widget using the same prototype that was 126 | // originally used, but inherit from the new version of the base 127 | $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); 128 | }); 129 | // remove the list of existing child constructors from the old constructor 130 | // so the old child constructors can be garbage collected 131 | delete existingConstructor._childConstructors; 132 | } else { 133 | base._childConstructors.push( constructor ); 134 | } 135 | 136 | $.widget.bridge( name, constructor ); 137 | }; 138 | 139 | $.widget.extend = function( target ) { 140 | var input = slice.call( arguments, 1 ), 141 | inputIndex = 0, 142 | inputLength = input.length, 143 | key, 144 | value; 145 | for ( ; inputIndex < inputLength; inputIndex++ ) { 146 | for ( key in input[ inputIndex ] ) { 147 | value = input[ inputIndex ][ key ]; 148 | if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { 149 | // Clone objects 150 | if ( $.isPlainObject( value ) ) { 151 | target[ key ] = $.isPlainObject( target[ key ] ) ? 152 | $.widget.extend( {}, target[ key ], value ) : 153 | // Don't extend strings, arrays, etc. with objects 154 | $.widget.extend( {}, value ); 155 | // Copy everything else by reference 156 | } else { 157 | target[ key ] = value; 158 | } 159 | } 160 | } 161 | } 162 | return target; 163 | }; 164 | 165 | $.widget.bridge = function( name, object ) { 166 | var fullName = object.prototype.widgetFullName || name; 167 | $.fn[ name ] = function( options ) { 168 | var isMethodCall = typeof options === "string", 169 | args = slice.call( arguments, 1 ), 170 | returnValue = this; 171 | 172 | // allow multiple hashes to be passed on init 173 | options = !isMethodCall && args.length ? 174 | $.widget.extend.apply( null, [ options ].concat(args) ) : 175 | options; 176 | 177 | if ( isMethodCall ) { 178 | this.each(function() { 179 | var methodValue, 180 | instance = $.data( this, fullName ); 181 | if ( !instance ) { 182 | return $.error( "cannot call methods on " + name + " prior to initialization; " + 183 | "attempted to call method '" + options + "'" ); 184 | } 185 | if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { 186 | return $.error( "no such method '" + options + "' for " + name + " widget instance" ); 187 | } 188 | methodValue = instance[ options ].apply( instance, args ); 189 | if ( methodValue !== instance && methodValue !== undefined ) { 190 | returnValue = methodValue && methodValue.jquery ? 191 | returnValue.pushStack( methodValue.get() ) : 192 | methodValue; 193 | return false; 194 | } 195 | }); 196 | } else { 197 | this.each(function() { 198 | var instance = $.data( this, fullName ); 199 | if ( instance ) { 200 | instance.option( options || {} )._init(); 201 | } else { 202 | $.data( this, fullName, new object( options, this ) ); 203 | } 204 | }); 205 | } 206 | 207 | return returnValue; 208 | }; 209 | }; 210 | 211 | $.Widget = function( /* options, element */ ) {}; 212 | $.Widget._childConstructors = []; 213 | 214 | $.Widget.prototype = { 215 | widgetName: "widget", 216 | widgetEventPrefix: "", 217 | defaultElement: "
    ", 218 | options: { 219 | disabled: false, 220 | 221 | // callbacks 222 | create: null 223 | }, 224 | _createWidget: function( options, element ) { 225 | element = $( element || this.defaultElement || this )[ 0 ]; 226 | this.element = $( element ); 227 | this.uuid = uuid++; 228 | this.eventNamespace = "." + this.widgetName + this.uuid; 229 | this.options = $.widget.extend( {}, 230 | this.options, 231 | this._getCreateOptions(), 232 | options ); 233 | 234 | this.bindings = $(); 235 | this.hoverable = $(); 236 | this.focusable = $(); 237 | 238 | if ( element !== this ) { 239 | $.data( element, this.widgetFullName, this ); 240 | this._on( true, this.element, { 241 | remove: function( event ) { 242 | if ( event.target === element ) { 243 | this.destroy(); 244 | } 245 | } 246 | }); 247 | this.document = $( element.style ? 248 | // element within the document 249 | element.ownerDocument : 250 | // element is window or document 251 | element.document || element ); 252 | this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); 253 | } 254 | 255 | this._create(); 256 | this._trigger( "create", null, this._getCreateEventData() ); 257 | this._init(); 258 | }, 259 | _getCreateOptions: $.noop, 260 | _getCreateEventData: $.noop, 261 | _create: $.noop, 262 | _init: $.noop, 263 | 264 | destroy: function() { 265 | this._destroy(); 266 | // we can probably remove the unbind calls in 2.0 267 | // all event bindings should go through this._on() 268 | this.element 269 | .unbind( this.eventNamespace ) 270 | // 1.9 BC for #7810 271 | // TODO remove dual storage 272 | .removeData( this.widgetName ) 273 | .removeData( this.widgetFullName ) 274 | // support: jquery <1.6.3 275 | // http://bugs.jquery.com/ticket/9413 276 | .removeData( $.camelCase( this.widgetFullName ) ); 277 | this.widget() 278 | .unbind( this.eventNamespace ) 279 | .removeAttr( "aria-disabled" ) 280 | .removeClass( 281 | this.widgetFullName + "-disabled " + 282 | "ui-state-disabled" ); 283 | 284 | // clean up events and states 285 | this.bindings.unbind( this.eventNamespace ); 286 | this.hoverable.removeClass( "ui-state-hover" ); 287 | this.focusable.removeClass( "ui-state-focus" ); 288 | }, 289 | _destroy: $.noop, 290 | 291 | widget: function() { 292 | return this.element; 293 | }, 294 | 295 | option: function( key, value ) { 296 | var options = key, 297 | parts, 298 | curOption, 299 | i; 300 | 301 | if ( arguments.length === 0 ) { 302 | // don't return a reference to the internal hash 303 | return $.widget.extend( {}, this.options ); 304 | } 305 | 306 | if ( typeof key === "string" ) { 307 | // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } 308 | options = {}; 309 | parts = key.split( "." ); 310 | key = parts.shift(); 311 | if ( parts.length ) { 312 | curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); 313 | for ( i = 0; i < parts.length - 1; i++ ) { 314 | curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; 315 | curOption = curOption[ parts[ i ] ]; 316 | } 317 | key = parts.pop(); 318 | if ( arguments.length === 1 ) { 319 | return curOption[ key ] === undefined ? null : curOption[ key ]; 320 | } 321 | curOption[ key ] = value; 322 | } else { 323 | if ( arguments.length === 1 ) { 324 | return this.options[ key ] === undefined ? null : this.options[ key ]; 325 | } 326 | options[ key ] = value; 327 | } 328 | } 329 | 330 | this._setOptions( options ); 331 | 332 | return this; 333 | }, 334 | _setOptions: function( options ) { 335 | var key; 336 | 337 | for ( key in options ) { 338 | this._setOption( key, options[ key ] ); 339 | } 340 | 341 | return this; 342 | }, 343 | _setOption: function( key, value ) { 344 | this.options[ key ] = value; 345 | 346 | if ( key === "disabled" ) { 347 | this.widget() 348 | .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) 349 | .attr( "aria-disabled", value ); 350 | this.hoverable.removeClass( "ui-state-hover" ); 351 | this.focusable.removeClass( "ui-state-focus" ); 352 | } 353 | 354 | return this; 355 | }, 356 | 357 | enable: function() { 358 | return this._setOption( "disabled", false ); 359 | }, 360 | disable: function() { 361 | return this._setOption( "disabled", true ); 362 | }, 363 | 364 | _on: function( suppressDisabledCheck, element, handlers ) { 365 | var delegateElement, 366 | instance = this; 367 | 368 | // no suppressDisabledCheck flag, shuffle arguments 369 | if ( typeof suppressDisabledCheck !== "boolean" ) { 370 | handlers = element; 371 | element = suppressDisabledCheck; 372 | suppressDisabledCheck = false; 373 | } 374 | 375 | // no element argument, shuffle and use this.element 376 | if ( !handlers ) { 377 | handlers = element; 378 | element = this.element; 379 | delegateElement = this.widget(); 380 | } else { 381 | // accept selectors, DOM elements 382 | element = delegateElement = $( element ); 383 | this.bindings = this.bindings.add( element ); 384 | } 385 | 386 | $.each( handlers, function( event, handler ) { 387 | function handlerProxy() { 388 | // allow widgets to customize the disabled handling 389 | // - disabled as an array instead of boolean 390 | // - disabled class as method for disabling individual parts 391 | if ( !suppressDisabledCheck && 392 | ( instance.options.disabled === true || 393 | $( this ).hasClass( "ui-state-disabled" ) ) ) { 394 | return; 395 | } 396 | return ( typeof handler === "string" ? instance[ handler ] : handler ) 397 | .apply( instance, arguments ); 398 | } 399 | 400 | // copy the guid so direct unbinding works 401 | if ( typeof handler !== "string" ) { 402 | handlerProxy.guid = handler.guid = 403 | handler.guid || handlerProxy.guid || $.guid++; 404 | } 405 | 406 | var match = event.match( /^(\w+)\s*(.*)$/ ), 407 | eventName = match[1] + instance.eventNamespace, 408 | selector = match[2]; 409 | if ( selector ) { 410 | delegateElement.delegate( selector, eventName, handlerProxy ); 411 | } else { 412 | element.bind( eventName, handlerProxy ); 413 | } 414 | }); 415 | }, 416 | 417 | _off: function( element, eventName ) { 418 | eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; 419 | element.unbind( eventName ).undelegate( eventName ); 420 | }, 421 | 422 | _delay: function( handler, delay ) { 423 | function handlerProxy() { 424 | return ( typeof handler === "string" ? instance[ handler ] : handler ) 425 | .apply( instance, arguments ); 426 | } 427 | var instance = this; 428 | return setTimeout( handlerProxy, delay || 0 ); 429 | }, 430 | 431 | _hoverable: function( element ) { 432 | this.hoverable = this.hoverable.add( element ); 433 | this._on( element, { 434 | mouseenter: function( event ) { 435 | $( event.currentTarget ).addClass( "ui-state-hover" ); 436 | }, 437 | mouseleave: function( event ) { 438 | $( event.currentTarget ).removeClass( "ui-state-hover" ); 439 | } 440 | }); 441 | }, 442 | 443 | _focusable: function( element ) { 444 | this.focusable = this.focusable.add( element ); 445 | this._on( element, { 446 | focusin: function( event ) { 447 | $( event.currentTarget ).addClass( "ui-state-focus" ); 448 | }, 449 | focusout: function( event ) { 450 | $( event.currentTarget ).removeClass( "ui-state-focus" ); 451 | } 452 | }); 453 | }, 454 | 455 | _trigger: function( type, event, data ) { 456 | var prop, orig, 457 | callback = this.options[ type ]; 458 | 459 | data = data || {}; 460 | event = $.Event( event ); 461 | event.type = ( type === this.widgetEventPrefix ? 462 | type : 463 | this.widgetEventPrefix + type ).toLowerCase(); 464 | // the original event may come from any element 465 | // so we need to reset the target on the new event 466 | event.target = this.element[ 0 ]; 467 | 468 | // copy original event properties over to the new event 469 | orig = event.originalEvent; 470 | if ( orig ) { 471 | for ( prop in orig ) { 472 | if ( !( prop in event ) ) { 473 | event[ prop ] = orig[ prop ]; 474 | } 475 | } 476 | } 477 | 478 | this.element.trigger( event, data ); 479 | return !( $.isFunction( callback ) && 480 | callback.apply( this.element[0], [ event ].concat( data ) ) === false || 481 | event.isDefaultPrevented() ); 482 | } 483 | }; 484 | 485 | $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { 486 | $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { 487 | if ( typeof options === "string" ) { 488 | options = { effect: options }; 489 | } 490 | var hasOptions, 491 | effectName = !options ? 492 | method : 493 | options === true || typeof options === "number" ? 494 | defaultEffect : 495 | options.effect || defaultEffect; 496 | options = options || {}; 497 | if ( typeof options === "number" ) { 498 | options = { duration: options }; 499 | } 500 | hasOptions = !$.isEmptyObject( options ); 501 | options.complete = callback; 502 | if ( options.delay ) { 503 | element.delay( options.delay ); 504 | } 505 | if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { 506 | element[ method ]( options ); 507 | } else if ( effectName !== method && element[ effectName ] ) { 508 | element[ effectName ]( options.duration, options.easing, callback ); 509 | } else { 510 | element.queue(function( next ) { 511 | $( this )[ method ](); 512 | if ( callback ) { 513 | callback.call( element[ 0 ] ); 514 | } 515 | next(); 516 | }); 517 | } 518 | }; 519 | }); 520 | 521 | })( jQuery ); 522 | --------------------------------------------------------------------------------