diff --git a/README.md b/README.md index 0dd08bc..48ddc85 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[Booklight](https://chrome.google.com/webstore/detail/booklight/lkdhojpobehkcldjmileiancjjpdeakk) +Booklight ========== I got fed up wasting my time trying to navigate my way through bunch of bookmarks folder to arrange them. So if you are: @@ -8,28 +8,82 @@ I got fed up wasting my time trying to navigate my way through bunch of bookmark then you came to the right place. **Booklight** is a clean Chrome Extension to ease the way of adding a bookmark. -To Launch press (ctrl/Control + b) and thats it ! +- To launch press (ctrl/Control + b) +- To enable bookmarks search mode hit `space` after booklight is launched -[![booklightVideo](https://www.dropbox.com/s/dgu57k0424rnjhq/booklight_video.png?dl=1)](https://www.youtube.com/watch?v=8AB1kE6U-2g) +[Download from Chrome Store](https://chrome.google.com/webstore/detail/booklight/lkdhojpobehkcldjmileiancjjpdeakk) + +## Watch Booklight Video +[![booklightVideo](https://www.dropbox.com/s/dgu57k0424rnjhq/booklight_video.png?dl=1)](https://www.youtube.com/watch?v=fxqaToLRLNo) ### Features + - Filter bookmarks based on manual entry - Show the path of the current selected folder - Navigate easily through the folders tree using keyboard - if the folder is highlighted in blue this means that it contains sub-folders as well. The right arrow (->) keyboard key will go inside that folder. You can go back one step to the back using the left keyboard arrow (<-) - Bookmark directly when you find your target +- The ability to switch to urls search **NEW** +- Launching urls in current or new tab **NEW** +- Fuzzy search enabled for filtering on both folders and urls **NEW** +- Clean current URL before bookmarking (sometimes the url is polluted with query strings e.g `?source= ...` for various tracking information). To clean the url from those, hit `ctrl+alt+x` and this will solve this issue. + +![booklight](http://g.recordit.co/ZsvnnFqYdu.gif) + +## Bookmark Search & launch + +Booklight now has the ability to search on your bookmakrs **and it is blazing fast**. I have around 20,000 bookmarks ! and through smart lazy loading and fuzzy search, you can now easily search and launch bookmarks anywhere while browsing. +To switch to the url search mode just hit `space` and then you will see that you can now search urls by having the `|` symbol in the input box. +To launch a url in the current window, simply hit `enter` and to open it in a new tab hit `ctr\control + enter` +![booklight-urls](http://g.recordit.co/aala9MAKo9.gif) + +### Booklight Performance +I currently have over 1000 folders and 20,000 bookmarked urls. Booklight is blazing fast, to achieve this i implement various hacks to minimize DOM manipulations and most importantly lazy-loading of urls. The lazy loading happens in the following function: + +```javascript +lazyloader: function lazyloader(elements){ + + var lazyloader = this; + + this.elements = elements; + this.showLimit = 15; + this.urlsDOM = ''; + + this.load = function(empty, hide) { + + var urlsDOM = ''; + var currentAttachedUrls = this.urlsDOM == '' ? 0 : $('.booklight_list li[data-type="url"]').length; + var limit = this.elements.length > this.showLimit ? this.showLimit : this.elements.length; + var urlsToAdd = this.elements.slice(currentAttachedUrls, currentAttachedUrls + limit); + + // the idea is build a kind of lazy loading for urls to minimize the building of the DOM elements + urlsToAdd.forEach(function(url){ + urlsDOM += '
  • ' + + '' + + url.title + '
  • '; + }); + + lazyloader.urlsDOM += urlsDOM; + + booklight.UI.showSection(urlsDOM, empty, hide); + booklight.UI.updateCounter(); + } +} +``` +You can tweak the number of elements you want to show on every iteration and it works for both searching and filtering. ### Things i would like to do + - Add mouse interactions - Add better logic to the star icon (at the moment it only shows when the page is successfully bookmarked) but it will not update if remove the bookmark ... etc. -- Add fuzzy search for filtering from input box +- ~~Add fuzzy search for filtering from input box~~ - Smart folder suggestions - ~~Remember last location when going back to main screen or removing filters~~ **done** -## Life before Booklight -![before-booklight](http://g.recordit.co/uqYqp8o08e.gif) - -## Life AFTER Booklight -![before-booklight](http://g.recordit.co/mprXGGOr1k.gif) [Download from Chrome Store](https://chrome.google.com/webstore/detail/booklight/lkdhojpobehkcldjmileiancjjpdeakk) + +### Thoughts + + - [Google Chrome’s awful new bookmark manager (and how to switch it off)](http://blog.garethjmsaunders.co.uk/2015/04/19/google-chromes-awful-new-bookmark-manager-and-how-to-switch-it-off/) + - [Chrome users roast Google on spit of hate over revamped bookmarks manager](http://www.computerworld.com/article/2913426/web-browsers/chrome-users-roast-google-on-spit-of-hate-over-revamped-bookmarks-manager.html) diff --git a/background.js b/background.js index 08b8daf..58ba738 100644 --- a/background.js +++ b/background.js @@ -1,46 +1,63 @@ -var foldersList = [], urls = []; +var booklight = function booklight() { -chrome.bookmarks.getTree(function(bookmarksTree) { + var booklight = this; - foldersList = filterRecursively(bookmarksTree, "children", function(node) { - if (node.url) urls.push(node); - return !node.url && node.id > 0; - }).sort(function(a, b) { - // The sort functions make sure that we will have the last used folders on top - return b.dateGroupModified - a.dateGroupModified; - }); + this.foldersList = []; + this.urls = []; + + this.getBookmarks = function() { - chrome.storage.local.set({"booklight": foldersList }, function(bookmarks) { console.log("Setting the folders list into the local storage !!") }); - chrome.storage.local.set({"urls": urls }, function(bookmarks) { console.log("Setting the urls list into the local storage !!") }); + chrome.bookmarks.getTree(function(bookmarksTree) { - }); + booklight.foldersList = filterRecursively(bookmarksTree, "children", function(node) { + if (node.url) booklight.urls.push(node); + return !node.url && node.id > 0; + }).sort(function(a, b) { + // The sort functions make sure that we will have the last used folders on top + return b.dateGroupModified - a.dateGroupModified; + }); -// Recursively filter the passed TreeNodes -function filterRecursively(nodeArray, childrenProperty, filterFn, results) { + chrome.storage.local.set({"booklightFolders": booklight.foldersList }, function(bookmarks) { console.log("Setting the folders list into the local storage !!") }); + chrome.storage.local.set({"booklightUrls": booklight.urls }, function(bookmarks) { console.log("Setting the urls list into the local storage !!") }); + + }); - results = results || []; + // Recursively filter the passed TreeNodes + function filterRecursively(nodeArray, childrenProperty, filterFn, results) { + + results = results || []; + + nodeArray.forEach( function( node ) { + if (filterFn(node)) results.push({title: node.title, id: node.id, dateGroupModified: node.dateGroupModified, folder: isLeaf(node), parentId: node.parentId}); + if (node.children) filterRecursively(node.children, childrenProperty, filterFn, results); + }); + return results; + }; + + // Check if the current bookmark is a leaf (does not contain more folders) + function isLeaf(node) { + var leafyNodes = []; + node.children.forEach(function(child){ + if (!child.hasOwnProperty('children')) leafyNodes.push(1); + }); + var isLeaf = leafyNodes.length == node.children.length ? true : false; + return isLeaf; + } + } - nodeArray.forEach( function( node ) { - if (filterFn(node)) results.push({title: node.title, id: node.id, dateGroupModified: node.dateGroupModified, folder: isLeaf(node), parent: node.parentId}); - if (node.children) filterRecursively(node.children, childrenProperty, filterFn, results); - }); - return results; -}; + this.attachListeners = function() { -function isLeaf(node) { - var leafyNodes = []; - node.children.forEach(function(child){ - if (!child.hasOwnProperty('children')) leafyNodes.push(1); - }); - var isLeaf = leafyNodes.length == node.children.length ? true : false; - return isLeaf; + chrome.runtime.onMessage.addListener(function(request, sender, sendrequest) { + if (request.message == "booklight") { + console.log("adding: " + request.url + " title: " + request.title + " to folder id: " + request.folder); + chrome.bookmarks.create({ 'parentId': request.folder, 'title': request.title, 'url': request.url }); + sendrequest({message: "success"}); + } + }); + } } -chrome.runtime.onMessage.addListener( - function(request, sender, sendrequest) { - if (request.message == "booklight") { - console.log("adding: " + request.url + " title: " + request.title + " to folder id: " + request.folder); - chrome.bookmarks.create({ 'parentId': request.folder, 'title': request.title, 'url': request.url }); - sendrequest({message: "success"}); - } - }); +var booklight = new booklight(); + +booklight.attachListeners(); +booklight.getBookmarks(); \ No newline at end of file diff --git a/booklight.js b/booklight.js index eaf9160..a55a1d2 100644 --- a/booklight.js +++ b/booklight.js @@ -1,19 +1,35 @@ var booklight = function booklight() { var booklight = this; + // The array (stack) that will hold the navigation of the main elements and their subfolders - this.elementStack = []; + this.elementStack = []; + this.urls = []; + this.context = 'folder'; + this.foldersDOM = ''; + + this.urlsLazyloader; + this.searchLazyLoader; + this.fuzzyFolderSearch; + this.fuzzyURLsSearch; this.attachKeyboardEvents = function attachKeyboardEvents() { - key('control+b, ctrl+b', function(){ booklight.UI.show(); }); - key('esc, escape', function(){ booklight.UI.close(); }); - key('enter', 'input', function(){ booklight.manager.addBookmark(); }); - key('up', 'input', function(){ booklight.navigator.moveInList("UP") }); - key('down', 'input', function(){ booklight.navigator.moveInList("DOWN") }); - key('right', 'input', function(){ booklight.navigator.moveInList("RIGHT") }); - key('left', 'input', function(){ booklight.navigator.moveInList("LEFT") }); - key('control+x, ctrl+x', function(){ booklight.util.cleanURL(); }); + var globalListener = new window.keypress.Listener($('body')[0],{is_solitary: true}); + var booklightListener = new window.keypress.Listener($('#booklightManager')[0]); + var executionListener = new window.keypress.Listener($('.booklight>input')[0], {is_solitary : true}); + + globalListener.simple_combo("ctrl b", function() { booklight.UI.show() }); + globalListener.simple_combo("esc", function() { booklight.UI.close() }); + globalListener.simple_combo('ctrl alt x', function(){ booklight.util.cleanURL() }); + + executionListener.simple_combo('enter', function(){ booklight.manager.addBookmark() }); + executionListener.simple_combo('ctrl enter', function(){ booklight.manager.openURL('_blank') }); + + booklightListener.simple_combo('up', function(){ booklight.navigator.moveInList("UP") }); + booklightListener.simple_combo('down', function(){ booklight.navigator.moveInList("DOWN") }); + booklightListener.simple_combo('right', function(){ booklight.navigator.moveInList("RIGHT") }); + booklightListener.simple_combo('left', function(){ booklight.navigator.moveInList("LEFT") }); } @@ -30,7 +46,7 @@ var booklight = function booklight() { build : function build() { // Append the search lightbox to the body DOM element - $('body').append('
    '+ + $('body').append('
    '+ '' + ''+ '' + @@ -44,47 +60,134 @@ var booklight = function booklight() { booklight.resultBar = $('.booklight_resultsbar'); booklight.statusBar = $('.booklight_statusbar'); - // Get the bookmarks from the local storage - chrome.storage.local.get("booklight", function(bookmarks) { - booklight.resultBar.text(bookmarks.booklight.length + " folders found"); - bookmarks.booklight.forEach(function(bookmark){ - var elem = $('
  • ', { text: bookmark.title, id: bookmark.id, 'data-dateGroupModified': bookmark.dateGroupModified, 'data-parent': bookmark.parent}); - if (!bookmark.folder) elem.addClass('isFolder'); - booklight.bookmarksList.append(elem); - }); - }); + booklight.UI.addFolders(); + booklight.UI.getURLs(); + booklight.attachKeyboardEvents(); // Attach the filtering functions for the inputbox booklight.searchBar.on('input', function() { var filter = $(this).val(); - // Hide all the folders list and only show those matching the input query - $('.booklight_list li').hide(); - - // Check if you are inside a folder, filter only on that folders children - if (booklight.elementStack.length) { - var nestedFolderID = booklight.elementStack[booklight.elementStack.length - 1].id ; - booklight.bookmarksList.find('li[data-parent="' + nestedFolderID + '"]:contains(' + filter + ')').show(); - } else booklight.bookmarksList.find('li:contains(' + filter + ')').show(); + // Check if the user is switching into files or folders context + if (!filter) booklight.context = 'folder'; + // Check if the value entered is space which is the trigger for urls search + else if (filter == ' ') { $(this).val("|"); booklight.context = 'url'; booklight.urlsLazyloader.load(false, true); } + + if (filter) { + + // hide all the current elements + $('.booklight_list li').hide(); + + if (booklight.searchBar.val().indexOf('|') !== -1 ) { + var filter = filter.replace('|',''); + if (filter.length > 1) { + // Now we will be filtering on urls only. Create a new lazyloader instance for the urls + booklight.searchLazyLoader = new booklight.UI.lazyloader(booklight.fuzzyURLsSearch.search(filter)); + booklight.searchLazyLoader.load(true); + } else booklight.UI.showSection(booklight.urlsLazyloader.urlsDOM,false,true); + + } else { + if (context = "folder" && booklight.elementStack.length) { + var nestedFolderID = booklight.elementStack[booklight.elementStack.length - 1].id ; + booklight.fuzzyFolderSearch.search(filter).forEach(function(folder){ if (folder.parentId == nestedFolderID) booklight.bookmarksList.find('li#'+ folder.id).show() }); + } else { + booklight.fuzzyFolderSearch.search(filter).forEach(function(folder){ booklight.bookmarksList.find('li#'+ folder.id).show() }); + } + } + } else { + if (booklight.elementStack.length) { + booklight.bookmarksList.find('li[data-parent="'+ booklight.elementStack[booklight.elementStack.length - 1].id + '"]').show(); + } else booklight.context == "folder" ? booklight.UI.showSection(null, true, false, "url") : booklight.UI.showSection(booklight.urlsLazyloader.urlsDOM,false,true); + } booklight.UI.updateCounter(); booklight.UI.higlightFirstElement(); }); - }, show : function show() { + },addFolders: function addFolders() { + + // Get the bookmarks folders from the local storage + chrome.storage.local.get("booklightFolders", function(bookmarks) { + booklight.fuzzyFolderSearch = new Fuse(bookmarks.booklightFolders, { keys: ['title'], threshold: 0.3}); + bookmarks.booklightFolders.forEach(function(bookmark){ + booklight.foldersDOM += '
  • '; + }); + booklight.bookmarksList.append(booklight.foldersDOM); + }); + + },getURLs: function getURLs() { + + // Get the bookmarks urls from the local storage + chrome.storage.local.get("booklightUrls", function(urls) { + booklight.fuzzyURLsSearch = new Fuse(urls.booklightUrls, { keys: ['title'], threshold: 0.4}); + booklight.urls = urls.booklightUrls; + // Create a new lazyloader instance for the urls + booklight.urlsLazyloader = new booklight.UI.lazyloader(urls.booklightUrls); + }); + + },lazyloader: function lazyloader(elements){ + + var lazyloader = this; + + this.elements = elements; + this.showLimit = 15; + this.urlsDOM = ''; + + this.load = function(empty, hide) { + + var urlsDOM = ''; + var currentAttachedUrls = this.urlsDOM == '' ? 0 : $('.booklight_list li[data-type="url"]:visible').length; + var limit = this.elements.length > this.showLimit ? this.showLimit : this.elements.length; + var urlsToAdd = this.elements.slice(currentAttachedUrls, currentAttachedUrls + limit); + + if (urlsToAdd.length) { + // the idea is build a kind of lazy loading for urls to minimize the building of the DOM elements + urlsToAdd.forEach(function(url){ + urlsDOM += '
  • ' + + '' + + url.title + '
  • '; + }); + lazyloader.urlsDOM += urlsDOM; + + booklight.UI.showSection(urlsDOM, empty, hide); + booklight.UI.updateCounter(); + } + + } + + },show : function show() { + + booklight.context = "folder"; // Show the booklight main UI window and all of its elements if they were hidden from a previous filter operation booklight.booklightBox.show(); - $('.booklight_list li').show(); + booklight.UI.showContext(); // Empty the searchbar input box and make it focused for direct query entry booklight.searchBar.val('').focus(); + booklight.searchBar.attr('placeholder', 'Filter...'); // Highlight the first element of the results booklight.UI.higlightFirstElement(); + booklight.UI.updateCounter(); },close : function close() { booklight.booklightBox.hide(); + booklight.elementStack = []; + booklight.UI.showSection(null, true, false, "url"); + + },showContext: function showHideContext() { + + $('.booklight_list li[data-type="' + booklight.context + '"]').show(); + + },showSection: function showSection(section, empty, hide, context){ + + if (empty) $('.booklight_list li[data-type="' + context + '"]').remove(); + if (hide) $('.booklight_list li').hide(); + + section ? booklight.bookmarksList.append(section) : $('.booklight_list li[data-type="' + booklight.context + '"]').show(); },focusItem : function(index, subFolder, isMouse) { @@ -118,7 +221,7 @@ var booklight = function booklight() { },higlightFirstElement: function(text) { - booklight.UI.focusItem($('.booklight_list li:visible').first().index(), text); + booklight.UI.focusItem($('.booklight_list li[data-type="' + booklight.context + '"]:visible').first().index(), text); },updateCounter: function() { @@ -129,15 +232,15 @@ var booklight = function booklight() { // Check if the root parent for the current node is not the bookmarks bar or other bookmarks var parentsList = getStatus(element, []); + if (element.attr('data-type') == "url") parentsList.shift(); + booklight.statusBar.text(parentsList.reverse().join(' > ')); // This function will recursively fetch the parent hierarchy for a current folder function getStatus (element, parentsArray) { var parentID = element.attr('data-parent'); - if (!parentID) return parentsArray; - if (parentID == "1" && parentID == "2") return parentsArray; parentsArray.push(element.text()); return getStatus($('#' + parentID), parentsArray); @@ -153,7 +256,10 @@ var booklight = function booklight() { booklight.UI.updateCounter(); booklight.searchBar.val(''); - id && booklight.elementStack.length ? booklight.UI.focusItem(id) : booklight.UI.higlightFirstElement(isFolder); + + isFolder ? booklight.UI.higlightFirstElement(isFolder) : booklight.UI.focusItem(id); + //id && booklight.elementStack.length ? booklight.UI.focusItem(id) : booklight.UI.higlightFirstElement(isFolder); + }, isRoot: function(){ if (booklight.searchBar.attr('placeholder').indexOf('>') === -1) return true; else return false; @@ -177,6 +283,11 @@ var booklight = function booklight() { switch (direction) { case ('DOWN') : { index !== lastElementIndex ? booklight.UI.focusItem($('.booklight_list li:visible.activeFolder').nextAll('li:visible').first().index()) : booklight.UI.focusItem(firstElementIndex); + if (booklight.context == 'url' && index >= lastElementIndex - 3) { + // Now we have checked that we are in a url context and the urls have been lazyloaded, we need to fetch more + // We need now to check if the lazy loader is for search results or for normal urls fetch + booklight.searchBar.val().length > 1 ? booklight.searchLazyLoader.load(false, false) : booklight.urlsLazyloader.load(false, false) ; + } } break; case ('UP') : { index !== firstElementIndex ? booklight.UI.focusItem($('.booklight_list li:visible.activeFolder').prevAll('li:visible').first().index()) : booklight.UI.focusItem(lastElementIndex); @@ -228,15 +339,24 @@ var booklight = function booklight() { addBookmark: function(url, title, folder) { // Extract the parameters needed to add a bookmark in the Chrome API - var url = window.location.href; - var title = document.title; - var folder = $('.booklight_list li.activeFolder').attr('id'); + var element = $('.booklight_list li.activeFolder'); + var url = window.location.href; + var title = document.title; + var folder = element.attr('id'); + var type = element.attr('data-type'); + + if (type !== "folder") { + booklight.manager.openURL("_self") + } else { + chrome.runtime.sendMessage({message: "booklight", url: url, folder: folder, title: title}, function(response) { + if (response.message == "success"){ + $('span.isBooklit').show(); + } + }); + } - chrome.runtime.sendMessage({message: "booklight", url: url, folder: folder, title: title}, function(response) { - if (response.message == "success"){ - $('span.isBooklit').show(); - } - }); + },openURL: function openURL(target) { + window.open($('.booklight_list li.activeFolder').attr('data-url'), target); } } @@ -254,5 +374,4 @@ var booklight = function booklight() { var booklight = new booklight(); -booklight.attachKeyboardEvents(); -booklight.UI.build(); +booklight.UI.build(); \ No newline at end of file diff --git a/css/booklight.css b/css/booklight.css index 403166f..6a8b0a7 100644 --- a/css/booklight.css +++ b/css/booklight.css @@ -12,7 +12,8 @@ .booklight input[type="text"]::-webkit-input-placeholder {color: #BEB9B9;} .booklight input[type="text"]:focus{border:none;box-shadow: none} .booklight_list {position: relative;overflow-y: scroll;height: 215px;list-style: none;width: 96%;padding: 0 5px 0 0;color: #777; margin: 0 auto;} -.booklight_list>li{margin: 0; padding: 0 5px; cursor:pointer;line-height: 1.5em;font-size: 13px; text-align: left} +.booklight_list>li{margin: 0; padding: 0 5px; cursor:pointer;line-height: 1.5em;font-size: 13px; text-align: left; list-style: none; width:100%;} +.booklight_list > li > img {vertical-align: middle;margin: 0 10px 0 0;} .booklight_list>li.isFolder {color:#3498db;} .booklight_resultsbar {position: absolute;font-size: 9px;top: 55px;right: 10px;color: #ddd;} .booklight_list>li.activeFolder {background: #2ecc71; color: #fff;} diff --git a/css/fontello.css b/css/fontello.css index ff3d090..10e6ed6 100644 --- a/css/fontello.css +++ b/css/fontello.css @@ -17,40 +17,4 @@ src: url('../font/fontello.svg?76547315#fontello') format('svg'); } } -*/ - - [class^="icon-"]:before, [class*=" icon-"]:before { - font-family: "fontello-booklight"; - font-style: normal; - font-weight: normal; - speak: none; - - display: inline-block; - text-decoration: inherit; - width: 1em; - margin-right: .2em; - text-align: center; - /* opacity: .8; */ - - /* For safety - reset parent styles, that can break glyph codes*/ - font-variant: normal; - text-transform: none; - - /* fix buttons height, for twitter bootstrap */ - line-height: 1em; - - /* Animation center compensation - margins should be symmetric */ - /* remove if not needed */ - margin-left: .2em; - - /* you can be more comfortable with increased icons size */ - /* font-size: 120%; */ - - /* Uncomment for 3D effect */ - /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ -} - -.icon-folder-empty:before { content: '\e800'; } /* '' */ -.icon-angle-double-right:before { content: '\e801'; } /* '' */ -.icon-lightbulb:before { content: '\e802'; } /* '' */ -.icon-star:before { content: '\e803'; } /* '' */ \ No newline at end of file +*/ \ No newline at end of file diff --git a/helper.js b/helper.js index b847556..6254540 100644 --- a/helper.js +++ b/helper.js @@ -1,16 +1,10 @@ // Additional Helper classes and overriders // replace a string at a certain range with another string + function replaceRange(s, start, end, substitute) { return s.substring(0, start) + substitute + s.substring(end); } -// Overriding the filter function to make it work on the input boxes -key.filter = function(event){ - var tagName = (event.target || event.srcElement).tagName; - key.setScope(/^(INPUT)$/.test(tagName) ? 'input' : 'other'); - return true; -} - // Overriding the default jQuery contains to make it case insensitive $.expr[":"].contains = $.expr.createPseudo(function(arg) { return function( elem ) { diff --git a/lib/fuse.js b/lib/fuse.js new file mode 100644 index 0000000..423a3e5 --- /dev/null +++ b/lib/fuse.js @@ -0,0 +1,342 @@ +/** + * Fuse - Lightweight fuzzy-search + * + * Copyright (c) 2012 Kirollos Risk . + * All Rights Reserved. Apache Software License 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function () { + /** + * Adapted from "Diff, Match and Patch", by Google + * + * http://code.google.com/p/google-diff-match-patch/ + * + * Modified by: Kirollos Risk + * ----------------------------------------------- + * Details: the algorithm and structure was modified to allow the creation of + * instances with a method inside which does the actual + * bitap search. The (the string that is searched for) is only defined + * once per instance and thus it eliminates redundant re-creation when searching + * over a list of strings. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + */ + function Searcher(pattern, options) { + options = options || {}; + + // Aproximately where in the text is the pattern expected to be found? + var MATCH_LOCATION = options.location || 0, + + // Determines how close the match must be to the fuzzy location (specified above). + // An exact letter match which is 'distance' characters away from the fuzzy location + // would score as a complete mismatch. A distance of '0' requires the match be at + // the exact location specified, a threshold of '1000' would require a perfect match + // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold. + MATCH_DISTANCE = options.distance || 100, + + // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match + // (of both letters and location), a threshold of '1.0' would match anything. + MATCH_THRESHOLD = options.threshold || 0.6, + + + pattern = options.caseSensitive ? pattern : pattern.toLowerCase(), + patternLen = pattern.length; + + if (patternLen > 32) { + throw new Error('Pattern length is too long'); + } + + var matchmask = 1 << (patternLen - 1); + + /** + * Initialise the alphabet for the Bitap algorithm. + * @return {Object} Hash of character locations. + * @private + */ + var pattern_alphabet = (function () { + var mask = {}, + i = 0; + + for (i = 0; i < patternLen; i++) { + mask[pattern.charAt(i)] = 0; + } + + for (i = 0; i < patternLen; i++) { + mask[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); + } + + return mask; + })(); + + /** + * Compute and return the score for a match with errors and = start; j--) { + // The alphabet is a sparse hash, so the following line generates warnings. + charMatch = pattern_alphabet[text.charAt(j - 1)]; + if (i === 0) { + // First pass: exact match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; + } else { + // Subsequent passes: fuzzy match. + rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | (((lastRd[j + 1] | lastRd[j]) << 1) | 1) | lastRd[j + 1]; + } + if (rd[j] & matchmask) { + score = match_bitapScore(i, j - 1); + // This match will almost certainly be better than any existing match. + // But check anyway. + if (score <= scoreThreshold) { + // Told you so. + scoreThreshold = score; + bestLoc = j - 1; + locations.push(bestLoc); + + if (bestLoc > MATCH_LOCATION) { + // When passing loc, don't exceed our current distance from loc. + start = Math.max(1, 2 * MATCH_LOCATION - bestLoc); + } else { + // Already passed loc, downhill from here on in. + break; + } + } + } + } + // No hope for a (better) match at greater error levels. + if (match_bitapScore(i + 1, MATCH_LOCATION) > scoreThreshold) { + break; + } + lastRd = rd; + } + + return { + isMatch: bestLoc >= 0, + score: score + }; + + } + } + + /** + * @param {Array} list + * @param {Object} options + * @public + */ + function Fuse(list, options) { + options = options || {}; + var keys = options.keys; + + /** + * Searches for all the items whose keys (fuzzy) match the pattern. + * @param {String} pattern The pattern string to fuzzy search on. + * @return {Array} A list of all serch matches. + * @public + */ + this.search = function (pattern) { + //console.time('total'); + + var searcher = new Searcher(pattern, options), + i, j, item, text, dataLen = list.length, + bitapResult, rawResults = [], resultMap = {}, + rawResultsLen, existingResult, results = [], + compute = null; + + //console.time('search'); + + /** + * Calls for bitap analysis. Builds the raw result list. + * @param {String} text The pattern string to fuzzy search on. + * @param {String|Int} entity If the is an Array, then entity will be an index, + * otherwise it's the item object. + * @param {Int} index + * @return {Object|Int} + * @private + */ + function analyzeText(text, entity, index) { + // Check if the text can be searched + if (text !== undefined && text !== null && typeof text === 'string') { + + // Get the result + bitapResult = searcher.search(text); + + // If a match is found, add the item to , including its score + if (bitapResult.isMatch) { + + //console.log(bitapResult.score); + + // Check if the item already exists in our results + existingResult = resultMap[index]; + if (existingResult) { + // Use the lowest score + existingResult.score = Math.min(existingResult.score, bitapResult.score); + } else { + // Add it to the raw result list + resultMap[index] = { + item: entity, + score: bitapResult.score + }; + rawResults.push(resultMap[index]); + } + } + } + } + + // Check the first item in the list, if it's a string, then we assume + // that every item in the list is also a string, and thus it's a flattened array. + if (typeof list[0] === 'string') { + // Iterate over every item + for (i = 0; i < dataLen; i++) { + analyzeText(list[i], i, i); + } + } else { + // Otherwise, the first item is an Object (hopefully), and thus the searching + // is done on the values of the keys of each item. + + // Iterate over every item + for (i = 0; i < dataLen; i++) { + item = list[i]; + // Iterate over every key + for (j = 0; j < keys.length; j++) { + analyzeText(item[keys[j]], item, i); + } + } + } + + //console.timeEnd('search'); + + // Sort the results, form lowest to highest score + //console.time('sort'); + rawResults.sort(function (a, b) { + return a.score - b.score; + }); + //console.timeEnd('sort'); + + // From the results, push into a new array only the item identifier (if specified) + // of the entire item. This is because we don't want to return the , + // since it contains other metadata; + //console.time('build'); + rawResultsLen = rawResults.length; + for (i = 0; i < rawResultsLen; i++) { + results.push(options.id ? rawResults[i].item[options.id] : rawResults[i].item); + } + + //console.timeEnd('build'); + + //console.timeEnd('total'); + + return results; + } + } + + //Export to Common JS Loader + if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { + if (typeof module.setExports === 'function') { + module.setExports(Fuse); + } else { + module.exports = Fuse; + } + } else { + window.Fuse = Fuse; + } + +})(); \ No newline at end of file diff --git a/lib/hideSeek.js b/lib/hideSeek.js deleted file mode 100644 index 2b01a89..0000000 --- a/lib/hideSeek.js +++ /dev/null @@ -1,202 +0,0 @@ -/** - * HideSeek jQuery plugin - * - * @copyright Copyright 2013, Dimitris Krestos - * @license Apache License, Version 2.0 (http://www.opensource.org/licenses/apache2.0.php) - * @link http://vdw.staytuned.gr - * @version v0.5.5 - * - * Dependencies are include in minified versions at the bottom: - * 1. Highlight v4 by Johann Burkard - * - */ - - /* Sample html structure - - - - - or - - -
    - item 1 - ... - item 2 -
    - - or any similar structure... - - */ - -;(function($, window, undefined) { - "use strict"; - - $.fn.hideseek = function(options) { - - var defaults = { - list: '.hideseek-data', - nodata: '', - attribute: 'text', - highlight: false, - ignore: '', - navigation: false - }; - - var options = $.extend(defaults, options); - - return this.each(function() { - - var $this = $(this); - - // Ungly overwrite - options.list = $this.data('list') || options.list; - options.nodata = $this.data('nodata') || options.nodata; - options.attribute = $this.data('attribute') || options.attribute; - options.highlight = $this.data('highlight') || options.highlight; - options.ignore = $this.data('ignore') || options.ignore; - - var $list = $(options.list); - - if (options.navigation) $this.attr('autocomplete', 'off'); - - $this.keyup(function(e) { - - if (e.keyCode != 38 && e.keyCode != 40 && e.keyCode != 13) { - - var q = $this.val().toLowerCase(); - - $list.children(options.ignore.trim() ? ":not(" + options.ignore + ")" : '').removeClass('selected').each(function() { - - var data = (options.attribute != 'text') ? $(this).attr(options.attribute).toLowerCase() : $(this).text().toLowerCase(); - - if (data.indexOf(q) == -1) { - - $(this).hide(); - - $this.trigger('_after_each'); - - } else { - - options.highlight ? $(this).removeHighlight().highlight(q).show() : $(this).show(); - - $this.trigger('_after_each'); - - } - - }); - - // No results message - if (options.nodata) { - - $list.find('.no-results').remove(); - - if (!$list.children(':not([style*="display: none"])').length) { - - $list - .children() - .first() - .clone() - .removeHighlight() - .addClass('no-results') - .show() - .prependTo(options.list) - .text(options.nodata); - - } - - } - - $this.trigger('_after'); - - }; - - // Navigation - function current(element) { - return element.children('.selected:visible'); - }; - - function prev(element) { - return current(element).prevAll(":visible:first"); - }; - - function next(element) { - return current(element).nextAll(":visible:first"); - }; - - if (options.navigation) { - - if (e.keyCode == 38) { - - if (current($list).length) { - - prev($list).addClass('selected'); - - current($list).last().removeClass('selected'); - - } else { - - $list.children(':visible').last().addClass('selected'); - - }; - - } else if (e.keyCode == 40) { - - if (current($list).length) { - - next($list).addClass('selected'); - - current($list).first().removeClass('selected'); - - } else { - - $list.children(':visible').first().addClass('selected'); - - }; - - } else if (e.keyCode == 13) { - - if (current($list).find('a').length) { - - document.location = current($list).find('a').attr('href'); - - } else { - - $this.val(current($list).text()); - - }; - - }; - - }; - - }); - - }); - - }; - - $(document).ready(function () { $('[data-toggle="hideseek"]').hideseek(); }); - -})(jQuery); - -/* - -highlight v4 - -Highlights arbitrary terms. - - - -MIT license. - -Johann Burkard - - - -*/ -jQuery.fn.highlight=function(t){function e(t,i){var n=0;if(3==t.nodeType){var a=t.data.toUpperCase().indexOf(i);if(a>=0){var s=document.createElement("mark");s.className="highlight";var r=t.splitText(a);r.splitText(i.length);var o=r.cloneNode(!0);s.appendChild(o),r.parentNode.replaceChild(s,r),n=1}}else if(1==t.nodeType&&t.childNodes&&!/(script|style)/i.test(t.tagName))for(var h=0;h= 0 && ((_ref = _convert_key_to_readable(e.keyCode)) !== "cmd" && _ref !== "shift" && _ref !== "alt" && _ref !== "caps" && _ref !== "tab")) { + return this._receive_input(e, false); + } + }; + + Listener.prototype._cmd_bug_check = function(combo_keys) { + if (_metakey === "cmd" && __indexOf.call(this._keys_down, "cmd") >= 0 && __indexOf.call(combo_keys, "cmd") < 0) { + return false; + } + return true; + }; + + Listener.prototype._prevent_default = function(e, should_prevent) { + if ((should_prevent || this.should_suppress_event_defaults) && !this.should_force_event_defaults) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + if (e.stopPropagation) { + return e.stopPropagation(); + } + } + }; + + Listener.prototype._get_active_combos = function(key) { + var active_combos, keys_down; + active_combos = []; + keys_down = _filter_array(this._keys_down, function(down_key) { + return down_key !== key; + }); + keys_down.push(key); + this._match_combo_arrays(keys_down, (function(_this) { + return function(match) { + if (_this._cmd_bug_check(match.keys)) { + return active_combos.push(match); + } + }; + })(this)); + this._fuzzy_match_combo_arrays(keys_down, (function(_this) { + return function(match) { + if (__indexOf.call(active_combos, match) >= 0) { + return; + } + if (!(match.is_solitary || !_this._cmd_bug_check(match.keys))) { + return active_combos.push(match); + } + }; + })(this)); + return active_combos; + }; + + Listener.prototype._get_potential_combos = function(key) { + var combo, potentials, _i, _len, _ref; + potentials = []; + _ref = this._registered_combos; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + combo = _ref[_i]; + if (combo.is_sequence) { + continue; + } + if (__indexOf.call(combo.keys, key) >= 0 && this._cmd_bug_check(combo.keys)) { + potentials.push(combo); + } + } + return potentials; + }; + + Listener.prototype._add_to_active_combos = function(combo) { + var active_combo, active_key, active_keys, already_replaced, combo_key, i, should_prepend, should_replace, _i, _j, _k, _len, _len1, _ref, _ref1; + should_replace = false; + should_prepend = true; + already_replaced = false; + if (__indexOf.call(this._active_combos, combo) >= 0) { + return true; + } else if (this._active_combos.length) { + for (i = _i = 0, _ref = this._active_combos.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + active_combo = this._active_combos[i]; + if (!(active_combo && active_combo.is_exclusive && combo.is_exclusive)) { + continue; + } + active_keys = active_combo.keys; + if (!should_replace) { + for (_j = 0, _len = active_keys.length; _j < _len; _j++) { + active_key = active_keys[_j]; + should_replace = true; + if (__indexOf.call(combo.keys, active_key) < 0) { + should_replace = false; + break; + } + } + } + if (should_prepend && !should_replace) { + _ref1 = combo.keys; + for (_k = 0, _len1 = _ref1.length; _k < _len1; _k++) { + combo_key = _ref1[_k]; + should_prepend = false; + if (__indexOf.call(active_keys, combo_key) < 0) { + should_prepend = true; + break; + } + } + } + if (should_replace) { + if (already_replaced) { + active_combo = this._active_combos.splice(i, 1)[0]; + if (active_combo != null) { + active_combo.reset(); + } + } else { + active_combo = this._active_combos.splice(i, 1, combo)[0]; + if (active_combo != null) { + active_combo.reset(); + } + already_replaced = true; + } + should_prepend = false; + } + } + } + if (should_prepend) { + this._active_combos.unshift(combo); + } + return should_replace || should_prepend; + }; + + Listener.prototype._remove_from_active_combos = function(combo) { + var active_combo, i, _i, _ref; + for (i = _i = 0, _ref = this._active_combos.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + active_combo = this._active_combos[i]; + if (active_combo === combo) { + combo = this._active_combos.splice(i, 1)[0]; + combo.reset(); + break; + } + } + }; + + Listener.prototype._get_possible_sequences = function() { + var combo, i, j, match, matches, sequence, _i, _j, _k, _len, _ref, _ref1, _ref2; + matches = []; + _ref = this._registered_combos; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + combo = _ref[_i]; + for (j = _j = 1, _ref1 = this._sequence.length; 1 <= _ref1 ? _j <= _ref1 : _j >= _ref1; j = 1 <= _ref1 ? ++_j : --_j) { + sequence = this._sequence.slice(-j); + if (!combo.is_sequence) { + continue; + } + if (__indexOf.call(combo.keys, "shift") < 0) { + sequence = _filter_array(sequence, function(key) { + return key !== "shift"; + }); + if (!sequence.length) { + continue; + } + } + for (i = _k = 0, _ref2 = sequence.length; 0 <= _ref2 ? _k < _ref2 : _k > _ref2; i = 0 <= _ref2 ? ++_k : --_k) { + if (combo.keys[i] === sequence[i]) { + match = true; + } else { + match = false; + break; + } + } + if (match) { + matches.push(combo); + } + } + } + return matches; + }; + + Listener.prototype._add_key_to_sequence = function(key, e) { + var combo, sequence_combos, _i, _len; + this._sequence.push(key); + sequence_combos = this._get_possible_sequences(); + if (sequence_combos.length) { + for (_i = 0, _len = sequence_combos.length; _i < _len; _i++) { + combo = sequence_combos[_i]; + this._prevent_default(e, combo.prevent_default); + } + if (this._sequence_timer) { + clearTimeout(this._sequence_timer); + } + if (this.sequence_delay > -1) { + this._sequence_timer = setTimeout(function() { + return this._sequence = []; + }, this.sequence_delay); + } + } else { + this._sequence = []; + } + }; + + Listener.prototype._get_sequence = function(key) { + var combo, i, j, match, seq_key, sequence, _i, _j, _k, _len, _ref, _ref1, _ref2; + _ref = this._registered_combos; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + combo = _ref[_i]; + if (!combo.is_sequence) { + continue; + } + for (j = _j = 1, _ref1 = this._sequence.length; 1 <= _ref1 ? _j <= _ref1 : _j >= _ref1; j = 1 <= _ref1 ? ++_j : --_j) { + sequence = (_filter_array(this._sequence, function(seq_key) { + if (__indexOf.call(combo.keys, "shift") >= 0) { + return true; + } + return seq_key !== "shift"; + })).slice(-j); + if (combo.keys.length !== sequence.length) { + continue; + } + for (i = _k = 0, _ref2 = sequence.length; 0 <= _ref2 ? _k < _ref2 : _k > _ref2; i = 0 <= _ref2 ? ++_k : --_k) { + seq_key = sequence[i]; + if (__indexOf.call(combo.keys, "shift") < 0) { + if (seq_key === "shift") { + continue; + } + } + if (key === "shift" && __indexOf.call(combo.keys, "shift") < 0) { + continue; + } + if (combo.keys[i] === seq_key) { + match = true; + } else { + match = false; + break; + } + } + } + if (match) { + return combo; + } + } + return false; + }; + + Listener.prototype._receive_input = function(e, is_keydown) { + var key; + if (this._prevent_capture) { + if (this._keys_down.length) { + this._keys_down = []; + } + return; + } + key = _convert_key_to_readable(e.keyCode); + if (!is_keydown && !this._keys_down.length && (key === "alt" || key === _metakey)) { + return; + } + if (!key) { + return; + } + if (is_keydown) { + return this._key_down(key, e); + } else { + return this._key_up(key, e); + } + }; + + Listener.prototype._fire = function(event, combo, key_event, is_autorepeat) { + if (typeof combo["on_" + event] === "function") { + this._prevent_default(key_event, combo["on_" + event].call(combo["this"], key_event, combo.count, is_autorepeat) !== true); + } + if (event === "release") { + combo.count = 0; + } + if (event === "keyup") { + return combo.keyup_fired = true; + } + }; + + Listener.prototype._match_combo_arrays = function(potential_match, match_handler) { + var source_combo, _i, _len, _ref; + _ref = this._registered_combos; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + source_combo = _ref[_i]; + if ((!source_combo.is_unordered && _compare_arrays_sorted(potential_match, source_combo.keys)) || (source_combo.is_unordered && _compare_arrays(potential_match, source_combo.keys))) { + match_handler(source_combo); + } + } + }; + + Listener.prototype._fuzzy_match_combo_arrays = function(potential_match, match_handler) { + var source_combo, _i, _len, _ref; + _ref = this._registered_combos; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + source_combo = _ref[_i]; + if ((!source_combo.is_unordered && _is_array_in_array_sorted(source_combo.keys, potential_match)) || (source_combo.is_unordered && _is_array_in_array(source_combo.keys, potential_match))) { + match_handler(source_combo); + } + } + }; + + Listener.prototype._keys_remain = function(combo) { + var key, keys_remain, _i, _len, _ref; + _ref = combo.keys; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + key = _ref[_i]; + if (__indexOf.call(this._keys_down, key) >= 0) { + keys_remain = true; + break; + } + } + return keys_remain; + }; + + Listener.prototype._key_down = function(key, e) { + var combo, combos, event_mod, i, mod, potential, potential_combos, sequence_combo, shifted_key, _i, _j, _k, _len, _len1, _ref; + shifted_key = _convert_to_shifted_key(key, e); + if (shifted_key) { + key = shifted_key; + } + this._add_key_to_sequence(key, e); + sequence_combo = this._get_sequence(key); + if (sequence_combo) { + this._fire("keydown", sequence_combo, e); + } + for (mod in _modifier_event_mapping) { + event_mod = _modifier_event_mapping[mod]; + if (!e[event_mod]) { + continue; + } + if (mod === key || __indexOf.call(this._keys_down, mod) >= 0) { + continue; + } + this._keys_down.push(mod); + } + for (mod in _modifier_event_mapping) { + event_mod = _modifier_event_mapping[mod]; + if (mod === key) { + continue; + } + if (__indexOf.call(this._keys_down, mod) >= 0 && !e[event_mod]) { + if (mod === "cmd" && _metakey !== "cmd") { + continue; + } + for (i = _i = 0, _ref = this._keys_down.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + if (this._keys_down[i] === mod) { + this._keys_down.splice(i, 1); + } + } + } + } + combos = this._get_active_combos(key); + potential_combos = this._get_potential_combos(key); + for (_j = 0, _len = combos.length; _j < _len; _j++) { + combo = combos[_j]; + this._handle_combo_down(combo, potential_combos, key, e); + } + if (potential_combos.length) { + for (_k = 0, _len1 = potential_combos.length; _k < _len1; _k++) { + potential = potential_combos[_k]; + this._prevent_default(e, potential.prevent_default); + } + } + if (__indexOf.call(this._keys_down, key) < 0) { + this._keys_down.push(key); + } + }; + + Listener.prototype._handle_combo_down = function(combo, potential_combos, key, e) { + var is_autorepeat, is_other_exclusive, potential_combo, result, _i, _len; + if (__indexOf.call(combo.keys, key) < 0) { + return false; + } + this._prevent_default(e, combo && combo.prevent_default); + is_autorepeat = false; + if (__indexOf.call(this._keys_down, key) >= 0) { + is_autorepeat = true; + if (!combo.allows_key_repeat()) { + return false; + } + } + result = this._add_to_active_combos(combo, key); + combo.keyup_fired = false; + is_other_exclusive = false; + if (combo.is_exclusive) { + for (_i = 0, _len = potential_combos.length; _i < _len; _i++) { + potential_combo = potential_combos[_i]; + if (potential_combo.is_exclusive && potential_combo.keys.length > combo.keys.length) { + is_other_exclusive = true; + break; + } + } + } + if (!is_other_exclusive) { + if (combo.is_counting && typeof combo.on_keydown === "function") { + combo.count += 1; + } + if (result) { + return this._fire("keydown", combo, e, is_autorepeat); + } + } + }; + + Listener.prototype._key_up = function(key, e) { + var active_combo, active_combos_length, combo, combos, i, sequence_combo, shifted_key, unshifted_key, _i, _j, _k, _l, _len, _len1, _len2, _ref, _ref1, _ref2, _ref3; + unshifted_key = key; + shifted_key = _convert_to_shifted_key(key, e); + if (shifted_key) { + key = shifted_key; + } + shifted_key = _keycode_shifted_keys[unshifted_key]; + if (e.shiftKey) { + if (!(shifted_key && __indexOf.call(this._keys_down, shifted_key) >= 0)) { + key = unshifted_key; + } + } else { + if (!(unshifted_key && __indexOf.call(this._keys_down, unshifted_key) >= 0)) { + key = shifted_key; + } + } + sequence_combo = this._get_sequence(key); + if (sequence_combo) { + this._fire("keyup", sequence_combo, e); + } + if (__indexOf.call(this._keys_down, key) < 0) { + return false; + } + for (i = _i = 0, _ref = this._keys_down.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + if ((_ref1 = this._keys_down[i]) === key || _ref1 === shifted_key || _ref1 === unshifted_key) { + this._keys_down.splice(i, 1); + break; + } + } + active_combos_length = this._active_combos.length; + combos = []; + _ref2 = this._active_combos; + for (_j = 0, _len = _ref2.length; _j < _len; _j++) { + active_combo = _ref2[_j]; + if (__indexOf.call(active_combo.keys, key) >= 0) { + combos.push(active_combo); + } + } + for (_k = 0, _len1 = combos.length; _k < _len1; _k++) { + combo = combos[_k]; + this._handle_combo_up(combo, e, key); + } + if (active_combos_length > 1) { + _ref3 = this._active_combos; + for (_l = 0, _len2 = _ref3.length; _l < _len2; _l++) { + active_combo = _ref3[_l]; + if (active_combo === void 0 || __indexOf.call(combos, active_combo) >= 0) { + continue; + } + if (!this._keys_remain(active_combo)) { + this._remove_from_active_combos(active_combo); + } + } + } + }; + + Listener.prototype._handle_combo_up = function(combo, e, key) { + var keys_down, keys_remaining; + this._prevent_default(e, combo && combo.prevent_default); + keys_remaining = this._keys_remain(combo); + if (!combo.keyup_fired) { + keys_down = this._keys_down.slice(); + keys_down.push(key); + if (!combo.is_solitary || _compare_arrays(keys_down, combo.keys)) { + this._fire("keyup", combo, e); + if (combo.is_counting && typeof combo.on_keyup === "function" && typeof combo.on_keydown !== "function") { + combo.count += 1; + } + } + } + if (!keys_remaining) { + this._fire("release", combo, e); + this._remove_from_active_combos(combo); + } + }; + + Listener.prototype.simple_combo = function(keys, callback) { + return this.register_combo({ + keys: keys, + on_keydown: callback + }); + }; + + Listener.prototype.counting_combo = function(keys, count_callback) { + return this.register_combo({ + keys: keys, + is_counting: true, + is_unordered: false, + on_keydown: count_callback + }); + }; + + Listener.prototype.sequence_combo = function(keys, callback) { + return this.register_combo({ + keys: keys, + on_keydown: callback, + is_sequence: true + }); + }; + + Listener.prototype.register_combo = function(combo_dictionary) { + var combo, property, value, _ref; + if (typeof combo_dictionary["keys"] === "string") { + combo_dictionary["keys"] = combo_dictionary["keys"].split(" "); + } + _ref = this._defaults; + for (property in _ref) { + if (!__hasProp.call(_ref, property)) continue; + value = _ref[property]; + if (combo_dictionary[property] === void 0) { + combo_dictionary[property] = value; + } + } + combo = new Combo(combo_dictionary); + if (_validate_combo(combo)) { + this._registered_combos.push(combo); + return combo; + } + }; + + Listener.prototype.register_many = function(combo_array) { + var combo, _i, _len, _results; + _results = []; + for (_i = 0, _len = combo_array.length; _i < _len; _i++) { + combo = combo_array[_i]; + _results.push(this.register_combo(combo)); + } + return _results; + }; + + Listener.prototype.unregister_combo = function(keys_or_combo) { + var combo, unregister_combo, _i, _len, _ref, _results; + if (!keys_or_combo) { + return false; + } + unregister_combo = (function(_this) { + return function(combo) { + var i, _i, _ref, _results; + _results = []; + for (i = _i = 0, _ref = _this._registered_combos.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + if (combo === _this._registered_combos[i]) { + _this._registered_combos.splice(i, 1); + break; + } else { + _results.push(void 0); + } + } + return _results; + }; + })(this); + if (keys_or_combo instanceof Combo) { + return unregister_combo(keys_or_combo); + } else { + if (typeof keys_or_combo === "string") { + keys_or_combo = keys_or_combo.split(" "); + } + _ref = this._registered_combos; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + combo = _ref[_i]; + if (combo == null) { + continue; + } + if ((combo.is_unordered && _compare_arrays(keys_or_combo, combo.keys)) || (!combo.is_unordered && _compare_arrays_sorted(keys_or_combo, combo.keys))) { + _results.push(unregister_combo(combo)); + } else { + _results.push(void 0); + } + } + return _results; + } + }; + + Listener.prototype.unregister_many = function(combo_array) { + var combo, _i, _len, _results; + _results = []; + for (_i = 0, _len = combo_array.length; _i < _len; _i++) { + combo = combo_array[_i]; + _results.push(this.unregister_combo(combo)); + } + return _results; + }; + + Listener.prototype.get_registered_combos = function() { + return this._registered_combos; + }; + + Listener.prototype.reset = function() { + return this._registered_combos = []; + }; + + Listener.prototype.listen = function() { + return this._prevent_capture = false; + }; + + Listener.prototype.stop_listening = function() { + return this._prevent_capture = true; + }; + + Listener.prototype.get_meta_key = function() { + return _metakey; + }; + + return Listener; + + })(); + + _decide_meta_key = function() { + if (navigator.userAgent.indexOf("Mac OS X") !== -1) { + _metakey = "cmd"; + } + }; + + _change_keycodes_by_browser = function() { + if (navigator.userAgent.indexOf("Opera") !== -1) { + _keycode_dictionary["17"] = "cmd"; + } + }; + + _convert_key_to_readable = function(k) { + return _keycode_dictionary[k]; + }; + + _filter_array = function(array, callback) { + var element; + if (array.filter) { + return array.filter(callback); + } else { + return (function() { + var _i, _len, _results; + _results = []; + for (_i = 0, _len = array.length; _i < _len; _i++) { + element = array[_i]; + if (callback(element)) { + _results.push(element); + } + } + return _results; + })(); + } + }; + + _compare_arrays = function(a1, a2) { + var item, _i, _len; + if (a1.length !== a2.length) { + return false; + } + for (_i = 0, _len = a1.length; _i < _len; _i++) { + item = a1[_i]; + if (__indexOf.call(a2, item) >= 0) { + continue; + } + return false; + } + return true; + }; + + _compare_arrays_sorted = function(a1, a2) { + var i, _i, _ref; + if (a1.length !== a2.length) { + return false; + } + for (i = _i = 0, _ref = a1.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + if (a1[i] !== a2[i]) { + return false; + } + } + return true; + }; + + _is_array_in_array = function(a1, a2) { + var item, _i, _len; + for (_i = 0, _len = a1.length; _i < _len; _i++) { + item = a1[_i]; + if (__indexOf.call(a2, item) < 0) { + return false; + } + } + return true; + }; + + _index_of_in_array = Array.prototype.indexOf || function(a, item) { + var i, _i, _ref; + for (i = _i = 0, _ref = a.length; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) { + if (a[i] === item) { + return i; + } + } + return -1; + }; + + _is_array_in_array_sorted = function(a1, a2) { + var index, item, prev, _i, _len; + prev = 0; + for (_i = 0, _len = a1.length; _i < _len; _i++) { + item = a1[_i]; + index = _index_of_in_array.call(a2, item); + if (index >= prev) { + prev = index; + } else { + return false; + } + } + return true; + }; + + _log_error = function() { + if (keypress.debug) { + return console.log.apply(console, arguments); + } + }; + + _key_is_valid = function(key) { + var valid, valid_key, _; + valid = false; + for (_ in _keycode_dictionary) { + valid_key = _keycode_dictionary[_]; + if (key === valid_key) { + valid = true; + break; + } + } + if (!valid) { + for (_ in _keycode_shifted_keys) { + valid_key = _keycode_shifted_keys[_]; + if (key === valid_key) { + valid = true; + break; + } + } + } + return valid; + }; + + _validate_combo = function(combo) { + var alt_name, i, key, mod_key, non_modifier_keys, property, validated, value, _i, _j, _k, _len, _len1, _ref, _ref1; + validated = true; + if (!combo.keys.length) { + _log_error("You're trying to bind a combo with no keys:", combo); + } + for (i = _i = 0, _ref = combo.keys.length; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + key = combo.keys[i]; + alt_name = _keycode_alternate_names[key]; + if (alt_name) { + key = combo.keys[i] = alt_name; + } + if (key === "meta") { + combo.keys.splice(i, 1, _metakey); + } + if (key === "cmd") { + _log_error("Warning: use the \"meta\" key rather than \"cmd\" for Windows compatibility"); + } + } + _ref1 = combo.keys; + for (_j = 0, _len = _ref1.length; _j < _len; _j++) { + key = _ref1[_j]; + if (!_key_is_valid(key)) { + _log_error("Do not recognize the key \"" + key + "\""); + validated = false; + } + } + if (__indexOf.call(combo.keys, "meta") >= 0 || __indexOf.call(combo.keys, "cmd") >= 0) { + non_modifier_keys = combo.keys.slice(); + for (_k = 0, _len1 = _modifier_keys.length; _k < _len1; _k++) { + mod_key = _modifier_keys[_k]; + if ((i = _index_of_in_array.call(non_modifier_keys, mod_key)) > -1) { + non_modifier_keys.splice(i, 1); + } + } + if (non_modifier_keys.length > 1) { + _log_error("META and CMD key combos cannot have more than 1 non-modifier keys", combo, non_modifier_keys); + validated = false; + } + } + for (property in combo) { + value = combo[property]; + if (_factory_defaults[property] === "undefined") { + _log_error("The property " + property + " is not a valid combo property. Your combo has still been registered."); + } + } + return validated; + }; + + _convert_to_shifted_key = function(key, e) { + var k; + if (!e.shiftKey) { + return false; + } + k = _keycode_shifted_keys[key]; + if (k != null) { + return k; + } + return false; + }; + + _modifier_event_mapping = { + "cmd": "metaKey", + "ctrl": "ctrlKey", + "shift": "shiftKey", + "alt": "altKey" + }; + + _keycode_alternate_names = { + "escape": "esc", + "control": "ctrl", + "command": "cmd", + "break": "pause", + "windows": "cmd", + "option": "alt", + "caps_lock": "caps", + "apostrophe": "\'", + "semicolon": ";", + "tilde": "~", + "accent": "`", + "scroll_lock": "scroll", + "num_lock": "num" + }; + + _keycode_shifted_keys = { + "/": "?", + ".": ">", + ",": "<", + "\'": "\"", + ";": ":", + "[": "{", + "]": "}", + "\\": "|", + "`": "~", + "=": "+", + "-": "_", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")" + }; + + _keycode_dictionary = { + 0: "\\", + 8: "backspace", + 9: "tab", + 12: "num", + 13: "enter", + 16: "shift", + 17: "ctrl", + 18: "alt", + 19: "pause", + 20: "caps", + 27: "esc", + 32: "space", + 33: "pageup", + 34: "pagedown", + 35: "end", + 36: "home", + 37: "left", + 38: "up", + 39: "right", + 40: "down", + 44: "print", + 45: "insert", + 46: "delete", + 48: "0", + 49: "1", + 50: "2", + 51: "3", + 52: "4", + 53: "5", + 54: "6", + 55: "7", + 56: "8", + 57: "9", + 65: "a", + 66: "b", + 67: "c", + 68: "d", + 69: "e", + 70: "f", + 71: "g", + 72: "h", + 73: "i", + 74: "j", + 75: "k", + 76: "l", + 77: "m", + 78: "n", + 79: "o", + 80: "p", + 81: "q", + 82: "r", + 83: "s", + 84: "t", + 85: "u", + 86: "v", + 87: "w", + 88: "x", + 89: "y", + 90: "z", + 91: "cmd", + 92: "cmd", + 93: "cmd", + 96: "num_0", + 97: "num_1", + 98: "num_2", + 99: "num_3", + 100: "num_4", + 101: "num_5", + 102: "num_6", + 103: "num_7", + 104: "num_8", + 105: "num_9", + 106: "num_multiply", + 107: "num_add", + 108: "num_enter", + 109: "num_subtract", + 110: "num_decimal", + 111: "num_divide", + 112: "f1", + 113: "f2", + 114: "f3", + 115: "f4", + 116: "f5", + 117: "f6", + 118: "f7", + 119: "f8", + 120: "f9", + 121: "f10", + 122: "f11", + 123: "f12", + 124: "print", + 144: "num", + 145: "scroll", + 186: ";", + 187: "=", + 188: ",", + 189: "-", + 190: ".", + 191: "/", + 192: "`", + 219: "[", + 220: "\\", + 221: "]", + 222: "\'", + 223: "`", + 224: "cmd", + 225: "alt", + 57392: "ctrl", + 63289: "num", + 59: ";", + 61: "-", + 173: "=" + }; + + keypress._keycode_dictionary = _keycode_dictionary; + + keypress._is_array_in_array_sorted = _is_array_in_array_sorted; + + _decide_meta_key(); + + _change_keycodes_by_browser(); + + if (typeof define === "function" && define.amd) { + define([], function() { + return keypress; + }); + } else if (typeof exports !== "undefined" && exports !== null) { + exports.keypress = keypress; + } else { + window.keypress = keypress; + } + +}).call(this); \ No newline at end of file diff --git a/lib/keymaster.js b/lib/keymaster.js deleted file mode 100644 index bd0942e..0000000 --- a/lib/keymaster.js +++ /dev/null @@ -1,296 +0,0 @@ -// keymaster.js -// (c) 2011-2013 Thomas Fuchs -// keymaster.js may be freely distributed under the MIT license. - -;(function(global){ - var k, - _handlers = {}, - _mods = { 16: false, 18: false, 17: false, 91: false }, - _scope = 'all', - // modifier keys - _MODIFIERS = { - '⇧': 16, shift: 16, - '⌥': 18, alt: 18, option: 18, - '⌃': 17, ctrl: 17, control: 17, - '⌘': 91, command: 91 - }, - // special keys - _MAP = { - backspace: 8, tab: 9, clear: 12, - enter: 13, 'return': 13, - esc: 27, escape: 27, space: 32, - left: 37, up: 38, - right: 39, down: 40, - del: 46, 'delete': 46, - home: 36, end: 35, - pageup: 33, pagedown: 34, - ',': 188, '.': 190, '/': 191, - '`': 192, '-': 189, '=': 187, - ';': 186, '\'': 222, - '[': 219, ']': 221, '\\': 220 - }, - code = function(x){ - return _MAP[x] || x.toUpperCase().charCodeAt(0); - }, - _downKeys = []; - - for(k=1;k<20;k++) _MAP['f'+k] = 111+k; - - // IE doesn't support Array#indexOf, so have a simple replacement - function index(array, item){ - var i = array.length; - while(i--) if(array[i]===item) return i; - return -1; - } - - // for comparing mods before unassignment - function compareArray(a1, a2) { - if (a1.length != a2.length) return false; - for (var i = 0; i < a1.length; i++) { - if (a1[i] !== a2[i]) return false; - } - return true; - } - - var modifierMap = { - 16:'shiftKey', - 18:'altKey', - 17:'ctrlKey', - 91:'metaKey' - }; - function updateModifierKey(event) { - for(k in _mods) _mods[k] = event[modifierMap[k]]; - }; - - // handle keydown event - function dispatch(event) { - var key, handler, k, i, modifiersMatch, scope; - key = event.keyCode; - - if (index(_downKeys, key) == -1) { - _downKeys.push(key); - } - - // if a modifier key, set the key. property to true and return - if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko - if(key in _mods) { - _mods[key] = true; - // 'assignKey' from inside this closure is exported to window.key - for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true; - return; - } - updateModifierKey(event); - - // see if we need to ignore the keypress (filter() can can be overridden) - // by default ignore key presses if a select, textarea, or input is focused - if(!assignKey.filter.call(this, event)) return; - - // abort if no potentially matching shortcuts found - if (!(key in _handlers)) return; - - scope = getScope(); - - // for each potential shortcut - for (i = 0; i < _handlers[key].length; i++) { - handler = _handlers[key][i]; - - // see if it's in the current scope - if(handler.scope == scope || handler.scope == 'all'){ - // check if modifiers match if any - modifiersMatch = handler.mods.length > 0; - for(k in _mods) - if((!_mods[k] && index(handler.mods, +k) > -1) || - (_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false; - // call the handler and stop the event if neccessary - if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){ - if(handler.method(event, handler)===false){ - if(event.preventDefault) event.preventDefault(); - else event.returnValue = false; - if(event.stopPropagation) event.stopPropagation(); - if(event.cancelBubble) event.cancelBubble = true; - } - } - } - } - }; - - // unset modifier keys on keyup - function clearModifier(event){ - var key = event.keyCode, k, - i = index(_downKeys, key); - - // remove key from _downKeys - if (i >= 0) { - _downKeys.splice(i, 1); - } - - if(key == 93 || key == 224) key = 91; - if(key in _mods) { - _mods[key] = false; - for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false; - } - }; - - function resetModifiers() { - for(k in _mods) _mods[k] = false; - for(k in _MODIFIERS) assignKey[k] = false; - }; - - // parse and assign shortcut - function assignKey(key, scope, method){ - var keys, mods; - keys = getKeys(key); - if (method === undefined) { - method = scope; - scope = 'all'; - } - - // for each shortcut - for (var i = 0; i < keys.length; i++) { - // set modifier keys if any - mods = []; - key = keys[i].split('+'); - if (key.length > 1){ - mods = getMods(key); - key = [key[key.length-1]]; - } - // convert to keycode and... - key = key[0] - key = code(key); - // ...store handler - if (!(key in _handlers)) _handlers[key] = []; - _handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods }); - } - }; - - // unbind all handlers for given key in current scope - function unbindKey(key, scope) { - var multipleKeys, keys, - mods = [], - i, j, obj; - - multipleKeys = getKeys(key); - - for (j = 0; j < multipleKeys.length; j++) { - keys = multipleKeys[j].split('+'); - - if (keys.length > 1) { - mods = getMods(keys); - } - - key = keys[keys.length - 1]; - key = code(key); - - if (scope === undefined) { - scope = getScope(); - } - if (!_handlers[key]) { - return; - } - for (i = 0; i < _handlers[key].length; i++) { - obj = _handlers[key][i]; - // only clear handlers if correct scope and mods match - if (obj.scope === scope && compareArray(obj.mods, mods)) { - _handlers[key][i] = {}; - } - } - } - }; - - // Returns true if the key with code 'keyCode' is currently down - // Converts strings into key codes. - function isPressed(keyCode) { - if (typeof(keyCode)=='string') { - keyCode = code(keyCode); - } - return index(_downKeys, keyCode) != -1; - } - - function getPressedKeyCodes() { - return _downKeys.slice(0); - } - - function filter(event){ - var tagName = (event.target || event.srcElement).tagName; - // ignore keypressed in any elements that support keyboard data input - return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); - } - - // initialize key. to false - for(k in _MODIFIERS) assignKey[k] = false; - - // set current scope (default 'all') - function setScope(scope){ _scope = scope || 'all' }; - function getScope(){ return _scope || 'all' }; - - // delete all handlers for a given scope - function deleteScope(scope){ - var key, handlers, i; - - for (key in _handlers) { - handlers = _handlers[key]; - for (i = 0; i < handlers.length; ) { - if (handlers[i].scope === scope) handlers.splice(i, 1); - else i++; - } - } - }; - - // abstract key logic for assign and unassign - function getKeys(key) { - var keys; - key = key.replace(/\s/g, ''); - keys = key.split(','); - if ((keys[keys.length - 1]) == '') { - keys[keys.length - 2] += ','; - } - return keys; - } - - // abstract mods logic for assign and unassign - function getMods(key) { - var mods = key.slice(0, key.length - 1); - for (var mi = 0; mi < mods.length; mi++) - mods[mi] = _MODIFIERS[mods[mi]]; - return mods; - } - - // cross-browser events - function addEvent(object, event, method) { - if (object.addEventListener) - object.addEventListener(event, method, false); - else if(object.attachEvent) - object.attachEvent('on'+event, function(){ method(window.event) }); - }; - - // set the handlers globally on document - addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48 - addEvent(document, 'keyup', clearModifier); - - // reset modifiers to false whenever the window is (re)focused. - addEvent(window, 'focus', resetModifiers); - - // store previously defined key - var previousKey = global.key; - - // restore previously defined key and return reference to our key object - function noConflict() { - var k = global.key; - global.key = previousKey; - return k; - } - - // set window.key and window.key.set/get/deleteScope, and the default filter - global.key = assignKey; - global.key.setScope = setScope; - global.key.getScope = getScope; - global.key.deleteScope = deleteScope; - global.key.filter = filter; - global.key.isPressed = isPressed; - global.key.getPressedKeyCodes = getPressedKeyCodes; - global.key.noConflict = noConflict; - global.key.unbind = unbindKey; - - if(typeof module !== 'undefined') module.exports = assignKey; - -})(this); \ No newline at end of file diff --git a/listeners.js b/listeners.js new file mode 100644 index 0000000..e69de29 diff --git a/manifest.json b/manifest.json index 1d71574..d66f3d1 100644 --- a/manifest.json +++ b/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 2, "name": "Booklight", - "description": "Easily add bookmarks to any category. Includes spotlight-like search with mouse/keyboard support. Default key: cmd+b (win:ctrl+b)", - "version": "1.1", + "description": "Easily add bookmarks to any category. Includes spotlight-like search with mouse/keyboard support. Default key: control/ctrl+b", + "version": "2.2.1", "author": "Ahmad Assaf", "homepage_url": "https://github.com/ahmadassaf/booklight", "background": { @@ -30,8 +30,8 @@ "css": ["css/booklight.css", "css/fontello.css"], "js": [ "lib/jquery.js", - "lib/hideSeek.js", - "lib/keymaster.js", + "lib/fuse.js", + "lib/keyboard.js", "helper.js", "booklight.js" ],