diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 index a9a18ea..eb8c9e0 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2014 Szymon Piłkowski +Copyright (c) 2018 Dmitry Galakhov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md old mode 100644 new mode 100755 index d0b1e5e..7404d1d --- a/README.md +++ b/README.md @@ -1,18 +1,43 @@ -chrome-better-bookmark -====================== +# Tree First Bookmarks -Chrome Extension that lets you easily add bookmarks to any category. Includes spotlight-like search with mouse/keyboard support. +### This fork of "Better Bookmark" is heavily optimised for people who likes the hierarchical folders' structure in their bookmarks and wants to stick to it having tons of (organised) folders. -WebStore URL: https://chrome.google.com/webstore/detail/better-bookmark/pniopfmciclllcpockpkgceikipiibol +- Better Bookmark has been renamed to "Tree First Bookmarks" and gets a new icon from now on. +- The redesign has happened. +- Better-Bookmark-Button extension is now equipped with captions showing up the full path to the current bookmark on hovered category. +- Now you can also choose the parent directory for the new folder that is being created (in the original version all folders were put into the "Other Bookmarks" with a flat structure first). +- Another new feature — The Sub Tree — helps you to get a quick overview of sub-folders in a chosen directory (click on any radio button to activate it). +- UI has been also improved. Arrows, breadcrumb and descriptions were added, input fields and text blocks were moved to key positions. +- Fuse.js library (fuzzy search) updated to v3.3.0 and max amount of characters for the search pattern has been changed, therefore, "Pattern length is too long" error shouldn't now block the search (or bitapRegexSearch will be used instead). +- Clickable breadcrumbs allow you to change a parent directory (to go up/down the tree) by clicking on one of the links in a breadcrumb (start by clicking any radio button). -key binding -=========== +

Tree First Bookmarks

