├── .gitattributes ├── capture.gif ├── refresh.png ├── code ├── images │ ├── icon.png │ ├── icon-16.png │ ├── icon-48.png │ ├── icon-64.png │ ├── loading.gif │ ├── close-32.png │ ├── gh-light-32.png │ └── logo.svg ├── js │ ├── safari │ │ └── popup.js │ ├── chrome │ │ ├── background.js │ │ ├── popup.js │ │ └── omnibox.js │ ├── libs │ │ ├── hogan-3.0.1.js │ │ └── typeahead.bundle.js │ └── content.js ├── chrome.json ├── firefox.json ├── html │ ├── chrome.html │ └── safari.html ├── Info.plist └── scss │ └── content.sass ├── store ├── firefox-addon.png ├── chrome-extension.png ├── safari-extension.png ├── chrome_store_screenshots_p1.png ├── chrome_store_screenshots_p2.jpg ├── github-autocomplete-awesome_920x680.png ├── github-awesome-autocomplete_1400x560.png └── github-awesome-autocomplete_440x280.png ├── sketch └── Awesome-Autocomp-GH.sketch ├── search-github-repositories-address-bar.png ├── .gitignore ├── lint-options.json ├── LICENSE ├── scripts ├── crxmake.sh └── release.sh ├── package.json ├── CONTRIBUTING.md ├── CHANGELOG.md ├── README.md └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | code/css/content.css -diff 2 | -------------------------------------------------------------------------------- /capture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/capture.gif -------------------------------------------------------------------------------- /refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/refresh.png -------------------------------------------------------------------------------- /code/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/code/images/icon.png -------------------------------------------------------------------------------- /code/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/code/images/icon-16.png -------------------------------------------------------------------------------- /code/images/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/code/images/icon-48.png -------------------------------------------------------------------------------- /code/images/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/code/images/icon-64.png -------------------------------------------------------------------------------- /code/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/code/images/loading.gif -------------------------------------------------------------------------------- /store/firefox-addon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/firefox-addon.png -------------------------------------------------------------------------------- /code/images/close-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/code/images/close-32.png -------------------------------------------------------------------------------- /store/chrome-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/chrome-extension.png -------------------------------------------------------------------------------- /store/safari-extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/safari-extension.png -------------------------------------------------------------------------------- /code/images/gh-light-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/code/images/gh-light-32.png -------------------------------------------------------------------------------- /sketch/Awesome-Autocomp-GH.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/sketch/Awesome-Autocomp-GH.sketch -------------------------------------------------------------------------------- /store/chrome_store_screenshots_p1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/chrome_store_screenshots_p1.png -------------------------------------------------------------------------------- /store/chrome_store_screenshots_p2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/chrome_store_screenshots_p2.jpg -------------------------------------------------------------------------------- /search-github-repositories-address-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/search-github-repositories-address-bar.png -------------------------------------------------------------------------------- /store/github-autocomplete-awesome_920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/github-autocomplete-awesome_920x680.png -------------------------------------------------------------------------------- /store/github-awesome-autocomplete_1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/github-awesome-autocomplete_1400x560.png -------------------------------------------------------------------------------- /store/github-awesome-autocomplete_440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algolia/github-awesome-autocomplete/HEAD/store/github-awesome-autocomplete_440x280.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | mykey.pem 3 | node_modules 4 | build/*.crx 5 | build/*.zip 6 | build/firefox 7 | build/*.xpi 8 | build/unpacked-dev 9 | build/unpacked-prod 10 | build/firefox-unpacked-dev 11 | build/firefox-unpacked-prod 12 | build/*.safariextension 13 | *.swp 14 | .sass-cache 15 | CertificateSigningRequest.certSigningRequest 16 | -------------------------------------------------------------------------------- /lint-options.json: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "latedef": true, 7 | "newcap": true, 8 | "noarg": true, 9 | "nonew": true, 10 | "undef": true, 11 | "unused": true, 12 | "trailing": true, 13 | "boss": true, 14 | "eqnull": true, 15 | "globals": { 16 | "beforeEach": true, 17 | "describe": true, 18 | "afterEach": true, 19 | "it": true, 20 | "chrome": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /code/js/safari/popup.js: -------------------------------------------------------------------------------- 1 | /* global document, safari */ 2 | document.getElementById('refresh-button').addEventListener('click', function() { 3 | safari.application.activeBrowserWindow.openTab().url = 'https://github.algolia.com/signin'; 4 | safari.self.hide(); 5 | return false; 6 | }); 7 | 8 | document.getElementById('reset-login').addEventListener('click', function() { 9 | // FIXME 10 | safari.self.hide(); 11 | return false; 12 | }); 13 | 14 | var gotoLink = function(link) { 15 | var newTab = safari.application.activeBrowserWindow.openTab(); 16 | newTab.url = link.getAttribute('href'); 17 | safari.self.hide(); 18 | }; 19 | 20 | var links = ['github-repository', 'github-issues', 'algolia-link']; 21 | for (var i = 0; i < links.length; ++i) { 22 | var link = document.getElementById(links[i]); 23 | link.addEventListener('click', gotoLink.bind(null, link)); 24 | } 25 | -------------------------------------------------------------------------------- /code/js/chrome/background.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // When the extension is installed or upgraded ... 6 | chrome.runtime.onInstalled.addListener(function() { 7 | // Replace all rules ... 8 | chrome.declarativeContent.onPageChanged.removeRules(undefined, function() { 9 | // With a new rule ... 10 | chrome.declarativeContent.onPageChanged.addRules([ 11 | { 12 | // That fires when a page's URL contains a 'g' ... 13 | conditions: [ 14 | new chrome.declarativeContent.PageStateMatcher({ 15 | pageUrl: { hostEquals: 'github.com' }, 16 | }) 17 | ], 18 | // And shows the extension's page action. 19 | actions: [ new chrome.declarativeContent.ShowPageAction() ] 20 | } 21 | ]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /code/js/chrome/popup.js: -------------------------------------------------------------------------------- 1 | /* global document, chrome, self */ 2 | 3 | document.getElementById('refresh-button').addEventListener('click', function() { 4 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 5 | chrome.tabs.sendMessage(tabs[0].id, {type: 'connect-with-github'}); 6 | }); 7 | return false; 8 | }); 9 | 10 | document.getElementById('reset-login').addEventListener('click', function() { 11 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 12 | chrome.tabs.sendMessage(tabs[0].id, {type: 'reset-login'}); 13 | }); 14 | return false; 15 | }); 16 | 17 | var gotoLink = function(link) { 18 | chrome.tabs.executeScript({ code: 'location.href="' + link.getAttribute('href') +'"' }); 19 | self.close(); 20 | }; 21 | 22 | var links = ['github-repository', 'github-issues', 'algolia-link']; 23 | for (var i = 0; i < links.length; ++i) { 24 | var link = document.getElementById(links[i]); 25 | link.addEventListener('click', gotoLink.bind(null, link)); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Algolia 4 | http://www.algolia.com/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /scripts/crxmake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Purpose: Pack a Chromium extension directory into crx format 4 | 5 | if test $# -ne 2; then 6 | echo "Usage: crxmake.sh " 7 | exit 1 8 | fi 9 | 10 | dir=$1 11 | key=$2 12 | name=$(basename "$dir") 13 | crx="$name.crx" 14 | pub="$name.pub" 15 | sig="$name.sig" 16 | zip="$name.zip" 17 | trap 'rm -f "$pub" "$sig" "$zip"' EXIT 18 | 19 | # zip up the crx dir 20 | cwd=$(pwd -P) 21 | (cd "$dir" && zip -qr -9 -X "$cwd/$zip" .) 22 | 23 | # signature 24 | openssl sha1 -sha1 -binary -sign "$key" < "$zip" > "$sig" 25 | 26 | # public key 27 | openssl rsa -pubout -outform DER < "$key" > "$pub" 2>/dev/null 28 | 29 | byte_swap () { 30 | # Take "abcdefgh" and return it as "ghefcdab" 31 | echo "${1:6:2}${1:4:2}${1:2:2}${1:0:2}" 32 | } 33 | 34 | crmagic_hex="4372 3234" # Cr24 35 | version_hex="0200 0000" # 2 36 | pub_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$pub" | awk '{print $5}'))) 37 | sig_len_hex=$(byte_swap $(printf '%08x\n' $(ls -l "$sig" | awk '{print $5}'))) 38 | ( 39 | echo "$crmagic_hex $version_hex $pub_len_hex $sig_len_hex" | xxd -r -p 40 | cat "$pub" "$sig" "$zip" 41 | ) > "$crx" 42 | echo "Wrote $crx" 43 | -------------------------------------------------------------------------------- /code/chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Awesome Autocomplete for GitHub", 3 | "description": "Add instant search capability to GitHub.", 4 | "background": { 5 | "scripts": [ 6 | "js/chrome/background.js", 7 | "js/libs/algoliasearch.js", 8 | "js/chrome/omnibox.js" 9 | ], 10 | "persistent": false 11 | }, 12 | "omnibox": { "keyword" : "aa" }, 13 | "manifest_version": 2, 14 | "icons": { 15 | "16": "images/icon-16.png", 16 | "48": "images/icon-48.png", 17 | "64": "images/icon-64.png", 18 | "128": "images/icon.png" 19 | }, 20 | "page_action": { 21 | "default_title": "Awesome Autocomplete for GitHub", 22 | "default_popup": "html/chrome.html", 23 | "default_icon" : "images/icon.png" 24 | }, 25 | "content_scripts": [{ 26 | "matches": [ "https://github.com/*" ], 27 | "js": [ 28 | "js/libs/jquery-3.1.1.min.js", 29 | "js/libs/hogan-3.0.1.js", 30 | "js/libs/typeahead.bundle.js", 31 | "js/libs/algoliasearch.js", 32 | "js/content.js" 33 | ], 34 | "css" : ["css/content.css"], 35 | "run_at": "document_end" 36 | }], 37 | "permissions": [ 38 | "https://github.com/*", 39 | "activeTab", 40 | "storage", 41 | "declarativeContent", 42 | "https://github.algolia.com/*", 43 | "*://*.algolia.net/*" 44 | ], 45 | "web_accessible_resources": [ "images/*" ] 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Awesome Autocomplete for GitHub", 3 | "name": "github-awesome-autocomplete", 4 | "version": "1.7.0", 5 | "description": "Add instant search capabilities to GitHub's search bar.", 6 | "homepage": "http://www.algolia.com", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "browserify": "~12.0.1", 10 | "chrome-webstore-upload-cli": "^1.1.1", 11 | "conventional-changelog-cli": "^1.3.4", 12 | "eventemitter2": "^0.4.14", 13 | "grunt": "^1.0.1", 14 | "grunt-browserify": "~4.0.1", 15 | "grunt-contrib-clean": "~0.7.0", 16 | "grunt-contrib-copy": "~0.8.2", 17 | "grunt-contrib-jshint": "~0.11.3", 18 | "grunt-contrib-uglify": "~0.11.0", 19 | "grunt-contrib-watch": "~0.6.1", 20 | "grunt-exec": "~0.4.6", 21 | "grunt-mkdir": "~0.1.2", 22 | "grunt-mocha-test": "~0.12.7", 23 | "grunt-sass": "^1.1.0", 24 | "jpm": "1.0.4", 25 | "mocha": "^2.4.5", 26 | "web-ext": "^2.2.2" 27 | }, 28 | "engines": { 29 | "node": ">= 0.10.0" 30 | }, 31 | "scripts": { 32 | "build": "grunt", 33 | "dev": "grunt dev", 34 | "test": "grunt test", 35 | "changelog": "conventional-changelog --preset angular --infile CHANGELOG.md --same-file", 36 | "changelog:unreleased": "conventional-changelog --preset angular --output-unreleased" 37 | }, 38 | "export-symbol": "extensionSkeleton.exports" 39 | } 40 | -------------------------------------------------------------------------------- /code/firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Awesome Autocomplete for GitHub", 3 | "applications": { 4 | "gecko": { 5 | "id": "github-awesome-autocomplete@algolia.com" 6 | } 7 | }, 8 | "description": "Add instant search capability to GitHub.", 9 | "manifest_version": 2, 10 | "background": { 11 | "scripts": [ 12 | "js/libs/algoliasearch.js", 13 | "js/chrome/omnibox.js" 14 | ] 15 | }, 16 | "icons": { 17 | "16": "images/icon-16.png", 18 | "48": "images/icon-48.png", 19 | "64": "images/icon-64.png", 20 | "128": "images/icon.png" 21 | }, 22 | "omnibox": { "keyword" : "aa" }, 23 | "browser_action": { 24 | "default_title": "Awesome Autocomplete for GitHub", 25 | "default_popup": "html/chrome.html", 26 | "default_icon" : "images/icon.png" 27 | }, 28 | "content_scripts": [{ 29 | "matches": [ "https://github.com/*" ], 30 | "js": [ 31 | "js/libs/jquery-3.1.1.min.js", 32 | "js/libs/hogan-3.0.1.js", 33 | "js/libs/typeahead.bundle.js", 34 | "js/libs/algoliasearch.js", 35 | "js/content.js" 36 | ], 37 | "css" : ["css/content.css"], 38 | "run_at": "document_end" 39 | }], 40 | "permissions": [ 41 | "https://github.com/*", 42 | "activeTab", 43 | "storage", 44 | "https://github.algolia.com/*", 45 | "*://*.algolia.net/*" 46 | ], 47 | "web_accessible_resources": [ "images/*" ] 48 | 49 | } 50 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | readonly CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 6 | if [ "$CURRENT_BRANCH" != master ]; then 7 | echo "You must be on 'master' branch to publish a release, aborting..." 8 | exit 1 9 | fi 10 | 11 | if ! git diff-index --quiet HEAD --; then 12 | echo "Working tree is not clean, aborting..." 13 | exit 1 14 | fi 15 | 16 | if ! yarn; then 17 | echo "Failed to install yarn dependencies, aborting..." 18 | exit 1 19 | fi 20 | 21 | yarn run changelog:unreleased 22 | 23 | # Only update the package.json version 24 | # We need to update changelog before tagging 25 | # And publishing. 26 | yarn version --no-git-tag-version 27 | 28 | if ! yarn run changelog; then 29 | echo "Failed to update changelog, aborting..." 30 | exit 1 31 | fi 32 | 33 | readonly PACKAGE_VERSION=$(< package.json grep version \ 34 | | head -1 \ 35 | | awk -F: '{ print $2 }' \ 36 | | sed 's/[",]//g' \ 37 | | tr -d '[:space:]') 38 | 39 | # Update version in code/Info.plist 40 | readonly SEMVER_REGEX="[[:digit:]]\.[[:digit:]]\.[[:digit:]]" 41 | sed -i " 42 | /CFBundleShortVersionString<\/key>/ { 43 | N 44 | s/\(\)$SEMVER_REGEX\(<\/string>\)/\1$PACKAGE_VERSION\2/ 45 | }" ./code/Info.plist 46 | 47 | # Build the archives so that they can be submitted 48 | if ! yarn run grunt; then 49 | echo "Build failed, aborting..." 50 | exit 1 51 | fi 52 | 53 | # Gives user a chance to review and eventually abort. 54 | git add --patch 55 | git commit --message="chore(release): $PACKAGE_VERSION" 56 | git push origin HEAD 57 | git tag "$PACKAGE_VERSION" 58 | git push --tags 59 | 60 | echo "Tagged version $PACKAGE_VERSION." 61 | echo "You now need to manually deploy the extensions on the different marketplaces." 62 | echo "The archives in ./build are ready to be published." 63 | echo "Find the instructions in the CONTRIBUTING.md file here: https://github.com/algolia/github-awesome-autocomplete/blob/master/CONTRIBUTING.md." 64 | -------------------------------------------------------------------------------- /code/html/chrome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 |
17 |

Awesome Autocomplete for GitHub

18 |
19 | 20 | 21 | 26 | 27 |

About this extension

28 |

Working everyday on building a powerful and user friendly search engine, we've become obsessed with search experience, assessing it on every website or mobile app we happen to land on. At Algolia we use GitHub everyday and their searchbar was a persistent discussion, so we remade it like we thought it should be for a daily use.

29 | 30 |

About Algolia

31 |

Algolia provides a developer-friendly SaaS API for database search. It enables any website or mobile app with content to easily provide their end-users with a fast and meaningful access to database objects. With Algolia instant search, people can search and explore your content in just a few keystrokes.

32 | 35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | ### Styling 4 | 5 | If you need to change the appearance of the dropdown, you can easily make it so that autocomplete dropdown doesn't close so that you can inspect it. 6 | 7 | To do so, simply comment out the three lines here: https://github.com/algolia/github-awesome-autocomplete/blob/master/code/js/libs/typeahead.bundle.js#L1523.L1525 8 | 9 | Make sure you re-build after making the change. 10 | Also, do not forget to remove the commented lines when committing the changes. 11 | 12 | ## RELEASING 13 | 14 | Make sure you are on a clean `master` branch. 15 | 16 | Run & follow the instructions: 17 | 18 | ```bash 19 | scripts/release.sh 20 | ``` 21 | This will do several things: 22 | - Ask for the new version 23 | - Update the version number throughout the codebase 24 | - Add the latest changes to the CHANGELOG.md 25 | - Commit everything to a new `chore(release): $PACKAGE_VERSION` commit 26 | - Push the changes to the git repository 27 | - Tag the release commit with the version number and push the tags to the repository 28 | 29 | After this, you still need to manually publish the extensions on the different marketplaces. 30 | All archives are ready in the `build` directory. 31 | 32 | ### Release Chrome extension 33 | 34 | **By leveraging the Chrome WebStore API endpoints (recommended):** 35 | 36 | ```bash 37 | yarn run webstore upload \ 38 | --source "build/unpacked-prod" \ 39 | --extension-id "djkfdjpoelphhdclfjhnffmnlnoknfnd" \ 40 | --client-id "to-be-filled" \ 41 | --client-secret "to-be-filled" \ 42 | --refresh-token "to-be-filled" \ 43 | --auto-publish 44 | ``` 45 | 46 | **Manually:** 47 | 48 | You need to be invited to the Google Group owning the extension. 49 | 50 | You can then send a new version from: https://chrome.google.com/webstore/developer/dashboard/u58fa06d3b25b4f42f07dbb3900b3ceb5 51 | 52 | ### Release Firefox extension 53 | 54 | Go here: https://addons.mozilla.org/en-US/developers/addon/github-awesome-autocomplete/versions/submit/agreement 55 | 56 | When asked for the archive, send the zip archive named: `build/firefox-x.x.x.zip`. 57 | 58 | Next steps are straight forward. 59 | 60 | ### Release Safari extension 61 | 62 | TODO 63 | -------------------------------------------------------------------------------- /code/html/safari.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 |
21 |

Awesome Autocomplete GitHub

22 |
23 | 24 | 25 | 30 | 31 |

About this extension

32 |

Working everyday on building a powerful and user friendly search engine, we've become obsessed with search experience, assessing it on every website or mobile app we happen to land on. At Algolia we use GitHub everyday and their searchbar was a persistent discussion, so we remade it like we thought it should be for a daily use.

33 | 34 |

About Algolia

35 |

Algolia provides a developer-friendly SaaS API for database search. It enables any website or mobile app with content to easily provide their end-users with a fast and meaningful access to database objects. With Algolia instant search, people can search and explore your content in just a few keystrokes.

36 | 39 |
40 |
41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /code/js/chrome/omnibox.js: -------------------------------------------------------------------------------- 1 | /* global AlgoliaSearch */ 2 | var algolia = new AlgoliaSearch( 3 | "TLCDTR8BIO", 4 | "686cce2f5dd3c38130b303e1c842c3e3" 5 | ); 6 | var repositories = algolia.initIndex("repositories"); 7 | 8 | // Provide help text to the user. 9 | // chrome.omnibox.setDefaultSuggestion({ 10 | // description: "Find GitHub repositories..." 11 | // }); 12 | 13 | var isFirefox = false; 14 | if (typeof browser !== 'undefined') { 15 | isFirefox = true; 16 | } 17 | 18 | function formatSuggestion(hit) { 19 | var content = hit._highlightResult.description.value; 20 | content = content.replace(/<(\/)?em>/g, "<$1match>"); 21 | 22 | var descriptionUrl = 23 | "https://github.com/" + 24 | hit._highlightResult.full_name.value + 25 | ""; 26 | descriptionUrl = descriptionUrl.replace(/<(\/)?em>/g, "<$1match>"); 27 | 28 | return { 29 | content: "https://github.com/" + hit.full_name, 30 | description: descriptionUrl + " " + content + "" 31 | }; 32 | } 33 | 34 | function formatFirefoxSuggestion(hit) { 35 | return { 36 | content: "https://github.com/" + hit.full_name, 37 | description: hit.full_name + ": " + hit.description 38 | }; 39 | } 40 | 41 | // Update the suggestions whenever the input is changed. 42 | chrome.omnibox.onInputChanged.addListener(function(text, addSuggestions) { 43 | repositories.search(text, function(success, content) { 44 | if (!success) { 45 | addSuggestions([ 46 | { 47 | content: "https://github.com/search?q=" + text, 48 | description: "An error occurred while fetching data from Algolia" 49 | } 50 | ]); 51 | return; 52 | } 53 | 54 | if (content.hits.length === 0) { 55 | addSuggestions([ 56 | { 57 | content: "https://github.com/search?q=" + text, 58 | description: "No results found, search on GitHub by pressing [ENTER]" 59 | } 60 | ]); 61 | return; 62 | } 63 | var hits = content.hits; 64 | var suggestions = []; 65 | 66 | for (var index in hits) { 67 | if (isFirefox) { 68 | suggestions.push(formatFirefoxSuggestion(hits[index])); 69 | } else { 70 | suggestions.push(formatSuggestion(hits[index])); 71 | } 72 | } 73 | addSuggestions(suggestions); 74 | }); 75 | }); 76 | 77 | // Open the page based on how the user clicks on a suggestion. 78 | chrome.omnibox.onInputEntered.addListener(function(text, disposition) { 79 | var url = text; 80 | 81 | if (url.lastIndexOf("https://github.com/", 0) !== 0) { 82 | url = "https://github.com/search?q=" + text; 83 | } 84 | 85 | switch (disposition) { 86 | case "currentTab": 87 | chrome.tabs.update({ url: url }); 88 | break; 89 | case "newForegroundTab": 90 | chrome.tabs.create({ url: url }); 91 | break; 92 | case "newBackgroundTab": 93 | chrome.tabs.create({ url: url, active: false }); 94 | break; 95 | } 96 | }); 97 | -------------------------------------------------------------------------------- /code/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | Algolia 7 | Builder Version 8 | 11601.3.9 9 | CFBundleDisplayName 10 | github-awesome-autocomplete 11 | CFBundleIdentifier 12 | com.algolia.github-awesome-autocomplete 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | 1.7.0 17 | CFBundleVersion 18 | 1 19 | Chrome 20 | 21 | Database Quota 22 | 1048576 23 | Popovers 24 | 25 | 26 | Filename 27 | html/safari.html 28 | Height 29 | 590 30 | Identifier 31 | algoliaPopover 32 | Width 33 | 400 34 | 35 | 36 | Toolbar Items 37 | 38 | 39 | Identifier 40 | algoliaPopover 41 | Image 42 | images/icon.png 43 | Include By Default 44 | 45 | Label 46 | algoliaButton 47 | Palette Label 48 | Algolia 49 | Popover 50 | algoliaPopover 51 | Tool Tip 52 | Connect Algolia with GitHub 53 | 54 | 55 | 56 | Content 57 | 58 | Scripts 59 | 60 | End 61 | 62 | js/libs/jquery-3.1.1.min.js 63 | js/libs/hogan-3.0.1.js 64 | js/libs/typeahead.bundle.js 65 | js/libs/algoliasearch.js 66 | js/content.js 67 | 68 | 69 | Stylesheets 70 | 71 | css/content.css 72 | 73 | Whitelist 74 | 75 | https://github.com/* 76 | https://github.algolia.com/* 77 | 78 | 79 | Description 80 | Add instant search capabilities to GitHub's search bar. 81 | DeveloperIdentifier 82 | 5RDFJYUV23 83 | ExtensionInfoDictionaryVersion 84 | 1.0 85 | Permissions 86 | 87 | Website Access 88 | 89 | Allowed Domains 90 | 91 | github.com 92 | 93 | Include Secure Pages 94 | 95 | Level 96 | Some 97 | 98 | 99 | Website 100 | https://www.algolia.com 101 | 102 | 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [1.7.0](https://github.com/algolia/github-awesome-autocomplete/compare/1.6.0...1.7.0) (2018-02-22) 3 | 4 | 5 | ### Features 6 | 7 | * **autocomplete:** add an key binding to search globally ([ee0cddf](https://github.com/algolia/github-awesome-autocomplete/commit/ee0cddf)) 8 | 9 | 10 | 11 | 12 | # [1.6.0](https://github.com/algolia/github-awesome-autocomplete/compare/1.5.0...1.6.0) (2017-11-08) 13 | 14 | 15 | ### Features 16 | 17 | * **omnibox:** allow searching repositories from the address bar ([0661fb0](https://github.com/algolia/github-awesome-autocomplete/commit/0661fb0)) 18 | 19 | 20 | 21 | 22 | # [1.5.0](https://github.com/algolia/github-awesome-autocomplete/compare/1.4.3...1.5.0) (2017-11-08) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **autocomplete:** ensure autocomplete is displayed above header actions ([847356a](https://github.com/algolia/github-awesome-autocomplete/commit/847356a)) 28 | 29 | 30 | ### Features 31 | 32 | * **assets:** update icons ([28999ad](https://github.com/algolia/github-awesome-autocomplete/commit/28999ad)) 33 | 34 | 35 | 36 | 37 | ## [1.4.3](https://github.com/algolia/github-awesome-autocomplete/compare/1.4.2...1.4.3) (2017-11-07) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * display full repo name on issues ([f6554d1](https://github.com/algolia/github-awesome-autocomplete/commit/f6554d1)) 43 | * limit web accessible resources to what is needed ([932d554](https://github.com/algolia/github-awesome-autocomplete/commit/932d554)) 44 | * **style:** restore repo name color to gray in issues ([b0a9efe](https://github.com/algolia/github-awesome-autocomplete/commit/b0a9efe)) 45 | 46 | 47 | 48 | CHANGELOG 49 | 50 | 2017-11-07 1.4.2 51 | * Add release dependencies 52 | * Add application ID to Firefox WebExtension manifest 53 | 54 | 2017-11-07 1.4.1 55 | * Make sure we use an unmodified version of jQuery 56 | 57 | 2017-11-07 1.4.0 58 | * Move to webextension for Firefox (#56) 59 | * Fixed white color for issue numbers 60 | 61 | 2017-08-18 1.3.4 62 | * Dropdown menu fine-tuning (enlarge it) 63 | 64 | 2017-02-13 1.3.3 65 | * Upgrade to jQuery 3.1.1 66 | 67 | 2017-02-13 1.3.2 68 | * Updating style to fit GH's latest black-header design 69 | 70 | 2017-01-04 1.3.1 71 | * Move to jQuery 2.0 to comply with Mozilla's new policies (https://github.com/mozilla/addons-linter/blob/master/docs/third-party-libraries.md) 72 | 73 | 2017-01-04 1.3.0 74 | * Move to the compliant `Awesome Autocomplete for GitHub` naming :) 75 | * Update the Algolia logo 76 | * Fixed the `Refresh Private repositories` feature 77 | * Add a `reset` button 78 | 79 | 2016-04-02 1.2.6 80 | * Update the CSS to fit GH's recent DOM updates. 81 | 82 | 2016-03-24 1.2.5 83 | * Update the code to the new GitHub CSS classes. 84 | 85 | 2016-03-17 1.2.4 86 | * Handle the new `.scoped-search` feature (search in "This organization" or "This repository") 87 | 88 | 2016-02-20 1.2.3 89 | * CSS fixes 90 | 91 | 2016-02-10 1.2.2 92 | * Cannot use `require` in the content-script on Firefox 93 | 94 | 2016-01-20 1.2.1 95 | * Firefox/Safari: remove remote font usage 96 | * Sanitize every single inputs 97 | 98 | 2016-01-14 1.2.0 99 | * Move to the new `users` and `repositories` indices which include projects & users created after 01/01/2015 100 | * Code cleanup 101 | 102 | 2015-08-09 1.1.4 103 | * Reflect GitHub's recent form changes 104 | 105 | 2015-05-14 1.1.3 106 | * Reflect GitHub's recent form/id changes 107 | 108 | 2015-01-11 1.1.2 109 | * Minor fixes 110 | 111 | 2015-01-08 1.1.1 112 | * Use non-minified sources 113 | * Removed JSONP support (security purpose) 114 | 115 | 2015-01-04 1.1.0 116 | * Firefox port 117 | * Private repositories & issues indexing using oauth 118 | 119 | 2014-12-23 1.0.0 120 | * Initial release 121 | -------------------------------------------------------------------------------- /code/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 26 | 27 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awesome Autocomplete for GitHub 2 | 3 | By working every day on building the best search engine, we've become obsessed with our own search experience on the websites and mobile applications we use. GitHub is quite big for us, we use their search bar every day but it was not optimal for our needs: so we just re-built Github's search the way we thought it should be and we now share it with the community via this [Chrome](https://chrome.google.com/webstore/detail/github-awesome-autocomple/djkfdjpoelphhdclfjhnffmnlnoknfnd), [Firefox](https://addons.mozilla.org/en-US/firefox/addon/github-awesome-autocomplete/) and [Safari](https://github.algolia.com/github-awesome-autocomplete.safariextz) extensions. 4 | 5 | Algolia provides a developer-friendly SaaS API for database search. It enables any website or mobile application to easily provide its end-users with an instant and relevant search. With Algolia's unique find as you type experience, users can find what they're looking for in just a few keystrokes. Feel free to give Algolia a try with our 14-days FREE trial at [Algolia](https://www.algolia.com). 6 | 7 | At [Algolia](https://www.algolia.com), we're git *addicts* and love using GitHub to store every single idea or project we work on. We use it both for our private and public repositories ([12 API clients](https://www.algolia.com/doc), [DocSearch](https://community.algolia.com/docsearch), [HN Search](https://github.com/algolia/hn-search) or various [d](https://github.com/algolia/instant-search-demo) [e](https://community.algolia.com/instantsearch.js/examples/media/) [m](https://community.algolia.com/instantsearch.js/examples/e-commerce/) [o](https://community.algolia.com/instantsearch.js/examples/tourism/)). 8 | 9 | ### Installation 10 | 11 | Install it from the stores: 12 | 13 | [![chrome](store/chrome-extension.png)](https://chrome.google.com/webstore/detail/github-awesome-autocomple/djkfdjpoelphhdclfjhnffmnlnoknfnd) 14 | [![firefox](store/firefox-addon.png)](https://addons.mozilla.org/en-US/firefox/addon/github-awesome-autocomplete/) 15 | [![safari](store/safari-extension.png)](https://github.algolia.com/github-awesome-autocomplete.safariextz) 16 | 17 | ### Features 18 | 19 | This extension replaces GitHub's search bar and add auto-completion (instant-search & suggestion) capabilities on: 20 | 21 | * top public repositories 22 | * last active users 23 | * your private repositories 24 | * default is without Algolia: done locally in your browser using vanilla JS search 25 | * ability to use Algolia (typo-tolerant & relevance improved) through a "Connect with GitHub" (oauth2) 26 | * your issues 27 | * only available if you choose to "Connect with GitHub" 28 | 29 | ![Algolia GitHub Awesome Autocomplete](https://cloud.githubusercontent.com/assets/9317857/22865228/d0428c68-f15f-11e6-86e5-ba4afa55e8b6.gif) 30 | 31 | From version 1.6.0, you can now also find GitHub repositories directly from the address bar, by typing `aa`. 32 | 33 | ![find GitHub repositories from the address bar](search-github-repositories-address-bar.png) 34 | 35 | *Address bar autocompletion does not work on Safari.* 36 | 37 | 38 | ### How does it work? 39 | 40 | * We continuously retrieve active repositories and users using [GitHub Archive](http://www.githubarchive.org/)'s dataset 41 | * Users and repositories are stored in 2 [Algolia](https://www.algolia.com/) indices: `users` and `repositories` 42 | * The results are fetched using [Algolia's JavaScript API client](https://github.com/algolia/algoliasearch-client-js) 43 | * The UI uses Twitter's [typeahead.js](http://twitter.github.io/typeahead.js/) library to display the auto-completion menu 44 | 45 | ### FAQ 46 | 47 | #### Are my private repositories sent somewhere? 48 | 49 | By default your list of private repositories remains in your local storage. You can allow us to crawl your private repositories with a "Connect with GitHub" (oauth2) action. Your private repositories are then stored securely in our index and only you will be able to search them. 50 | 51 | #### My private repository is not searchable, what can I do? 52 | 53 | You need to refresh your local list of private repositories: 54 | 55 | ![refresh](refresh.png) 56 | 57 | ## Development 58 | 59 | ### Installation 60 | 61 | ```sh 62 | $ git clone https://github.com/algolia/chrome-awesome-autocomplete.git 63 | 64 | # in case you don't have Grunt yet 65 | $ sudo npm install -g grunt-cli 66 | ``` 67 | 68 | ### Build instructions 69 | 70 | ```sh 71 | $ cd chrome-awesome-autocomplete 72 | 73 | # install dependencies 74 | $ npm install 75 | 76 | # generate your private key (required for Chrome) 77 | $ openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt > mykey.pem 78 | 79 | # build it 80 | $ grunt 81 | ``` 82 | 83 | When developing, write unit-tests, use `dev` Grunt task to check that your JS code passes linting tests and unit-tests. 84 | 85 | When ready to try out the extension in the browser, use default Grunt task to build it. In `build` directory you'll find develop version of the extension in `unpacked-dev` subdirectory (with source maps), and production (uglified) version in `unpacked-prod` directory. 86 | 87 | #### Chrome 88 | 89 | The `.crx` packed version is created from `unpacked-prod` sources. 90 | 91 | #### Firefox 92 | 93 | The `.zip` archive is created from `build/firefox-unpacked-prod`. 94 | 95 | #### Safari 96 | 97 | The `safariextz` archive is created from Safari. 98 | 99 | ### Grunt tasks 100 | 101 | * `clean`: clean `build` directory 102 | * `test`: JS-lint and mocha test, single run 103 | * `dev`: continuous `test` loop 104 | * default: `clean`, `test`, build step (copy all necessary files to `build` 105 | directory, browserify JS sources, prepare production version (using uglify), 106 | pack the `crx` and `xpi` 107 | 108 | ### Publishing 109 | 110 | All publishing instructions can be found in the [CONTRIBUTING.md file](CONTRIBUTING.md). 111 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | var pkg = grunt.file.readJSON('package.json'); 4 | var chrome_mnf = grunt.file.readJSON('code/chrome.json'); 5 | var firefox_mnf = grunt.file.readJSON('code/firefox.json'); 6 | 7 | var fileMaps = { browserify: {}, uglify: {} }; 8 | var file, files = grunt.file.expand({cwd:'code/js'}, ['*.js', 'chrome/*.js', 'safari/*.js']); 9 | for (var i = 0; i < files.length; i++) { 10 | file = files[i]; 11 | fileMaps.browserify['build/unpacked-dev/js/' + file] = 'code/js/' + file; 12 | fileMaps.uglify['build/unpacked-prod/js/' + file] = 'build/unpacked-dev/js/' + file; 13 | } 14 | 15 | // 16 | // config 17 | // 18 | 19 | grunt.initConfig({ 20 | 21 | clean: [ 22 | 'build/unpacked-dev', 23 | 'build/unpacked-prod', 24 | 'build/firefox-unpacked-dev', 25 | 'build/firefox-unpacked-prod', 26 | 'build/*.crx', 27 | 'build/*.safariextension', 28 | 'build/*.zip' 29 | ], 30 | 31 | mkdir: { 32 | unpacked: { options: { create: ['build/unpacked-dev', 'build/unpacked-prod', 'build/firefox-unpacked-dev', 'build/firefox-unpacked-prod', 'build/github-awesome-autocomplete.safariextension'] } }, 33 | js: { options: { create: ['build/unpacked-dev/js'] } }, 34 | css: { options: { create: ['build/unpacked-dev/css'] } } 35 | }, 36 | 37 | jshint: { 38 | options: grunt.file.readJSON('lint-options.json'), // see http://www.jshint.com/docs/options/ 39 | all: { src: ['package.json', 'lint-options.json', 'Gruntfile.js', 'code/**/*.js', 40 | 'code/**/*.json', '!code/js/libs/*'] } 41 | }, 42 | 43 | copy: { 44 | main: { files: [ { 45 | expand: true, 46 | cwd: 'code/', 47 | src: ['html/**', 'images/**', 'js/libs/**', 'Info.plist'], // Info.plist is used for Safari 48 | dest: 'build/unpacked-dev/' 49 | } ] }, 50 | firefox: { files: [ { 51 | expand: true, 52 | cwd: 'build/unpacked-dev/', 53 | src: ['**', '!Info.plist', '!js/chrome/background.js'], 54 | dest: 'build/firefox-unpacked-dev/' 55 | } ] }, 56 | safari: { files: [ { 57 | expand: true, 58 | cwd: 'build/unpacked-dev/', 59 | src: ['**'], 60 | dest: 'build/github-awesome-autocomplete.safariextension/' 61 | } ] }, 62 | prod: { files: [ { 63 | expand: true, 64 | cwd: 'build/unpacked-dev/', 65 | src: ['**', '!Info.plist'], 66 | dest: 'build/unpacked-prod/' 67 | } ] }, 68 | prodfirefox: { files: [ { 69 | expand: true, 70 | cwd: 'build/firefox-unpacked-dev/', 71 | src: ['**'], 72 | dest: 'build/firefox-unpacked-prod/' 73 | } ] }, 74 | artifact: { files: [ { 75 | expand: true, 76 | cwd: 'build/', 77 | src: [pkg.name + '-' + pkg.version + '.crx'], 78 | dest: process.env.CIRCLE_ARTIFACTS 79 | } ] } 80 | }, 81 | 82 | browserify: { 83 | build: { 84 | files: fileMaps.browserify, 85 | options: { 86 | browserifyOptions: { 87 | debug: true, // for source maps 88 | standalone: pkg['export-symbol'] 89 | } 90 | } 91 | } 92 | }, 93 | 94 | exec: { 95 | crx: { 96 | cmd: [ 97 | './scripts/crxmake.sh build/unpacked-prod ./mykey.pem', 98 | 'mv -v ./unpacked-prod.crx "build/' + pkg.name + '-' + pkg.version + '.crx"', 99 | '(cd build && zip -r "' + pkg.name + '-' + pkg.version + '.zip" unpacked-prod)' 100 | ].join(' && ') 101 | }, 102 | zip_firefox_extension: 'cd build/firefox-unpacked-prod && zip -r firefox-' + pkg.version + '.zip . && mv firefox-' + pkg.version + '.zip ../' 103 | }, 104 | 105 | uglify: { 106 | min: { files: fileMaps.uglify } 107 | }, 108 | 109 | watch: { 110 | js: { 111 | files: ['package.json', 'lint-options.json', 'Gruntfile.js', 'code/**/*.js', 112 | 'code/**/*.json', '!code/js/libs/*', 'code/**/*.sass', 'code/**/*.html'], 113 | tasks: ['default'] 114 | } 115 | }, 116 | 117 | sass: { 118 | dist: { 119 | options: { 120 | style: 'expanded' 121 | }, 122 | files: { 123 | 'build/unpacked-dev/css/content.css': 'code/scss/content.sass' 124 | } 125 | } 126 | } 127 | 128 | }); 129 | 130 | grunt.loadNpmTasks('grunt-contrib-clean'); 131 | grunt.loadNpmTasks('grunt-mkdir'); 132 | grunt.loadNpmTasks('grunt-contrib-jshint'); 133 | grunt.loadNpmTasks('grunt-mocha-test'); 134 | grunt.loadNpmTasks('grunt-contrib-copy'); 135 | grunt.loadNpmTasks('grunt-browserify'); 136 | grunt.loadNpmTasks('grunt-exec'); 137 | grunt.loadNpmTasks('grunt-contrib-uglify'); 138 | grunt.loadNpmTasks('grunt-contrib-watch'); 139 | grunt.loadNpmTasks('grunt-sass'); 140 | 141 | // 142 | // custom tasks 143 | // 144 | 145 | grunt.registerTask('manifests', 146 | 'Extend manifest.json with extra fields from package.json', 147 | function() { 148 | var fields = ['version', 'description']; 149 | for (var i = 0; i < fields.length; i++) { 150 | var field = fields[i]; 151 | chrome_mnf[field] = pkg[field]; 152 | firefox_mnf[field] = pkg[field]; 153 | } 154 | grunt.file.write('build/unpacked-dev/manifest.json', JSON.stringify(chrome_mnf, null, 4) + '\n'); 155 | grunt.file.write('build/unpacked-prod/manifest.json', JSON.stringify(chrome_mnf, null, 4) + '\n'); 156 | grunt.file.write('build/firefox-unpacked-dev/manifest.json', JSON.stringify(firefox_mnf, null, 4) + '\n'); 157 | grunt.file.write('build/firefox-unpacked-prod/manifest.json', JSON.stringify(firefox_mnf, null, 4) + '\n'); 158 | grunt.log.ok('chrome\'s & firefox manifest.json generated'); 159 | } 160 | ); 161 | 162 | // 163 | // testing-related tasks 164 | // 165 | 166 | grunt.registerTask('test', ['jshint']); 167 | grunt.registerTask('test-cont', ['default', 'watch']); 168 | grunt.registerTask('dev', ['test-cont']); 169 | 170 | // 171 | // DEFAULT 172 | // 173 | 174 | grunt.registerTask('default', ['clean', 'test', 'mkdir:css', 'sass', 'mkdir:unpacked', 'copy:main', 175 | 'mkdir:js', 'browserify', 'copy:prod', 'copy:firefox', 'copy:prodfirefox', 'copy:safari', 'manifests', 'uglify', 'exec']); 176 | 177 | }; 178 | -------------------------------------------------------------------------------- /code/scss/content.sass: -------------------------------------------------------------------------------- 1 | $gh-gray: #f5f5f5 2 | $gh-dark: #333333 3 | $gh-border-light: #EEEEEE 4 | $gh-border-dark: #DDDDDD 5 | $gh-text-color: #888888 6 | $gh-highlight-color: #FFFFC6 7 | $gh-primary-color: #3879D9 8 | 9 | .aa-popup 10 | .aa-popup-content 11 | padding: 20px 12 | h1 13 | text-align: center 14 | font-size: 1em 15 | line-height: 4em 16 | border-bottom: solid 1px $gh-border-light 17 | margin: 0 18 | h2 19 | float: left 20 | width: 100% 21 | margin: 1.8em 0 .5em 22 | font-size: 1.2em 23 | font-weight: 200 24 | color: $gh-text-color 25 | p 26 | float: left 27 | font-size: 1em 28 | line-height: 1.5em 29 | margin: .5em 0 30 | color: $gh-dark 31 | ul 32 | float: right 33 | list-style: none 34 | margin: 0 35 | padding: 10px 0 0 36 | li 37 | display: inline-block 38 | margin-left: 10px 39 | line-height: 2em 40 | button 41 | width: 100% 42 | font-size: 1.2em 43 | border-radius: 3px 44 | background-color: $gh-primary-color 45 | border: solid 1px darken(#3879D9,10%) 46 | color: white 47 | padding: 5px 10px 48 | cursor: pointer 49 | img 50 | vertical-align: bottom 51 | .aa-algolia-logo 52 | clear: both 53 | width: 100px 54 | display: block 55 | margin: 0 auto 56 | img 57 | width: 100% 58 | a 59 | color: $gh-primary-color 60 | text-decoration: none 61 | &:hover 62 | text-decoration: underline 63 | a:focus, button:focus 64 | outline: 0 !important 65 | box-shadow: none !important 66 | 67 | 68 | // common 69 | .awesome-autocomplete 70 | position: relative 71 | .tt-input 72 | width: 100% 73 | border: 0 74 | .tt-hint 75 | color: #999 76 | .tt-cursor 77 | background-color: #FFF9EA 78 | 79 | .tt-dropdown-menu 80 | font-weight: normal 81 | padding: 0 82 | border: 1px solid $gh-border-dark 83 | border-top: none 84 | background-color: white 85 | box-shadow: 0 3px 12px rgba(0,0,0,0.15) 86 | box-sizing: border-box; 87 | .octicon 88 | color: $gh-text-color 89 | .octicon.octicon-star 90 | vertical-align: middle 91 | .aa-query 92 | padding: 8px 93 | color: $gh-text-color 94 | display: block 95 | width: 100% 96 | white-space: nowrap 97 | overflow: hidden 98 | text-overflow: ellipsis 99 | .aa-query-cursor strong, .aa-query-default em 100 | color: $gh-dark 101 | .aa-branding 102 | padding: 10px 103 | text-align: right 104 | font-size: .9em 105 | color: $gh-text-color 106 | img 107 | height: 18px 108 | vertical-align: bottom 109 | svg 110 | margin-bottom: -4px 111 | .aa-category 112 | padding: 5px 8px 113 | font-weight: bold 114 | text-align: left 115 | background-color: $gh-gray 116 | color: $gh-text-color 117 | .tt-suggestion 118 | padding: 10px 10px 119 | text-align: left 120 | border-bottom: 1px solid $gh-border-light 121 | cursor: pointer 122 | &:last-child 123 | border-bottom: 0 124 | em 125 | font-style: normal 126 | background-color: $gh-highlight-color 127 | color: $gh-dark 128 | .aa-suggestion 129 | &:hover 130 | text-decoration: none 131 | .aa-name 132 | text-decoration: underline 133 | .aa-infos 134 | float: right 135 | line-height: 3em 136 | color: $gh-text-color 137 | font-size: .9em 138 | i 139 | font-size: 14px 140 | .aa-name 141 | font-size: 1.1em 142 | &.aa-user 143 | height: 35px 144 | .aa-thumbnail 145 | float: left 146 | position: relative 147 | top: 3px 148 | width: 30px 149 | height: 30px 150 | margin-right: 10px 151 | border-radius: 3px 152 | overflow: hidden 153 | background-color: $gh-gray 154 | img 155 | width: 100% 156 | .aa-name 157 | font-weight: bold 158 | .aa-login 159 | color: $gh-text-color 160 | .aa-company 161 | color: $gh-dark 162 | i 163 | font-size: 14px 164 | color: $gh-text-color 165 | &.aa-repo 166 | height: 3em 167 | .aa-thumbnail 168 | float: left 169 | position: relative 170 | top: 3px 171 | width: 30px 172 | height: 30px 173 | margin-right: 10px 174 | border-radius: 3px 175 | overflow: hidden 176 | background-color: $gh-gray 177 | img 178 | width: 100% 179 | .aa-name 180 | display: block 181 | white-space: nowrap 182 | overflow: hidden 183 | text-overflow: ellipsis 184 | font-weight: normal 185 | .aa-repo-name 186 | font-weight: bold 187 | .aa-description 188 | height: 1.3em 189 | display: block 190 | white-space: nowrap 191 | overflow: hidden 192 | text-overflow: ellipsis 193 | color: $gh-text-color 194 | &.aa-issue 195 | .aa-thumbnail 196 | float: left 197 | position: relative 198 | top: 3px 199 | width: 30px 200 | height: 30px 201 | margin-right: 10px 202 | border-radius: 3px 203 | overflow: hidden 204 | background-color: $gh-gray 205 | img 206 | width: 100% 207 | .aa-name 208 | display: block 209 | white-space: nowrap 210 | overflow: hidden 211 | text-overflow: ellipsis 212 | font-weight: normal 213 | .aa-repo-name 214 | color: $gh-text-color 215 | .aa-issue-number 216 | font-weight: bold 217 | color: $gh-text-color 218 | .aa-issue-body 219 | clear: both 220 | color: $gh-text-color 221 | overflow: hidden 222 | text-overflow: ellipsis 223 | &.aa-your-repo 224 | height: 1.5em 225 | .aa-name 226 | width: 85% 227 | display: block 228 | white-space: nowrap 229 | overflow: hidden 230 | text-overflow: ellipsis 231 | font-weight: normal 232 | .aa-repo 233 | font-weight: bold 234 | i 235 | font-size: 14px 236 | .tt-dataset-current-repo 237 | .tt-suggestion 238 | padding: 0 239 | .icon-delete 240 | -webkit-transition: opacity 80ms 241 | transition: opacity 80ms 242 | position: absolute 243 | padding: 0 244 | margin: 0 245 | display: block 246 | right: 6px 247 | bottom: 5px 248 | height: 16px 249 | width: 16px 250 | z-index: 100 251 | opacity: 0 252 | visibility: hidden 253 | &.active 254 | opacity: .5 255 | visibility: visible 256 | &.flex-table-item 257 | .icon-delete 258 | bottom: 8px 259 | right: 22px 260 | 261 | // top-menu search bar 262 | .js-site-search 263 | .awesome-autocomplete 264 | .twitter-typeahead 265 | width: 100% 266 | .tt-dropdown-menu 267 | margin-top: 6px 268 | width: 360px 269 | .js-site-search.scoped-search 270 | .header-search-wrapper 271 | position: relative 272 | .awesome-autocomplete 273 | .twitter-typeahead 274 | position: static 275 | width: 162px 276 | .tt-dropdown-menu 277 | width: 360px 278 | .site-header-nav-secondary 279 | .js-site-search-form 280 | .awesome-autocomplete 281 | .twitter-typeahead 282 | width: 160px 283 | .tt-dropdown-menu 284 | width: 360px 285 | .icon-delete 286 | bottom: 8px 287 | .site-header-nav-secondary .js-site-search-form .awesome-autocomplete .twitter-typeahead .tt-dropdown-menu 288 | margin-left: -200px 289 | .js-site-search .awesome-autocomplete.repo-scope .twitter-typeahead .tt-dropdown-menu 290 | margin-left: -110px 291 | .js-site-search .awesome-autocomplete.org-scope .twitter-typeahead .tt-dropdown-menu 292 | margin-left: -122px 293 | // results page search bar 294 | form#search_form 295 | .awesome-autocomplete 296 | .twitter-typeahead 297 | width: inherit 298 | .tt-input 299 | padding: 7px 8px !important 300 | border: 1px solid $gh-border-dark 301 | &:focus 302 | border-color: #51a7e8 303 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 5px rgba(81, 167, 232, 0.5); 304 | .tt-dropdown-menu 305 | width: 100% 306 | .pagehead .repohead-details-container ul.pagehead-actions 307 | z-index: initial 308 | -------------------------------------------------------------------------------- /code/js/libs/hogan-3.0.1.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2011 Twitter, Inc. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 17 | 18 | var Hogan = {}; 19 | 20 | (function (Hogan) { 21 | Hogan.Template = function (codeObj, text, compiler, options) { 22 | codeObj = codeObj || {}; 23 | this.r = codeObj.code || this.r; 24 | this.c = compiler; 25 | this.options = options || {}; 26 | this.text = text || ''; 27 | this.partials = codeObj.partials || {}; 28 | this.subs = codeObj.subs || {}; 29 | this.buf = ''; 30 | } 31 | 32 | window.Hogan = Hogan; 33 | 34 | Hogan.Template.prototype = { 35 | // render: replaced by generated code. 36 | r: function (context, partials, indent) { return ''; }, 37 | 38 | // variable escaping 39 | v: hoganEscape, 40 | 41 | // triple stache 42 | t: coerceToString, 43 | 44 | render: function render(context, partials, indent) { 45 | return this.ri([context], partials || {}, indent); 46 | }, 47 | 48 | // render internal -- a hook for overrides that catches partials too 49 | ri: function (context, partials, indent) { 50 | return this.r(context, partials, indent); 51 | }, 52 | 53 | // ensurePartial 54 | ep: function(symbol, partials) { 55 | var partial = this.partials[symbol]; 56 | 57 | // check to see that if we've instantiated this partial before 58 | var template = partials[partial.name]; 59 | if (partial.instance && partial.base == template) { 60 | return partial.instance; 61 | } 62 | 63 | if (typeof template == 'string') { 64 | if (!this.c) { 65 | throw new Error("No compiler available."); 66 | } 67 | template = this.c.compile(template, this.options); 68 | } 69 | 70 | if (!template) { 71 | return null; 72 | } 73 | 74 | // We use this to check whether the partials dictionary has changed 75 | this.partials[symbol].base = template; 76 | 77 | if (partial.subs) { 78 | // Make sure we consider parent template now 79 | if (!partials.stackText) partials.stackText = {}; 80 | for (key in partial.subs) { 81 | if (!partials.stackText[key]) { 82 | partials.stackText[key] = (this.activeSub !== undefined && partials.stackText[this.activeSub]) ? partials.stackText[this.activeSub] : this.text; 83 | } 84 | } 85 | template = createSpecializedPartial(template, partial.subs, partial.partials, 86 | this.stackSubs, this.stackPartials, partials.stackText); 87 | } 88 | this.partials[symbol].instance = template; 89 | 90 | return template; 91 | }, 92 | 93 | // tries to find a partial in the current scope and render it 94 | rp: function(symbol, context, partials, indent) { 95 | var partial = this.ep(symbol, partials); 96 | if (!partial) { 97 | return ''; 98 | } 99 | 100 | return partial.ri(context, partials, indent); 101 | }, 102 | 103 | // render a section 104 | rs: function(context, partials, section) { 105 | var tail = context[context.length - 1]; 106 | 107 | if (!isArray(tail)) { 108 | section(context, partials, this); 109 | return; 110 | } 111 | 112 | for (var i = 0; i < tail.length; i++) { 113 | context.push(tail[i]); 114 | section(context, partials, this); 115 | context.pop(); 116 | } 117 | }, 118 | 119 | // maybe start a section 120 | s: function(val, ctx, partials, inverted, start, end, tags) { 121 | var pass; 122 | 123 | if (isArray(val) && val.length === 0) { 124 | return false; 125 | } 126 | 127 | if (typeof val == 'function') { 128 | val = this.ms(val, ctx, partials, inverted, start, end, tags); 129 | } 130 | 131 | pass = !!val; 132 | 133 | if (!inverted && pass && ctx) { 134 | ctx.push((typeof val == 'object') ? val : ctx[ctx.length - 1]); 135 | } 136 | 137 | return pass; 138 | }, 139 | 140 | // find values with dotted names 141 | d: function(key, ctx, partials, returnFound) { 142 | var found, 143 | names = key.split('.'), 144 | val = this.f(names[0], ctx, partials, returnFound), 145 | doModelGet = this.options.modelGet, 146 | cx = null; 147 | 148 | if (key === '.' && isArray(ctx[ctx.length - 2])) { 149 | val = ctx[ctx.length - 1]; 150 | } else { 151 | for (var i = 1; i < names.length; i++) { 152 | found = findInScope(names[i], val, doModelGet); 153 | if (found != null) { 154 | cx = val; 155 | val = found; 156 | } else { 157 | val = ''; 158 | } 159 | } 160 | } 161 | 162 | if (returnFound && !val) { 163 | return false; 164 | } 165 | 166 | if (!returnFound && typeof val == 'function') { 167 | ctx.push(cx); 168 | val = this.mv(val, ctx, partials); 169 | ctx.pop(); 170 | } 171 | 172 | return val; 173 | }, 174 | 175 | // find values with normal names 176 | f: function(key, ctx, partials, returnFound) { 177 | var val = false, 178 | v = null, 179 | found = false, 180 | doModelGet = this.options.modelGet; 181 | 182 | for (var i = ctx.length - 1; i >= 0; i--) { 183 | v = ctx[i]; 184 | val = findInScope(key, v, doModelGet); 185 | if (val != null) { 186 | found = true; 187 | break; 188 | } 189 | } 190 | 191 | if (!found) { 192 | return (returnFound) ? false : ""; 193 | } 194 | 195 | if (!returnFound && typeof val == 'function') { 196 | val = this.mv(val, ctx, partials); 197 | } 198 | 199 | return val; 200 | }, 201 | 202 | // higher order templates 203 | ls: function(func, cx, partials, text, tags) { 204 | var oldTags = this.options.delimiters; 205 | 206 | this.options.delimiters = tags; 207 | this.b(this.ct(coerceToString(func.call(cx, text)), cx, partials)); 208 | this.options.delimiters = oldTags; 209 | 210 | return false; 211 | }, 212 | 213 | // compile text 214 | ct: function(text, cx, partials) { 215 | if (this.options.disableLambda) { 216 | throw new Error('Lambda features disabled.'); 217 | } 218 | return this.c.compile(text, this.options).render(cx, partials); 219 | }, 220 | 221 | // template result buffering 222 | b: function(s) { this.buf += s; }, 223 | 224 | fl: function() { var r = this.buf; this.buf = ''; return r; }, 225 | 226 | // method replace section 227 | ms: function(func, ctx, partials, inverted, start, end, tags) { 228 | var textSource, 229 | cx = ctx[ctx.length - 1], 230 | result = func.call(cx); 231 | 232 | if (typeof result == 'function') { 233 | if (inverted) { 234 | return true; 235 | } else { 236 | textSource = (this.activeSub && this.subsText && this.subsText[this.activeSub]) ? this.subsText[this.activeSub] : this.text; 237 | return this.ls(result, cx, partials, textSource.substring(start, end), tags); 238 | } 239 | } 240 | 241 | return result; 242 | }, 243 | 244 | // method replace variable 245 | mv: function(func, ctx, partials) { 246 | var cx = ctx[ctx.length - 1]; 247 | var result = func.call(cx); 248 | 249 | if (typeof result == 'function') { 250 | return this.ct(coerceToString(result.call(cx)), cx, partials); 251 | } 252 | 253 | return result; 254 | }, 255 | 256 | sub: function(name, context, partials, indent) { 257 | var f = this.subs[name]; 258 | if (f) { 259 | this.activeSub = name; 260 | f(context, partials, this, indent); 261 | this.activeSub = false; 262 | } 263 | } 264 | 265 | }; 266 | 267 | //Find a key in an object 268 | function findInScope(key, scope, doModelGet) { 269 | var val, checkVal; 270 | 271 | if (scope && typeof scope == 'object') { 272 | 273 | if (scope[key] != null) { 274 | val = scope[key]; 275 | 276 | // try lookup with get for backbone or similar model data 277 | } else if (doModelGet && scope.get && typeof scope.get == 'function') { 278 | val = scope.get(key); 279 | } 280 | } 281 | 282 | return val; 283 | } 284 | 285 | function createSpecializedPartial(instance, subs, partials, stackSubs, stackPartials, stackText) { 286 | function PartialTemplate() {}; 287 | PartialTemplate.prototype = instance; 288 | function Substitutions() {}; 289 | Substitutions.prototype = instance.subs; 290 | var key; 291 | var partial = new PartialTemplate(); 292 | partial.subs = new Substitutions(); 293 | partial.subsText = {}; //hehe. substext. 294 | partial.buf = ''; 295 | 296 | stackSubs = stackSubs || {}; 297 | partial.stackSubs = stackSubs; 298 | partial.subsText = stackText; 299 | for (key in subs) { 300 | if (!stackSubs[key]) stackSubs[key] = subs[key]; 301 | } 302 | for (key in stackSubs) { 303 | partial.subs[key] = stackSubs[key]; 304 | } 305 | 306 | stackPartials = stackPartials || {}; 307 | partial.stackPartials = stackPartials; 308 | for (key in partials) { 309 | if (!stackPartials[key]) stackPartials[key] = partials[key]; 310 | } 311 | for (key in stackPartials) { 312 | partial.partials[key] = stackPartials[key]; 313 | } 314 | 315 | return partial; 316 | } 317 | 318 | var rAmp = /&/g, 319 | rLt = //g, 321 | rApos = /\'/g, 322 | rQuot = /\"/g, 323 | hChars = /[&<>\"\']/; 324 | 325 | function coerceToString(val) { 326 | return String((val === null || val === undefined) ? '' : val); 327 | } 328 | 329 | function hoganEscape(str) { 330 | str = coerceToString(str); 331 | return hChars.test(str) ? 332 | str 333 | .replace(rAmp, '&') 334 | .replace(rLt, '<') 335 | .replace(rGt, '>') 336 | .replace(rApos, ''') 337 | .replace(rQuot, '"') : 338 | str; 339 | } 340 | 341 | var isArray = Array.isArray || function(a) { 342 | return Object.prototype.toString.call(a) === '[object Array]'; 343 | }; 344 | 345 | })(typeof exports !== 'undefined' ? exports : Hogan); 346 | 347 | 348 | 349 | (function (Hogan) { 350 | // Setup regex assignments 351 | // remove whitespace according to Mustache spec 352 | var rIsWhitespace = /\S/, 353 | rQuot = /\"/g, 354 | rNewline = /\n/g, 355 | rCr = /\r/g, 356 | rSlash = /\\/g; 357 | 358 | Hogan.tags = { 359 | '#': 1, '^': 2, '<': 3, '$': 4, 360 | '/': 5, '!': 6, '>': 7, '=': 8, '_v': 9, 361 | '{': 10, '&': 11, '_t': 12 362 | }; 363 | 364 | Hogan.scan = function scan(text, delimiters) { 365 | var len = text.length, 366 | IN_TEXT = 0, 367 | IN_TAG_TYPE = 1, 368 | IN_TAG = 2, 369 | state = IN_TEXT, 370 | tagType = null, 371 | tag = null, 372 | buf = '', 373 | tokens = [], 374 | seenTag = false, 375 | i = 0, 376 | lineStart = 0, 377 | otag = '{{', 378 | ctag = '}}'; 379 | 380 | function addBuf() { 381 | if (buf.length > 0) { 382 | tokens.push({tag: '_t', text: new String(buf)}); 383 | buf = ''; 384 | } 385 | } 386 | 387 | function lineIsWhitespace() { 388 | var isAllWhitespace = true; 389 | for (var j = lineStart; j < tokens.length; j++) { 390 | isAllWhitespace = 391 | (Hogan.tags[tokens[j].tag] < Hogan.tags['_v']) || 392 | (tokens[j].tag == '_t' && tokens[j].text.match(rIsWhitespace) === null); 393 | if (!isAllWhitespace) { 394 | return false; 395 | } 396 | } 397 | 398 | return isAllWhitespace; 399 | } 400 | 401 | function filterLine(haveSeenTag, noNewLine) { 402 | addBuf(); 403 | 404 | if (haveSeenTag && lineIsWhitespace()) { 405 | for (var j = lineStart, next; j < tokens.length; j++) { 406 | if (tokens[j].text) { 407 | if ((next = tokens[j+1]) && next.tag == '>') { 408 | // set indent to token value 409 | next.indent = tokens[j].text.toString() 410 | } 411 | tokens.splice(j, 1); 412 | } 413 | } 414 | } else if (!noNewLine) { 415 | tokens.push({tag:'\n'}); 416 | } 417 | 418 | seenTag = false; 419 | lineStart = tokens.length; 420 | } 421 | 422 | function changeDelimiters(text, index) { 423 | var close = '=' + ctag, 424 | closeIndex = text.indexOf(close, index), 425 | delimiters = trim( 426 | text.substring(text.indexOf('=', index) + 1, closeIndex) 427 | ).split(' '); 428 | 429 | otag = delimiters[0]; 430 | ctag = delimiters[delimiters.length - 1]; 431 | 432 | return closeIndex + close.length - 1; 433 | } 434 | 435 | if (delimiters) { 436 | delimiters = delimiters.split(' '); 437 | otag = delimiters[0]; 438 | ctag = delimiters[1]; 439 | } 440 | 441 | for (i = 0; i < len; i++) { 442 | if (state == IN_TEXT) { 443 | if (tagChange(otag, text, i)) { 444 | --i; 445 | addBuf(); 446 | state = IN_TAG_TYPE; 447 | } else { 448 | if (text.charAt(i) == '\n') { 449 | filterLine(seenTag); 450 | } else { 451 | buf += text.charAt(i); 452 | } 453 | } 454 | } else if (state == IN_TAG_TYPE) { 455 | i += otag.length - 1; 456 | tag = Hogan.tags[text.charAt(i + 1)]; 457 | tagType = tag ? text.charAt(i + 1) : '_v'; 458 | if (tagType == '=') { 459 | i = changeDelimiters(text, i); 460 | state = IN_TEXT; 461 | } else { 462 | if (tag) { 463 | i++; 464 | } 465 | state = IN_TAG; 466 | } 467 | seenTag = i; 468 | } else { 469 | if (tagChange(ctag, text, i)) { 470 | tokens.push({tag: tagType, n: trim(buf), otag: otag, ctag: ctag, 471 | i: (tagType == '/') ? seenTag - otag.length : i + ctag.length}); 472 | buf = ''; 473 | i += ctag.length - 1; 474 | state = IN_TEXT; 475 | if (tagType == '{') { 476 | if (ctag == '}}') { 477 | i++; 478 | } else { 479 | cleanTripleStache(tokens[tokens.length - 1]); 480 | } 481 | } 482 | } else { 483 | buf += text.charAt(i); 484 | } 485 | } 486 | } 487 | 488 | filterLine(seenTag, true); 489 | 490 | return tokens; 491 | } 492 | 493 | function cleanTripleStache(token) { 494 | if (token.n.substr(token.n.length - 1) === '}') { 495 | token.n = token.n.substring(0, token.n.length - 1); 496 | } 497 | } 498 | 499 | function trim(s) { 500 | if (s.trim) { 501 | return s.trim(); 502 | } 503 | 504 | return s.replace(/^\s*|\s*$/g, ''); 505 | } 506 | 507 | function tagChange(tag, text, index) { 508 | if (text.charAt(index) != tag.charAt(0)) { 509 | return false; 510 | } 511 | 512 | for (var i = 1, l = tag.length; i < l; i++) { 513 | if (text.charAt(index + i) != tag.charAt(i)) { 514 | return false; 515 | } 516 | } 517 | 518 | return true; 519 | } 520 | 521 | // the tags allowed inside super templates 522 | var allowedInSuper = {'_t': true, '\n': true, '$': true, '/': true}; 523 | 524 | function buildTree(tokens, kind, stack, customTags) { 525 | var instructions = [], 526 | opener = null, 527 | tail = null, 528 | token = null; 529 | 530 | tail = stack[stack.length - 1]; 531 | 532 | while (tokens.length > 0) { 533 | token = tokens.shift(); 534 | 535 | if (tail && tail.tag == '<' && !(token.tag in allowedInSuper)) { 536 | throw new Error('Illegal content in < super tag.'); 537 | } 538 | 539 | if (Hogan.tags[token.tag] <= Hogan.tags['$'] || isOpener(token, customTags)) { 540 | stack.push(token); 541 | token.nodes = buildTree(tokens, token.tag, stack, customTags); 542 | } else if (token.tag == '/') { 543 | if (stack.length === 0) { 544 | throw new Error('Closing tag without opener: /' + token.n); 545 | } 546 | opener = stack.pop(); 547 | if (token.n != opener.n && !isCloser(token.n, opener.n, customTags)) { 548 | throw new Error('Nesting error: ' + opener.n + ' vs. ' + token.n); 549 | } 550 | opener.end = token.i; 551 | return instructions; 552 | } else if (token.tag == '\n') { 553 | token.last = (tokens.length == 0) || (tokens[0].tag == '\n'); 554 | } 555 | 556 | instructions.push(token); 557 | } 558 | 559 | if (stack.length > 0) { 560 | throw new Error('missing closing tag: ' + stack.pop().n); 561 | } 562 | 563 | return instructions; 564 | } 565 | 566 | function isOpener(token, tags) { 567 | for (var i = 0, l = tags.length; i < l; i++) { 568 | if (tags[i].o == token.n) { 569 | token.tag = '#'; 570 | return true; 571 | } 572 | } 573 | } 574 | 575 | function isCloser(close, open, tags) { 576 | for (var i = 0, l = tags.length; i < l; i++) { 577 | if (tags[i].c == close && tags[i].o == open) { 578 | return true; 579 | } 580 | } 581 | } 582 | 583 | function stringifySubstitutions(obj) { 584 | var items = []; 585 | for (var key in obj) { 586 | items.push('"' + esc(key) + '": function(c,p,t,i) {' + obj[key] + '}'); 587 | } 588 | return "{ " + items.join(",") + " }"; 589 | } 590 | 591 | function stringifyPartials(codeObj) { 592 | var partials = []; 593 | for (var key in codeObj.partials) { 594 | partials.push('"' + esc(key) + '":{name:"' + esc(codeObj.partials[key].name) + '", ' + stringifyPartials(codeObj.partials[key]) + "}"); 595 | } 596 | return "partials: {" + partials.join(",") + "}, subs: " + stringifySubstitutions(codeObj.subs); 597 | } 598 | 599 | Hogan.stringify = function(codeObj, text, options) { 600 | return "{code: function (c,p,i) { " + Hogan.wrapMain(codeObj.code) + " }," + stringifyPartials(codeObj) + "}"; 601 | } 602 | 603 | var serialNo = 0; 604 | Hogan.generate = function(tree, text, options) { 605 | serialNo = 0; 606 | var context = { code: '', subs: {}, partials: {} }; 607 | Hogan.walk(tree, context); 608 | 609 | if (options.asString) { 610 | return this.stringify(context, text, options); 611 | } 612 | 613 | return this.makeTemplate(context, text, options); 614 | } 615 | 616 | Hogan.wrapMain = function(code) { 617 | return 'var t=this;t.b(i=i||"");' + code + 'return t.fl();'; 618 | } 619 | 620 | Hogan.template = Hogan.Template; 621 | 622 | Hogan.makeTemplate = function(codeObj, text, options) { 623 | var template = this.makePartials(codeObj); 624 | template.code = new Function('c', 'p', 'i', this.wrapMain(codeObj.code)); 625 | return new this.template(template, text, this, options); 626 | } 627 | 628 | Hogan.makePartials = function(codeObj) { 629 | var key, template = {subs: {}, partials: codeObj.partials, name: codeObj.name}; 630 | for (key in template.partials) { 631 | template.partials[key] = this.makePartials(template.partials[key]); 632 | } 633 | for (key in codeObj.subs) { 634 | template.subs[key] = new Function('c', 'p', 't', 'i', codeObj.subs[key]); 635 | } 636 | return template; 637 | } 638 | 639 | function esc(s) { 640 | return s.replace(rSlash, '\\\\') 641 | .replace(rQuot, '\\\"') 642 | .replace(rNewline, '\\n') 643 | .replace(rCr, '\\r'); 644 | } 645 | 646 | function chooseMethod(s) { 647 | return (~s.indexOf('.')) ? 'd' : 'f'; 648 | } 649 | 650 | function createPartial(node, context) { 651 | var prefix = "<" + (context.prefix || ""); 652 | var sym = prefix + node.n + serialNo++; 653 | context.partials[sym] = {name: node.n, partials: {}}; 654 | context.code += 't.b(t.rp("' + esc(sym) + '",c,p,"' + (node.indent || '') + '"));'; 655 | return sym; 656 | } 657 | 658 | Hogan.codegen = { 659 | '#': function(node, context) { 660 | context.code += 'if(t.s(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,1),' + 661 | 'c,p,0,' + node.i + ',' + node.end + ',"' + node.otag + " " + node.ctag + '")){' + 662 | 't.rs(c,p,' + 'function(c,p,t){'; 663 | Hogan.walk(node.nodes, context); 664 | context.code += '});c.pop();}'; 665 | }, 666 | 667 | '^': function(node, context) { 668 | context.code += 'if(!t.s(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,1),c,p,1,0,0,"")){'; 669 | Hogan.walk(node.nodes, context); 670 | context.code += '};'; 671 | }, 672 | 673 | '>': createPartial, 674 | '<': function(node, context) { 675 | var ctx = {partials: {}, code: '', subs: {}, inPartial: true}; 676 | Hogan.walk(node.nodes, ctx); 677 | var template = context.partials[createPartial(node, context)]; 678 | template.subs = ctx.subs; 679 | template.partials = ctx.partials; 680 | }, 681 | 682 | '$': function(node, context) { 683 | var ctx = {subs: {}, code: '', partials: context.partials, prefix: node.n}; 684 | Hogan.walk(node.nodes, ctx); 685 | context.subs[node.n] = ctx.code; 686 | if (!context.inPartial) { 687 | context.code += 't.sub("' + esc(node.n) + '",c,p,i);'; 688 | } 689 | }, 690 | 691 | '\n': function(node, context) { 692 | context.code += write('"\\n"' + (node.last ? '' : ' + i')); 693 | }, 694 | 695 | '_v': function(node, context) { 696 | context.code += 't.b(t.v(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,0)));'; 697 | }, 698 | 699 | '_t': function(node, context) { 700 | context.code += write('"' + esc(node.text) + '"'); 701 | }, 702 | 703 | '{': tripleStache, 704 | 705 | '&': tripleStache 706 | } 707 | 708 | function tripleStache(node, context) { 709 | context.code += 't.b(t.t(t.' + chooseMethod(node.n) + '("' + esc(node.n) + '",c,p,0)));'; 710 | } 711 | 712 | function write(s) { 713 | return 't.b(' + s + ');'; 714 | } 715 | 716 | Hogan.walk = function(nodelist, context) { 717 | var func; 718 | for (var i = 0, l = nodelist.length; i < l; i++) { 719 | func = Hogan.codegen[nodelist[i].tag]; 720 | func && func(nodelist[i], context); 721 | } 722 | return context; 723 | } 724 | 725 | Hogan.parse = function(tokens, text, options) { 726 | options = options || {}; 727 | return buildTree(tokens, '', [], options.sectionTags || []); 728 | } 729 | 730 | Hogan.cache = {}; 731 | 732 | Hogan.cacheKey = function(text, options) { 733 | return [text, !!options.asString, !!options.disableLambda, options.delimiters, !!options.modelGet].join('||'); 734 | } 735 | 736 | Hogan.compile = function(text, options) { 737 | options = options || {}; 738 | var key = Hogan.cacheKey(text, options); 739 | var template = this.cache[key]; 740 | 741 | if (template) { 742 | var partials = template.partials; 743 | for (var name in partials) { 744 | delete partials[name].instance; 745 | } 746 | return template; 747 | } 748 | 749 | template = this.generate(this.parse(this.scan(text, options.delimiters), text, options), text, options); 750 | return this.cache[key] = template; 751 | } 752 | })(typeof exports !== 'undefined' ? exports : Hogan); 753 | -------------------------------------------------------------------------------- /code/js/content.js: -------------------------------------------------------------------------------- 1 | /* global document, window, location, screen, self, $, AlgoliaSearch, Bloodhound, safari, Hogan */ 2 | 3 | ///////////////////////////// 4 | // 5 | // VARIABLES 6 | // 7 | ///////////////////////////// 8 | 9 | var NB_REPOS = 3; 10 | var NB_MY_REPOS = 2; 11 | var NB_USERS = 3; 12 | var NB_ISSUES = 3; 13 | 14 | ///////////////////////////// 15 | // 16 | // TEMPLATES 17 | // 18 | ///////////////////////////// 19 | 20 | var octiconStar = ''; 21 | var octiconFork = ''; 22 | var octiconRepo = ''; 23 | var octiconLock = ''; 24 | var octiconComment = ''; 25 | var octiconOrganization = ''; 26 | 27 | var templateYourRepo = Hogan.compile('
' + 28 | '' + 29 | '{{#fork}}' + octiconFork + '{{/fork}}{{^fork}}{{#private}}' + octiconLock + '{{/private}}{{^private}}' + octiconRepo + '{{/private}}{{/fork}} ' + 30 | '{{{ owner }}}/{{{ highlightedName }}}' + 31 | '' + 32 | '
'); 33 | var templateRepo = Hogan.compile('
' + 34 | '
' + 35 | '
' + octiconStar + ' {{ watchers }}
' + 36 | '' + 37 | '{{#is_fork}}' + octiconFork + '{{/is_fork}} ' + 38 | '{{^is_fork}}{{#is_private}}' + octiconLock + '{{/is_private}}{{^is_private}}' + octiconRepo + '{{/is_private}}{{/is_fork}} ' + 39 | '{{{ owner }}}/{{{ _highlightResult.name.value }}}' + 40 | '' + 41 | '
{{{ _snippetResult.description.value }}}
' + 42 | '
'); 43 | var templateIssue = Hogan.compile('
' + 44 | '
' + 45 | '
' + octiconComment + ' {{ comments_count }}
' + 46 | '' + 47 | 'Issue #{{ number }}: {{{ _highlightResult.title.value }}}
' + 48 | '{{#repository.is_fork}}' + octiconFork + '{{/repository.is_fork}} ' + 49 | '{{^repository.is_fork}}{{#repository.is_private}}' + octiconLock + '{{/repository.is_private}}{{^repository.is_private}}' + octiconRepo + '{{/repository.is_private}}{{/repository.is_fork}} ' + 50 | '{{{ _highlightResult.repository.full_name.value }}}' + 51 | '
' + 52 | '
{{{ _snippetResult.body.value }}}
' + 53 | '
'); 54 | var templateUser = Hogan.compile('
' + 55 | '
' + 56 | ''+ 57 | '{{#name}}{{{ _highlightResult.name.value }}} {{/name}}' + 58 | '' + 59 | '' + 60 | '{{#company}}
' + octiconOrganization + ' {{{ _highlightResult.company.value }}}{{/company}}' + 61 | '
'); 62 | 63 | ///////////////////////////// 64 | // 65 | // STORAGE & IMAGES 66 | // 67 | ///////////////////////////// 68 | 69 | var firefox = typeof self !== 'undefined' && typeof self.port !== 'undefined'; 70 | 71 | function getURL(asset) { 72 | if (firefox) { 73 | if (asset === 'images/logo.svg') { 74 | return self.options.logoUrl; 75 | } else if (asset === 'images/close-32.png') { 76 | return self.options.closeImgUrl; 77 | } else { 78 | return asset; 79 | } 80 | } else if (typeof chrome !== 'undefined') { 81 | return chrome.extension.getURL(asset); 82 | } else if (typeof safari !== 'undefined') { 83 | return safari.extension.baseURI + asset; 84 | } else { 85 | return asset; 86 | } 87 | } 88 | 89 | var simpleStorage = {}; 90 | var storage = { 91 | set: function(key, value) { 92 | if (firefox) { 93 | self.port.emit('update-storage', [key, value]); 94 | } else if (typeof chrome !== 'undefined') { 95 | var v = {}; 96 | v[key] = value; 97 | chrome.storage.local.set(v); 98 | } else { 99 | window.localStorage.setItem(key, value); 100 | } 101 | }, 102 | 103 | get: function(key, cb) { 104 | if (firefox) { 105 | self.port.emit('read-storage'); 106 | cb(simpleStorage); 107 | } else if (typeof chrome !== 'undefined') { 108 | chrome.storage.local.get(key, cb); 109 | } else { 110 | cb(window.localStorage.getItem(key)); 111 | } 112 | } 113 | }; 114 | 115 | ///////////////////////////// 116 | // 117 | // REPOSITORIES CRAWL 118 | // 119 | ///////////////////////////// 120 | 121 | var crawledRepositories = []; 122 | 123 | function crawlPage(url, organization) { 124 | $.get(url).done(function(data) { 125 | // parse HTML-based list of repositories 126 | $('
    ' + data + '
').find('li').each(function() { 127 | var isPrivate = $(this).hasClass('private'); 128 | var isFork = $(this).hasClass('fork'); 129 | var owner = $(this).find('span.owner').text() || $(this).find('span.repo-and-owner').attr('title').split('/')[0]; 130 | var name = $(this).find('span.repo').text(); 131 | var fullName = owner + '/' + name; 132 | crawledRepositories.push({ 133 | organization: organization, 134 | full_name: fullName, 135 | owner: owner, 136 | name: name, 137 | fork: isFork, 138 | private: isPrivate 139 | }); 140 | }); 141 | 142 | // save to local storage 143 | storage.set('crawledRepositories', crawledRepositories); 144 | }); 145 | } 146 | 147 | function crawlRepositories() { 148 | crawledRepositories = []; 149 | 150 | // crawl your repositories 151 | crawlPage('/dashboard/ajax_your_repos'); 152 | 153 | // get the homepage to fetch your list of organization 154 | $.ajax('/', { headers: { 'X-Requested-With' : 'fake' } }).done(function(data) { 155 | $(data).find('.select-menu-list a.select-menu-item').each(function() { 156 | var href = $(this).attr('href'); 157 | if (href.indexOf('/orgs/') === 0) { 158 | // and crawl all of them 159 | var organization = href.split('/')[2]; 160 | crawlPage('/organizations/' + organization + '/ajax_your_repos', organization); 161 | } 162 | }); 163 | }); 164 | } 165 | 166 | storage.get('crawledRepositories', function(result) { 167 | if (result && result.crawledRepositories && result.crawledRepositories.length > 0) { 168 | crawledRepositories = result.crawledRepositories; 169 | } else { 170 | crawlRepositories(); 171 | } 172 | }); 173 | 174 | ///////////////////////////// 175 | // 176 | // PRIVATE SEARCH 177 | // 178 | ///////////////////////////// 179 | 180 | var privateAlgolia, privateRepositories, privateIssues; 181 | 182 | function setupPrivate(data) { 183 | storage.set('private', data); 184 | if (data && data.uid && data.api_key) { 185 | privateAlgolia = new AlgoliaSearch('TLCDTR8BIO', data.api_key); 186 | privateAlgolia.setSecurityTags('user_' + data.uid); 187 | privateRepositories = privateAlgolia.initIndex('my_repositories_production'); 188 | privateIssues = privateAlgolia.initIndex('issues_production'); 189 | } 190 | } 191 | 192 | function reloadPrivate() { 193 | $.get('https://github.algolia.com/private?' + new Date().getTime(), function(data) { 194 | setupPrivate(data); 195 | }); 196 | } 197 | 198 | function connectWithGitHub() { 199 | var width = 1050; 200 | var height = 700; 201 | var left = (screen.width - width) / 2 - 16; 202 | var top = (screen.height - height) / 2 - 50; 203 | var windowFeatures = 'menubar=no,toolbar=no,status=no,width=' + width + ',height=' + height + ',left=' + left + ',top=' + top; 204 | if (typeof safari === 'undefined') { 205 | var win = window.open("https://github.algolia.com/signin", "authPopup", windowFeatures); 206 | win.onunload = function() { 207 | reloadPrivate(); 208 | }; 209 | } 210 | } 211 | 212 | function reset() { 213 | storage.set('private', {}); 214 | storage.set('crawledRepositories', []); 215 | $.get('https://github.algolia.com/reset'); 216 | } 217 | 218 | storage.get('private', function(result) { 219 | if (result) { 220 | setupPrivate(result.private); 221 | } 222 | if (!privateAlgolia) { 223 | reloadPrivate(); 224 | } 225 | }); 226 | 227 | if (firefox) { 228 | self.port.on('connect-with-github', connectWithGitHub); 229 | self.port.on('reset-login', reset); 230 | } else if (typeof chrome !== 'undefined') { 231 | chrome.runtime.onMessage.addListener(function(request) { 232 | if (request.type === 'connect-with-github') { 233 | connectWithGitHub(); 234 | } else if (request.type === 'reset-login') { 235 | reset(); 236 | } 237 | }); 238 | } else if (typeof safari !== 'undefined') { 239 | safari.self.addEventListener('message', function(message) { 240 | if (message.name === 'connect-with-github') { 241 | connectWithGitHub(); 242 | } else if (message.name === 'reset-login') { 243 | reset(); 244 | } 245 | }, false); 246 | } 247 | 248 | ///////////////////////////// 249 | // 250 | // HELPERS 251 | // 252 | ///////////////////////////// 253 | 254 | function tokenize(d) { 255 | if (!d) { 256 | return []; 257 | } 258 | var tokens = d.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/[^a-zA-Z0-9]/g, ' ').replace(/ +/g, ' ').toLowerCase().trim().split(' '); 259 | tokens.push(d.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()); 260 | return tokens; 261 | } 262 | 263 | // parse the DuckDuckGo-like query 264 | function parseQuery(q) { 265 | var p = { 266 | q: q, 267 | privateRepositories: true, 268 | topRepositories: true, 269 | issues: true, 270 | users: true 271 | }; 272 | if (q && q.length > 2 && q[0] === '!' && q[2] === ' ') { 273 | var command = q[1]; 274 | if (command === 'i') { 275 | p.privateRepositories = p.topRepositories = p.users = false; 276 | } else if (command === 'p') { 277 | p.issues = p.topRepositories = p.users = false; 278 | } else if (command === 'r') { 279 | p.issues = p.users = false; 280 | } else if (command === 'u') { 281 | p.issues = p.topRepositories = p.privateRepositories = false; 282 | } else { 283 | return p; 284 | } 285 | p.q = q.slice(3); 286 | } 287 | return p; 288 | } 289 | 290 | function sanitize(text) { 291 | return $('
').text(text).html(); 292 | } 293 | 294 | ///////////////////////////// 295 | // 296 | // TYPEAHEAD 297 | // 298 | ///////////////////////////// 299 | 300 | $(document).ready(function() { 301 | var $searchContainer = $('form#search_form, .js-site-search-form'); 302 | var $q = $searchContainer.find('input[name="q"]'); 303 | var $scopeBadge = $searchContainer.find('.header-search-scope'); 304 | var $scopedSearch = $q.closest('.header-search-wrapper'); 305 | 306 | function isRepository() { 307 | return $scopeBadge.is(':visible') && $scopeBadge.text() === 'This repository'; 308 | } 309 | 310 | function isOrganization() { 311 | return $scopeBadge.is(':visible') && $scopeBadge.text() === 'This organization'; 312 | } 313 | 314 | $q.parent().addClass('awesome-autocomplete'); 315 | $q.parent().append(''); 316 | 317 | // clear input 318 | var $clearInputIcon = $('.awesome-autocomplete .icon-delete'); 319 | $clearInputIcon.on('click touch', function(event) { 320 | event.preventDefault(); 321 | $q.typeahead('val', ''); 322 | $clearInputIcon.removeClass('active'); 323 | $q.focus(); 324 | }); 325 | 326 | // handle action 327 | var $form = $q.closest('form'); 328 | var submit = function(q, action) { 329 | action = action || $form.attr('action'); 330 | location.href = action + '?utf8=✓&q=' + encodeURIComponent(q); 331 | }; 332 | 333 | // public index 334 | var algolia = new AlgoliaSearch('TLCDTR8BIO', '686cce2f5dd3c38130b303e1c842c3e3'); 335 | var users = algolia.initIndex('users'); 336 | 337 | // setup auto-completion menu 338 | $q.typeahead({ highlight: false, hint: false }, [ 339 | // top-menu help 340 | { 341 | source: function(q, cb) { cb([]); }, // force empty 342 | name: 'default', 343 | templates: { 344 | empty: function(data) { 345 | return '
Press <Enter> to ' + 346 | 'search for "' + sanitize(data.query) + '"' + 347 | (isRepository() ? ' in this repository' : '') + 348 | (isOrganization() ? ' in this organization' : '') + 349 | '' + '
Press <Shift+Enter> to ' + 350 | 'search for "' + sanitize(data.query) + '"' +' in all repositories'+ 351 | '' + 352 | '' + 353 | '
'; 354 | } 355 | } 356 | }, 357 | // this repository/organization 358 | { 359 | source: function(q, cb) { 360 | var hits = []; 361 | if (isRepository() || isOrganization()) { 362 | hits.push({ query: q }); 363 | } 364 | cb(hits); 365 | }, 366 | name: 'current-repo', 367 | displayKey: 'query', 368 | templates: { 369 | suggestion: function(hit) { 370 | return '
' + octiconRepo + '  Search "' + sanitize(hit.query) + '" in ' + (isRepository() ? 'this repository' : 'this organization') + '
'; 371 | } 372 | } 373 | }, 374 | // private repositories 375 | { 376 | source: function(q, cb) { 377 | var parsedQuery = parseQuery(q); 378 | if (!parsedQuery.privateRepositories) { 379 | cb([]); 380 | return; 381 | } 382 | if (privateRepositories) { 383 | privateRepositories.search(parsedQuery.q, function(success, content) { 384 | if (success) { 385 | for (var i = 0; i < content.hits.length; ++i) { 386 | var hit = content.hits[i]; 387 | hit.query = q; 388 | hit.watchers = hit.watchers_count; 389 | } 390 | cb(content.hits); 391 | } else { 392 | cb([]); 393 | } 394 | }, { attributesToSnippet: ['description:50'], hitsPerPage: NB_MY_REPOS }); 395 | } else { 396 | var engine = new Bloodhound({ 397 | name: 'private', 398 | local: crawledRepositories, 399 | datumTokenizer: function(d) { return tokenize(d.owner).concat(tokenize(d.name)); }, 400 | queryTokenizer: tokenize, 401 | limit: NB_MY_REPOS 402 | }); 403 | engine.initialize().done(function() { 404 | engine.get(parsedQuery.q, function(suggestions) { 405 | var queryWords = tokenize(parsedQuery.q); 406 | var re = new RegExp('(' + $.map(queryWords, function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); }).join('|') + ')', 'ig'); 407 | for (var i = 0; i < suggestions.length; ++i) { 408 | var sugg = suggestions[i]; 409 | sugg.highlightedName = sugg.name.replace(re, '$1'); 410 | sugg.query = q; 411 | } 412 | cb(suggestions); 413 | }); 414 | }); 415 | } 416 | }, 417 | name: 'private', 418 | displayKey: 'query', 419 | templates: { 420 | header: '
Your Repositories
', 421 | suggestion: function(hit) { 422 | var template = privateRepositories ? templateRepo : templateYourRepo; 423 | return template.render(hit); 424 | } 425 | } 426 | }, 427 | // top repositories 428 | { 429 | source: function(q, cb) { 430 | var parsedQuery = parseQuery(q); 431 | if (!parsedQuery.topRepositories) { 432 | cb([]); 433 | return; 434 | } 435 | var params = { attributesToSnippet: ['description:50'] }; 436 | algolia.startQueriesBatch(); 437 | algolia.addQueryInBatch('repositories', parsedQuery.q, $.extend({ hitsPerPage: parseInt(NB_REPOS / 2 + 1, 10), numericFilters: 'watchers>1000', restrictSearchableAttributes: 'name' }, params)); 438 | algolia.addQueryInBatch('repositories', parsedQuery.q, $.extend({ hitsPerPage: NB_REPOS }, params)); 439 | algolia.sendQueriesBatch(function(success, content) { 440 | var suggestions = []; 441 | if (success) { 442 | var dedup = {}; 443 | for (var i = 0; i < content.results.length && suggestions.length < NB_REPOS; ++i) { 444 | for (var j = 0; j < content.results[i].hits.length && suggestions.length < NB_REPOS; ++j) { 445 | var hit = content.results[i].hits[j]; 446 | if (dedup[hit.objectID]) { 447 | continue; 448 | } 449 | dedup[hit.objectID] = true; 450 | hit.query = q; 451 | suggestions.push(hit); 452 | } 453 | } 454 | } 455 | cb(suggestions); 456 | }); 457 | }, 458 | name: 'repos', 459 | displayKey: 'query', 460 | templates: { 461 | header: '
Top Repositories
', 462 | suggestion: function(hit) { return templateRepo.render(hit); } 463 | } 464 | }, 465 | // private issues 466 | { 467 | source: function(q, cb) { 468 | var parsedQuery = parseQuery(q); 469 | if (!parsedQuery.issues) { 470 | cb([]); 471 | return; 472 | } 473 | if (privateIssues) { 474 | privateIssues.search(parsedQuery.q, function(success, content) { 475 | if (success) { 476 | for (var i = 0; i < content.hits.length; ++i) { 477 | var hit = content.hits[i]; 478 | hit.query = q; 479 | } 480 | cb(content.hits); 481 | } else { 482 | cb([]); 483 | } 484 | }, { attributesToSnippet: ['body:20'], hitsPerPage: NB_ISSUES }); 485 | } else { 486 | cb([]); 487 | } 488 | }, 489 | name: 'issues', 490 | displayKey: 'query', 491 | templates: { 492 | header: '
Your Issues
', 493 | suggestion: function(hit) { return templateIssue.render(hit); } 494 | } 495 | }, 496 | // users 497 | { 498 | source: function(q, cb) { 499 | var parsedQuery = parseQuery(q); 500 | if (!parsedQuery.users) { 501 | cb([]); 502 | return; 503 | } 504 | users.search(parsedQuery.q, function(success, content) { 505 | var hits = []; 506 | if (success) { 507 | for (var i = 0; i < content.hits.length; ++i) { 508 | var hit = content.hits[i]; 509 | hit.query = q; 510 | hits.push(hit); 511 | } 512 | } 513 | cb(hits); 514 | }, { hitsPerPage: NB_USERS, attributesToRetrieve: ['login', 'name', 'id', 'company', 'followers'] }); 515 | }, 516 | name: 'users', 517 | displayKey: 'query', 518 | templates: { 519 | header: '
Last Active Users
', 520 | suggestion: function(hit) { return templateUser.render(hit); } 521 | } 522 | }, 523 | // branding 524 | { 525 | source: function(q, cb) { cb([]); }, // force empty 526 | name: 'branding', 527 | templates: { 528 | empty: function() { 529 | return '
' + 530 | 'With from ' + 531 | '
'; 532 | } 533 | } 534 | } 535 | ]).on('typeahead:selected', function(event, suggestion, dataset) { 536 | if (dataset === 'current-repo') { 537 | submit(suggestion.query, $('.js-site-search-form').data('data-scoped-search-url')); 538 | } else if (dataset === 'users') { 539 | location.href = 'https://github.com/' + suggestion.login; 540 | } else if (dataset === 'repos' || dataset === 'private') { 541 | location.href = 'https://github.com/' + suggestion.full_name; 542 | } else if (dataset === 'issues') { 543 | location.href = 'https://github.com/' + suggestion.repository.owner + '/' + suggestion.repository.name + '/issues/' + suggestion.number; 544 | } else { 545 | console.log('Unknown dataset: ' + dataset); 546 | } 547 | }).on('typeahead:cursorchanged', function(event, suggestion, dataset) { 548 | var $container = $('.aa-query'); 549 | $container.find('span').hide(); 550 | if (dataset === 'users') { 551 | $container.find('span.aa-query-cursor').html('go to ' + sanitize(suggestion.login) + '\'s profile').show(); 552 | } else if (dataset === 'repos' || dataset === 'private') { 553 | $container.find('span.aa-query-cursor').html('go to ' + sanitize(suggestion.full_name) + '').show(); 554 | } else if (dataset === 'issues') { 555 | $container.find('span.aa-query-cursor').html('go to ' + sanitize(suggestion.repository.owner) + '/' + sanitize(suggestion.repository.name) + ' #' + sanitize(suggestion.number) + '').show(); 556 | } else { 557 | $container.find('span.aa-query-default').show(); 558 | } 559 | }).on('keyup', function() { 560 | if ($(this).val().length > 0) { 561 | $clearInputIcon.addClass('active'); 562 | } else { 563 | $clearInputIcon.removeClass('active'); 564 | } 565 | }).on('keypress', function(e) { 566 | $scopedSearch.removeClass('repo-scope org-scope'); 567 | if (isRepository()) { 568 | $scopedSearch.addClass('repo-scope'); 569 | } else if (isOrganization()) { 570 | $scopedSearch.addClass('org-scope'); 571 | } 572 | if (e.keyCode === 13) { // enter 573 | if (e.shiftKey) { 574 | submit($(this).val(), $('.js-site-search-form').data('unscoped-search-url')); 575 | e.preventDefault(); 576 | } 577 | else { 578 | submit($(this).val()); 579 | } 580 | } 581 | }); 582 | }); 583 | -------------------------------------------------------------------------------- /code/js/libs/typeahead.bundle.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * typeahead.js 0.10.4 3 | * https://github.com/twitter/typeahead.js 4 | * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT 5 | */ 6 | 7 | (function($) { 8 | var _ = function() { 9 | "use strict"; 10 | return { 11 | isMsie: function() { 12 | return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; 13 | }, 14 | isBlankString: function(str) { 15 | return !str || /^\s*$/.test(str); 16 | }, 17 | escapeRegExChars: function(str) { 18 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 19 | }, 20 | isString: function(obj) { 21 | return typeof obj === "string"; 22 | }, 23 | isNumber: function(obj) { 24 | return typeof obj === "number"; 25 | }, 26 | isArray: $.isArray, 27 | isFunction: $.isFunction, 28 | isObject: $.isPlainObject, 29 | isUndefined: function(obj) { 30 | return typeof obj === "undefined"; 31 | }, 32 | toStr: function toStr(s) { 33 | return _.isUndefined(s) || s === null ? "" : s + ""; 34 | }, 35 | bind: $.proxy, 36 | each: function(collection, cb) { 37 | $.each(collection, reverseArgs); 38 | function reverseArgs(index, value) { 39 | return cb(value, index); 40 | } 41 | }, 42 | map: $.map, 43 | filter: $.grep, 44 | every: function(obj, test) { 45 | var result = true; 46 | if (!obj) { 47 | return result; 48 | } 49 | $.each(obj, function(key, val) { 50 | if (!(result = test.call(null, val, key, obj))) { 51 | return false; 52 | } 53 | }); 54 | return !!result; 55 | }, 56 | some: function(obj, test) { 57 | var result = false; 58 | if (!obj) { 59 | return result; 60 | } 61 | $.each(obj, function(key, val) { 62 | if (result = test.call(null, val, key, obj)) { 63 | return false; 64 | } 65 | }); 66 | return !!result; 67 | }, 68 | mixin: $.extend, 69 | getUniqueId: function() { 70 | var counter = 0; 71 | return function() { 72 | return counter++; 73 | }; 74 | }(), 75 | templatify: function templatify(obj) { 76 | return $.isFunction(obj) ? obj : template; 77 | function template() { 78 | return String(obj); 79 | } 80 | }, 81 | defer: function(fn) { 82 | setTimeout(fn, 0); 83 | }, 84 | debounce: function(func, wait, immediate) { 85 | var timeout, result; 86 | return function() { 87 | var context = this, args = arguments, later, callNow; 88 | later = function() { 89 | timeout = null; 90 | if (!immediate) { 91 | result = func.apply(context, args); 92 | } 93 | }; 94 | callNow = immediate && !timeout; 95 | clearTimeout(timeout); 96 | timeout = setTimeout(later, wait); 97 | if (callNow) { 98 | result = func.apply(context, args); 99 | } 100 | return result; 101 | }; 102 | }, 103 | throttle: function(func, wait) { 104 | var context, args, timeout, result, previous, later; 105 | previous = 0; 106 | later = function() { 107 | previous = new Date(); 108 | timeout = null; 109 | result = func.apply(context, args); 110 | }; 111 | return function() { 112 | var now = new Date(), remaining = wait - (now - previous); 113 | context = this; 114 | args = arguments; 115 | if (remaining <= 0) { 116 | clearTimeout(timeout); 117 | timeout = null; 118 | previous = now; 119 | result = func.apply(context, args); 120 | } else if (!timeout) { 121 | timeout = setTimeout(later, remaining); 122 | } 123 | return result; 124 | }; 125 | }, 126 | noop: function() {} 127 | }; 128 | }(); 129 | var VERSION = "0.10.4"; 130 | var tokenizers = function() { 131 | "use strict"; 132 | return { 133 | nonword: nonword, 134 | whitespace: whitespace, 135 | obj: { 136 | nonword: getObjTokenizer(nonword), 137 | whitespace: getObjTokenizer(whitespace) 138 | } 139 | }; 140 | function whitespace(str) { 141 | str = _.toStr(str); 142 | return str ? str.split(/\s+/) : []; 143 | } 144 | function nonword(str) { 145 | str = _.toStr(str); 146 | return str ? str.split(/\W+/) : []; 147 | } 148 | function getObjTokenizer(tokenizer) { 149 | return function setKey() { 150 | var args = [].slice.call(arguments, 0); 151 | return function tokenize(o) { 152 | var tokens = []; 153 | _.each(args, function(k) { 154 | tokens = tokens.concat(tokenizer(_.toStr(o[k]))); 155 | }); 156 | return tokens; 157 | }; 158 | }; 159 | } 160 | }(); 161 | var LruCache = function() { 162 | "use strict"; 163 | function LruCache(maxSize) { 164 | this.maxSize = _.isNumber(maxSize) ? maxSize : 100; 165 | this.reset(); 166 | if (this.maxSize <= 0) { 167 | this.set = this.get = $.noop; 168 | } 169 | } 170 | _.mixin(LruCache.prototype, { 171 | set: function set(key, val) { 172 | var tailItem = this.list.tail, node; 173 | if (this.size >= this.maxSize) { 174 | this.list.remove(tailItem); 175 | delete this.hash[tailItem.key]; 176 | } 177 | if (node = this.hash[key]) { 178 | node.val = val; 179 | this.list.moveToFront(node); 180 | } else { 181 | node = new Node(key, val); 182 | this.list.add(node); 183 | this.hash[key] = node; 184 | this.size++; 185 | } 186 | }, 187 | get: function get(key) { 188 | var node = this.hash[key]; 189 | if (node) { 190 | this.list.moveToFront(node); 191 | return node.val; 192 | } 193 | }, 194 | reset: function reset() { 195 | this.size = 0; 196 | this.hash = {}; 197 | this.list = new List(); 198 | } 199 | }); 200 | function List() { 201 | this.head = this.tail = null; 202 | } 203 | _.mixin(List.prototype, { 204 | add: function add(node) { 205 | if (this.head) { 206 | node.next = this.head; 207 | this.head.prev = node; 208 | } 209 | this.head = node; 210 | this.tail = this.tail || node; 211 | }, 212 | remove: function remove(node) { 213 | node.prev ? node.prev.next = node.next : this.head = node.next; 214 | node.next ? node.next.prev = node.prev : this.tail = node.prev; 215 | }, 216 | moveToFront: function(node) { 217 | this.remove(node); 218 | this.add(node); 219 | } 220 | }); 221 | function Node(key, val) { 222 | this.key = key; 223 | this.val = val; 224 | this.prev = this.next = null; 225 | } 226 | return LruCache; 227 | }(); 228 | var PersistentStorage = function() { 229 | "use strict"; 230 | var ls, methods; 231 | try { 232 | ls = window.localStorage; 233 | ls.setItem("~~~", "!"); 234 | ls.removeItem("~~~"); 235 | } catch (err) { 236 | ls = null; 237 | } 238 | function PersistentStorage(namespace) { 239 | this.prefix = [ "__", namespace, "__" ].join(""); 240 | this.ttlKey = "__ttl__"; 241 | this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); 242 | } 243 | if (ls && window.JSON) { 244 | methods = { 245 | _prefix: function(key) { 246 | return this.prefix + key; 247 | }, 248 | _ttlKey: function(key) { 249 | return this._prefix(key) + this.ttlKey; 250 | }, 251 | get: function(key) { 252 | if (this.isExpired(key)) { 253 | this.remove(key); 254 | } 255 | return decode(ls.getItem(this._prefix(key))); 256 | }, 257 | set: function(key, val, ttl) { 258 | if (_.isNumber(ttl)) { 259 | ls.setItem(this._ttlKey(key), encode(now() + ttl)); 260 | } else { 261 | ls.removeItem(this._ttlKey(key)); 262 | } 263 | return ls.setItem(this._prefix(key), encode(val)); 264 | }, 265 | remove: function(key) { 266 | ls.removeItem(this._ttlKey(key)); 267 | ls.removeItem(this._prefix(key)); 268 | return this; 269 | }, 270 | clear: function() { 271 | var i, key, keys = [], len = ls.length; 272 | for (i = 0; i < len; i++) { 273 | if ((key = ls.key(i)).match(this.keyMatcher)) { 274 | keys.push(key.replace(this.keyMatcher, "")); 275 | } 276 | } 277 | for (i = keys.length; i--; ) { 278 | this.remove(keys[i]); 279 | } 280 | return this; 281 | }, 282 | isExpired: function(key) { 283 | var ttl = decode(ls.getItem(this._ttlKey(key))); 284 | return _.isNumber(ttl) && now() > ttl ? true : false; 285 | } 286 | }; 287 | } else { 288 | methods = { 289 | get: _.noop, 290 | set: _.noop, 291 | remove: _.noop, 292 | clear: _.noop, 293 | isExpired: _.noop 294 | }; 295 | } 296 | _.mixin(PersistentStorage.prototype, methods); 297 | return PersistentStorage; 298 | function now() { 299 | return new Date().getTime(); 300 | } 301 | function encode(val) { 302 | return JSON.stringify(_.isUndefined(val) ? null : val); 303 | } 304 | function decode(val) { 305 | return JSON.parse(val); 306 | } 307 | }(); 308 | var Transport = function() { 309 | "use strict"; 310 | var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); 311 | function Transport(o) { 312 | o = o || {}; 313 | this.cancelled = false; 314 | this.lastUrl = null; 315 | this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax; 316 | this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get; 317 | this._cache = o.cache === false ? new LruCache(0) : sharedCache; 318 | } 319 | Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { 320 | maxPendingRequests = num; 321 | }; 322 | Transport.resetCache = function resetCache() { 323 | sharedCache.reset(); 324 | }; 325 | _.mixin(Transport.prototype, { 326 | _get: function(url, o, cb) { 327 | var that = this, jqXhr; 328 | if (this.cancelled || url !== this.lastUrl) { 329 | return; 330 | } 331 | if (jqXhr = pendingRequests[url]) { 332 | jqXhr.done(done).fail(fail); 333 | } else if (pendingRequestsCount < maxPendingRequests) { 334 | pendingRequestsCount++; 335 | pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always); 336 | } else { 337 | this.onDeckRequestArgs = [].slice.call(arguments, 0); 338 | } 339 | function done(resp) { 340 | cb && cb(null, resp); 341 | that._cache.set(url, resp); 342 | } 343 | function fail() { 344 | cb && cb(true); 345 | } 346 | function always() { 347 | pendingRequestsCount--; 348 | delete pendingRequests[url]; 349 | if (that.onDeckRequestArgs) { 350 | that._get.apply(that, that.onDeckRequestArgs); 351 | that.onDeckRequestArgs = null; 352 | } 353 | } 354 | }, 355 | get: function(url, o, cb) { 356 | var resp; 357 | if (_.isFunction(o)) { 358 | cb = o; 359 | o = {}; 360 | } 361 | this.cancelled = false; 362 | this.lastUrl = url; 363 | if (resp = this._cache.get(url)) { 364 | _.defer(function() { 365 | cb && cb(null, resp); 366 | }); 367 | } else { 368 | this._get(url, o, cb); 369 | } 370 | return !!resp; 371 | }, 372 | cancel: function() { 373 | this.cancelled = true; 374 | } 375 | }); 376 | return Transport; 377 | function callbackToDeferred(fn) { 378 | return function customSendWrapper(url, o) { 379 | var deferred = $.Deferred(); 380 | fn(url, o, onSuccess, onError); 381 | return deferred; 382 | function onSuccess(resp) { 383 | _.defer(function() { 384 | deferred.resolve(resp); 385 | }); 386 | } 387 | function onError(err) { 388 | _.defer(function() { 389 | deferred.reject(err); 390 | }); 391 | } 392 | }; 393 | } 394 | }(); 395 | var SearchIndex = function() { 396 | "use strict"; 397 | function SearchIndex(o) { 398 | o = o || {}; 399 | if (!o.datumTokenizer || !o.queryTokenizer) { 400 | $.error("datumTokenizer and queryTokenizer are both required"); 401 | } 402 | this.datumTokenizer = o.datumTokenizer; 403 | this.queryTokenizer = o.queryTokenizer; 404 | this.reset(); 405 | } 406 | _.mixin(SearchIndex.prototype, { 407 | bootstrap: function bootstrap(o) { 408 | this.datums = o.datums; 409 | this.trie = o.trie; 410 | }, 411 | add: function(data) { 412 | var that = this; 413 | data = _.isArray(data) ? data : [ data ]; 414 | _.each(data, function(datum) { 415 | var id, tokens; 416 | id = that.datums.push(datum) - 1; 417 | tokens = normalizeTokens(that.datumTokenizer(datum)); 418 | _.each(tokens, function(token) { 419 | var node, chars, ch; 420 | node = that.trie; 421 | chars = token.split(""); 422 | while (ch = chars.shift()) { 423 | node = node.children[ch] || (node.children[ch] = newNode()); 424 | node.ids.push(id); 425 | } 426 | }); 427 | }); 428 | }, 429 | get: function get(query) { 430 | var that = this, tokens, matches; 431 | tokens = normalizeTokens(this.queryTokenizer(query)); 432 | _.each(tokens, function(token) { 433 | var node, chars, ch, ids; 434 | if (matches && matches.length === 0) { 435 | return false; 436 | } 437 | node = that.trie; 438 | chars = token.split(""); 439 | while (node && (ch = chars.shift())) { 440 | node = node.children[ch]; 441 | } 442 | if (node && chars.length === 0) { 443 | ids = node.ids.slice(0); 444 | matches = matches ? getIntersection(matches, ids) : ids; 445 | } else { 446 | matches = []; 447 | return false; 448 | } 449 | }); 450 | return matches ? _.map(unique(matches), function(id) { 451 | return that.datums[id]; 452 | }) : []; 453 | }, 454 | reset: function reset() { 455 | this.datums = []; 456 | this.trie = newNode(); 457 | }, 458 | serialize: function serialize() { 459 | return { 460 | datums: this.datums, 461 | trie: this.trie 462 | }; 463 | } 464 | }); 465 | return SearchIndex; 466 | function normalizeTokens(tokens) { 467 | tokens = _.filter(tokens, function(token) { 468 | return !!token; 469 | }); 470 | tokens = _.map(tokens, function(token) { 471 | return token.toLowerCase(); 472 | }); 473 | return tokens; 474 | } 475 | function newNode() { 476 | return { 477 | ids: [], 478 | children: {} 479 | }; 480 | } 481 | function unique(array) { 482 | var seen = {}, uniques = []; 483 | for (var i = 0, len = array.length; i < len; i++) { 484 | if (!seen[array[i]]) { 485 | seen[array[i]] = true; 486 | uniques.push(array[i]); 487 | } 488 | } 489 | return uniques; 490 | } 491 | function getIntersection(arrayA, arrayB) { 492 | var ai = 0, bi = 0, intersection = []; 493 | arrayA = arrayA.sort(compare); 494 | arrayB = arrayB.sort(compare); 495 | var lenArrayA = arrayA.length, lenArrayB = arrayB.length; 496 | while (ai < lenArrayA && bi < lenArrayB) { 497 | if (arrayA[ai] < arrayB[bi]) { 498 | ai++; 499 | } else if (arrayA[ai] > arrayB[bi]) { 500 | bi++; 501 | } else { 502 | intersection.push(arrayA[ai]); 503 | ai++; 504 | bi++; 505 | } 506 | } 507 | return intersection; 508 | function compare(a, b) { 509 | return a - b; 510 | } 511 | } 512 | }(); 513 | var oParser = function() { 514 | "use strict"; 515 | return { 516 | local: getLocal, 517 | prefetch: getPrefetch, 518 | remote: getRemote 519 | }; 520 | function getLocal(o) { 521 | return o.local || null; 522 | } 523 | function getPrefetch(o) { 524 | var prefetch, defaults; 525 | defaults = { 526 | url: null, 527 | thumbprint: "", 528 | ttl: 24 * 60 * 60 * 1e3, 529 | filter: null, 530 | ajax: {} 531 | }; 532 | if (prefetch = o.prefetch || null) { 533 | prefetch = _.isString(prefetch) ? { 534 | url: prefetch 535 | } : prefetch; 536 | prefetch = _.mixin(defaults, prefetch); 537 | prefetch.thumbprint = VERSION + prefetch.thumbprint; 538 | prefetch.ajax.type = prefetch.ajax.type || "GET"; 539 | prefetch.ajax.dataType = prefetch.ajax.dataType || "json"; 540 | !prefetch.url && $.error("prefetch requires url to be set"); 541 | } 542 | return prefetch; 543 | } 544 | function getRemote(o) { 545 | var remote, defaults; 546 | defaults = { 547 | url: null, 548 | cache: true, 549 | wildcard: "%QUERY", 550 | replace: null, 551 | rateLimitBy: "debounce", 552 | rateLimitWait: 300, 553 | send: null, 554 | filter: null, 555 | ajax: {} 556 | }; 557 | if (remote = o.remote || null) { 558 | remote = _.isString(remote) ? { 559 | url: remote 560 | } : remote; 561 | remote = _.mixin(defaults, remote); 562 | remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); 563 | remote.ajax.type = remote.ajax.type || "GET"; 564 | remote.ajax.dataType = remote.ajax.dataType || "json"; 565 | delete remote.rateLimitBy; 566 | delete remote.rateLimitWait; 567 | !remote.url && $.error("remote requires url to be set"); 568 | } 569 | return remote; 570 | function byDebounce(wait) { 571 | return function(fn) { 572 | return _.debounce(fn, wait); 573 | }; 574 | } 575 | function byThrottle(wait) { 576 | return function(fn) { 577 | return _.throttle(fn, wait); 578 | }; 579 | } 580 | } 581 | }(); 582 | (function(root) { 583 | "use strict"; 584 | var old, keys; 585 | old = root.Bloodhound; 586 | keys = { 587 | data: "data", 588 | protocol: "protocol", 589 | thumbprint: "thumbprint" 590 | }; 591 | root.Bloodhound = Bloodhound; 592 | function Bloodhound(o) { 593 | if (!o || !o.local && !o.prefetch && !o.remote) { 594 | $.error("one of local, prefetch, or remote is required"); 595 | } 596 | this.limit = o.limit || 5; 597 | this.sorter = getSorter(o.sorter); 598 | this.dupDetector = o.dupDetector || ignoreDuplicates; 599 | this.local = oParser.local(o); 600 | this.prefetch = oParser.prefetch(o); 601 | this.remote = oParser.remote(o); 602 | this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null; 603 | this.index = new SearchIndex({ 604 | datumTokenizer: o.datumTokenizer, 605 | queryTokenizer: o.queryTokenizer 606 | }); 607 | this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null; 608 | } 609 | Bloodhound.noConflict = function noConflict() { 610 | root.Bloodhound = old; 611 | return Bloodhound; 612 | }; 613 | Bloodhound.tokenizers = tokenizers; 614 | _.mixin(Bloodhound.prototype, { 615 | _loadPrefetch: function loadPrefetch(o) { 616 | var that = this, serialized, deferred; 617 | if (serialized = this._readFromStorage(o.thumbprint)) { 618 | this.index.bootstrap(serialized); 619 | deferred = $.Deferred().resolve(); 620 | } else { 621 | deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse); 622 | } 623 | return deferred; 624 | function handlePrefetchResponse(resp) { 625 | that.clear(); 626 | that.add(o.filter ? o.filter(resp) : resp); 627 | that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl); 628 | } 629 | }, 630 | _getFromRemote: function getFromRemote(query, cb) { 631 | var that = this, url, uriEncodedQuery; 632 | if (!this.transport) { 633 | return; 634 | } 635 | query = query || ""; 636 | uriEncodedQuery = encodeURIComponent(query); 637 | url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery); 638 | return this.transport.get(url, this.remote.ajax, handleRemoteResponse); 639 | function handleRemoteResponse(err, resp) { 640 | err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp); 641 | } 642 | }, 643 | _cancelLastRemoteRequest: function cancelLastRemoteRequest() { 644 | this.transport && this.transport.cancel(); 645 | }, 646 | _saveToStorage: function saveToStorage(data, thumbprint, ttl) { 647 | if (this.storage) { 648 | this.storage.set(keys.data, data, ttl); 649 | this.storage.set(keys.protocol, location.protocol, ttl); 650 | this.storage.set(keys.thumbprint, thumbprint, ttl); 651 | } 652 | }, 653 | _readFromStorage: function readFromStorage(thumbprint) { 654 | var stored = {}, isExpired; 655 | if (this.storage) { 656 | stored.data = this.storage.get(keys.data); 657 | stored.protocol = this.storage.get(keys.protocol); 658 | stored.thumbprint = this.storage.get(keys.thumbprint); 659 | } 660 | isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol; 661 | return stored.data && !isExpired ? stored.data : null; 662 | }, 663 | _initialize: function initialize() { 664 | var that = this, local = this.local, deferred; 665 | deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve(); 666 | local && deferred.done(addLocalToIndex); 667 | this.transport = this.remote ? new Transport(this.remote) : null; 668 | return this.initPromise = deferred.promise(); 669 | function addLocalToIndex() { 670 | that.add(_.isFunction(local) ? local() : local); 671 | } 672 | }, 673 | initialize: function initialize(force) { 674 | return !this.initPromise || force ? this._initialize() : this.initPromise; 675 | }, 676 | add: function add(data) { 677 | this.index.add(data); 678 | }, 679 | get: function get(query, cb) { 680 | var that = this, matches = [], cacheHit = false; 681 | matches = this.index.get(query); 682 | matches = this.sorter(matches).slice(0, this.limit); 683 | matches.length < this.limit ? cacheHit = this._getFromRemote(query, returnRemoteMatches) : this._cancelLastRemoteRequest(); 684 | if (!cacheHit) { 685 | (matches.length > 0 || !this.transport) && cb && cb(matches); 686 | } 687 | function returnRemoteMatches(remoteMatches) { 688 | var matchesWithBackfill = matches.slice(0); 689 | _.each(remoteMatches, function(remoteMatch) { 690 | var isDuplicate; 691 | isDuplicate = _.some(matchesWithBackfill, function(match) { 692 | return that.dupDetector(remoteMatch, match); 693 | }); 694 | !isDuplicate && matchesWithBackfill.push(remoteMatch); 695 | return matchesWithBackfill.length < that.limit; 696 | }); 697 | cb && cb(that.sorter(matchesWithBackfill)); 698 | } 699 | }, 700 | clear: function clear() { 701 | this.index.reset(); 702 | }, 703 | clearPrefetchCache: function clearPrefetchCache() { 704 | this.storage && this.storage.clear(); 705 | }, 706 | clearRemoteCache: function clearRemoteCache() { 707 | this.transport && Transport.resetCache(); 708 | }, 709 | ttAdapter: function ttAdapter() { 710 | return _.bind(this.get, this); 711 | } 712 | }); 713 | return Bloodhound; 714 | function getSorter(sortFn) { 715 | return _.isFunction(sortFn) ? sort : noSort; 716 | function sort(array) { 717 | return array.sort(sortFn); 718 | } 719 | function noSort(array) { 720 | return array; 721 | } 722 | } 723 | function ignoreDuplicates() { 724 | return false; 725 | } 726 | })(this); 727 | var html = function() { 728 | return { 729 | wrapper: '', 730 | dropdown: '', 731 | dataset: '
', 732 | suggestions: '', 733 | suggestion: '
' 734 | }; 735 | }(); 736 | var css = function() { 737 | "use strict"; 738 | var css = { 739 | wrapper: { 740 | position: "relative", 741 | display: "inline-block" 742 | }, 743 | hint: { 744 | position: "absolute", 745 | top: "0", 746 | left: "0", 747 | borderColor: "transparent", 748 | boxShadow: "none", 749 | opacity: "1" 750 | }, 751 | input: { 752 | position: "relative", 753 | verticalAlign: "top", 754 | backgroundColor: "transparent" 755 | }, 756 | inputWithNoHint: { 757 | position: "relative", 758 | verticalAlign: "top" 759 | }, 760 | dropdown: { 761 | position: "absolute", 762 | top: "100%", 763 | left: "0", 764 | zIndex: "100", 765 | display: "none" 766 | }, 767 | suggestions: { 768 | display: "block" 769 | }, 770 | suggestion: { 771 | whiteSpace: "nowrap", 772 | cursor: "pointer" 773 | }, 774 | suggestionChild: { 775 | whiteSpace: "normal" 776 | }, 777 | ltr: { 778 | left: "0", 779 | right: "auto" 780 | }, 781 | rtl: { 782 | left: "auto", 783 | right: " 0" 784 | } 785 | }; 786 | if (_.isMsie()) { 787 | _.mixin(css.input, { 788 | backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" 789 | }); 790 | } 791 | if (_.isMsie() && _.isMsie() <= 7) { 792 | _.mixin(css.input, { 793 | marginTop: "-1px" 794 | }); 795 | } 796 | return css; 797 | }(); 798 | var EventBus = function() { 799 | "use strict"; 800 | var namespace = "typeahead:"; 801 | function EventBus(o) { 802 | if (!o || !o.el) { 803 | $.error("EventBus initialized without el"); 804 | } 805 | this.$el = $(o.el); 806 | } 807 | _.mixin(EventBus.prototype, { 808 | trigger: function(type) { 809 | var args = [].slice.call(arguments, 1); 810 | this.$el.trigger(namespace + type, args); 811 | } 812 | }); 813 | return EventBus; 814 | }(); 815 | var EventEmitter = function() { 816 | "use strict"; 817 | var splitter = /\s+/, nextTick = getNextTick(); 818 | return { 819 | onSync: onSync, 820 | onAsync: onAsync, 821 | off: off, 822 | trigger: trigger 823 | }; 824 | function on(method, types, cb, context) { 825 | var type; 826 | if (!cb) { 827 | return this; 828 | } 829 | types = types.split(splitter); 830 | cb = context ? bindContext(cb, context) : cb; 831 | this._callbacks = this._callbacks || {}; 832 | while (type = types.shift()) { 833 | this._callbacks[type] = this._callbacks[type] || { 834 | sync: [], 835 | async: [] 836 | }; 837 | this._callbacks[type][method].push(cb); 838 | } 839 | return this; 840 | } 841 | function onAsync(types, cb, context) { 842 | return on.call(this, "async", types, cb, context); 843 | } 844 | function onSync(types, cb, context) { 845 | return on.call(this, "sync", types, cb, context); 846 | } 847 | function off(types) { 848 | var type; 849 | if (!this._callbacks) { 850 | return this; 851 | } 852 | types = types.split(splitter); 853 | while (type = types.shift()) { 854 | delete this._callbacks[type]; 855 | } 856 | return this; 857 | } 858 | function trigger(types) { 859 | var type, callbacks, args, syncFlush, asyncFlush; 860 | if (!this._callbacks) { 861 | return this; 862 | } 863 | types = types.split(splitter); 864 | args = [].slice.call(arguments, 1); 865 | while ((type = types.shift()) && (callbacks = this._callbacks[type])) { 866 | syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); 867 | asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); 868 | syncFlush() && nextTick(asyncFlush); 869 | } 870 | return this; 871 | } 872 | function getFlush(callbacks, context, args) { 873 | return flush; 874 | function flush() { 875 | var cancelled; 876 | for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { 877 | cancelled = callbacks[i].apply(context, args) === false; 878 | } 879 | return !cancelled; 880 | } 881 | } 882 | function getNextTick() { 883 | var nextTickFn; 884 | if (window.setImmediate) { 885 | nextTickFn = function nextTickSetImmediate(fn) { 886 | setImmediate(function() { 887 | fn(); 888 | }); 889 | }; 890 | } else { 891 | nextTickFn = function nextTickSetTimeout(fn) { 892 | setTimeout(function() { 893 | fn(); 894 | }, 0); 895 | }; 896 | } 897 | return nextTickFn; 898 | } 899 | function bindContext(fn, context) { 900 | return fn.bind ? fn.bind(context) : function() { 901 | fn.apply(context, [].slice.call(arguments, 0)); 902 | }; 903 | } 904 | }(); 905 | var highlight = function(doc) { 906 | "use strict"; 907 | var defaults = { 908 | node: null, 909 | pattern: null, 910 | tagName: "strong", 911 | className: null, 912 | wordsOnly: false, 913 | caseSensitive: false 914 | }; 915 | return function hightlight(o) { 916 | var regex; 917 | o = _.mixin({}, defaults, o); 918 | if (!o.node || !o.pattern) { 919 | return; 920 | } 921 | o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; 922 | regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); 923 | traverse(o.node, hightlightTextNode); 924 | function hightlightTextNode(textNode) { 925 | var match, patternNode, wrapperNode; 926 | if (match = regex.exec(textNode.data)) { 927 | wrapperNode = doc.createElement(o.tagName); 928 | o.className && (wrapperNode.className = o.className); 929 | patternNode = textNode.splitText(match.index); 930 | patternNode.splitText(match[0].length); 931 | wrapperNode.appendChild(patternNode.cloneNode(true)); 932 | textNode.parentNode.replaceChild(wrapperNode, patternNode); 933 | } 934 | return !!match; 935 | } 936 | function traverse(el, hightlightTextNode) { 937 | var childNode, TEXT_NODE_TYPE = 3; 938 | for (var i = 0; i < el.childNodes.length; i++) { 939 | childNode = el.childNodes[i]; 940 | if (childNode.nodeType === TEXT_NODE_TYPE) { 941 | i += hightlightTextNode(childNode) ? 1 : 0; 942 | } else { 943 | traverse(childNode, hightlightTextNode); 944 | } 945 | } 946 | } 947 | }; 948 | function getRegex(patterns, caseSensitive, wordsOnly) { 949 | var escapedPatterns = [], regexStr; 950 | for (var i = 0, len = patterns.length; i < len; i++) { 951 | escapedPatterns.push(_.escapeRegExChars(patterns[i])); 952 | } 953 | regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; 954 | return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); 955 | } 956 | }(window.document); 957 | var Input = function() { 958 | "use strict"; 959 | var specialKeyCodeMap; 960 | specialKeyCodeMap = { 961 | 9: "tab", 962 | 27: "esc", 963 | 37: "left", 964 | 39: "right", 965 | 13: "enter", 966 | 38: "up", 967 | 40: "down" 968 | }; 969 | function Input(o) { 970 | var that = this, onBlur, onFocus, onKeydown, onInput; 971 | o = o || {}; 972 | if (!o.input) { 973 | $.error("input is missing"); 974 | } 975 | onBlur = _.bind(this._onBlur, this); 976 | onFocus = _.bind(this._onFocus, this); 977 | onKeydown = _.bind(this._onKeydown, this); 978 | onInput = _.bind(this._onInput, this); 979 | this.$hint = $(o.hint); 980 | this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); 981 | if (this.$hint.length === 0) { 982 | this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; 983 | } 984 | if (!_.isMsie()) { 985 | this.$input.on("input.tt", onInput); 986 | } else { 987 | this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { 988 | if (specialKeyCodeMap[$e.which || $e.keyCode]) { 989 | return; 990 | } 991 | _.defer(_.bind(that._onInput, that, $e)); 992 | }); 993 | } 994 | this.query = this.$input.val(); 995 | this.$overflowHelper = buildOverflowHelper(this.$input); 996 | } 997 | Input.normalizeQuery = function(str) { 998 | return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); 999 | }; 1000 | _.mixin(Input.prototype, EventEmitter, { 1001 | _onBlur: function onBlur() { 1002 | this.resetInputValue(); 1003 | this.trigger("blurred"); 1004 | }, 1005 | _onFocus: function onFocus() { 1006 | this.trigger("focused"); 1007 | }, 1008 | _onKeydown: function onKeydown($e) { 1009 | var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; 1010 | this._managePreventDefault(keyName, $e); 1011 | if (keyName && this._shouldTrigger(keyName, $e)) { 1012 | this.trigger(keyName + "Keyed", $e); 1013 | } 1014 | }, 1015 | _onInput: function onInput() { 1016 | this._checkInputValue(); 1017 | }, 1018 | _managePreventDefault: function managePreventDefault(keyName, $e) { 1019 | var preventDefault, hintValue, inputValue; 1020 | switch (keyName) { 1021 | case "tab": 1022 | hintValue = this.getHint(); 1023 | inputValue = this.getInputValue(); 1024 | preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); 1025 | break; 1026 | 1027 | case "up": 1028 | case "down": 1029 | preventDefault = !withModifier($e); 1030 | break; 1031 | 1032 | default: 1033 | preventDefault = false; 1034 | } 1035 | preventDefault && $e.preventDefault(); 1036 | }, 1037 | _shouldTrigger: function shouldTrigger(keyName, $e) { 1038 | var trigger; 1039 | switch (keyName) { 1040 | case "tab": 1041 | trigger = !withModifier($e); 1042 | break; 1043 | 1044 | default: 1045 | trigger = true; 1046 | } 1047 | return trigger; 1048 | }, 1049 | _checkInputValue: function checkInputValue() { 1050 | var inputValue, areEquivalent, hasDifferentWhitespace; 1051 | inputValue = this.getInputValue(); 1052 | areEquivalent = areQueriesEquivalent(inputValue, this.query); 1053 | hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false; 1054 | this.query = inputValue; 1055 | if (!areEquivalent) { 1056 | this.trigger("queryChanged", this.query); 1057 | } else if (hasDifferentWhitespace) { 1058 | this.trigger("whitespaceChanged", this.query); 1059 | } 1060 | }, 1061 | focus: function focus() { 1062 | this.$input.focus(); 1063 | }, 1064 | blur: function blur() { 1065 | this.$input.blur(); 1066 | }, 1067 | getQuery: function getQuery() { 1068 | return this.query; 1069 | }, 1070 | setQuery: function setQuery(query) { 1071 | this.query = query; 1072 | }, 1073 | getInputValue: function getInputValue() { 1074 | return this.$input.val(); 1075 | }, 1076 | setInputValue: function setInputValue(value, silent) { 1077 | this.$input.val(value); 1078 | silent ? this.clearHint() : this._checkInputValue(); 1079 | }, 1080 | resetInputValue: function resetInputValue() { 1081 | this.setInputValue(this.query, true); 1082 | }, 1083 | getHint: function getHint() { 1084 | return this.$hint.val(); 1085 | }, 1086 | setHint: function setHint(value) { 1087 | this.$hint.val(value); 1088 | }, 1089 | clearHint: function clearHint() { 1090 | this.setHint(""); 1091 | }, 1092 | clearHintIfInvalid: function clearHintIfInvalid() { 1093 | var val, hint, valIsPrefixOfHint, isValid; 1094 | val = this.getInputValue(); 1095 | hint = this.getHint(); 1096 | valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; 1097 | isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); 1098 | !isValid && this.clearHint(); 1099 | }, 1100 | getLanguageDirection: function getLanguageDirection() { 1101 | return (this.$input.css("direction") || "ltr").toLowerCase(); 1102 | }, 1103 | hasOverflow: function hasOverflow() { 1104 | var constraint = this.$input.width() - 2; 1105 | this.$overflowHelper.text(this.getInputValue()); 1106 | return this.$overflowHelper.width() >= constraint; 1107 | }, 1108 | isCursorAtEnd: function() { 1109 | var valueLength, selectionStart, range; 1110 | valueLength = this.$input.val().length; 1111 | selectionStart = this.$input[0].selectionStart; 1112 | if (_.isNumber(selectionStart)) { 1113 | return selectionStart === valueLength; 1114 | } else if (document.selection) { 1115 | range = document.selection.createRange(); 1116 | range.moveStart("character", -valueLength); 1117 | return valueLength === range.text.length; 1118 | } 1119 | return true; 1120 | }, 1121 | destroy: function destroy() { 1122 | this.$hint.off(".tt"); 1123 | this.$input.off(".tt"); 1124 | this.$hint = this.$input = this.$overflowHelper = null; 1125 | } 1126 | }); 1127 | return Input; 1128 | function buildOverflowHelper($input) { 1129 | return $('').css({ 1130 | position: "absolute", 1131 | visibility: "hidden", 1132 | whiteSpace: "pre", 1133 | fontFamily: $input.css("font-family"), 1134 | fontSize: $input.css("font-size"), 1135 | fontStyle: $input.css("font-style"), 1136 | fontVariant: $input.css("font-variant"), 1137 | fontWeight: $input.css("font-weight"), 1138 | wordSpacing: $input.css("word-spacing"), 1139 | letterSpacing: $input.css("letter-spacing"), 1140 | textIndent: $input.css("text-indent"), 1141 | textRendering: $input.css("text-rendering"), 1142 | textTransform: $input.css("text-transform") 1143 | }).insertAfter($input); 1144 | } 1145 | function areQueriesEquivalent(a, b) { 1146 | return Input.normalizeQuery(a) === Input.normalizeQuery(b); 1147 | } 1148 | function withModifier($e) { 1149 | return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; 1150 | } 1151 | }(); 1152 | var Dataset = function() { 1153 | "use strict"; 1154 | var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum"; 1155 | function Dataset(o) { 1156 | o = o || {}; 1157 | o.templates = o.templates || {}; 1158 | if (!o.source) { 1159 | $.error("missing source"); 1160 | } 1161 | if (o.name && !isValidName(o.name)) { 1162 | $.error("invalid dataset name: " + o.name); 1163 | } 1164 | this.query = null; 1165 | this.highlight = !!o.highlight; 1166 | this.name = o.name || _.getUniqueId(); 1167 | this.source = o.source; 1168 | this.displayFn = getDisplayFn(o.display || o.displayKey); 1169 | this.templates = getTemplates(o.templates, this.displayFn); 1170 | this.$el = $(html.dataset.replace("%CLASS%", this.name)); 1171 | } 1172 | Dataset.extractDatasetName = function extractDatasetName(el) { 1173 | return $(el).data(datasetKey); 1174 | }; 1175 | Dataset.extractValue = function extractDatum(el) { 1176 | return $(el).data(valueKey); 1177 | }; 1178 | Dataset.extractDatum = function extractDatum(el) { 1179 | return $(el).data(datumKey); 1180 | }; 1181 | _.mixin(Dataset.prototype, EventEmitter, { 1182 | _render: function render(query, suggestions) { 1183 | if (!this.$el) { 1184 | return; 1185 | } 1186 | var that = this, hasSuggestions; 1187 | this.$el.empty(); 1188 | hasSuggestions = suggestions && suggestions.length; 1189 | if (!hasSuggestions && this.templates.empty) { 1190 | this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); 1191 | } else if (hasSuggestions) { 1192 | this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); 1193 | } 1194 | this.trigger("rendered"); 1195 | function getEmptyHtml() { 1196 | return that.templates.empty({ 1197 | query: query, 1198 | isEmpty: true 1199 | }); 1200 | } 1201 | function getSuggestionsHtml() { 1202 | var $suggestions, nodes; 1203 | $suggestions = $(html.suggestions).css(css.suggestions); 1204 | nodes = _.map(suggestions, getSuggestionNode); 1205 | $suggestions.append.apply($suggestions, nodes); 1206 | that.highlight && highlight({ 1207 | className: "tt-highlight", 1208 | node: $suggestions[0], 1209 | pattern: query 1210 | }); 1211 | return $suggestions; 1212 | function getSuggestionNode(suggestion) { 1213 | var $el; 1214 | $el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion); 1215 | $el.children().each(function() { 1216 | $(this).css(css.suggestionChild); 1217 | }); 1218 | return $el; 1219 | } 1220 | } 1221 | function getHeaderHtml() { 1222 | return that.templates.header({ 1223 | query: query, 1224 | isEmpty: !hasSuggestions 1225 | }); 1226 | } 1227 | function getFooterHtml() { 1228 | return that.templates.footer({ 1229 | query: query, 1230 | isEmpty: !hasSuggestions 1231 | }); 1232 | } 1233 | }, 1234 | getRoot: function getRoot() { 1235 | return this.$el; 1236 | }, 1237 | update: function update(query) { 1238 | var that = this; 1239 | this.query = query; 1240 | this.canceled = false; 1241 | this.source(query, render); 1242 | function render(suggestions) { 1243 | if (!that.canceled && query === that.query) { 1244 | that._render(query, suggestions); 1245 | } 1246 | } 1247 | }, 1248 | cancel: function cancel() { 1249 | this.canceled = true; 1250 | }, 1251 | clear: function clear() { 1252 | this.cancel(); 1253 | this.$el.empty(); 1254 | this.trigger("rendered"); 1255 | }, 1256 | isEmpty: function isEmpty() { 1257 | return this.$el.is(":empty"); 1258 | }, 1259 | destroy: function destroy() { 1260 | this.$el = null; 1261 | } 1262 | }); 1263 | return Dataset; 1264 | function getDisplayFn(display) { 1265 | display = display || "value"; 1266 | return _.isFunction(display) ? display : displayFn; 1267 | function displayFn(obj) { 1268 | return obj[display]; 1269 | } 1270 | } 1271 | function getTemplates(templates, displayFn) { 1272 | return { 1273 | empty: templates.empty && _.templatify(templates.empty), 1274 | header: templates.header && _.templatify(templates.header), 1275 | footer: templates.footer && _.templatify(templates.footer), 1276 | suggestion: templates.suggestion || suggestionTemplate 1277 | }; 1278 | function suggestionTemplate(context) { 1279 | return "

" + displayFn(context) + "

"; 1280 | } 1281 | } 1282 | function isValidName(str) { 1283 | return /^[_a-zA-Z0-9-]+$/.test(str); 1284 | } 1285 | }(); 1286 | var Dropdown = function() { 1287 | "use strict"; 1288 | function Dropdown(o) { 1289 | var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave; 1290 | o = o || {}; 1291 | if (!o.menu) { 1292 | $.error("menu is required"); 1293 | } 1294 | this.isOpen = false; 1295 | this.isEmpty = true; 1296 | this.datasets = _.map(o.datasets, initializeDataset); 1297 | onSuggestionClick = _.bind(this._onSuggestionClick, this); 1298 | onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); 1299 | onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); 1300 | this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave); 1301 | _.each(this.datasets, function(dataset) { 1302 | that.$menu.append(dataset.getRoot()); 1303 | dataset.onSync("rendered", that._onRendered, that); 1304 | }); 1305 | } 1306 | _.mixin(Dropdown.prototype, EventEmitter, { 1307 | _onSuggestionClick: function onSuggestionClick($e) { 1308 | this.trigger("suggestionClicked", $($e.currentTarget)); 1309 | }, 1310 | _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { 1311 | this._removeCursor(); 1312 | this._setCursor($($e.currentTarget), true); 1313 | }, 1314 | _onSuggestionMouseLeave: function onSuggestionMouseLeave() { 1315 | this._removeCursor(); 1316 | }, 1317 | _onRendered: function onRendered() { 1318 | this.isEmpty = _.every(this.datasets, isDatasetEmpty); 1319 | this.isEmpty ? this._hide() : this.isOpen && this._show(); 1320 | this.trigger("datasetRendered"); 1321 | function isDatasetEmpty(dataset) { 1322 | return dataset.isEmpty(); 1323 | } 1324 | }, 1325 | _hide: function() { 1326 | this.$menu.hide(); 1327 | }, 1328 | _show: function() { 1329 | this.$menu.css("display", "block"); 1330 | }, 1331 | _getSuggestions: function getSuggestions() { 1332 | return this.$menu.find(".tt-suggestion"); 1333 | }, 1334 | _getCursor: function getCursor() { 1335 | return this.$menu.find(".tt-cursor").first(); 1336 | }, 1337 | _setCursor: function setCursor($el, silent) { 1338 | $el.first().addClass("tt-cursor"); 1339 | !silent && this.trigger("cursorMoved"); 1340 | }, 1341 | _removeCursor: function removeCursor() { 1342 | this._getCursor().removeClass("tt-cursor"); 1343 | }, 1344 | _moveCursor: function moveCursor(increment) { 1345 | var $suggestions, $oldCursor, newCursorIndex, $newCursor; 1346 | if (!this.isOpen) { 1347 | return; 1348 | } 1349 | $oldCursor = this._getCursor(); 1350 | $suggestions = this._getSuggestions(); 1351 | this._removeCursor(); 1352 | newCursorIndex = $suggestions.index($oldCursor) + increment; 1353 | newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; 1354 | if (newCursorIndex === -1) { 1355 | this.trigger("cursorRemoved"); 1356 | return; 1357 | } else if (newCursorIndex < -1) { 1358 | newCursorIndex = $suggestions.length - 1; 1359 | } 1360 | this._setCursor($newCursor = $suggestions.eq(newCursorIndex)); 1361 | this._ensureVisible($newCursor); 1362 | }, 1363 | _ensureVisible: function ensureVisible($el) { 1364 | var elTop, elBottom, menuScrollTop, menuHeight; 1365 | elTop = $el.position().top; 1366 | elBottom = elTop + $el.outerHeight(true); 1367 | menuScrollTop = this.$menu.scrollTop(); 1368 | menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10); 1369 | if (elTop < 0) { 1370 | this.$menu.scrollTop(menuScrollTop + elTop); 1371 | } else if (menuHeight < elBottom) { 1372 | this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); 1373 | } 1374 | }, 1375 | close: function close() { 1376 | if (this.isOpen) { 1377 | this.isOpen = false; 1378 | this._removeCursor(); 1379 | this._hide(); 1380 | this.trigger("closed"); 1381 | } 1382 | }, 1383 | open: function open() { 1384 | if (!this.isOpen) { 1385 | this.isOpen = true; 1386 | !this.isEmpty && this._show(); 1387 | this.trigger("opened"); 1388 | } 1389 | }, 1390 | setLanguageDirection: function setLanguageDirection(dir) { 1391 | this.$menu.css(dir === "ltr" ? css.ltr : css.rtl); 1392 | }, 1393 | moveCursorUp: function moveCursorUp() { 1394 | this._moveCursor(-1); 1395 | }, 1396 | moveCursorDown: function moveCursorDown() { 1397 | this._moveCursor(+1); 1398 | }, 1399 | getDatumForSuggestion: function getDatumForSuggestion($el) { 1400 | var datum = null; 1401 | if ($el.length) { 1402 | datum = { 1403 | raw: Dataset.extractDatum($el), 1404 | value: Dataset.extractValue($el), 1405 | datasetName: Dataset.extractDatasetName($el) 1406 | }; 1407 | } 1408 | return datum; 1409 | }, 1410 | getDatumForCursor: function getDatumForCursor() { 1411 | return this.getDatumForSuggestion(this._getCursor().first()); 1412 | }, 1413 | getDatumForTopSuggestion: function getDatumForTopSuggestion() { 1414 | return this.getDatumForSuggestion(this._getSuggestions().first()); 1415 | }, 1416 | update: function update(query) { 1417 | _.each(this.datasets, updateDataset); 1418 | function updateDataset(dataset) { 1419 | dataset.update(query); 1420 | } 1421 | }, 1422 | empty: function empty() { 1423 | _.each(this.datasets, clearDataset); 1424 | this.isEmpty = true; 1425 | function clearDataset(dataset) { 1426 | dataset.clear(); 1427 | } 1428 | }, 1429 | isVisible: function isVisible() { 1430 | return this.isOpen && !this.isEmpty; 1431 | }, 1432 | destroy: function destroy() { 1433 | this.$menu.off(".tt"); 1434 | this.$menu = null; 1435 | _.each(this.datasets, destroyDataset); 1436 | function destroyDataset(dataset) { 1437 | dataset.destroy(); 1438 | } 1439 | } 1440 | }); 1441 | return Dropdown; 1442 | function initializeDataset(oDataset) { 1443 | return new Dataset(oDataset); 1444 | } 1445 | }(); 1446 | var Typeahead = function() { 1447 | "use strict"; 1448 | var attrsKey = "ttAttrs"; 1449 | function Typeahead(o) { 1450 | var $menu, $input, $hint; 1451 | o = o || {}; 1452 | if (!o.input) { 1453 | $.error("missing input"); 1454 | } 1455 | this.isActivated = false; 1456 | this.autoselect = !!o.autoselect; 1457 | this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; 1458 | this.$node = buildDom(o.input, o.withHint); 1459 | $menu = this.$node.find(".tt-dropdown-menu"); 1460 | $input = this.$node.find(".tt-input"); 1461 | $hint = this.$node.find(".tt-hint"); 1462 | $input.on("blur.tt", function($e) { 1463 | var active, isActive, hasActive; 1464 | active = document.activeElement; 1465 | isActive = $menu.is(active); 1466 | hasActive = $menu.has(active).length > 0; 1467 | if (_.isMsie() && (isActive || hasActive)) { 1468 | $e.preventDefault(); 1469 | $e.stopImmediatePropagation(); 1470 | _.defer(function() { 1471 | $input.focus(); 1472 | }); 1473 | } 1474 | }); 1475 | $menu.on("mousedown.tt", function($e) { 1476 | $e.preventDefault(); 1477 | }); 1478 | this.eventBus = o.eventBus || new EventBus({ 1479 | el: $input 1480 | }); 1481 | this.dropdown = new Dropdown({ 1482 | menu: $menu, 1483 | datasets: o.datasets 1484 | }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this); 1485 | this.input = new Input({ 1486 | input: $input, 1487 | hint: $hint 1488 | }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this); 1489 | this._setLanguageDirection(); 1490 | } 1491 | _.mixin(Typeahead.prototype, { 1492 | _onSuggestionClicked: function onSuggestionClicked(type, $el) { 1493 | var datum; 1494 | if (datum = this.dropdown.getDatumForSuggestion($el)) { 1495 | this._select(datum); 1496 | } 1497 | }, 1498 | _onCursorMoved: function onCursorMoved() { 1499 | var datum = this.dropdown.getDatumForCursor(); 1500 | this.input.setInputValue(datum.value, true); 1501 | this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName); 1502 | }, 1503 | _onCursorRemoved: function onCursorRemoved() { 1504 | this.input.resetInputValue(); 1505 | this._updateHint(); 1506 | }, 1507 | _onDatasetRendered: function onDatasetRendered() { 1508 | this._updateHint(); 1509 | }, 1510 | _onOpened: function onOpened() { 1511 | this._updateHint(); 1512 | this.eventBus.trigger("opened"); 1513 | }, 1514 | _onClosed: function onClosed() { 1515 | this.input.clearHint(); 1516 | this.eventBus.trigger("closed"); 1517 | }, 1518 | _onFocused: function onFocused() { 1519 | this.isActivated = true; 1520 | this.dropdown.open(); 1521 | }, 1522 | _onBlurred: function onBlurred() { 1523 | this.isActivated = false; 1524 | this.dropdown.empty(); 1525 | this.dropdown.close(); 1526 | }, 1527 | _onEnterKeyed: function onEnterKeyed(type, $e) { 1528 | var cursorDatum, topSuggestionDatum; 1529 | cursorDatum = this.dropdown.getDatumForCursor(); 1530 | topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); 1531 | if (cursorDatum) { 1532 | this._select(cursorDatum); 1533 | $e.preventDefault(); 1534 | } else if (this.autoselect && topSuggestionDatum) { 1535 | this._select(topSuggestionDatum); 1536 | $e.preventDefault(); 1537 | } 1538 | }, 1539 | _onTabKeyed: function onTabKeyed(type, $e) { 1540 | var datum; 1541 | if (datum = this.dropdown.getDatumForCursor()) { 1542 | this._select(datum); 1543 | $e.preventDefault(); 1544 | } else { 1545 | this._autocomplete(true); 1546 | } 1547 | }, 1548 | _onEscKeyed: function onEscKeyed() { 1549 | this.dropdown.close(); 1550 | this.input.resetInputValue(); 1551 | }, 1552 | _onUpKeyed: function onUpKeyed() { 1553 | var query = this.input.getQuery(); 1554 | this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp(); 1555 | this.dropdown.open(); 1556 | }, 1557 | _onDownKeyed: function onDownKeyed() { 1558 | var query = this.input.getQuery(); 1559 | this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown(); 1560 | this.dropdown.open(); 1561 | }, 1562 | _onLeftKeyed: function onLeftKeyed() { 1563 | this.dir === "rtl" && this._autocomplete(); 1564 | }, 1565 | _onRightKeyed: function onRightKeyed() { 1566 | this.dir === "ltr" && this._autocomplete(); 1567 | }, 1568 | _onQueryChanged: function onQueryChanged(e, query) { 1569 | this.input.clearHintIfInvalid(); 1570 | query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty(); 1571 | this.dropdown.open(); 1572 | this._setLanguageDirection(); 1573 | }, 1574 | _onWhitespaceChanged: function onWhitespaceChanged() { 1575 | this._updateHint(); 1576 | this.dropdown.open(); 1577 | }, 1578 | _setLanguageDirection: function setLanguageDirection() { 1579 | var dir; 1580 | if (this.dir !== (dir = this.input.getLanguageDirection())) { 1581 | this.dir = dir; 1582 | this.$node.css("direction", dir); 1583 | this.dropdown.setLanguageDirection(dir); 1584 | } 1585 | }, 1586 | _updateHint: function updateHint() { 1587 | var datum, val, query, escapedQuery, frontMatchRegEx, match; 1588 | datum = this.dropdown.getDatumForTopSuggestion(); 1589 | if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { 1590 | val = this.input.getInputValue(); 1591 | query = Input.normalizeQuery(val); 1592 | escapedQuery = _.escapeRegExChars(query); 1593 | frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); 1594 | match = frontMatchRegEx.exec(datum.value); 1595 | match ? this.input.setHint(val + match[1]) : this.input.clearHint(); 1596 | } else { 1597 | this.input.clearHint(); 1598 | } 1599 | }, 1600 | _autocomplete: function autocomplete(laxCursor) { 1601 | var hint, query, isCursorAtEnd, datum; 1602 | hint = this.input.getHint(); 1603 | query = this.input.getQuery(); 1604 | isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); 1605 | if (hint && query !== hint && isCursorAtEnd) { 1606 | datum = this.dropdown.getDatumForTopSuggestion(); 1607 | datum && this.input.setInputValue(datum.value); 1608 | this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName); 1609 | } 1610 | }, 1611 | _select: function select(datum) { 1612 | this.input.setQuery(datum.value); 1613 | this.input.setInputValue(datum.value, true); 1614 | this._setLanguageDirection(); 1615 | this.eventBus.trigger("selected", datum.raw, datum.datasetName); 1616 | this.dropdown.close(); 1617 | _.defer(_.bind(this.dropdown.empty, this.dropdown)); 1618 | }, 1619 | open: function open() { 1620 | this.dropdown.open(); 1621 | }, 1622 | close: function close() { 1623 | this.dropdown.close(); 1624 | }, 1625 | setVal: function setVal(val) { 1626 | val = _.toStr(val); 1627 | if (this.isActivated) { 1628 | this.input.setInputValue(val); 1629 | } else { 1630 | this.input.setQuery(val); 1631 | this.input.setInputValue(val, true); 1632 | } 1633 | this._setLanguageDirection(); 1634 | }, 1635 | getVal: function getVal() { 1636 | return this.input.getQuery(); 1637 | }, 1638 | destroy: function destroy() { 1639 | this.input.destroy(); 1640 | this.dropdown.destroy(); 1641 | destroyDomStructure(this.$node); 1642 | this.$node = null; 1643 | } 1644 | }); 1645 | return Typeahead; 1646 | function buildDom(input, withHint) { 1647 | var $input, $wrapper, $dropdown, $hint; 1648 | $input = $(input); 1649 | $wrapper = $(html.wrapper).css(css.wrapper); 1650 | $dropdown = $(html.dropdown).css(css.dropdown); 1651 | $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input)); 1652 | $hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder required").prop("readonly", true).attr({ 1653 | autocomplete: "off", 1654 | spellcheck: "false", 1655 | tabindex: -1 1656 | }); 1657 | $input.data(attrsKey, { 1658 | dir: $input.attr("dir"), 1659 | autocomplete: $input.attr("autocomplete"), 1660 | spellcheck: $input.attr("spellcheck"), 1661 | style: $input.attr("style") 1662 | }); 1663 | $input.addClass("tt-input").attr({ 1664 | autocomplete: "off", 1665 | spellcheck: false 1666 | }).css(withHint ? css.input : css.inputWithNoHint); 1667 | try { 1668 | !$input.attr("dir") && $input.attr("dir", "auto"); 1669 | } catch (e) {} 1670 | return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown); 1671 | } 1672 | function getBackgroundStyles($el) { 1673 | return { 1674 | backgroundAttachment: $el.css("background-attachment"), 1675 | backgroundClip: $el.css("background-clip"), 1676 | backgroundColor: $el.css("background-color"), 1677 | backgroundImage: $el.css("background-image"), 1678 | backgroundOrigin: $el.css("background-origin"), 1679 | backgroundPosition: $el.css("background-position"), 1680 | backgroundRepeat: $el.css("background-repeat"), 1681 | backgroundSize: $el.css("background-size") 1682 | }; 1683 | } 1684 | function destroyDomStructure($node) { 1685 | var $input = $node.find(".tt-input"); 1686 | _.each($input.data(attrsKey), function(val, key) { 1687 | _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); 1688 | }); 1689 | $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node); 1690 | $node.remove(); 1691 | } 1692 | }(); 1693 | (function() { 1694 | "use strict"; 1695 | var old, typeaheadKey, methods; 1696 | old = $.fn.typeahead; 1697 | typeaheadKey = "ttTypeahead"; 1698 | methods = { 1699 | initialize: function initialize(o, datasets) { 1700 | datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); 1701 | o = o || {}; 1702 | return this.each(attach); 1703 | function attach() { 1704 | var $input = $(this), eventBus, typeahead; 1705 | _.each(datasets, function(d) { 1706 | d.highlight = !!o.highlight; 1707 | }); 1708 | typeahead = new Typeahead({ 1709 | input: $input, 1710 | eventBus: eventBus = new EventBus({ 1711 | el: $input 1712 | }), 1713 | withHint: _.isUndefined(o.hint) ? true : !!o.hint, 1714 | minLength: o.minLength, 1715 | autoselect: o.autoselect, 1716 | datasets: datasets 1717 | }); 1718 | $input.data(typeaheadKey, typeahead); 1719 | } 1720 | }, 1721 | open: function open() { 1722 | return this.each(openTypeahead); 1723 | function openTypeahead() { 1724 | var $input = $(this), typeahead; 1725 | if (typeahead = $input.data(typeaheadKey)) { 1726 | typeahead.open(); 1727 | } 1728 | } 1729 | }, 1730 | close: function close() { 1731 | return this.each(closeTypeahead); 1732 | function closeTypeahead() { 1733 | var $input = $(this), typeahead; 1734 | if (typeahead = $input.data(typeaheadKey)) { 1735 | typeahead.close(); 1736 | } 1737 | } 1738 | }, 1739 | val: function val(newVal) { 1740 | return !arguments.length ? getVal(this.first()) : this.each(setVal); 1741 | function setVal() { 1742 | var $input = $(this), typeahead; 1743 | if (typeahead = $input.data(typeaheadKey)) { 1744 | typeahead.setVal(newVal); 1745 | } 1746 | } 1747 | function getVal($input) { 1748 | var typeahead, query; 1749 | if (typeahead = $input.data(typeaheadKey)) { 1750 | query = typeahead.getVal(); 1751 | } 1752 | return query; 1753 | } 1754 | }, 1755 | destroy: function destroy() { 1756 | return this.each(unattach); 1757 | function unattach() { 1758 | var $input = $(this), typeahead; 1759 | if (typeahead = $input.data(typeaheadKey)) { 1760 | typeahead.destroy(); 1761 | $input.removeData(typeaheadKey); 1762 | } 1763 | } 1764 | } 1765 | }; 1766 | $.fn.typeahead = function(method) { 1767 | var tts; 1768 | if (methods[method] && method !== "initialize") { 1769 | tts = this.filter(function() { 1770 | return !!$(this).data(typeaheadKey); 1771 | }); 1772 | return methods[method].apply(tts, [].slice.call(arguments, 1)); 1773 | } else { 1774 | return methods.initialize.apply(this, arguments); 1775 | } 1776 | }; 1777 | $.fn.typeahead.noConflict = function noConflict() { 1778 | $.fn.typeahead = old; 1779 | return this; 1780 | }; 1781 | })(); 1782 | })(window.jQuery); --------------------------------------------------------------------------------