+ +# Original chrome-better-bookmark + +Chrome Extension that lets you easily add bookmarks to any category. Includes spotlight-like weighted search (http://fusejs.io) with mouse/keyboard support. + +_WebStore URL of the original simplified version of this extension_: https://chrome.google.com/webstore/detail/better-bookmark/pniopfmciclllcpockpkgceikipiibol + +The new advanced _Tree First Bookmarks_ extension can be found here: https://chrome.google.com/webstore/detail/tree-first-bookmarks/lempbilidejiiljkciadplnekoflbmnl + +# key binding: cmd + b / ctrl + b Chrome allows you to set your own key binding for every extension. See https://github.com/ardcore/chrome-better-bookmark/issues/1 -TODO -==== +# TODO's + +- [ ] Show the location of the bookmark as the full path in the breadcrumb when a user opens the extension and the page was already bookmarked +- [ ] Add options (font size and style, focus style, key bindings, sorting options, etc.) +- [ ] Add the position variants (top or bottom) of the tooltip into the extension's options +- [ ] TBD: icon should be greyed out by default, highlighted if the page is already bookmarked +- [x] Default state of the children's toggler (if a user navigates between nodes it switches back to the disabled state) +- [x] TBD: subcategory indentation + +# Thanks to + +Big thanks goes to [ardcore](https://github.com/ardcore) and his initial version of open sourced repo of [chrome-better-bookmark](https://github.com/ardcore/chrome-better-bookmark)s. + +The evolution of development and the pull request can be found [here](https://github.com/ardcore/chrome-better-bookmark/pull/6). - - options (font size and style, focus style, key bindings, sorting options) - - TBD: icon should be greyed out by default, highlighted if the page is already bookmarked - - TBD: subcategory indentation? +Meanwhile, I've switched to the refined 2nd version of the [tree-first-bookmarks](https://github.com/galakhov/tree-first-bookmarks-v2) diff --git a/_locales/en/messages.json b/_locales/en/messages.json old mode 100644 new mode 100755 index 25abe17..c5e7767 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1,6 +1,30 @@ { "new": { - "message": "new", - "description": "New category caption." + "message": "Look for a folder by entering its name above first.
Nothing found? Type in the desired name and click on it below to create a new folder.", + "description": "New category text" + }, + "chooseparent": { + "message": "You can also link your new folder to a parent one by clicking on any radio button down below", + "description": "Choose parent category text" + }, + "iconup": { + "message": "↑ Look above", + "description": "Parent category icon up" + }, + "icondown": { + "message": "Look below ↓", + "description": "Parent category icon down" + }, + "caption": { + "message": "You are adding this folder to a chosen parent directory shown the breadcrumb below", + "description": "Caption to show on mouse over" + }, + "anotherparentdir": { + "message": "You can now as well click on any listed sub folder to save your bookmark or make it a parent folder", + "description": "Chosen parent directory" + }, + "checkbox": { + "message": "Show/hide children", + "description": "Show children toogler text" } -} \ No newline at end of file +} diff --git a/background.html b/background.html old mode 100644 new mode 100755 index c7fe86f..9b5d051 --- a/background.html +++ b/background.html @@ -1,36 +1,185 @@ + - +
- - \ No newline at end of file + + diff --git a/bookmark-icon-trees-128.png b/bookmark-icon-trees-128.png new file mode 100644 index 0000000..cb99d6e Binary files /dev/null and b/bookmark-icon-trees-128.png differ diff --git a/bookmark.png b/bookmark.png deleted file mode 100644 index 3eeca31..0000000 Binary files a/bookmark.png and /dev/null differ diff --git a/fuse.js b/fuse.js deleted file mode 100644 index 423a3e5..0000000 --- a/fuse.js +++ /dev/null @@ -1,342 +0,0 @@ -/** - * 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/fuse.min.js b/fuse.min.js new file mode 100644 index 0000000..fae19ef --- /dev/null +++ b/fuse.min.js @@ -0,0 +1,9 @@ +/*! + * Fuse.js v3.3.0 - Lightweight fuzzy-search (http://fusejs.io) + * + * Copyright (c) 2012-2017 Kirollos Risk (http://kiro.me) + * All Rights Reserved. Apache Software License 2.0 + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("Fuse",[],t):"object"==typeof exports?exports.Fuse=t():e.Fuse=t()}(this,function(){return function(e){function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var r={};return t.m=e,t.c=r,t.i=function(e){return e},t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=8)}([function(e,t,r){"use strict";e.exports=function(e){return Array.isArray?Array.isArray(e):"[object Array]"===Object.prototype.toString.call(e)}},function(e,t,r){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=function(){function e(e,t){for(var r=0;rr)return i(e,this.pattern,n);var o=this.options,s=o.location,c=o.distance,h=o.threshold,l=o.findAllMatches,u=o.minMatchCharLength;return a(e,this.pattern,this.patternAlphabet,{location:s,distance:c,threshold:h,findAllMatches:l,minMatchCharLength:u})}}]),e}();e.exports=c},function(e,t,r){"use strict";var n=r(0),o=function e(t,r,o){if(r){var i=r.indexOf("."),a=r,s=null;-1!==i&&(a=r.slice(0,i),s=r.slice(i+1));var c=t[a];if(null!==c&&void 0!==c)if(s||"string"!=typeof c&&"number"!=typeof c)if(n(c))for(var h=0,l=c.length;h0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,r=[],n=-1,o=-1,i=0,a=e.length;i=t&&r.push([n,o]),n=-1)}return e[i-1]&&i-n>=t&&r.push([n,i-1]),r}},function(e,t,r){"use strict";e.exports=function(e){for(var t={},r=e.length,n=0;n2&&void 0!==arguments[2]?arguments[2]:/ +/g,n=new RegExp(t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&").replace(r,"|")),o=e.match(n),i=!!o,a=[];if(i)for(var s=0,c=o.length;s=P;T-=1){var E=T-1,K=r[e.charAt(E)];if(K&&(S[E]=1),z[T]=(z[T+1]<<1|1)&K,0!==I&&(z[T]|=(L[T+1]|L[T])<<1|1|L[T+1]),z[T]&C&&(w=n(t,{errors:I,currentLocation:E,expectedLocation:g,distance:h}))<=m){if(m=w,(k=E)<=g)break;P=Math.max(1,2*g-k)}}if(n(t,{errors:I+1,currentLocation:g,expectedLocation:g,distance:h})>m)break;L=z}return{isMatch:k>=0,score:0===w?.001:w,matchedIndices:o(S,p)}}},function(e,t,r){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var o=function(){function e(e,t){for(var r=0;r0&&void 0!==arguments[0]?arguments[0]:"",t=[];if(this.options.tokenize)for(var r=e.split(this.options.tokenSeparator),n=0,o=r.length;n0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1],r=this.list,n={},o=[];if("string"==typeof r[0]){for(var i=0,a=r.length;i1)throw new Error("Key weight has to be > 0 and <= 1");d=d.name}else s[d]={weight:1};this._analyze({key:d,value:this.options.getFn(l,d),record:l,index:c},{resultMap:n,results:o,tokenSearchers:e,fullSearcher:t})}return{weights:s,results:o}}},{key:"_analyze",value:function(e,t){var r=e.key,n=e.arrayIndex,o=void 0===n?-1:n,i=e.value,a=e.record,c=e.index,h=t.tokenSearchers,l=void 0===h?[]:h,u=t.fullSearcher,f=void 0===u?[]:u,d=t.resultMap,v=void 0===d?{}:d,p=t.results,g=void 0===p?[]:p;if(void 0!==i&&null!==i){var y=!1,m=-1,k=0;if("string"==typeof i){this._log("\nKey: "+(""===r?"-":r));var x=f.search(i);if(this._log('Full text: "'+i+'", score: '+x.score),this.options.tokenize){for(var S=i.split(this.options.tokenSeparator),M=[],b=0;b-1&&(P=(P+m)/2),this._log("Score average:",P);var j=!this.options.tokenize||!this.options.matchAllTokens||k>=l.length;if(this._log("\nCheck Matches: "+j),(y||x.isMatch)&&j){var z=v[c];z?z.output.push({key:r,arrayIndex:o,value:i,score:P,matchedIndices:x.matchedIndices}):(v[c]={item:a,output:[{key:r,arrayIndex:o,value:i,score:P,matchedIndices:x.matchedIndices}]},g.push(v[c]))}}else if(s(i))for(var T=0,E=i.length;T-1&&(a.arrayIndex=i.arrayIndex),t.matches.push(a)}}}),this.options.includeScore&&r.push(function(e,t){t.score=e.score});for(var n=0,o=e.length;n \ No newline at end of file + + diff --git a/init.js b/init.js old mode 100644 new mode 100755 index d3ff42a..91c5e66 --- a/init.js +++ b/init.js @@ -4,14 +4,8 @@ chrome.runtime.onStartup.addListener(function() { registerEvents(); }); - function registerEvents() { + chrome.bookmarks.onCreated.addListener(function(id, node) {}); - chrome.bookmarks.onCreated.addListener( function(id, node) { - }) - - chrome.tabs.onActivated.addListener(function( info ) { - }) - - -} \ No newline at end of file + chrome.tabs.onActivated.addListener(function(info) {}); +} diff --git a/manifest.json b/manifest.json old mode 100644 new mode 100755 index 87ade04..d54a497 --- a/manifest.json +++ b/manifest.json @@ -1,34 +1,34 @@ { "manifest_version": 2, - "name": "Better Bookmark", - "description": "Easily add bookmarks to any category. Includes spotlight-like search with mouse/keyboard support. Default key: cmd+b (win:ctrl+b)", - "version": "1.3.1", - "author": "Szymon Pilkowski", - "homepage_url": "https://github.com/ardcore/chrome-better-bookmark", + "name": "Tree First Bookmarks", + "description": "Save a bookmark to any sub-category found by the spotlight-like search. Press cmd+b and use a sub-tree, a breadcrumb, tooltips, etc.", + "version": "2.0.2.9", + "author": "Szymon Pilkowski, Dmitry Galakhov", + "homepage_url": "https://github.com/galakhov/tree-first-bookmarks", "default_locale": "en", "background": { - "page": "init.html" + "page": "init.html", + "persistent": false }, - "icons": { - "16": "bookmark.png", - "48": "bookmark.png", - "128": "bookmark.png" + "icons": { + "16": "bookmark-icon-trees-128.png", + "48": "bookmark-icon-trees-128.png", + "128": "bookmark-icon-trees-128.png" }, "browser_action": { "default_icon": { - "19": "bookmark.png", - "38": "bookmark.png" + "19": "bookmark-icon-trees-128.png", + "38": "bookmark-icon-trees-128.png" }, - "default_title": "Better Bookmark Button", + "default_title": "Tree First Bookmarks", "default_popup": "background.html" }, "commands": { "_execute_browser_action": { - "suggested_key": { "default": "Ctrl+B", "windows": "Ctrl+B", @@ -39,8 +39,5 @@ } }, - "permissions": [ - "bookmarks", - "tabs" - ] -} \ No newline at end of file + "permissions": ["bookmarks", "tabs", "contextMenus"] +} diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..23ec04f Binary files /dev/null and b/screenshot.png differ diff --git a/selectCategory.js b/selectCategory.js old mode 100644 new mode 100755 index eba7a09..4453bef --- a/selectCategory.js +++ b/selectCategory.js @@ -1,220 +1,705 @@ -var categoryNodes = []; -var wrapper; -var focusedElement; -var fuzzySearch; -var currentNodeCount = 0; - -var DOWN_KEYCODE = 40; -var UP_KEYCODE = 38; -var CONFIRM_KEYCODE = 13; +var categoryNodes = [] +var wrapper +var searchWrapper +var focusedElement +var fuzzySearch +var currentNodeCount = 0 +var parentClicked = false + +var DOWN_KEYCODE = 40 +var UP_KEYCODE = 38 +var CONFIRM_KEYCODE = 13 + +// chrome.windows.getCurrent(function(wind) { +// var maxWidth = window.screen.availWidth; +// var maxHeight = window.screen.availHeight; +// var updateInfo = { +// left: 0, +// top: 0, +// width: maxWidth, +// height: maxHeight +// }; +// console.log(wind); +// //// chrome.windows.update(wind.id, updateInfo); +// }); function filterRecursively(nodeArray, childrenProperty, filterFn, results) { + results = results || [] - results = results || []; + nodeArray.forEach(function(node) { + if (filterFn(node)) { + results.push(node) + } + if (node.children) { + filterRecursively(node.children, childrenProperty, filterFn, results) + } + }) - nodeArray.forEach( function( node ) { - if (filterFn(node)) results.push( node ); - if (node.children) filterRecursively(node.children, childrenProperty, filterFn, results); - }); + return results +} - return results; +function getFullPathRecursively(el, node, titles) { + var currentNode = node + + if (currentNode && currentNode.parentId) { + var getParentNode = chrome.bookmarks.get(currentNode.parentId, function( + parentNode + ) { + if (parentNode[0] && parentNode[0].parentId > 0) { + titles.unshift(parentNode[0].title) + currentNode = parentNode[0] + getFullPathRecursively(el, currentNode, titles) + } else { + setTimeout(function() { + el.setAttribute('data-tooltip', titles.join(' > ')) + }, 0) + } + }) + } +} -}; +function createUiElement(node, captions = true) { + var el = document.createElement('div') + el.setAttribute('data-id', node.id) + el.setAttribute('data-count', node.children.length) + el.setAttribute('data-title', node.title) + el.setAttribute('data-parent-id', node.parentId) + el.classList.add('bookmark') + if (captions && node && node.parentId) { + var getParentNode = chrome.bookmarks.get(node.parentId, function( + parentNode + ) { + if (parentNode[0] && parentNode[0].parentId > 0) { + getFullPathRecursively(el, node, []) + } + }) + } + // TODO: get position of the tooltip from the extension options and pass it over here + el.setAttribute('data-tooltip-position', 'bottom') // position of the tooltip + el.innerHTML = "" + node.title + '' + el = appendRadioButtonParentSelector(el, node.parentId) + return el +} + +function getBreadcrumbByStartingNode(el, node, links) { + var currentNode = node + if (!currentNode || !currentNode.parentId) { + return + } + var getParentNode = chrome.bookmarks.get(currentNode.parentId, function( + parentNode + ) { + if (currentNode && parentNode[0] && parentNode[0].parentId > 0) { + if (links.length <= 0) { + links.unshift(currentNode) + } + links.unshift(parentNode[0]) + currentNode = parentNode[0] + getBreadcrumbByStartingNode(el, currentNode, links) + } else { + // on click on the root node, which is already saved in the "currentNode" + if (links.length <= 1) { + links.unshift(currentNode) + } + var activeClass = '' + setTimeout(function() { + for (var i = 0; i < links.length; i++) { + if (i === links.length - 1) { + activeClass = ' current-node' + } + links[ + i + ] = `${links[i].title}` + } + if (el.firstChild) { + el.removeChild(el.firstChild) + } + el.innerHTML = + "

" + + links.join(' > ') + + '

' + + // generate links for all breadcumb's nodes + var createdBreadcrumb = document.querySelector( + '.bookmarks__parent-chosen' + ) + if (createdBreadcrumb) { + createdBreadcrumb.addEventListener('click', handleBreadcrumbLink) + } + }, 0) + } + }) +} -function createUiElement(node) { +function showFullPathOfParentDir(parentSelected, breadcrumbSeparator = '') { + if (parentSelected === null) { + return + } - var el = document.createElement("span"); - el.setAttribute("data-id", node.id); - el.setAttribute("data-count", node.children.length); - el.setAttribute("data-title", node.title); - el.innerHTML = node.title; + var dirName = parentSelected.getAttribute('data-title') + var dirId = parentSelected.getAttribute('data-id') - return el; + var outputFooter = document.querySelector('.bookmarks__parents-output') + if (!outputFooter.classList.contains('visible')) { + outputFooter.classList.add('visible') + } + var output = document.querySelector('.bookmarks__breadcrumb') + if (output !== null && dirId) { + if (breadcrumbSeparator) { + // remove old event listeners, as the breadcumb will be rerendered + var linksInOutput = document.querySelector('.bookmarks__parent-chosen') + if (linksInOutput) { + linksInOutput.removeEventListener('click', handleBreadcrumbLink) + } + var getParentNode = chrome.bookmarks.get(dirId, function(parentNode) { + if (parentNode[0] && parentNode[0].parentId > 0) { + getBreadcrumbByStartingNode(output, parentNode[0], []) + } else { + output.innerHTML = + "

" + dirName + '

' + // just rerender the tree with children + generateTreeOfSelectedNode(dirId) + } + }) + } else { + output.innerHTML = + "

" + dirName + '

' + } + // set value of the currently clicked radio button to the hidden input + var hiddenInput = document.querySelector('.bookmarks__parents-hidden') + hiddenInput.setAttribute('value', parentSelected.getAttribute('data-id')) + } } -function triggerClick(element) { +function handleBreadcrumbLink(el) { + if (event.target.nodeName.toLowerCase() !== 'a') { + return + } + // switch tree (parent) to the clicked breadcumb + var nodeId = event.target.getAttribute('data-id-link') + var output = document.querySelector('.bookmarks__breadcrumb') + + // remove old event listeners, as the breadcumb will be rerendered + var linksInOutput = document.querySelector('.bookmarks__parent-chosen') + if (linksInOutput) { + linksInOutput.removeEventListener('click', handleBreadcrumbLink) + } - var categoryId = element.getAttribute("data-id"); - var newCategoryTitle; + // rerender the breadcumb itself + if (nodeId && nodeId > 0) { + var getParentNode = chrome.bookmarks.get(nodeId, function(parentNode) { + if (parentNode[0] && parentNode[0].parentId > 0) { + getBreadcrumbByStartingNode(output, parentNode[0], []) + } + }) + } - if (categoryId == "NEW") { + // rerender the tree with children + generateTreeOfSelectedNode(nodeId) + + /* // add breadcumb point to the search field to filter the results displayed below + var searchInput = document.querySelector(".spotlight-searcht input"); + searchInput.value = event.target.textContent; + searchInput.focus(); + __triggerKeyboardEvent(searchInput, 8); + + var foundFolders = document.querySelectorAll("#wrapper .bookmarks__title"); + var arrayOfFoundFolders = []; + for ( + i = -1, l = foundFolders.length; + ++i !== l; + arrayOfFoundFolders[i] = foundFolders[i] + ); + const filteredItems = query => { + return arrayOfFoundFolders.filter( + el => el.textContent.toLowerCase().indexOf(query.toLowerCase()) > -1 + ); + }; + */ +} - newCategoryTitle = element.getAttribute("data-title"); +/* +function __triggerKeyboardEvent(el, keyCode) { + var eventObj = document.createEventObject + ? document.createEventObject() + : document.createEvent("Events"); - chrome.bookmarks.create({ - title: newCategoryTitle - }, function(res) { - processBookmark(res.id); - }) + if (eventObj.initEvent) { + eventObj.initEvent("keydown", true, true); + } - } else { + eventObj.keyCode = keyCode; + eventObj.which = keyCode; - processBookmark(categoryId); + el.dispatchEvent + ? el.dispatchEvent(eventObj) + : el.fireEvent("onkeydown", eventObj); +} +*/ +function splitString(originalString, separator) { + var arrayOfStrings = originalString.split(separator) + for (var i = 0; i < arrayOfStrings.length; i++) { + arrayOfStrings[i] = + "" + arrayOfStrings[i] + '' } - + return arrayOfStrings.join(' > ') } -function processBookmark(categoryId) { +function generateTreeOfSelectedNode(nodeId) { + if (nodeId) { + chrome.bookmarks.getSubTree(nodeId, drawSubTree) + } +} - getCurrentUrlData(function(url, title) { +function getDirectoriesInChildren(categoryNodes) { + return filterRecursively(categoryNodes, 'children', function(node) { + return !node.url && node.id > 0 + }) +} - if (title && categoryId && url) { - addBookmarkToCategory(categoryId, title, url); - window.close(); +function drawSubTree(categoryNodes) { + if (categoryNodes[0] && categoryNodes[0].children.length > 0) { + var outputSection = document.querySelector('.bookmarks__parents-output') + var footer = document.querySelector('.bookmarks__parents-output-footer') + while (footer.firstChild) { + // remove the previously generated tree first + footer.removeChild(footer.firstChild) } + footer.removeEventListener('click', handleAddBookmark) + footer.removeEventListener('click', handleRadioButtons) - }); + var categoryChildren = getDirectoriesInChildren(categoryNodes[0].children) + var elementsWithUi = [] + categoryChildren.forEach(function(node) { + elementsWithUi.push(createUiElement(node, false)) + }) + if (categoryChildren.length > 0) { + // If there are children, add a class to a parent to avoid showing border + if (!outputSection.classList.contains('with-children')) { + outputSection.classList.add('with-children') + } + + // show children in the sub tree on toggler click + if (!footer.classList.contains('children-hidden')) { + footer.classList.add('children-hidden') + + // reset the toggler state after navigating to the new node + var togglerStateChange = document.querySelector( + '.bookmarks__parents-children-toggler' + ) + var footer = document.querySelector('.bookmarks__parents-output-footer') + // var footerList = footer.querySelector('ul li') + if ( + togglerStateChange + // && + // footerList && + // footerList.length > 0 && + // !footer.classList.contains('children-hidden') + ) { + togglerStateChange.click() + } + } + // Tooltip first + var tooltip = + "

" + + chrome.i18n.getMessage('anotherparentdir') + + '

' + + // if there are children: i.e. subdirectories + var footerUl = document.createElement('ul') + var rootNodeId = categoryNodes[0].id + var secondParent, + currentNodeParentId, + newEl, + firstLevel, + secondlevelEntered = false + secondParent = rootNodeId + elementsWithUi.forEach(function(element) { + // make a tree + currentNodeParentId = element.getAttribute('data-parent-id') + if (currentNodeParentId !== rootNodeId) { + var footerUlLi = document.createElement('li') + if (currentNodeParentId === secondParent && firstLevel === true) { + // style element differently (indented of two levels) + if (!secondlevelEntered) { + element.classList.add('bookmark--second-level') + secondlevelEntered = true + } else { + element.classList.add('bookmark--second-level-indentation') + } + } else { + firstLevel = true + secondlevelEntered = false + secondParent = element.getAttribute('data-id') + } + // append a list element (indented of one level) + footerUlLi.appendChild(element) + footerUl.appendChild(footerUlLi) + } else { + // append a root element without a list wrapper + footerUl.appendChild(element) + firstLevel = false + } + }) + // render the sub tree + footer.appendChild(footerUl) + footer.innerHTML += tooltip + // add events for all bookmarks + footer.addEventListener('click', handleAddBookmark) + // add events for all radio buttons (parent selectors) + footer.addEventListener('click', handleRadioButtons) + } else { + if (outputSection.classList.contains('with-children')) { + outputSection.classList.remove('with-children') + } + } + } } -function addBookmarkToCategory(categoryId, title, url) { +function appendRadioButtonParentSelector(el, parentId) { + var theInput = document.createElement('input') + theInput.setAttribute('type', 'radio') + theInput.setAttribute('name', 'parents-id') + theInput.setAttribute('class', 'bookmarks__parents-id-selector') + theInput.setAttribute('value', parentId) + el.appendChild(theInput) + return el +} - chrome.bookmarks.create({ - 'parentId': categoryId, - 'title': title, - 'url': url - }); +function handleRadioButtons(el) { + var parentSelected = el.target.parentNode + var footerWrapper = el.target.closest('.bookmarks__parents-output-footer') + if (footerWrapper) { + // TODO: If we're in the sub tree + if (!footerWrapper.classList.contains('sub-tree')) { + footerWrapper.classList.add('sub-tree') + } + //:not(> ) + } else { + parentClicked = true // for focusing + } + showFullPathOfParentDir(parentSelected, ' > ') + generateTreeOfSelectedNode(parentSelected.getAttribute('data-id')) +} +function handleAddBookmark(e) { + triggerClick(e.target) } -function getCurrentUrlData(callbackFn) { +function triggerClick(element) { + if (element.nodeName.toLowerCase() === 'span') { + // clicked on .bookmarks__parents-create-dir-name span + element = element.parentNode + } + // else if (element.nodeName.toLowerCase() === "p") { + // // clicked on p.bookmarks__parents-create-dir-name + // element = element.parentNode; + // } + + var categoryId = element.getAttribute('data-id') + var newCategoryTitle + + if (categoryId == 'NEW') { + newCategoryTitle = element.getAttribute('data-title') + + var checkedElId = document.querySelector('.bookmarks__parents-hidden') + var selectedParentId = checkedElId.value != '' ? checkedElId.value : null + chrome.bookmarks.create( + { + parentId: selectedParentId, + title: newCategoryTitle + }, + function(res) { + processBookmark(res.id) + } + ) + } else { + processBookmark(categoryId) + } +} - chrome.tabs.query({'active': true, 'currentWindow': true}, function (tabs) { - callbackFn(tabs[0].url, tabs[0].title) - }); +function processBookmark(categoryId) { + getCurrentUrlData(function(url, title) { + if (title && categoryId && url) { + addBookmarkToCategory(categoryId, title, url) + window.close() + } + }) +} +function addBookmarkToCategory(categoryId, title, url) { + chrome.bookmarks.create({ + parentId: categoryId, + title: title, + url: url + }) } -function createUiFromNodes( categoryNodes ) { +function getCurrentUrlData(callbackFn) { + chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { + callbackFn(tabs[0].url, tabs[0].title) + }) +} - var categoryUiElements = []; - currentNodeCount = categoryNodes.length; +function createUiFromNodes(categoryNodes) { + wrapper.removeEventListener('click', handleRadioButtons) + var categoryUiElements = [] + currentNodeCount = categoryNodes.length - categoryNodes.forEach( function( node ) { - categoryUiElements.push( createUiElement(node) ); + categoryNodes.forEach(function(node) { + categoryUiElements.push(createUiElement(node)) }) - categoryUiElements.forEach( function( element ) { - wrapper.appendChild( element ); - }); + categoryUiElements.forEach(function(element) { + wrapper.appendChild(element) + }) -}; + wrapper.addEventListener('click', handleRadioButtons) +} function resetUi() { - - wrapper.innerHTML = ""; - -}; + var newDirInputWrapper = document.querySelector( + '.bookmarks__parents-create-wrapper' + ) + var newDirInput = newDirInputWrapper.querySelector( + '.bookmarks__parents-create' + ) + if (newDirInput) { + // remove existing input field before the next update (see addCreateCategoryButton) + newDirInputWrapper.removeChild(newDirInput) + } + // update the folders in the whole tree according to the entered search string + wrapper.innerHTML = '' +} function focusItem(index) { - - if (focusedElement) focusedElement.classList.remove("focus"); - focusedElement = wrapper.childNodes[index]; - focusedElement.classList.add("focus"); - - focusedElement.scrollIntoView(false); - + if (focusedElement) { + focusedElement.classList.remove('focus') + } + focusedElement = wrapper.childNodes[index] + if (focusedElement) { + focusedElement.classList.add('focus') + focusedElement.scrollIntoView(false) + } } function addCreateCategoryButton(categoryName) { + // TODO: create options + // TODO: parse the position of the tooltip from extension's options + var el = document.createElement('div') + el.setAttribute('data-id', 'NEW') + el.setAttribute('data-title', categoryName) + el.setAttribute('data-tooltip-position', 'bottom') // set position of the tooltip + el.classList.add('bookmarks__parents-create') + el.setAttribute('data-tooltip', chrome.i18n.getMessage('caption')) + el.innerHTML = + "" + categoryName + '

' + document.querySelector('.bookmarks__parents-create-wrapper').appendChild(el) + currentNodeCount = currentNodeCount + 1 +} - var el = document.createElement("span"); - el.setAttribute("data-id", "NEW"); - el.setAttribute("data-title", categoryName); - el.classList.add("create"); - el.innerHTML = chrome.i18n.getMessage("new") + ": " + categoryName; - - wrapper.appendChild(el); - currentNodeCount = currentNodeCount + 1; +function addHiddenOutput() { + // add hidden element to output a parent directory later + var output = document.createElement('div') + output.setAttribute('class', 'bookmarks__parents-output') + var breadcrumb = document.createElement('div') + breadcrumb.setAttribute('class', 'bookmarks__breadcrumb') + searchWrapper.appendChild(breadcrumb) + + var input = document.createElement('input') + input.setAttribute('type', 'hidden') + input.setAttribute('name', 'parentid') + input.setAttribute('class', 'bookmarks__parents-hidden') + output.appendChild(input) + + var inputCheckbox = document.createElement('input') + inputCheckbox.setAttribute('type', 'checkbox') + inputCheckbox.setAttribute('name', 'children-toggler') + inputCheckbox.setAttribute('class', 'bookmarks__parents-children-toggler') + inputCheckbox.setAttribute('title', chrome.i18n.getMessage('checkbox')) + output.appendChild(inputCheckbox) + + var footer = document.createElement('footer') + footer.setAttribute('class', 'bookmarks__parents-output-footer') + output.appendChild(footer) + + return output +} +function addNewDirectoryTextAbove() { + var newDirWrapperCaptionsAbove = document.createElement('div') + newDirWrapperCaptionsAbove.setAttribute( + 'class', + 'bookmarks__parents-create-wrapper-desc-text' + ) + newDirWrapperCaptionsAbove.innerHTML = + "

' + + chrome.i18n.getMessage('new') + + '

' + return newDirWrapperCaptionsAbove } -function createInitialTree() { +function addNewDirectoryTextBelow() { + var newDirWrapperCaptionsBelow = document.createElement('div') + newDirWrapperCaptionsBelow.setAttribute( + 'class', + 'bookmarks__parents-create-wrapper-desc' + ) + newDirWrapperCaptionsBelow.innerHTML = + "

" + + chrome.i18n.getMessage('chooseparent') + + '

' + return newDirWrapperCaptionsBelow +} - chrome.bookmarks.getTree( function(t) { +function addNewDirectoryClickableWrapper() { + var newDirWrapper = document.createElement('div') + newDirWrapper.setAttribute('class', 'bookmarks__parents-create-wrapper') + return newDirWrapper +} - wrapper = document.getElementById("wrapper"); +function createInitialTree() { + chrome.bookmarks.getTree(function(t) { + wrapper = document.getElementById('wrapper') + searchWrapper = document.getElementById('search').parentNode var options = { - keys: ['title'], - threshold: 0.4 + shouldSort: true, + threshold: 0.4, + location: 0, + distance: 100, + maxPatternLength: 33, + minMatchCharLength: 2, + keys: ['title'] } - - categoryNodes = filterRecursively(t, "children", function(node) { - return !node.url && node.id > 0; + + categoryNodes = filterRecursively(t, 'children', function(node) { + return !node.url && node.id > 0 // include folders only }).sort(function(a, b) { - return b.dateGroupModified - a.dateGroupModified; + return b.dateGroupModified - a.dateGroupModified }) - createUiFromNodes( categoryNodes ); - - wrapper.style.width = wrapper.clientWidth + "px"; - - if (currentNodeCount > 0) focusItem(0); + createUiFromNodes(categoryNodes) - fuzzySearch = new Fuse(categoryNodes, options); + // wrapper.style.width = wrapper.clientWidth + "px"; - wrapper.addEventListener("click", function(e) { - triggerClick(e.target); - }) - - }); + if (currentNodeCount > 0) { + focusItem(0) + } + fuzzySearch = new Fuse(categoryNodes, options) + + var newDirWrapperAbove = addNewDirectoryTextAbove() + searchWrapper.appendChild(newDirWrapperAbove) + var newDirInputWrapper = addNewDirectoryClickableWrapper() + searchWrapper.appendChild(newDirInputWrapper) + var hiddenOutput = addHiddenOutput() + wrapper.parentNode.insertBefore(hiddenOutput, wrapper) + + //var childrenToggler = document.querySelector('input[type="checkbox"]'); + var childrenToggler = hiddenOutput.querySelector( + '.bookmarks__parents-children-toggler' + ) + childrenToggler.addEventListener('click', toggleChildren) + + var newDirWrapperBelow = addNewDirectoryTextBelow() + wrapper.parentNode.insertBefore(newDirWrapperBelow, wrapper) + // Add bookmarks' clicks for the tree + wrapper.addEventListener('click', handleAddBookmark) + // Add a bookmarks' click to the bookmarks__parents-create-wrapper + newDirInputWrapper.addEventListener('click', handleAddBookmark) + }) } -(function() { +function toggleChildren(e) { + var footerDiv = document.querySelector('.bookmarks__parents-output-footer') - var searchElement = document.getElementById("search"); - var text = ""; - var newNodes; - var index = 0; + if (e.target.checked == true) { + if (footerDiv.classList.contains('children-hidden')) { + footerDiv.classList.remove('children-hidden') + } + } else { + if (!footerDiv.classList.contains('children-hidden')) { + footerDiv.classList.add('children-hidden') + } + } +} - createInitialTree(); +;(function() { + var searchElement = document.getElementById('search') + var text = '' + var newNodes + var index = 0 - searchElement.addEventListener("keydown", function(e) { + createInitialTree() + searchElement.addEventListener('keydown', function(e) { if (e.keyCode == UP_KEYCODE) { - e.preventDefault(); - index = index - 1; - if (index < 0) index = currentNodeCount - 1; - focusItem(index); - + e.preventDefault() + index = index - 1 + if (index < 0) { + index = currentNodeCount - 1 + } + focusItem(index) } else if (e.keyCode == DOWN_KEYCODE) { - e.preventDefault(); - index = index + 1; - if (index >= currentNodeCount) index = 0; - focusItem(index); - + e.preventDefault() + index = index + 1 + if (index >= currentNodeCount) { + index = 0 + } + focusItem(index) } else if (e.keyCode == CONFIRM_KEYCODE) { - if (currentNodeCount > 0) triggerClick(focusedElement); - + if (currentNodeCount > 0) { + triggerClick(focusedElement) + } } else { // to get updated input value, we need to schedule it to the next tick - setTimeout( function() { - text = document.getElementById("search").value; + setTimeout(function() { + text = document.getElementById('search').value if (text.length) { - newNodes = fuzzySearch.search(text); - resetUi(); - createUiFromNodes(newNodes) - if (newNodes.length) focusItem(0); + newNodes = fuzzySearch.search(text) + resetUi() + createUiFromNodes(newNodes) - if (!newNodes.length || text !== newNodes[0].title) { - addCreateCategoryButton(text); + if (newNodes.length && parentClicked === false) { + focusItem(0) } + if (!newNodes.length || text !== newNodes[0].title) { + addCreateCategoryButton(text) + } } else { - resetUi(); - createUiFromNodes(categoryNodes); - if (currentNodeCount > 0) focusItem(0); + resetUi() + createUiFromNodes(categoryNodes) + if (currentNodeCount > 0) { + focusItem(0) + } } - index = 0; - }, 0); + index = 0 + }, 0) } - }) - searchElement.focus(); - -})(); \ No newline at end of file + searchElement.focus() +})()