├── .gitignore ├── public ├── icons │ ├── icon-128.png │ ├── star-dark-16.png │ ├── star-dark-24.png │ └── star-dark-32.png ├── images │ ├── folder-open.svg │ ├── folder-open-light.svg │ ├── folder.svg │ ├── folder-light.svg │ └── document-code.svg ├── manifest.json ├── popup.html ├── _locales │ ├── zh_CN │ │ └── messages.json │ ├── en │ │ └── messages.json │ └── pl │ │ └── messages.json └── options.html ├── .jshintrc ├── config ├── paths.js ├── webpack.common.js └── webpack.config.js ├── src ├── variables.scss ├── background.js ├── options.js ├── options.scss ├── popup.scss └── popup.js ├── README.md ├── LICENSE ├── package.json └── size-plugin.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist.pem -------------------------------------------------------------------------------- /public/icons/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angusjune/bookmarkie/HEAD/public/icons/icon-128.png -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 8, 3 | "browser": true, 4 | "node": true, 5 | "strict":"global" 6 | } -------------------------------------------------------------------------------- /public/icons/star-dark-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angusjune/bookmarkie/HEAD/public/icons/star-dark-16.png -------------------------------------------------------------------------------- /public/icons/star-dark-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angusjune/bookmarkie/HEAD/public/icons/star-dark-24.png -------------------------------------------------------------------------------- /public/icons/star-dark-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angusjune/bookmarkie/HEAD/public/icons/star-dark-32.png -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | const PATHS = { 6 | src: path.resolve(__dirname, '../src'), 7 | build: path.resolve(__dirname, '../build'), 8 | }; 9 | 10 | module.exports = PATHS; 11 | -------------------------------------------------------------------------------- /src/variables.scss: -------------------------------------------------------------------------------- 1 | $theme-primary: #3674E0; 2 | $theme-secondary: #3674E0; 3 | $theme-warn: #DD6B74; 4 | 5 | $text-primary: rgba(0, 0, 0, 0.8); 6 | $text-secondary: rgba(0, 0, 0, 0.55); 7 | 8 | $theme-primary-dark: #8ab4f9; 9 | $theme-secondary-dark: #8ab4f9; 10 | 11 | $text-primary-dark: rgba(0, 0, 0, 0.8); -------------------------------------------------------------------------------- /public/images/folder-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/folder-open-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/folder-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookmarkie 2 | 3 | An elegant Chrome MV3 bookmark viewer. 4 | 5 | This is the version in dev and may not work on your device. To download the stable version, go to [Chrome Web Store][download]. 6 | 7 | [download]: https://chrome.google.com/webstore/detail/even-neater-bookmarks/ahlphbdcaacfhkiajebghpngknafklbj 8 | 9 | ## Build 10 | 11 | 1. run `yarn install` 12 | 13 | 2. run `yarn run build` to build 14 | 15 | ## Develop 16 | 17 | 1. run `yarn install` and `yarn run watch` 18 | 19 | 2. Load folder `build` in your Chrome extension settings page 20 | 21 | 3. Code 22 | -------------------------------------------------------------------------------- /public/images/document-code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extName__", 3 | "version": "2.3.3", 4 | "short_name": "Bookmarkie", 5 | "manifest_version": 3, 6 | "description": "__MSG_extDesc__", 7 | "icons": { 8 | "128": "icons/icon-128.png" 9 | }, 10 | "background": { 11 | "service_worker": "background.js" 12 | }, 13 | "action": { 14 | "default_icon": { 15 | "16": "icons/star-dark-16.png", 16 | "24": "icons/star-dark-24.png", 17 | "32": "icons/star-dark-32.png" 18 | }, 19 | "default_popup": "popup.html", 20 | "default_title": "__MSG_extActionTitle__" 21 | }, 22 | "options_ui": { 23 | "page": "options.html", 24 | "open_in_tab": false 25 | }, 26 | "permissions": [ 27 | "bookmarks", 28 | "tabs", 29 | "storage" 30 | ], 31 | "minimum_chrome_version": "88", 32 | "default_locale": "en" 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Angus Zhu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookmarkie", 3 | "version": "2.0.6", 4 | "description": "An elegant bookmark viewer for Chrome", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack --mode=production --config config/webpack.config.js", 8 | "watch": "webpack --watch --mode=development --config config/webpack.config.js", 9 | "build:dev": "webpack --watch --mode=development --devtool source-map --config config/webpack.config.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/angusjune/bookmarkie.git" 14 | }, 15 | "author": "Angus Zhu", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/angusjune/bookmarkie/issues" 19 | }, 20 | "homepage": "https://github.com/angusjune/bookmarkie#readme", 21 | "devDependencies": { 22 | "@types/chrome": "^0.0.135", 23 | "clean-webpack-plugin": "^4.0.0-alpha.0", 24 | "copy-webpack-plugin": "^8.1.1", 25 | "css-loader": "^5.2.4", 26 | "file-loader": "^6.2.0", 27 | "mini-css-extract-plugin": "^1.5.0", 28 | "sass": "^1.32.11", 29 | "sass-loader": "^11.0.1", 30 | "size-plugin": "^2.0.2", 31 | "webpack": "^5.35.0", 32 | "webpack-cli": "^4.6.0", 33 | "webpack-merge": "^5.7.3" 34 | }, 35 | "dependencies": { 36 | "@material/button": "^11.0.0", 37 | "@material/card": "^11.0.0", 38 | "@material/list": "^11.0.0", 39 | "@material/ripple": "^11.0.0", 40 | "@material/switch": "^11.0.0", 41 | "@material/typography": "^11.0.0", 42 | "canvg": "^3.0.7", 43 | "lit-element": "^2.4.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /config/webpack.common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SizePlugin = require('size-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | 7 | const PATHS = require('./paths'); 8 | 9 | // To re-use webpack configuration across templates, 10 | // CLI maintains a common webpack configuration file - `webpack.common.js`. 11 | // Whenever user creates an extension, CLI adds `webpack.common.js` file 12 | // in template's `config` folder 13 | const common = { 14 | output: { 15 | // the build folder to output bundles and assets in. 16 | path: PATHS.build, 17 | // the filename template for entry chunks 18 | filename: '[name].js', 19 | }, 20 | devtool: 'source-map', 21 | stats: { 22 | all: false, 23 | errors: true, 24 | builtAt: true, 25 | }, 26 | module: { 27 | rules: [ 28 | // Help webpack in understanding CSS files imported in .js files 29 | { 30 | test: /\.css$/, 31 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 32 | }, 33 | // Check for images imported in .js files and 34 | { 35 | test: /\.(png|jpe?g|gif|svg)$/i, 36 | use: [ 37 | { 38 | loader: 'file-loader', 39 | options: { 40 | outputPath: 'images', 41 | name: '[name].[ext]', 42 | }, 43 | }, 44 | ], 45 | }, 46 | { 47 | test: /\.(scss|sass)$/i, 48 | use: [ 49 | { loader: MiniCssExtractPlugin.loader}, 50 | { loader: 'css-loader'}, 51 | { 52 | loader: "sass-loader", 53 | options: { 54 | sassOptions: { 55 | includePaths: ["./node_modules"] 56 | } 57 | } 58 | } 59 | ] 60 | } 61 | ], 62 | }, 63 | plugins: [ 64 | // Print file sizes 65 | new SizePlugin(), 66 | // Copy static assets from `public` folder to `build` folder 67 | new CopyWebpackPlugin({ 68 | patterns: [ 69 | { 70 | from: '**/*', 71 | context: 'public', 72 | }, 73 | ] 74 | }), 75 | // Extract CSS into separate files 76 | new MiniCssExtractPlugin({ 77 | filename: '[name].css', 78 | }), 79 | ], 80 | }; 81 | 82 | module.exports = common; 83 | -------------------------------------------------------------------------------- /size-plugin.json: -------------------------------------------------------------------------------- 1 | [{"timestamp":1764095240919,"files":[{"filename":"background.js","previous":299,"size":58013,"diff":57714},{"filename":"options.css","previous":8832,"size":9084,"diff":252},{"filename":"options.html","previous":1162,"size":1759,"diff":597},{"filename":"options.js","previous":5885,"size":5816,"diff":-69},{"filename":"popup.css","previous":4835,"size":4818,"diff":-17},{"filename":"popup.html","previous":1270,"size":1246,"diff":-24},{"filename":"popup.js","previous":12086,"size":12055,"diff":-31}]},{"timestamp":1619679337154,"files":[{"filename":"background.js","previous":297,"size":299,"diff":2},{"filename":"options.css","previous":8837,"size":8832,"diff":-5},{"filename":"options.html","previous":1161,"size":1162,"diff":1},{"filename":"options.js","previous":5885,"size":5885,"diff":0},{"filename":"popup.css","previous":4820,"size":4835,"diff":15},{"filename":"popup.html","previous":1262,"size":1270,"diff":8},{"filename":"popup.js","previous":8102,"size":12086,"diff":3984}]},{"timestamp":1619406856723,"files":[{"filename":"background.js","previous":297,"size":297,"diff":0},{"filename":"options.css","previous":8820,"size":8837,"diff":17},{"filename":"options.html","previous":1161,"size":1161,"diff":0},{"filename":"options.js","previous":5885,"size":5885,"diff":0},{"filename":"popup.css","previous":4820,"size":4820,"diff":0},{"filename":"popup.html","previous":1262,"size":1262,"diff":0},{"filename":"popup.js","previous":8102,"size":8102,"diff":0}]},{"timestamp":1619080830124,"files":[{"filename":"background.js","previous":724,"size":297,"diff":-427},{"filename":"options.css","previous":4271,"size":8820,"diff":4549},{"filename":"options.html","previous":1094,"size":1161,"diff":67},{"filename":"options.js","previous":7091,"size":5885,"diff":-1206},{"filename":"popup.css","previous":3867,"size":4820,"diff":953},{"filename":"popup.html","previous":1262,"size":1262,"diff":0},{"filename":"popup.js","previous":8480,"size":8102,"diff":-378}]},{"timestamp":1618986499614,"files":[{"filename":"background.js","previous":1632,"size":724,"diff":-908},{"filename":"options.css","previous":4619,"size":4271,"diff":-348},{"filename":"options.html","previous":1094,"size":1094,"diff":0},{"filename":"options.js","previous":18179,"size":7091,"diff":-11088},{"filename":"popup.css","previous":4193,"size":3867,"diff":-326},{"filename":"popup.html","previous":1262,"size":1262,"diff":0},{"filename":"popup.js","previous":13679,"size":8480,"diff":-5199}]}] 2 | -------------------------------------------------------------------------------- /config/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { merge } = require('webpack-merge'); 4 | 5 | const common = require('./webpack.common.js'); 6 | const PATHS = require('./paths'); 7 | 8 | // Merge webpack configuration files 9 | const config = merge(common, { 10 | entry: { 11 | popup: PATHS.src + '/popup.js', 12 | background: PATHS.src + '/background.js', 13 | options: PATHS.src + '/options.js', 14 | }, 15 | }); 16 | 17 | module.exports = config; 18 | 19 | 20 | // const path = require("path"); 21 | // const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 22 | // const CopyWebpackPlugin = require("copy-webpack-plugin"); 23 | // const HtmlWebpackPlugin = require('html-webpack-plugin'); 24 | 25 | // module.exports = { 26 | // mode: "production", 27 | // entry: { 28 | // "options.style": "./src/scss/options.scss", 29 | // "popup.style": "./src/scss/popup.scss", 30 | // background: "./src/js/background.js", 31 | // popup: "./src/js/popup.js", 32 | // options: "./src/js/options.js", 33 | // manifest: "./src/manifest.json", 34 | // }, 35 | // plugins: [ 36 | // new CleanWebpackPlugin({cleanStaleWebpackAssets: false}), // resolve conflict with `CopyWebpackPlugin` 37 | // new CopyWebpackPlugin([ 38 | // { from: "src/images", to: "images" }, 39 | // { from: "_locales/**", to: "./", context: "src/" } 40 | // ]), 41 | // new HtmlWebpackPlugin({ 42 | // // filename: 'popup.html', 43 | // template: 'src/popup.html' 44 | // }), 45 | // new HtmlWebpackPlugin({ 46 | // // filename: 'options.html', 47 | // template: 'src/options.html' 48 | // }), 49 | // ], 50 | // output: { 51 | // filename: '[name].js', 52 | // path: path.resolve(__dirname, "dist") 53 | // }, 54 | // module: { 55 | // rules: [ 56 | // { 57 | // test: /\.scss$/, 58 | // use: [ 59 | // { 60 | // loader: "file-loader", 61 | // options: { 62 | // name: "[name].css" 63 | // } 64 | // }, 65 | // { loader: "extract-loader" }, 66 | // { loader: "css-loader" }, 67 | // { 68 | // loader: "sass-loader", 69 | // options: { 70 | // sassOptions: { 71 | // includePaths: ["./node_modules"] 72 | // } 73 | // } 74 | // } 75 | // ] 76 | // }, 77 | // { 78 | // test: /\.js$/, 79 | // loader: "babel-loader", 80 | // query: { 81 | // presets: ["@babel/preset-env"] 82 | // } 83 | // }, 84 | // { 85 | // type: "javascript/auto", 86 | // test: /\.json$/, 87 | // use: [{ loader: "file-loader", options: { name: "[name].json" } }] 88 | // }, 89 | // { 90 | // test: /\.html$/, 91 | // use: [{ loader: "file-loader", options: { name: "[name].html" } }] 92 | // }, 93 | // { 94 | // test: /\.(svg|png)/, 95 | // use: [ 96 | // { 97 | // loader: "file-loader", 98 | // options: { name: "[name].[ext]", outputPath: "images" } 99 | // } 100 | // ] 101 | // } 102 | // ] 103 | // } 104 | // }; 105 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import Canvg, { presets } from 'canvg'; 4 | 5 | const storageCache = {}; 6 | let isBrowserDark = false; // Service workers have no window; rely on messages to update this flag. 7 | 8 | const initStorageCache = getAllStorageSyncData().then(items => { 9 | Object.assign(storageCache, items); 10 | setIcon(storageCache.iconType, storageCache.iconStyle, storageCache.iconStyleAuto, isBrowserDark); 11 | }); 12 | 13 | function getAllStorageSyncData() { 14 | return new Promise((resolve, reject) => { 15 | chrome.storage.sync.get(null, (items) => { 16 | if (chrome.runtime.lastError) { 17 | return reject(chrome.runtime.lastError); 18 | } 19 | resolve(items); 20 | }); 21 | }); 22 | } 23 | 24 | const setIcon = async (iconType = 'star', iconStyle = 'dark', iconStyleAuto = true, isBrowserDark = false) => { 25 | if (iconStyleAuto) { 26 | if (isBrowserDark) { 27 | iconStyle = 'light'; 28 | } else { 29 | iconStyle = 'dark'; 30 | } 31 | } 32 | 33 | const fill = { 34 | dark: '#444', 35 | light: '#fff', 36 | trinidad: '#d74c2e', 37 | yellow: '#f4a60e', 38 | pastel: '#6ad37c', 39 | naval: '#4277d7', 40 | orchid: '#993fd6', 41 | gold: '' 42 | }; 43 | 44 | const svg = { 45 | star: ``, 46 | bookmark: ``, 47 | stars: ``, 48 | }; 49 | 50 | 51 | const offscreen = new OffscreenCanvas(32, 32); 52 | const ctx = offscreen.getContext('2d'); 53 | const v = await Canvg.fromString(ctx, svg[iconType], presets.offscreen()); 54 | await v.render(); 55 | const blob = await offscreen.convertToBlob(); 56 | const pngUrl = URL.createObjectURL(blob); 57 | 58 | chrome.action.setIcon({ path: { 59 | 32: pngUrl 60 | }}); 61 | }; 62 | 63 | chrome.storage.onChanged.addListener((changes, namepsace) => { 64 | for (const [key, value] of Object.entries(changes)) { 65 | storageCache[key] = value.newValue; 66 | } 67 | if (changes.iconType || changes.iconStyle || changes.iconStyleAuto) { 68 | setIcon(storageCache.iconType, storageCache.iconStyle, storageCache.iconStyleAuto, isBrowserDark); 69 | } 70 | }); 71 | 72 | chrome.tabs.onUpdated.addListener(() => { 73 | setIcon(storageCache.iconType, storageCache.iconStyle, storageCache.iconStyleAuto, isBrowserDark); 74 | }); 75 | 76 | chrome.runtime.onMessage.addListener(message => { 77 | if (typeof message.isBrowserDark === 'boolean') { 78 | isBrowserDark = message.isBrowserDark; 79 | setIcon(storageCache.iconType, storageCache.iconStyle, storageCache.iconStyleAuto, isBrowserDark); 80 | } 81 | }); 82 | -------------------------------------------------------------------------------- /public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 | 19 | 20 |
21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 | 37 | 38 | 39 | 47 | 48 |
49 |
50 | 51 | 58 | 59 | 67 | 68 | 69 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import './options.scss'; 4 | import {MDCSwitch} from '@material/switch'; 5 | 6 | const os = (navigator.platform.toLowerCase().match(/mac|win|linux/i) || ['other'])[0]; 7 | 8 | const storageCache = {}; 9 | 10 | // i18n of text strings 11 | function getMessage() { 12 | // Distinct mac and windows 13 | if (os == 'mac') { 14 | document.querySelector('#optionOpenNewTab').dataset.msg = 'optionOpenNewTabMac'; 15 | } 16 | 17 | const elements = document.querySelectorAll('[data-msg]'); 18 | elements.forEach((el, i) => { 19 | el.textContent = chrome.i18n.getMessage(el.dataset.msg); 20 | }); 21 | } 22 | getMessage(); 23 | 24 | function optionSaved() { 25 | console.log('Saved'); 26 | } 27 | 28 | const ctrlOpenNewTabs = new MDCSwitch(document.querySelector('#openNewTabs')); 29 | const ctrlOpenTabsInBg = new MDCSwitch(document.querySelector('#openTabsInBg')); 30 | const ctrlCloseOtherFolders = new MDCSwitch(document.querySelector('#closeOtherFolders')); 31 | const ctrlPopupFixed = new MDCSwitch(document.querySelector('#popupFixed')); 32 | const ctrlPreserveState = new MDCSwitch(document.querySelector('#preserveState')); 33 | const ctrlIconStyleAuto = new MDCSwitch(document.querySelector('#iconStyleAuto')); 34 | 35 | const ctrlOpenNewTabsNative = document.querySelector('#openNewTabsNative'); 36 | const ctrlOpenTabsInBgNative = document.querySelector('#openTabsInBgNative'); 37 | const ctrlCloseOtherFoldersNative = document.querySelector('#closeOtherFoldersNative'); 38 | const ctrlPopupFixedNative = document.querySelector('#popupFixedNative'); 39 | const ctrlPreserveStateNative = document.querySelector('#preserveStateNative'); 40 | const ctrlIconStyleAutoNative = document.querySelector('#iconStyleAutoNative'); 41 | 42 | const iconStylePanel = document.querySelector('.icon-style-panel'); 43 | const iconColorList = document.querySelector('.icon-color-list'); 44 | 45 | const setIcon = (iconType = 'star', iconStyle = 'dark', iconStyleAuto = true) => { 46 | const isBrowserDark = window.matchMedia("(prefers-color-scheme: dark)").matches || chrome.extension.inIncognitoContext; 47 | 48 | const iconList = document.querySelector('.icon-list'); 49 | iconList.className = 'icon-list'; 50 | 51 | if (iconStyleAuto) { 52 | if (isBrowserDark) { 53 | iconStyle = 'light'; 54 | } else { 55 | iconStyle = 'dark'; 56 | } 57 | } 58 | iconList.classList.add(iconStyle); 59 | }; 60 | 61 | chrome.storage.sync.get({ 62 | openNewTabs: false, 63 | openTabsInBg: false, 64 | closeOtherFolders: false, 65 | popupFixed: false, 66 | preserveState: true, 67 | panelHeight: '500px', 68 | iconType: 'star', 69 | iconStyle: 'dark', 70 | iconStyleAuto: true, 71 | }, props => { 72 | Object.assign(storageCache, props); 73 | 74 | ctrlOpenNewTabs.checked = props.openNewTabs; 75 | ctrlOpenTabsInBg.checked = props.openTabsInBg; 76 | ctrlCloseOtherFolders.checked = props.closeOtherFolders; 77 | ctrlPopupFixed.checked = props.popupFixed; 78 | ctrlPreserveState.checked = props.preserveState; 79 | ctrlIconStyleAuto.checked = props.iconStyleAuto; 80 | 81 | ctrlIconStyleAuto.checked ? iconStylePanel.classList.add('hidden') : iconStylePanel.classList.remove('hidden'); 82 | 83 | if (props.iconStyle === 'colored') { 84 | storageCache.iconStyle = 'yellow'; 85 | chrome.storage.sync.set({ iconStyle: 'yellow'}); 86 | } 87 | 88 | document.querySelector(`[name=icon-type-group][value=${storageCache.iconType}]`).setAttribute('checked', true); 89 | document.querySelector(`[name=icon-style-group][value=${storageCache.iconStyle}]`).setAttribute('checked', true); 90 | 91 | setIcon(storageCache.iconType, storageCache.iconStyle, storageCache.iconStyleAuto); 92 | const isBrowserDark = window.matchMedia("(prefers-color-scheme: dark)").matches || chrome.extension.inIncognitoContext; 93 | chrome.runtime.sendMessage({ isBrowserDark: isBrowserDark }); 94 | }); 95 | 96 | /* Always open bookmarks in a new tab */ 97 | ctrlOpenNewTabsNative.addEventListener('change', () => { 98 | chrome.storage.sync.set({ 99 | openNewTabs: ctrlOpenNewTabs.checked 100 | }, optionSaved); 101 | }); 102 | 103 | /* Open bookmark in background while holding cmd */ 104 | ctrlOpenTabsInBgNative.addEventListener('change', () => { 105 | chrome.storage.sync.set({ 106 | openTabsInBg: ctrlOpenTabsInBg.checked 107 | },optionSaved); 108 | }); 109 | 110 | /* Close others folders when opening a new one */ 111 | ctrlCloseOtherFoldersNative.addEventListener('change', () => { 112 | chrome.storage.sync.set({ 113 | closeOtherFolders: ctrlCloseOtherFolders.checked 114 | }, optionSaved); 115 | }); 116 | 117 | /* Popup stay open when opening a bookmark */ 118 | ctrlPopupFixedNative.addEventListener('change', () => { 119 | chrome.storage.sync.set({ 120 | popupFixed: ctrlPopupFixed.checked 121 | }, optionSaved); 122 | }); 123 | 124 | /* Remember last state */ 125 | ctrlPreserveStateNative.addEventListener('change', () => { 126 | chrome.storage.sync.set({ 127 | preserveState: ctrlPreserveState.checked 128 | },optionSaved); 129 | }); 130 | 131 | ctrlIconStyleAutoNative.addEventListener('change', () => { 132 | chrome.storage.sync.set({ 133 | iconStyleAuto: ctrlIconStyleAuto.checked 134 | }, optionSaved); 135 | }); 136 | 137 | document.querySelectorAll('input[name=icon-type-group]').forEach(el => { 138 | el.addEventListener('change', res=> { 139 | const target = res.target; 140 | 141 | if (target.checked) { 142 | chrome.storage.sync.set({ 143 | iconType: target.value 144 | }, optionSaved); 145 | } 146 | }); 147 | }); 148 | 149 | // Setting icon color 150 | document.querySelectorAll('[name=icon-style-group]').forEach(el => { 151 | el.addEventListener('change', res=> { 152 | const target = res.target; 153 | 154 | if (target.checked) { 155 | chrome.storage.sync.set({ 156 | iconStyle: target.value 157 | }, optionSaved); 158 | } 159 | }); 160 | }); 161 | 162 | chrome.storage.onChanged.addListener(changes => { 163 | for (const [key, value] of Object.entries(changes)) { 164 | storageCache[key] = value.newValue; 165 | } 166 | 167 | if (changes.iconType || changes.iconStyle || changes.iconStyleAuto) { 168 | setIcon(storageCache.iconType, storageCache.iconStyle, storageCache.iconStyleAuto); 169 | } 170 | 171 | if (changes.iconStyleAuto) { 172 | const val = changes.iconStyleAuto.newValue; 173 | val ? iconStylePanel.classList.add('hidden') : iconStylePanel.classList.remove('hidden'); 174 | } 175 | }); -------------------------------------------------------------------------------- /public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessibility": { 3 | "message": "辅助功能设置" 4 | }, 5 | "confirmDeleteFolderBookmarks": { 6 | "message": "您确定要删除文件夹[$folderName$](包含 $bookmarkCount$ 个书签)?", 7 | "placeholders": { 8 | "bookmarkCount": { 9 | "content": "$2" 10 | }, 11 | "folderName": { 12 | "content": "$1" 13 | } 14 | } 15 | }, 16 | "confirmDeleteFolderSubfoldersBookmarks": { 17 | "message": "您确定要删除文件夹[$folderName$](包含 $subFolderCount$ 个子目录和 $bookmarkCount$ 个书签)?", 18 | "placeholders": { 19 | "bookmarkCount": { 20 | "content": "$3" 21 | }, 22 | "folderName": { 23 | "content": "$1" 24 | }, 25 | "subFolderCount": { 26 | "content": "$2" 27 | } 28 | } 29 | }, 30 | "confirmDeleteFolderSubfolders": { 31 | "message": "您确定要删除文件夹[$folderName$](包含 $subFolderCount$ 个子目录)?", 32 | "placeholders": { 33 | "folderName": { 34 | "content": "$1" 35 | }, 36 | "subFolderCount": { 37 | "content": "$2" 38 | } 39 | } 40 | }, 41 | "confirmOpenBookmarks": { 42 | "description": "打开所有书签的确认信息", 43 | "message": "您确定要打开 $count$ 个书签?", 44 | "placeholders": { 45 | "count": { 46 | "content": "$1" 47 | } 48 | } 49 | }, 50 | "confirmOpenBookmarksNewIncognitoWindow": { 51 | "description": "在隐身窗口中打开所有书签的确认信息", 52 | "message": "您确定要在隐身窗口中打开 $count$ 个书签?", 53 | "placeholders": { 54 | "count": { 55 | "content": "$1" 56 | } 57 | } 58 | }, 59 | "confirmOpenBookmarksNewWindow": { 60 | "description": "在新窗口中打开所有书签的确认信息", 61 | "message": "您确定要在新窗口中打开 $count$ 个书签?", 62 | "placeholders": { 63 | "count": { 64 | "content": "$1" 65 | } 66 | } 67 | }, 68 | "customIcon": { 69 | "message": "自定义图标" 70 | }, 71 | "customIconText": { 72 | "message": "图标尺寸必须为 19*19,否则图标将会被缩放以适应大小" 73 | }, 74 | "customStyles": { 75 | "message": "自定义样式" 76 | }, 77 | "customStylesText": { 78 | "message": "在此处输入你的自定义CSS", 79 | "placeholders": { 80 | "linkGithubGist": { 81 | "content": "$1" 82 | } 83 | } 84 | }, 85 | "deleteEllipsis": { 86 | "message": "删除 ..." 87 | }, 88 | "delete": { 89 | "message": "删除" 90 | }, 91 | "editBookmark": { 92 | "message": "编辑书签" 93 | }, 94 | "editFolder": { 95 | "message": "编辑文件夹" 96 | }, 97 | "edit": { 98 | "message": "编辑 ..." 99 | }, 100 | "errorOccured": { 101 | "message": "出错了" 102 | }, 103 | "extDesc": { 104 | "description": "扩展描述", 105 | "message": "简单,漂亮的弹出书签" 106 | }, 107 | "extName": { 108 | "description": "扩展名", 109 | "message": "Bookmarkie - 在弹窗中查看、管理你的书签" 110 | }, 111 | "extActionTitle": { 112 | "message": "查看书签" 113 | }, 114 | "general": { 115 | "message": "常规设置" 116 | }, 117 | "appearance": { 118 | "message": "样式设置" 119 | }, 120 | "msgHeightDefault": { 121 | "message": "默认" 122 | }, 123 | "heightCustom": { 124 | "message": "自定义" 125 | }, 126 | "ignore": { 127 | "message": "忽略" 128 | }, 129 | "name": { 130 | "description": "书签名或文件夹名", 131 | "message": "名字" 132 | }, 133 | "nope": { 134 | "message": "取消" 135 | }, 136 | "cancel": { 137 | "message": "取消" 138 | }, 139 | "noTitle": { 140 | "description": "没 http(s) 开头的书签或没有命名的文件夹的统一标题", 141 | "message": "(无标题)" 142 | }, 143 | "openBookmarksIncognitoWindow": { 144 | "message": "在隐身窗口中打开所有书签" 145 | }, 146 | "openBookmarks": { 147 | "message": "打开所有书签" 148 | }, 149 | "openBookmarksNewWindow": { 150 | "message": "在新窗口中打开所有书签" 151 | }, 152 | "open": { 153 | "description": "打开书签的动作", 154 | "message": "打开" 155 | }, 156 | "openIncognitoWindow": { 157 | "message": "在隐身窗口中打开" 158 | }, 159 | "openNewTab": { 160 | "message": "在新标签页中打开" 161 | }, 162 | "openNewWindow": { 163 | "message": "在新窗口中打开" 164 | }, 165 | "optionClickNewTab": { 166 | "message": "总在新标签页打开书签" 167 | }, 168 | "optionCloseUnusedFolders": { 169 | "message": "展开文件夹时折叠其它已展开的文件夹" 170 | }, 171 | "optionConfirmOpenFolder": { 172 | "message": "打开多个书签前需确认" 173 | }, 174 | "optionOpenNewTab": { 175 | "message": "鼠标中键或按住 Ctrl 键左击时在后台打开书签" 176 | }, 177 | "optionOpenNewTabMac": { 178 | "description": "For mac users", 179 | "message": "鼠标中键或按住 ⌘ 键左击时在后台打开书签" 180 | }, 181 | "optionPopupStays": { 182 | "message": "打开(鼠标左击)书签时不关闭弹出页面" 183 | }, 184 | "optionRememberPrevState": { 185 | "message": "记住上次状态(滚动条位置、所打开文件夹)" 186 | }, 187 | 188 | "icon": { 189 | "message": "图标" 190 | }, 191 | "style": { 192 | "message": "样式" 193 | }, 194 | "color": { 195 | "message": "颜色" 196 | }, 197 | 198 | "iconStyleAuto": { 199 | "message": "自动切换颜色" 200 | }, 201 | 202 | "dark": { 203 | "message": "深色" 204 | }, 205 | 206 | "light": { 207 | "message": "浅色" 208 | }, 209 | 210 | "colored": { 211 | "message": "黄色" 212 | }, 213 | 214 | "options": { 215 | "message": "选项" 216 | }, 217 | "optionZoom": { 218 | "message": "缩放" 219 | }, 220 | "parentFolder": { 221 | "description": "搜索结果列表中提示书签所在目录", 222 | "message": "所在目录:$folderName$", 223 | "placeholders": { 224 | "folderName": { 225 | "content": "$1", 226 | "example": "书签栏" 227 | } 228 | } 229 | }, 230 | "reportedToDeveloper": { 231 | "description": "此异常已提交给开发者", 232 | "message": "已提交给开发者" 233 | }, 234 | "reportError": { 235 | "message": "报告此错误" 236 | }, 237 | "reset": { 238 | "message": "重置" 239 | }, 240 | "resetSettings": { 241 | "message": "重置设置" 242 | }, 243 | "resetText": { 244 | "message": "重置 $extName$ 的所有设置", 245 | "placeholders": { 246 | "extName": { 247 | "content": "$1" 248 | } 249 | } 250 | }, 251 | "save": { 252 | "message": "保存" 253 | }, 254 | "searchBookmarks": { 255 | "description": "搜索框里的提示文字", 256 | "message": "搜索书签" 257 | }, 258 | "url": { 259 | "message": "网址" 260 | }, 261 | "confirm": { 262 | "message": "确认" 263 | }, 264 | "panelHeight": { 265 | "description": "选项菜单中弹出窗高度", 266 | "message": "弹出窗高度" 267 | }, 268 | "phWarning": { 269 | "description": "选项弹窗高度格式错误提示", 270 | "message": "使用以 px 或 % 为单位的数值,例如:600px, 92%" 271 | }, 272 | "viewBookmarksOnChrome": { 273 | "message": "Chrome 书签管理器" 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/options.scss: -------------------------------------------------------------------------------- 1 | @use "@material/list"; 2 | @use "@material/switch"; 3 | @use "@material/card"; 4 | @use "@material/typography"; 5 | 6 | @import "variables.scss"; 7 | 8 | @include list.deprecated-core-styles; 9 | @include switch.core-styles; 10 | @include card.core-styles; 11 | @include typography.core-styles; 12 | 13 | * { 14 | --theme-primary: #3674E0; 15 | --selection-bg: rgba(0, 0, 0, 0.04); 16 | --selection-selected-bg: #E0EAFC; 17 | --bg: #f2f2f2; 18 | --section-bg: #fff; 19 | 20 | --headline-color: rgba(0, 0, 0, 0.65); 21 | --subtitle-color: rgba(0, 0, 0, 0.9); 22 | --body-color: rgba(0, 0, 0, 0.9); 23 | 24 | --divider-bg: rgba(0, 0, 0, 0.08); 25 | 26 | --dark: #444; 27 | --light: #fff; 28 | --yellow: #f4a60e; 29 | --trinidad: #d74c2e; 30 | --pastel: #6ad37c; 31 | --naval: #4277d7; 32 | --orchid: #993fd6; 33 | --gold: linear-gradient(#FFCE7A, #BA851E); 34 | 35 | } 36 | 37 | body { 38 | background: var(--bg); 39 | padding-bottom: 32px; 40 | overflow-x: hidden; 41 | } 42 | 43 | .section { 44 | border-radius: 4px; 45 | margin: 16px 0; 46 | } 47 | 48 | .section .mdc-card { 49 | background: var(--section-bg); 50 | } 51 | 52 | .headline { 53 | color: var(--headline-color); 54 | } 55 | 56 | .subtitle { 57 | padding-left: 16px; 58 | padding-inline-start: 16px; 59 | color: var(--subtitle-color); 60 | } 61 | 62 | .icon-list { 63 | width: 100%; 64 | padding: 0 0 16px 16px; 65 | padding-inline-start: 16px; 66 | } 67 | 68 | .icon-list-item { 69 | list-style: none; 70 | float: left; 71 | margin-right: 10px; 72 | } 73 | 74 | .icon-list-item__graphic { 75 | display: block; 76 | width: 40px; 77 | height: 40px; 78 | background-color: var(--selection-bg); 79 | background-repeat: no-repeat; 80 | background-size: 24px 24px; 81 | background-position: center; 82 | border-radius: 4px; 83 | box-sizing: border-box; 84 | border: 2px solid transparent; 85 | overflow: hidden; 86 | display: flex; 87 | justify-content: center; 88 | align-items: center; 89 | 90 | svg { 91 | transform: scale(0.8); 92 | } 93 | } 94 | 95 | .icon-list-item__native-control { 96 | display: none; 97 | } 98 | 99 | .icon-list-item__native-control:checked ~ .icon-list-item__graphic { 100 | background-color: var(--selection-selected-bg); 101 | border: 2px solid var(--theme-primary); 102 | } 103 | 104 | .icon-list-item__graphic, .dark .icon-list-item__graphic { 105 | path { 106 | fill: #444; 107 | } 108 | } 109 | 110 | .light .icon-list-item__graphic { 111 | path { 112 | fill: #fff; 113 | } 114 | } 115 | 116 | @mixin graphic-path($colorCLass) { 117 | #{$colorCLass '.icon-list-item__graphic path'} { 118 | @content; 119 | } 120 | } 121 | 122 | .icon-list-item__graphic path { 123 | fill: var(--yellow); 124 | }; 125 | 126 | @include graphic-path('.yellow') { 127 | fill: var(--yellow); 128 | }; 129 | 130 | @include graphic-path('.trinidad') { 131 | fill: var(--trinidad); 132 | }; 133 | 134 | @include graphic-path('.pastel') { 135 | fill: var(--pastel); 136 | }; 137 | 138 | @include graphic-path('.naval') { 139 | fill: var(--naval); 140 | }; 141 | 142 | @include graphic-path('.orchid') { 143 | fill: var(--orchid); 144 | }; 145 | 146 | @include graphic-path('.gold') { 147 | fill: url(#gold); 148 | }; 149 | 150 | @include graphic-path('.metalic') { 151 | fill: url(#metalic); 152 | }; 153 | 154 | .icon-style-list { 155 | padding: 16px; 156 | padding-inline-start: 16px; 157 | } 158 | 159 | .icon-style-list-item { 160 | display: inline-block; 161 | } 162 | 163 | .icon-style-list-item__label { 164 | display: block; 165 | padding: 0 16px; 166 | height: 36px; 167 | line-height: 36px; 168 | border-radius: 18px; 169 | min-width: 40px; 170 | text-align: center; 171 | margin-right: 10px; 172 | background: var(--selection-bg); 173 | color: var(--body-color); 174 | transition: background 0.2s linear; 175 | border: 2px solid transparent; 176 | } 177 | 178 | .icon-list-item__native-control:checked ~ .icon-style-list-item__label { 179 | background: var(--selection-selected-bg); 180 | color: var(--theme-primary); 181 | font-weight: 700; 182 | border: 2px solid var(--theme-primary); 183 | } 184 | 185 | .icon-style-panel { 186 | overflow: hidden; 187 | transition: all 0.2s linear; 188 | } 189 | 190 | .icon-style-panel.hidden { 191 | height: 0; 192 | } 193 | 194 | .icon-color-list { 195 | padding: 16px; 196 | padding-inline-start: 16px; 197 | } 198 | 199 | .icon-color-list-item { 200 | display: inline-block; 201 | margin-right: 8px; 202 | } 203 | 204 | .icon-color-list-item__graphic { 205 | display: block; 206 | width: 28px; 207 | height: 28px; 208 | border-radius: 50%; 209 | box-sizing: border-box; 210 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08); 211 | 212 | .icon-color-list-item__native-control:checked ~ & { 213 | box-shadow: 0 0 0 2px var(--theme-primary), inset 0 0 0 2px var(--section-bg); 214 | } 215 | 216 | &.dark { 217 | background: var(--dark); 218 | } 219 | 220 | &.light { 221 | background: var(--light); 222 | } 223 | 224 | &.yellow { 225 | background: var(--yellow); 226 | } 227 | 228 | &.trinidad { 229 | background: var(--trinidad); 230 | } 231 | 232 | &.pastel { 233 | background: var(--pastel); 234 | } 235 | 236 | &.naval { 237 | background: var(--naval); 238 | } 239 | 240 | &.orchid { 241 | background: var(--orchid); 242 | } 243 | 244 | &.gold { 245 | background: var(--gold); 246 | } 247 | } 248 | 249 | .icon-color-list-item__native-control { 250 | display: none; 251 | } 252 | 253 | /* Overrides of Material Design Components */ 254 | 255 | 256 | .mdc-list-divider { 257 | border-bottom-color: var(--divider-bg); 258 | } 259 | 260 | .mdc-switch { 261 | @include switch.toggled-on-color($theme-primary); 262 | @include switch.ripple-size(30px); 263 | } 264 | 265 | .mdc-list-item { 266 | font-size: 14px; 267 | padding-top: 3px; 268 | padding-bottom: 3px; 269 | 270 | &__text { 271 | flex: 1; 272 | } 273 | 274 | &__meta { 275 | display: flex; 276 | align-items: center; 277 | } 278 | } 279 | 280 | 281 | 282 | @media (prefers-color-scheme: dark) { 283 | * { 284 | --selection-bg: #323639; /* toolbar color in dark mode */ 285 | --selection-selected-bg: #63686d; 286 | --bg: #202124; 287 | --section-bg: #292a2d; 288 | --theme-primary: #8ab4f9; 289 | 290 | --headline-color: rgba(255, 255, 255, 0.7); 291 | --subtitle-color: rgba(255, 255, 255, 0.9); 292 | --body-color: #fff; 293 | --mdc-theme-text-primary-on-background: #fff; 294 | --mdc-theme-text-secondary-on-background: rgba(255, 255, 255, 0.7); 295 | 296 | --divider-bg: rgba(255, 255, 255, 0.12); 297 | 298 | } 299 | 300 | .mdc-switch { 301 | @include switch.toggled-on-color($theme-primary-dark); 302 | @include switch.toggled-off-track-color(#fff); 303 | } 304 | 305 | .mdc-list-item__text { 306 | color: var(--headline-color); 307 | } 308 | } -------------------------------------------------------------------------------- /public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "accessibility": { 3 | "message": "Accessibility Settings" 4 | }, 5 | "confirmDeleteFolderBookmarks": { 6 | "message": "Are you sure you want to delete $folderName$ folder and $bookmarkCount$ bookmark(s) in it?", 7 | "placeholders": { 8 | "bookmarkCount": { 9 | "content": "$2" 10 | }, 11 | "folderName": { 12 | "content": "$1" 13 | } 14 | } 15 | }, 16 | "confirmDeleteFolderSubfoldersBookmarks": { 17 | "message": "Are you sure you want to delete $folderName$ folder, $subFolderCount$ sub-folder(s) and $bookmarkCount$ bookmark(s) in it?", 18 | "placeholders": { 19 | "bookmarkCount": { 20 | "content": "$3" 21 | }, 22 | "folderName": { 23 | "content": "$1" 24 | }, 25 | "subFolderCount": { 26 | "content": "$2" 27 | } 28 | } 29 | }, 30 | "confirmDeleteFolderSubfolders": { 31 | "message": "Are you sure you want to delete $folderName$ folder and $subFolderCount$ sub-folder(s) in it?", 32 | "placeholders": { 33 | "folderName": { 34 | "content": "$1" 35 | }, 36 | "subFolderCount": { 37 | "content": "$2" 38 | } 39 | } 40 | }, 41 | "confirmOpenBookmarks": { 42 | "description": "Confirmation message for opening all bookmarks.", 43 | "message": "Are you sure you want to open all $count$ bookmarks?", 44 | "placeholders": { 45 | "count": { 46 | "content": "$1" 47 | } 48 | } 49 | }, 50 | "confirmOpenBookmarksNewIncognitoWindow": { 51 | "description": "Confirmation message for opening all bookmarks in a new incognito window.", 52 | "message": "Are you sure you want to open all $count$ bookmarks in new incognito window?", 53 | "placeholders": { 54 | "count": { 55 | "content": "$1" 56 | } 57 | } 58 | }, 59 | "confirmOpenBookmarksNewWindow": { 60 | "description": "Confirmation message for opening all bookmarks in a new window.", 61 | "message": "Are you sure you want to open all $count$ bookmarks in new window?", 62 | "placeholders": { 63 | "count": { 64 | "content": "$1" 65 | } 66 | } 67 | }, 68 | "customIcon": { 69 | "message": "Custom Icon" 70 | }, 71 | "customIconText": { 72 | "message": "The icon must be 19×19 or it will be resized." 73 | }, 74 | "customStyles": { 75 | "message": "Custom Styles" 76 | }, 77 | "customStylesText": { 78 | "message": "You can custom the style with your own CSS.", 79 | "placeholders": { 80 | "linkGithubGist": { 81 | "content": "$1" 82 | } 83 | } 84 | }, 85 | "deleteEllipsis": { 86 | "message": "Delete..." 87 | }, 88 | "delete": { 89 | "message": "Delete" 90 | }, 91 | "editBookmark": { 92 | "message": "Edit bookmark" 93 | }, 94 | "editFolder": { 95 | "message": "Edit folder" 96 | }, 97 | "edit": { 98 | "message": "Edit..." 99 | }, 100 | "errorOccured": { 101 | "message": "Oops, an error occurred." 102 | }, 103 | "extDesc": { 104 | "description": "The extension description.", 105 | "message": "The most elegant popup bookmark tool." 106 | }, 107 | "extName": { 108 | "description": "The extension name.", 109 | "message": "Bookmarkie - Bookmarks in a popup" 110 | }, 111 | "extActionTitle": { 112 | "message": "Your Bookmarks" 113 | }, 114 | "general": { 115 | "message": "General" 116 | }, 117 | "appearance": { 118 | "message": "Appearance" 119 | }, 120 | "msgHeightDefault": { 121 | "message": "Default" 122 | }, 123 | "heightCustom": { 124 | "message": "Custom" 125 | }, 126 | "ignore": { 127 | "message": "Ignore" 128 | }, 129 | "name": { 130 | "description": "Name of bookmark or folder.", 131 | "message": "Name" 132 | }, 133 | "nope": { 134 | "message": "Nope" 135 | }, 136 | "cancel": { 137 | "message": "Cancel" 138 | }, 139 | "noTitle": { 140 | "description": "The title for untitled bookmarks with no http(s) URL or untitled folders.", 141 | "message": "(no title)" 142 | }, 143 | "openBookmarksIncognitoWindow": { 144 | "message": "Open all in Incognito Window" 145 | }, 146 | "openBookmarks": { 147 | "message": "Open All" 148 | }, 149 | "openBookmarksNewWindow": { 150 | "message": "Open All in New Window" 151 | }, 152 | "open": { 153 | "description": "Action to open bookmarks.", 154 | "message": "Open" 155 | }, 156 | "openIncognitoWindow": { 157 | "message": "Open in Incognito Window" 158 | }, 159 | "openNewTab": { 160 | "message": "Open in new tab" 161 | }, 162 | "openNewWindow": { 163 | "message": "Open in New Window" 164 | }, 165 | "optionClickNewTab": { 166 | "message": "Always open bookmarks in new tabs" 167 | }, 168 | "optionCloseUnusedFolders": { 169 | "message": "Collapse other folders when opening one" 170 | }, 171 | "optionConfirmOpenFolder": { 172 | "message": "Display confirmation when opening a lot of bookmarks from a folder" 173 | }, 174 | "optionOpenNewTab": { 175 | "message": "ctrl + click to open bookmarks in background" 176 | }, 177 | "optionOpenNewTabMac": { 178 | "description": "For mac users", 179 | "message": "⌘ + click to open bookmarks in background" 180 | }, 181 | "optionPopupStays": { 182 | "message": "Fix popup when opening bookmarks" 183 | }, 184 | "optionRememberPrevState": { 185 | "message": "Preserve scroll position and opened folders" 186 | }, 187 | 188 | "options": { 189 | "message": "Options" 190 | }, 191 | "icon": { 192 | "message": "Icon" 193 | }, 194 | "style": { 195 | "message": "Style" 196 | }, 197 | "color": { 198 | "message": "Color" 199 | }, 200 | 201 | "iconStyleAuto": { 202 | "message": "Switch icon color automatically" 203 | }, 204 | 205 | "dark": { 206 | "message": "Dark" 207 | }, 208 | 209 | "light": { 210 | "message": "Light" 211 | }, 212 | 213 | "colored": { 214 | "message": "Colored" 215 | }, 216 | 217 | "optionZoom": { 218 | "message": "Zoom" 219 | }, 220 | "parentFolder": { 221 | "description": "Parent folder information displayed in the tooltip of bookmarks in search results list.", 222 | "message": "Parent folder: \"$folderName$\"", 223 | "placeholders": { 224 | "folderName": { 225 | "content": "$1", 226 | "example": "Bookmarks bar" 227 | } 228 | } 229 | }, 230 | "reportedToDeveloper": { 231 | "description": "A message for an error occurred in the extension, has been reported to the developer.", 232 | "message": "This has been reported to the developer." 233 | }, 234 | "reportError": { 235 | "message": "Report Error" 236 | }, 237 | "reset": { 238 | "message": "Reset" 239 | }, 240 | "resetSettings": { 241 | "message": "Reset Settings" 242 | }, 243 | "resetText": { 244 | "message": "Reset all $extName$ options to default.", 245 | "placeholders": { 246 | "extName": { 247 | "content": "$1" 248 | } 249 | } 250 | }, 251 | "save": { 252 | "message": "Save" 253 | }, 254 | "searchBookmarks": { 255 | "description": "Placeholder text for search field.", 256 | "message": "Search Bookmarks" 257 | }, 258 | "url": { 259 | "message": "URL" 260 | }, 261 | "confirm": { 262 | "message": "Confirm" 263 | }, 264 | "panelHeight": { 265 | "description": "Option popup height", 266 | "message": "Popup Height" 267 | }, 268 | "phWarning": { 269 | "description": "", 270 | "message": "Use numbers with \"px\" or \"%\" as unit,e.g:600px, 92%" 271 | }, 272 | "viewBookmarksOnChrome": { 273 | "message": "Chrome Bookmark Manager" 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

General

12 | 13 |
14 | 15 | 33 | 51 | 52 |
53 | 54 | 73 | 74 | 75 | 108 | 109 |
110 |
111 | 112 | 113 |
114 |

Icon

115 | 116 |
117 | 118 |

Style

119 | 120 | 152 | 153 |
154 | 155 |

Color

156 | 157 | 174 | 175 | 212 | 213 |
214 | 215 |
216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /src/popup.scss: -------------------------------------------------------------------------------- 1 | @use "@material/button"; 2 | @use "@material/button/styles"; 3 | @use "@material/typography/mdc-typography"; 4 | 5 | @import "variables.scss"; 6 | 7 | * { 8 | --theme-primary: $theme-primary; 9 | --theme-warn: $theme-warn; 10 | --bg: #fafafa; 11 | --body-color: rgba(0, 0, 0, 0.8); 12 | 13 | --input-bg: rgba(0, 0, 0, 0.08); 14 | --list-focus-bg: rgba(0, 0, 0, 0.04); 15 | --menu-bg: #fff; 16 | --divider-bg: rgba(0, 0, 0, 0.12); 17 | 18 | --search-icon-fill: #5a5a5a; 19 | --folder-icon-url: url(chrome-extension://__MSG_@@extension_id__/images/folder.svg); 20 | --folder-open-icon-url: url(chrome-extension://__MSG_@@extension_id__/images/folder-open.svg); 21 | 22 | box-sizing: border-box; 23 | } 24 | 25 | body{ 26 | font-size: 14px; 27 | color: var(--body-color); 28 | background-color: var(--bg); 29 | margin: 0; 30 | padding: 0; 31 | cursor: default; 32 | -webkit-user-drag: none; 33 | -webkit-user-select: none; 34 | -webkit-font-smoothing: antialiased; 35 | overflow: hidden; 36 | width: 340px; 37 | min-height: 500px; 38 | max-height: 500px; /* prevent double scrollbars in popup */ 39 | } 40 | 41 | body.transitional{ 42 | transition: height 0.12s ease-out; 43 | will-change: height; 44 | } 45 | 46 | .container{ 47 | display: flex; 48 | flex-direction: column; 49 | position: absolute; 50 | top: 52px; 51 | right: 0; 52 | bottom: 0; 53 | left: 0; 54 | } 55 | 56 | .input { 57 | width: 100%; 58 | font-size: 14px; 59 | padding: 0.6rem 0.4rem; 60 | background-color: transparent; 61 | border: none; 62 | color: var(--body-color); 63 | transition: background 0.18s ease-out; 64 | will-change: background; 65 | 66 | &:focus { 67 | outline: 0; 68 | background-color: rgba(0, 0, 0, 0.06); 69 | } 70 | } 71 | 72 | .search { 73 | margin: 16px; 74 | } 75 | 76 | .search__textfield { 77 | position: relative; 78 | background: var(--input-bg); 79 | display: flex; 80 | align-items: center; 81 | border-radius: 18px; 82 | overflow: hidden; 83 | 84 | &__leading-icon { 85 | position: absolute; 86 | left: 8px; 87 | width: 20px; 88 | height: 20px; 89 | background-size: cover; 90 | opacity: 0.6; 91 | transition: opacity 0.12s; 92 | will-change: opacity; 93 | 94 | svg { 95 | width: 100%; 96 | height: 100%; 97 | } 98 | 99 | svg path { 100 | fill: var(--search-icon-fill); 101 | } 102 | 103 | .input:focus ~ & { 104 | opacity: 1; 105 | } 106 | } 107 | 108 | .input { 109 | padding: 0.4rem; 110 | padding-left: 36px; 111 | } 112 | } 113 | 114 | .input--search { 115 | border: 0; 116 | border-radius: 0; 117 | width: 100%; 118 | margin: 0; 119 | padding: 8px; 120 | padding-left: 14px; 121 | cursor: pointer; 122 | background-size: 16px 16px; 123 | background-repeat: no-repeat; 124 | background-position: 12px center; 125 | background-color: transparent; 126 | transition: all ease 0.15s; 127 | 128 | &:focus { 129 | cursor: text; 130 | background-image: var(--search-icon-url); 131 | padding-left: 34px; 132 | box-shadow: 0 2px 4px rgba(0,0,0,0.15); 133 | border: 0; 134 | } 135 | } 136 | 137 | 138 | .tree{ 139 | outline: 0; 140 | padding: 3px 0; 141 | flex: 1; 142 | position: relative; 143 | overflow-y: auto; 144 | 145 | ul { 146 | list-style: none; 147 | margin: 0; 148 | padding: 1px 0; 149 | display: block; 150 | } 151 | 152 | li { 153 | 154 | a, span { 155 | color: var(--body-color); 156 | display: inline-flex; 157 | align-items: center; 158 | line-height: 1.67rem; 159 | padding: 2px 8px; 160 | text-decoration: none; 161 | outline: 0; 162 | white-space: nowrap; 163 | overflow: hidden; 164 | flex: 6 1 auto; 165 | width: 100%; 166 | 167 | * { 168 | pointer-events: none; 169 | text-overflow: ellipsis; 170 | overflow: hidden; 171 | font-style: normal; 172 | } 173 | 174 | .icon { 175 | flex: 0 0 auto; 176 | } 177 | 178 | &.active, &:focus { 179 | background-color: var(--list-focus-bg); 180 | outline: 0; 181 | } 182 | } 183 | 184 | .arrow { 185 | display: inline-block; 186 | position: relative; 187 | width: 0; 188 | height: 0; 189 | border-width: 4px 0 4px 6px; 190 | border-color: transparent transparent transparent #A5A5A5; 191 | border-style: solid; 192 | margin: 0 8px 0 4px; 193 | transition: transform .15s; 194 | } 195 | 196 | &.open>span { 197 | font-weight: 600; 198 | 199 | .arrow { 200 | transform: rotate(90deg); 201 | } 202 | } 203 | } 204 | } 205 | 206 | .icon { 207 | display: block; 208 | width: 16px; 209 | height: 16px; 210 | margin-right: 8px; 211 | background-size: cover; 212 | background-repeat: no-repeat; 213 | 214 | &--folder { 215 | background-image: var(--folder-icon-url); 216 | } 217 | } 218 | 219 | [aria-expanded=true] > span .icon--folder { 220 | background-image: var(--folder-open-icon-url); 221 | } 222 | 223 | .list { 224 | &--url { 225 | .icon { 226 | margin-left: 6px; 227 | } 228 | } 229 | } 230 | 231 | 232 | 233 | .searchFocus .tree--results ul>li:first-child a{ 234 | background-color: rgba(43,93,205,.2); 235 | } 236 | 237 | .tree ul ul{ 238 | height: 0; 239 | overflow: hidden; 240 | } 241 | .tree .open>ul{ 242 | height: auto; 243 | } 244 | 245 | .context-menu { 246 | transition: opacity .3s; 247 | opacity: 0; 248 | margin: 0; 249 | padding: 8px 0; 250 | list-style: none; 251 | position: absolute; 252 | left: -999px; 253 | border-radius: 4px; 254 | background-color: var(--menu-bg); 255 | box-shadow: 0 2px 6px rgba(0,0,0,.2), 0 4px 12px rgba(0,0,0,0.04); 256 | outline: 0; 257 | max-width: 100%; 258 | 259 | hr{ 260 | border: 0; 261 | padding: 0; 262 | height: 1px; 263 | margin: 4px 0; 264 | background-color: var(--divider-bg); 265 | pointer-events: none; 266 | } 267 | } 268 | 269 | .cmd { 270 | display: block; 271 | padding: 0 16px; 272 | line-height: 32px; 273 | overflow: hidden; 274 | white-space: nowrap; 275 | text-overflow: ellipsis; 276 | text-transform: capitalize; 277 | 278 | &:focus, &:hover { 279 | background-color: var(--list-focus-bg); 280 | outline: 0; 281 | } 282 | } 283 | 284 | .context-menu.hide-editables hr, 285 | .context-menu.hide-editables .cmd--folder-edit, 286 | .context-menu.hide-editables .cmd--folder-delete{ 287 | display: none; 288 | } 289 | 290 | .dialog{ 291 | position: absolute; 292 | background: var(--menu-bg); 293 | opacity: 0; 294 | padding: 0.8rem; 295 | z-index: -1; 296 | 297 | pre{ 298 | white-space: pre-line; 299 | margin: 0; 300 | padding: 5px; 301 | -webkit-user-select: text; 302 | cursor: text; 303 | } 304 | 305 | &__textfield { 306 | position: relative; 307 | margin-bottom: 1rem; 308 | width: 100%; 309 | background: var(--input-bg); 310 | border-radius: 4px; 311 | overflow: hidden; 312 | 313 | &__underline { 314 | width: 100%; 315 | background: var(--theme-primary); 316 | transform: scaleX(0); 317 | height: 2px; 318 | position: absolute; 319 | bottom: 0; 320 | transition: transform 0.18s ease-out; 321 | will-change: transform; 322 | 323 | .input:focus ~ & { 324 | transform: scaleX(1); 325 | } 326 | 327 | .input:invalid ~ & { 328 | background: var(--theme-warn); 329 | } 330 | } 331 | } 332 | 333 | &--normal { 334 | width: 280px; 335 | left: 50%; 336 | top: 50%; 337 | transform: translate(-50%, -60%); 338 | border-radius: 8px; 339 | box-shadow: 0 15px 20px rgba(0,0,0,0.2); 340 | } 341 | 342 | &__footer { 343 | text-align: right; 344 | padding-top: 0.8rem; 345 | 346 | .button { 347 | margin-left: 2rem; 348 | } 349 | } 350 | } 351 | 352 | .transitional .dialog{ 353 | transition-property: opacity, top; 354 | transition-duration: .3s; 355 | } 356 | 357 | .resizer{ 358 | position: absolute; 359 | width: 4px; 360 | height: 100%; 361 | right: 0; 362 | top: 0; 363 | cursor: col-resize; 364 | } 365 | 366 | .cover{ 367 | position: absolute; 368 | top: 0; 369 | left: -100%; 370 | bottom: 0; 371 | width: 100%; 372 | opacity: 0; 373 | background-color: rgba(0,0,0,.6); 374 | } 375 | .transitional .cover{ 376 | transition: opacity .3s; 377 | } 378 | .needAlert .cover, 379 | .needConfirm .cover, 380 | .needEdit .cover{ 381 | left: 0; 382 | opacity: 1; 383 | } 384 | 385 | .needAlert .cover, .needConfirm .cover, .needEdit .cover { 386 | opacity: 1; 387 | } 388 | 389 | .bookmark-clone{ 390 | position: absolute; 391 | left: -999px; 392 | margin: -1em 0 0 -24px; 393 | white-space: nowrap; 394 | line-height: 1em; 395 | padding: .3em; 396 | padding-right: .5em; 397 | -webkit-mask-image: -webkit-gradient(linear, left top, 320 top, from(rgba(0,0,0,1)), to(rgba(0,0,0,0))); 398 | pointer-events: none; 399 | cursor: default; 400 | 401 | img { 402 | vertical-align: text-top; 403 | margin-right: 4px; 404 | } 405 | 406 | i { 407 | font-style: normal; 408 | } 409 | } 410 | 411 | .drop-overlay{ 412 | display: block; 413 | width: 100%; 414 | position: absolute; 415 | left: -999px; 416 | pointer-events: none; 417 | margin: -1px 0 0; 418 | 419 | &.bookmark{ 420 | margin-right: -3px; 421 | height: 3px; 422 | background-color: #000; 423 | border: 1px solid #fff; 424 | } 425 | 426 | &.folder{ 427 | background-color: rgba(43,93,205,.2); 428 | border: 1px solid #5594d2; 429 | border-radius: 2px; 430 | } 431 | } 432 | 433 | 434 | .needAlert .dialog--alert, 435 | .needConfirm .dialog--confirm, 436 | .needEdit .dialog--edit{ 437 | opacity: 1; 438 | z-index: 5; 439 | } 440 | 441 | // Overrides of material components 442 | 443 | .mdc-button { 444 | @include button.ink-color($theme-primary); 445 | } 446 | 447 | .mdc-button--unelevated { 448 | @include button.filled-accessible($theme-primary); 449 | } 450 | 451 | 452 | // Dark mode 453 | @media (prefers-color-scheme: dark) { 454 | * { 455 | --theme-primary: $theme-primary-dark; 456 | 457 | --bg: #202124; 458 | --body-color: rgba(255, 255, 255, 0.8); 459 | 460 | --menu-bg: #292c2f; 461 | --input-bg: #292c2f; 462 | 463 | --list-focus-bg: rgba(255, 255, 255, 0.03); 464 | 465 | --divider-bg: rgba(255, 255, 255, 0.12); 466 | 467 | --search-icon-fill: #fff; 468 | --folder-icon-url: url(chrome-extension://__MSG_@@extension_id__/images/folder-light.svg); 469 | --folder-open-icon-url: url(chrome-extension://__MSG_@@extension_id__/images/folder-open-light.svg); 470 | } 471 | 472 | .mdc-button { 473 | @include button.ink-color($theme-primary-dark); 474 | } 475 | 476 | .mdc-button--unelevated { 477 | @include button.filled-accessible($theme-primary-dark); 478 | } 479 | } -------------------------------------------------------------------------------- /public/_locales/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext_name": { 3 | "message": "Bookmarkie - zakładki w wyskakującym okienku" 4 | }, 5 | "ext_desc": { 6 | "message": "Przeglądaj i zarządzaj swoimi zakładkami w wyskakującym okienku." 7 | }, 8 | "edit": { 9 | "description": "one of the menu items", 10 | "message": "Edytuj" 11 | }, 12 | "delete": { 13 | "description": "one of the menu items and button text", 14 | "message": "Usuń" 15 | }, 16 | "copy_url": { 17 | "description": "one of the menu items", 18 | "message": "Kopiuj adres linku" 19 | }, 20 | "open_in_tab": { 21 | "description": "one of the menu items", 22 | "message": "Otwórz w nowej karcie" 23 | }, 24 | "open_all": { 25 | "description": "one of the menu items", 26 | "message": "Otwórz wszystkie ($count$)", 27 | "placeholders": { 28 | "count": { 29 | "content": "$1" 30 | } 31 | } 32 | }, 33 | "open_in_window": { 34 | "description": "one of the menu items", 35 | "message": "Otwórz w nowym oknie" 36 | }, 37 | "open_all_in_window": { 38 | "description": "one of the menu items", 39 | "message": "Otwórz wszystkie w nowym oknie" 40 | }, 41 | "open_in_incognito": { 42 | "description": "one of the menu items", 43 | "message": "Otwórz w oknie incognito" 44 | }, 45 | "open_all_in_incognito": { 46 | "description": "one of the menu items", 47 | "message": "Otwórz wszystkie w oknie incognito" 48 | }, 49 | "add_folder": { 50 | "description": "one of the menu items", 51 | "message": "Dodaj folder" 52 | }, 53 | "add_link": { 54 | "description": "one of the menu items", 55 | "message": "Dodaj zakładkę" 56 | }, 57 | "open_manager": { 58 | "description": "one of the menu items", 59 | "message": "Menedżer zakładek" 60 | }, 61 | "rename": { 62 | "description": "dialog title text", 63 | "message": "Zmień nazwę" 64 | }, 65 | "dialog_delete_text": { 66 | "description": "dialog text for delete confirmation", 67 | "message": "Usunąć folder $folderName$ oraz $bookmarkCount$ zakładek?", 68 | "placeholders": { 69 | "folderName": { 70 | "content": "$1" 71 | }, 72 | "bookmarkCount": { 73 | "content": "$2" 74 | }, 75 | "plural": { 76 | "content": "$3" 77 | } 78 | } 79 | }, 80 | "dialog_delete_single_text": { 81 | "description": "dialog text for delete confirmation", 82 | "message": "Usunąć $folderName$?", 83 | "placeholders": { 84 | "folderName": { 85 | "content": "$1" 86 | } 87 | } 88 | }, 89 | "dialog_open_url_text": { 90 | "description": "dialog text for open url confirmation", 91 | "message": "Otworzyć $bookmarkCount$ zakładek z folderu $folderName$?", 92 | "placeholders": { 93 | "bookmarkCount": { 94 | "content": "$1" 95 | }, 96 | "folderName": { 97 | "content": "$2" 98 | } 99 | } 100 | }, 101 | "label_name": { 102 | "description": "form label text", 103 | "message": "Nazwa" 104 | }, 105 | "label_url": { 106 | "description": "form label text", 107 | "message": "URL" 108 | }, 109 | "save": { 110 | "description": "button text", 111 | "message": "Zapisz" 112 | }, 113 | "add": { 114 | "description": "button text", 115 | "message": "Dodaj" 116 | }, 117 | "confirm": { 118 | "description": "button text", 119 | "message": "Potwierdź" 120 | }, 121 | "cancel": { 122 | "description": "button text", 123 | "message": "Anuluj" 124 | }, 125 | "bookmark_added": { 126 | "description": "notification text", 127 | "message": "Dodano" 128 | }, 129 | "bookmark_updated": { 130 | "description": "notification text", 131 | "message": "Zaktualizowano" 132 | }, 133 | "bookmark_deleted": { 134 | "description": "notification text", 135 | "message": "Usunięto" 136 | }, 137 | "link_copied": { 138 | "description": "notification text", 139 | "message": "Skopiowano" 140 | }, 141 | "search": { 142 | "description": "search placeholder text", 143 | "message": "Szukaj" 144 | }, 145 | "no_results": { 146 | "description": "no search results", 147 | "message": "Brak wyników" 148 | }, 149 | "no_results_desc": { 150 | "description": "no search results subtitle", 151 | "message": "Szukaj nazw zakładek lub adresów URL" 152 | }, 153 | "option_section_general": { 154 | "description": "option section title", 155 | "message": "Ogólne" 156 | }, 157 | "option_section_appearance": { 158 | "description": "option section title", 159 | "message": "Wygląd" 160 | }, 161 | "option_section_popup": { 162 | "description": "option section title", 163 | "message": "Wyskakujące okienko" 164 | }, 165 | "option_section_icon": { 166 | "description": "option section title", 167 | "message": "Ikona" 168 | }, 169 | "option_open_in_tab": { 170 | "description": "option text", 171 | "message": "Zawsze otwieraj w nowych kartach" 172 | }, 173 | "option_open_in_background": { 174 | "description": "option text", 175 | "message": "Zawsze otwieraj w tle" 176 | }, 177 | "option_hold_meta": { 178 | "description": "option text", 179 | "message": "Przytrzymaj $key$, aby otworzyć w tle", 180 | "placeholders": { 181 | "key": { 182 | "content": "$1" 183 | } 184 | } 185 | }, 186 | "option_middle_click_to_open_in_background": { 187 | "description": "option text", 188 | "message": "Środkowe kliknięcie otwiera w tle" 189 | }, 190 | "option_collapse_sibling": { 191 | "description": "option text", 192 | "message": "Zwiń foldery rodzeństwa po rozwinięciu" 193 | }, 194 | "option_fix_parent": { 195 | "description": "option text", 196 | "message": "Podczas przewijania pozostaw otwarty folder na górze" 197 | }, 198 | "option_preserve_state": { 199 | "description": "option text", 200 | "message": "Zapamiętaj otwarte foldery podczas zamykania wyskakującego okienka" 201 | }, 202 | "option_clear_search_results_on_close": { 203 | "description": "option text", 204 | "message": "Wyczyść wyniki wyszukiwania podczas zamykania wyskakującego okienka" 205 | }, 206 | "option_enable_drag": { 207 | "description": "option text", 208 | "message": "Zezwalaj na przeciąganie w celu rozmieszczenia zakładek" 209 | }, 210 | "option_show_search": { 211 | "description": "option text", 212 | "message": "Pokaż pasek wyszukiwania" 213 | }, 214 | "option_show_folders_in_search": { 215 | "description": "option text", 216 | "message": "Pokaż foldery w wynikach wyszukiwania" 217 | }, 218 | "option_hide_root_folder": { 219 | "description": "option text", 220 | "message": "Ukryj folder główny" 221 | }, 222 | "option_popup_size": { 223 | "description": "option text", 224 | "message": "Rozmiar" 225 | }, 226 | "option_node_density": { 227 | "description": "option text", 228 | "message": "Zagęszczenie listy" 229 | }, 230 | "option_folder_type": { 231 | "description": "option text", 232 | "message": "Styl folderów" 233 | }, 234 | "option_item_default": { 235 | "description": "option text", 236 | "message": "Domyślny" 237 | }, 238 | "option_item_small": { 239 | "description": "option text", 240 | "message": "Mały" 241 | }, 242 | "option_item_large": { 243 | "description": "option text", 244 | "message": "Duży" 245 | }, 246 | "option_item_compact": { 247 | "description": "option text", 248 | "message": "Kompaktowy" 249 | }, 250 | "option_item_comfortable": { 251 | "description": "option text", 252 | "message": "Wygodny" 253 | }, 254 | "option_color_scheme": { 255 | "description": "option text", 256 | "message": "Schemat kolorów" 257 | }, 258 | "option_theme_color": { 259 | "description": "option text", 260 | "message": "Motyw" 261 | }, 262 | "option_icon_type": { 263 | "description": "option text", 264 | "message": "Styl" 265 | }, 266 | "option_icon_color": { 267 | "description": "option text", 268 | "message": "Kolor" 269 | }, 270 | "color_auto": { 271 | "description": "color name in options", 272 | "message": "Automatyczny" 273 | }, 274 | "color_light": { 275 | "description": "color name in options", 276 | "message": "Jasny" 277 | }, 278 | "color_dark": { 279 | "description": "color name in options", 280 | "message": "Ciemny" 281 | }, 282 | "color_trinidad": { 283 | "description": "color name in options", 284 | "message": "Pomarańczowy" 285 | }, 286 | "color_yellow": { 287 | "description": "color name in options", 288 | "message": "Żółty" 289 | }, 290 | "color_pastel": { 291 | "description": "color name in options", 292 | "message": "Pastelowy" 293 | }, 294 | "color_naval": { 295 | "description": "color name in options", 296 | "message": "Niebieski" 297 | }, 298 | "color_orchid": { 299 | "description": "color name in options", 300 | "message": "Fioletowy" 301 | }, 302 | "color_blue": { 303 | "description": "color name in options", 304 | "message": "Niebieski" 305 | }, 306 | "color_warm_grey": { 307 | "description": "color name in options", 308 | "message": "Ciepły szary" 309 | }, 310 | "color_cool_grey": { 311 | "description": "color name in options", 312 | "message": "Chłodny szary" 313 | }, 314 | "color_beige": { 315 | "description": "color name in options", 316 | "message": "Beżowy" 317 | }, 318 | "context_menu_bookmark_manager": { 319 | "description": "context menu item", 320 | "message": "Menedżer zakładek" 321 | }, 322 | "context_menu_open_in_tab": { 323 | "description": "context menu item", 324 | "message": "Otwórz w karcie" 325 | }, 326 | "context_menu_open_in_side_panel": { 327 | "description": "context menu item", 328 | "message": "Otwórz w panelu bocznym" 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import './popup.scss'; 4 | 5 | import {MDCRipple} from '@material/ripple'; 6 | 7 | document.querySelectorAll('.mdc-button').forEach(el => { 8 | new MDCRipple(el); 9 | }); 10 | 11 | // OS in dark mode or browser in incognito context 12 | const isBrowserDark = window.matchMedia("(prefers-color-scheme: dark)").matches || chrome.extension.inIncognitoContext; 13 | // Let background knows if the browser is dark 14 | chrome.runtime.sendMessage({ isBrowserDark: isBrowserDark }); 15 | 16 | function $extend(original, extended){ 17 | for (var key in (extended || {})) original[key] = extended[key]; 18 | return original; 19 | } 20 | 21 | function $each(obj, fn, bind){ 22 | for (var key in obj){ 23 | if (obj.hasOwnProperty(key)) fn.call(bind, obj[key], key, obj); 24 | } 25 | } 26 | 27 | var $slice = Array.prototype.slice; 28 | 29 | $extend(String.prototype, { 30 | widont: function(){ 31 | return this.replace(/\s([^\s]+)$/i, ' $1'); 32 | }, 33 | toInt: function(base){ 34 | return parseInt(this, base || 10); 35 | }, 36 | hyphenate: function(){ 37 | return this.replace(/[A-Z]/g, function(match){ 38 | return ('-' + match.charAt(0).toLowerCase()); 39 | }); 40 | }, 41 | htmlspecialchars: function(){ 42 | return this.replace(/>/g, '>').replace(/ { 155 | openNewTabs = props.openNewTabs; 156 | openTabsInBg = props.openTabsInBg; 157 | closeOtherFolders = props.closeOtherFolders; 158 | popupFixed = props.popupFixed; // not useful 159 | preserveState = props.preserveState; 160 | panelHeight = props.panelHeight; 161 | 162 | console.log(props); 163 | init(); 164 | }); 165 | 166 | function init() { 167 | if (panelHeight != null) { 168 | document.body.style.height = panelHeight; 169 | } 170 | ready(window); 171 | } 172 | 173 | function ready (window) { 174 | var body = document.body; 175 | var _m = chrome.i18n.getMessage; 176 | 177 | // Error alert 178 | var AlertDialog = { 179 | open: function(dialog){ 180 | if (!dialog) return; 181 | document.querySelector('#alert-dialog-text').innerHTML = dialog; 182 | body.addClass('needAlert'); 183 | }, 184 | close: function(){ 185 | body.removeClass('needAlert'); 186 | } 187 | }; 188 | // popdown toast when an error occurs 189 | window.addEventListener('error', function(){ 190 | AlertDialog.open(_m('errorOccured')); 191 | }, false); 192 | 193 | // Platform detection 194 | var os = (navigator.platform.toLowerCase().match(/mac|win|linux/i) || ['other'])[0]; 195 | body.addClass(os); 196 | 197 | // Some i18n 198 | document.querySelector('#search-input').placeholder = _m('searchBookmarks'); 199 | document.querySelector('#edit-dialog-name').placeholder = _m('name'); 200 | document.querySelector('#edit-dialog-url').placeholder = _m('url'); 201 | $each({ 202 | 'bookmark-new-tab': 'openNewTab', 203 | 'bookmark-new-window': 'openNewWindow', 204 | 'bookmark-new-incognito-window': 'openIncognitoWindow', 205 | 'bookmark-edit': 'edit', 206 | 'bookmark-delete': 'delete', 207 | 'folder-window': 'openBookmarks', 208 | 'folder-new-window': 'openBookmarksNewWindow', 209 | 'folder-new-incognito-window': 'openBookmarksIncognitoWindow', 210 | 'folder-edit': 'edit', 211 | 'folder-delete': 'deleteEllipsis', 212 | 'edit-dialog-button': 'save', 213 | 'edit-dialog-cancel': 'cancel' 214 | }, function(msg, id){ 215 | var el = document.querySelector('#' + id); 216 | var m = _m(msg); 217 | // if (el.tagName == 'COMMAND') el.label = m; 218 | el.textContent = m; 219 | }); 220 | 221 | // RTL indicator 222 | var rtl = (body.getComputedStyle('direction') == 'rtl'); 223 | if (rtl) body.addClass('rtl'); 224 | 225 | // Init some variables 226 | var opens = localStorage.opens ? JSON.parse(localStorage.opens) : []; 227 | // var rememberState = !localStorage.dontRememberState; 228 | var a = document.createElement('a'); 229 | var httpsPattern = /^https?:\/\//i; 230 | 231 | // Adaptive bookmark tooltips 232 | var adaptBookmarkTooltips = function(){ 233 | var bookmarks = document.querySelectorAll('li.child a'); 234 | for (var i = 0, l = bookmarks.length; i < l; i++){ 235 | var bookmark = bookmarks[i]; 236 | if (bookmark.hasClass('titled')){ 237 | if (bookmark.scrollWidth <= bookmark.offsetWidth){ 238 | bookmark.title = bookmark.href; 239 | bookmark.removeClass('titled'); 240 | } 241 | } else if (bookmark.scrollWidth > bookmark.offsetWidth){ 242 | var text = bookmark.querySelector('i').textContent; 243 | var title = bookmark.title; 244 | if (text != title){ 245 | bookmark.title = text + '\n' + title; 246 | bookmark.addClass('titled'); 247 | } 248 | } 249 | } 250 | }; 251 | 252 | var generateBookmarkHTML = function(title, url, extras){ 253 | if (!extras) extras = ''; 254 | var u = url.htmlspecialchars(); 255 | var favicon = 'chrome://favicon/size/16@2x/' + u; 256 | 257 | var tooltipURL = url; 258 | if (/^javascript:/i.test(url)){ 259 | if (url.length > 140) tooltipURL = url.slice(0, 140) + '...'; 260 | favicon = 'images/document-code.svg'; 261 | } 262 | tooltipURL = tooltipURL.htmlspecialchars(); 263 | var name = title.htmlspecialchars() || (httpsPattern.test(url) ? url.replace(httpsPattern, '') : _m('noTitle')); 264 | return '' 265 | + '' + name + '' + ''; 266 | }; 267 | 268 | var generateHTML = function(data, level){ 269 | if (!level) level = 0; 270 | var paddingInlineStart = 14 * level + 8; 271 | var group = (level == 0) ? 'tree' : 'group'; 272 | var html = ''; 323 | return html; 324 | }; 325 | 326 | const $tree = document.querySelector('#tree'); 327 | chrome.bookmarks.getTree(function(tree){ 328 | var html = generateHTML(tree[0].children); 329 | $tree.innerHTML = html; 330 | 331 | // recall scroll position (from top of popup) when tree opened 332 | if (preserveState) $tree.scrollTop = localStorage.scrollTop || 0; 333 | 334 | var focusID = localStorage.focusID; 335 | if (typeof focusID != 'undefined' && focusID != null){ 336 | var focusEl = document.querySelector('#neat-tree-item-' + focusID); 337 | if (focusEl){ 338 | var oriOverflow = $tree.style.overflow; 339 | $tree.style.overflow = 'hidden'; 340 | focusEl.style.width = '100%'; 341 | focusEl.firstElementChild.addClass('focus'); 342 | setTimeout(function(){ 343 | $tree.style.overflow = oriOverflow; 344 | }, 1); 345 | } 346 | } 347 | 348 | setTimeout(adaptBookmarkTooltips, 100); 349 | 350 | tree = null; 351 | }); 352 | 353 | // Events for the tree 354 | $tree.addEventListener('scroll', function(){ 355 | localStorage.scrollTop = $tree.scrollTop; // store scroll position at each scroll event 356 | }); 357 | $tree.addEventListener('focus', function(e){ 358 | var el = e.target; 359 | var tagName = el.tagName; 360 | var focusEl = $tree.querySelector('.focus'); 361 | if (focusEl) focusEl.removeClass('focus'); 362 | if (tagName == 'A' || tagName == 'SPAN'){ 363 | var id = el.parentNode.id.replace('neat-tree-item-', ''); 364 | localStorage.focusID = id; 365 | } else { 366 | localStorage.focusID = null; 367 | } 368 | }, true); 369 | 370 | function isElementInViewport (el) { 371 | const rect = el.getBoundingClientRect(); 372 | return ( 373 | rect.top >= 30 && 374 | rect.bottom <= window.innerHeight - 30 375 | ); 376 | } 377 | 378 | var closeUnusedFolders = closeOtherFolders; 379 | $tree.addEventListener('click', function(e){ 380 | if (e.button != 0) return; 381 | var el = e.target; 382 | var tagName = el.tagName; 383 | if (tagName != 'SPAN') return; 384 | if (e.shiftKey || e.ctrlKey) return; 385 | var parent = el.parentNode; 386 | parent.toggleClass('open'); 387 | var expanded = parent.hasClass('open'); 388 | parent.setAttribute('aria-expanded', expanded); 389 | var children = parent.querySelector('ul'); 390 | if (!children){ 391 | var id = parent.id.replace('neat-tree-item-', ''); 392 | chrome.bookmarks.getChildren(id, function(children){ 393 | var html = generateHTML(children, parseInt(parent.parentNode.dataset.level) + 1); 394 | var div = document.createElement('div'); 395 | div.innerHTML = html; 396 | var ul = div.querySelector('ul'); 397 | ul.inject(parent); 398 | div.destroy(); 399 | setTimeout(adaptBookmarkTooltips, 100); 400 | }); 401 | } 402 | if (closeUnusedFolders && expanded){ 403 | var siblings = parent.getSiblings('li'); 404 | for (var i = 0, l = siblings.length; i < l; i++){ 405 | var li = siblings[i]; 406 | if (li.hasClass('parent')){ 407 | li.removeClass('open').setAttribute('aria-expanded', false); 408 | } 409 | } 410 | 411 | const focusEl = document.querySelector(`#neat-tree-item-${localStorage.focusID}`); 412 | const focusFolderList = focusEl.querySelector('.list--folder'); 413 | const isInView = isElementInViewport(focusFolderList); 414 | 415 | // if the expanded folder is not in view, reset its position 416 | if (!isInView) { 417 | $tree.scrollTop = ($tree.scrollTop + focusFolderList.getBoundingClientRect().top - 60); // 60 is the height of search bar plus height of the list element 418 | } 419 | } 420 | var opens = $tree.querySelectorAll('li.open'); 421 | opens = Array.map(function(li){ 422 | return li.id.replace('neat-tree-item-', ''); 423 | }, opens); 424 | 425 | // chrome.storage.sync.set({ 426 | // opens: JSON.stringify(opens) 427 | // }); 428 | localStorage.opens = JSON.stringify(opens); 429 | }); 430 | // Force middle clicks to trigger the focus event 431 | $tree.addEventListener('mouseup', function(e){ 432 | if (e.button != 1) return; 433 | var el = e.target; 434 | var tagName = el.tagName; 435 | if (tagName != 'A' && tagName != 'SPAN') return; 436 | el.focus(); 437 | }); 438 | 439 | // Search 440 | var $results = document.querySelector('#results'); 441 | var searchMode = false; 442 | var searchInput = document.querySelector('#search-input'); 443 | var prevValue = ''; 444 | 445 | var search = function(){ 446 | var value = searchInput.value.trim(); 447 | localStorage.searchQuery = value; 448 | if (value == ''){ 449 | prevValue = ''; 450 | searchMode = false; 451 | $tree.style.display = 'block'; 452 | $results.style.display = 'none'; 453 | return; 454 | } 455 | if (value == prevValue) return; 456 | prevValue = value; 457 | searchMode = true; 458 | 459 | chrome.bookmarks.search(value, function(results){ 460 | var v = value.toLowerCase(); 461 | var vPattern = new RegExp('^' + value.escapeRegExp().replace(/\s+/g, '.*'), 'ig'); 462 | 463 | if (results.length > 1){ 464 | results.sort(function(a, b){ 465 | var aTitle = a.title; 466 | var bTitle = b.title; 467 | var aIndexTitle = aTitle.toLowerCase().indexOf(v); 468 | var bIndexTitle = bTitle.toLowerCase().indexOf(v); 469 | if (aIndexTitle >= 0 || bIndexTitle >= 0){ 470 | if (aIndexTitle < 0) aIndexTitle = Infinity; 471 | if (bIndexTitle < 0) bIndexTitle = Infinity; 472 | return aIndexTitle - bIndexTitle; 473 | } 474 | var aTestTitle = vPattern.test(aTitle); 475 | var bTestTitle = vPattern.test(bTitle); 476 | if (aTestTitle && !bTestTitle) return -1; 477 | if (!aTestTitle && bTestTitle) return 1; 478 | return b.dateAdded - a.dateAdded; 479 | }); 480 | results = results.slice(0, 100); // 100 is enough 481 | } 482 | 483 | var html = ''; 495 | $tree.style.display = 'none'; 496 | $results.innerHTML = html; 497 | $results.style.display = 'block'; 498 | 499 | var lis = $results.querySelectorAll('li'); 500 | Array.forEach(function(li){ 501 | var parentId = li.dataset.parentid; 502 | chrome.bookmarks.get(parentId, function(node){ 503 | if (!node || !node.length) return; 504 | var a = li.querySelector('a'); 505 | a.title = _m('parentFolder', node[0].title) + '\n' + a.title; 506 | }); 507 | }, lis); 508 | 509 | results = null; 510 | vPattern = null; 511 | lis = null; 512 | }); 513 | 514 | }; 515 | searchInput.addEventListener('input', search); 516 | 517 | searchInput.addEventListener('keydown', function(e){ 518 | var key = e.keyCode; 519 | var focusID = localStorage.focusID; 520 | if (key == 40 && searchInput.value.length == searchInput.selectionEnd){ // down 521 | e.preventDefault(); 522 | var item; 523 | if (searchMode) { 524 | item = $results.querySelector('ul>li:first-child a'); 525 | } else { 526 | item = $tree.querySelector('ul>li:first-child').querySelector('span, a'); 527 | } 528 | if (item !== null) { 529 | item.focus(); 530 | } 531 | } else if (key == 13 && searchInput.value.length){ // enter 532 | var item = $results.querySelector('ul>li:first-child a'); 533 | if (item !== null) { 534 | item.focus(); 535 | setTimeout(function(){ 536 | var event = document.createEvent('MouseEvents'); 537 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 538 | item.dispatchEvent(event); 539 | }, 30); 540 | } 541 | } else if (key == 9 && !searchMode){ // tab 542 | if (typeof focusID != 'undefined' && focusID != null){ 543 | var focusEl = document.querySelector('#neat-tree-item-' + focusID); 544 | if (focusEl){ 545 | e.preventDefault(); 546 | focusEl.firstElementChild.focus(); 547 | } 548 | } else { 549 | var bound = $tree.scrollTop; 550 | var items = $tree.querySelectorAll('a, span'); 551 | var firstItem = Array.filter(function(item){ 552 | return !!item.parentElement.offsetHeight && ((item.offsetTop + item.offsetHeight) > bound); 553 | }, items)[0]; 554 | if (firstItem) firstItem.focus(); 555 | } 556 | // Pressing esc shouldn't close the popup when search field has value 557 | } else if (e.keyCode == 27 && searchInput.value){ // esc 558 | e.preventDefault(); 559 | searchInput.value = ''; 560 | search(); 561 | } 562 | }); 563 | 564 | searchInput.addEventListener('focus', function(){ 565 | body.addClass('searchFocus'); 566 | }); 567 | searchInput.addEventListener('blur', function(){ 568 | body.removeClass('searchFocus'); 569 | }); 570 | 571 | // Saved search query 572 | if (preserveState && localStorage.searchQuery){ 573 | searchInput.value = localStorage.searchQuery; 574 | search(); 575 | searchInput.select(); 576 | searchInput.scrollLeft = 0; 577 | } 578 | 579 | // Popup auto-height 580 | var resetHeight = function(){ 581 | // var zoomLevel = localStorage.zoom ? localStorage.zoom.toInt() / 100 : 1; 582 | let zoomLevel = 1; 583 | setTimeout(function(){ 584 | var neatTree = $tree.firstElementChild; 585 | if (neatTree){ 586 | var fullHeight = (neatTree.offsetHeight + $tree.offsetTop + 16) * zoomLevel; 587 | // Slide up faster than down 588 | body.style.transitionDuration = (fullHeight < window.innerHeight) ? '.3s' : '.1s'; 589 | var maxHeight = screen.height - window.screenY - 50; 590 | var height = Math.max(200, Math.min(fullHeight, maxHeight)); 591 | body.style.height = height + 'px'; 592 | localStorage.popupHeight = height; 593 | } 594 | }, 100); 595 | }; 596 | if (!searchMode) resetHeight(); 597 | $tree.addEventListener('click', resetHeight); 598 | $tree.addEventListener('keyup', resetHeight); 599 | 600 | // Confirm dialog event listeners 601 | document.querySelector('#confirm-dialog-button-1').addEventListener('click', function(){ 602 | ConfirmDialog.fn1(); 603 | ConfirmDialog.close(); 604 | }, false); 605 | 606 | document.querySelector('#confirm-dialog-button-2').addEventListener('click', function(){ 607 | ConfirmDialog.fn2(); 608 | ConfirmDialog.close(); 609 | }, false); 610 | 611 | document.querySelector('#edit-dialog-cancel').addEventListener('click', function(){ 612 | EditDialog.justClose(); 613 | return false; 614 | }, false); 615 | 616 | // Confirm dialog 617 | var ConfirmDialog = { 618 | open: function(opts){ 619 | if (!opts) return; 620 | document.querySelector('#confirm-dialog-text').innerHTML = opts.dialog.widont(); 621 | document.querySelector('#confirm-dialog-button-1').innerHTML = opts.button1; 622 | document.querySelector('#confirm-dialog-button-2').innerHTML = opts.button2; 623 | if (opts.fn1) ConfirmDialog.fn1 = opts.fn1; 624 | if (opts.fn2) ConfirmDialog.fn2 = opts.fn2; 625 | document.querySelector('#confirm-dialog-button-' + (opts.focusButton || 1)).focus(); 626 | document.body.addClass('needConfirm'); 627 | }, 628 | close: function(){ 629 | document.body.removeClass('needConfirm'); 630 | }, 631 | fn1: function(){}, 632 | fn2: function(){} 633 | }; 634 | 635 | // Edit dialog event listener 636 | document.querySelector('#edit-dialog').addEventListener('submit', function(){ 637 | EditDialog.close(); 638 | return false; 639 | }, false); 640 | 641 | // Edit dialog 642 | var EditDialog = window.EditDialog = { 643 | open: function(opts){ 644 | if (!opts) return; 645 | document.querySelector('#edit-dialog-text').innerHTML = opts.dialog.widont(); 646 | if (opts.fn) EditDialog.fn = opts.fn; 647 | var type = opts.type || 'bookmark'; 648 | var name = document.querySelector('#edit-dialog-name'); 649 | name.value = opts.name; 650 | name.focus(); 651 | name.select(); 652 | name.scrollLeft = 0; // very delicate, show first few words instead of last 653 | var url = document.querySelector('#edit-dialog-url'); 654 | if (type == 'bookmark'){ 655 | url.style.display = ''; 656 | url.disabled = false; 657 | url.value = opts.url; 658 | } else { 659 | url.style.display = 'none'; 660 | url.disabled = true; 661 | url.value = ''; 662 | } 663 | body.addClass('needEdit'); 664 | }, 665 | close: function(){ 666 | var urlInput = document.querySelector('#edit-dialog-url'); 667 | var url = urlInput.value; 668 | if (!urlInput.validity.valid){ 669 | urlInput.value = 'http://' + url; 670 | if (!urlInput.validity.valid) url = ''; // if still invalid, forget it. 671 | url = 'http://' + url; 672 | } 673 | EditDialog.fn(document.querySelector('#edit-dialog-name').value, url); 674 | body.removeClass('needEdit'); 675 | }, 676 | justClose: function() { 677 | body.removeClass('needEdit'); 678 | }, 679 | fn: function(){} 680 | }; 681 | 682 | // Bookmark handling 683 | var dontConfirmOpenFolder = false; 684 | var bookmarkClickStayOpen = popupFixed; 685 | var openBookmarksLimit = 10; 686 | var actions = { 687 | openBookmark: url => { 688 | let decodedURL; 689 | try { 690 | decodedURL = decodeURIComponent(url); 691 | } catch (e) { 692 | return; 693 | } 694 | chrome.tabs.update({ 695 | url: decodedURL 696 | }); 697 | 698 | if (!bookmarkClickStayOpen) setTimeout(window.close, 200); 699 | }, 700 | 701 | openBookmarkNewTab: (url, selected, blankTabCheck) => { 702 | 703 | const open = () => { 704 | chrome.tabs.create({ 705 | url: url, 706 | active: selected 707 | }); 708 | }; 709 | if (blankTabCheck){ 710 | chrome.tabs.query({ currentWindow: true, active: true }, tabs => { 711 | const tab = tabs[0]; 712 | 713 | if (/^chrome:\/\/newtab/i.test(tab.url)){ 714 | chrome.tabs.update(tab.id, { 715 | url: url 716 | }); 717 | if (!bookmarkClickStayOpen) setTimeout(window.close, 200); 718 | } else { 719 | open(); 720 | } 721 | }); 722 | } else { 723 | open(); 724 | } 725 | }, 726 | 727 | openBookmarkNewWindow: (url, incognito) => { 728 | chrome.windows.create({ 729 | url: url, 730 | incognito: incognito 731 | }); 732 | }, 733 | 734 | openBookmarks: (urls, selected) => { 735 | const urlsLen = urls.length; 736 | const open = function() { 737 | chrome.tabs.create({ 738 | url: urls.shift(), 739 | selected: selected // first tab will be selected 740 | }); 741 | for (let i = 0, l = urls.length; i < l; i++){ 742 | chrome.tabs.create({ 743 | url: urls[i], 744 | selected: false 745 | }); 746 | } 747 | }; 748 | if (!dontConfirmOpenFolder && urlsLen > openBookmarksLimit){ 749 | ConfirmDialog.open({ 750 | dialog: _m('confirmOpenBookmarks', ''+ urlsLen), 751 | button1: '' + _m('open') + '', 752 | button2: _m('nope'), 753 | fn1: open 754 | }); 755 | } else { 756 | open(); 757 | } 758 | }, 759 | 760 | openBookmarksNewWindow: function(urls, incognito){ 761 | var urlsLen = urls.length; 762 | var open = function(){ 763 | chrome.windows.create({ 764 | url: urls, 765 | incognito: incognito 766 | }); 767 | }; 768 | if (!dontConfirmOpenFolder && urlsLen > openBookmarksLimit){ 769 | var dialog = incognito ? _m('confirmOpenBookmarksNewIncognitoWindow', ''+urlsLen) : _m('confirmOpenBookmarksNewWindow', ''+urlsLen); 770 | ConfirmDialog.open({ 771 | dialog: dialog, 772 | button1: '' + _m('open') + '', 773 | button2: _m('nope'), 774 | fn1: open 775 | }); 776 | } else { 777 | open(); 778 | } 779 | }, 780 | 781 | editBookmarkFolder: function(id){ 782 | chrome.bookmarks.get(id, function(nodeList){ 783 | if (!nodeList.length) return; 784 | var node = nodeList[0]; 785 | var url = node.url; 786 | var isBookmark = !!url; 787 | var type = isBookmark ? 'bookmark' : 'folder'; 788 | var dialog = isBookmark ? _m('editBookmark') : _m('editFolder'); 789 | EditDialog.open({ 790 | dialog: dialog, 791 | type: type, 792 | name: node.title, 793 | url: decodeURIComponent(url), 794 | fn: function(name, url){ 795 | chrome.bookmarks.update(id, { 796 | title: name, 797 | url: isBookmark ? url : '' 798 | }, function(n){ 799 | var title = n.title; 800 | var url = n.url; 801 | var li = document.querySelector('#neat-tree-item-' + id); 802 | if (li){ 803 | if (isBookmark){ 804 | var css = li.querySelector('a').style.cssText; 805 | li.innerHTML = generateBookmarkHTML(title, url, 'style="' + css + '"'); 806 | } else { 807 | var i = li.querySelector('i'); 808 | var name = title || (httpsPattern.test(url) ? url.replace(httpsPattern, '') : _m('noTitle')); 809 | i.textContent = name; 810 | } 811 | } 812 | if (searchMode){ 813 | li = document.querySelector('#results-item-' + id); 814 | li.innerHTML = generateBookmarkHTML(title, url); 815 | } 816 | li.firstElementChild.focus(); 817 | }); 818 | } 819 | }); 820 | }); 821 | }, 822 | 823 | deleteBookmark: function(id){ 824 | var li1 = document.querySelector('#neat-tree-item-' + id); 825 | var li2 = document.querySelector('#results-item-' + id); 826 | chrome.bookmarks.remove(id, function(){ 827 | if (li1){ 828 | var nearLi1 = li1.nextElementSibling || li1.previousElementSibling; 829 | li1.destroy(); 830 | if (!searchMode && nearLi1) nearLi1.querySelector('a, span').focus(); 831 | } 832 | if (li2){ 833 | var nearLi2 = li2.nextElementSibling || li2.previousElementSibling; 834 | li2.destroy(); 835 | if (searchMode && nearLi2) nearLi2.querySelector('a, span').focus(); 836 | } 837 | }); 838 | }, 839 | 840 | deleteBookmarks: function(id, bookmarkCount, folderCount){ 841 | var li = document.querySelector('#neat-tree-item-' + id); 842 | var item = li.querySelector('span'); 843 | if (bookmarkCount || folderCount){ 844 | var dialog = ''; 845 | var folderName = '' + item.textContent.trim() + ''; 846 | if (bookmarkCount && folderCount){ 847 | dialog = _m('confirmDeleteFolderSubfoldersBookmarks', [folderName, folderCount, bookmarkCount]); 848 | } else if (bookmarkCount){ 849 | dialog = _m('confirmDeleteFolderBookmarks', [folderName, bookmarkCount]); 850 | } else { 851 | dialog = _m('confirmDeleteFolderSubfolders', [folderName, folderCount]); 852 | } 853 | ConfirmDialog.open({ 854 | dialog: dialog, 855 | button1: '' + _m('delete') + '', 856 | button2: _m('nope'), 857 | fn1: function(){ 858 | chrome.bookmarks.removeTree(id, function(){ 859 | li.destroy(); 860 | }); 861 | var nearLi = li.nextElementSibling || li.previousElementSibling; 862 | if (nearLi) nearLi.querySelector('a, span').focus(); 863 | }, 864 | fn2: function(){ 865 | li.querySelector('a, span').focus(); 866 | } 867 | }); 868 | } else { 869 | chrome.bookmarks.removeTree(id, function(){ 870 | li.destroy(); 871 | }); 872 | var nearLi = li.nextElementSibling || li.previousElementSibling; 873 | if (nearLi) nearLi.querySelector('a, span').focus(); 874 | } 875 | } 876 | }; 877 | 878 | let noOpenBookmark = false; 879 | const bookmarkHandler = e => { 880 | e.preventDefault(); 881 | if (e.button != 0) return; // force left-click 882 | if (noOpenBookmark){ // flag that disables opening bookmark 883 | noOpenBookmark = false; 884 | return; 885 | } 886 | var el = e.target; 887 | var ctrlMeta = (e.ctrlKey || e.metaKey); 888 | var shift = e.shiftKey; 889 | 890 | if (el.tagName == 'A'){ 891 | // clicks bookmark 892 | 893 | var url = el.href; 894 | if (ctrlMeta){ // ctrl/meta click 895 | actions.openBookmarkNewTab(url, openTabsInBg ? shift : !shift); 896 | } else { // click 897 | if (shift){ 898 | actions.openBookmarkNewWindow(url); 899 | } else { 900 | openNewTabs ? actions.openBookmarkNewTab(url, true, true) : actions.openBookmark(url); 901 | } 902 | } 903 | } else if (el.tagName == 'SPAN'){ 904 | // clicks folder 905 | 906 | var li = el.parentNode; 907 | var id = li.id.replace('neat-tree-item-', ''); 908 | chrome.bookmarks.getChildren(id, function(children){ 909 | var urls = Array.map(function(c){ 910 | return c.url; 911 | }, children).clean(); 912 | var urlsLen = urls.length; 913 | if (!urlsLen) return; 914 | if (ctrlMeta){ // ctrl/meta click 915 | actions.openBookmarks(urls, openTabsInBg ? shift : !shift); 916 | } else if (shift){ // shift click 917 | actions.openBookmarksNewWindow(urls); 918 | } 919 | }); 920 | } 921 | }; 922 | $tree.addEventListener('click', bookmarkHandler); 923 | $results.addEventListener('click', bookmarkHandler); 924 | var bookmarkHandlerMiddle = function(e){ 925 | e.preventDefault(); 926 | if (e.button != 1) return; // force middle-click 927 | var event = document.createEvent('MouseEvents'); 928 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, true, false, e.shiftKey, true, 0, null); 929 | e.target.dispatchEvent(event); 930 | }; 931 | $tree.addEventListener('auxclick', bookmarkHandlerMiddle); 932 | $results.addEventListener('auxclick', bookmarkHandlerMiddle); 933 | 934 | // Disable Chrome auto-scroll feature 935 | window.addEventListener('mousedown', function(e){ 936 | if (e.button == 1) e.preventDefault(); 937 | }); 938 | 939 | // Context menu 940 | var $bookmarkContextMenu = document.querySelector('#bookmark-context-menu'); 941 | var $folderContextMenu = document.querySelector('#folder-context-menu'); 942 | 943 | var clearMenu = function(e){ 944 | currentContext = null; 945 | var active = body.querySelector('.active'); 946 | if (active){ 947 | active.removeClass('active'); 948 | // This is kinda hacky. Oh well. 949 | if (e){ 950 | var el = e.target; 951 | if (el == $tree || el == $results) active.focus(); 952 | } 953 | } 954 | $bookmarkContextMenu.style.left = '-999px'; 955 | $bookmarkContextMenu.style.opacity = 0; 956 | $folderContextMenu.style.left = '-999px'; 957 | $folderContextMenu.style.opacity = 0; 958 | }; 959 | 960 | body.addEventListener('click', clearMenu); 961 | $tree.addEventListener('scroll', clearMenu); 962 | $results.addEventListener('scroll', clearMenu); 963 | $tree.addEventListener('focus', clearMenu, true); 964 | $results.addEventListener('focus', clearMenu, true); 965 | 966 | var currentContext = null; 967 | var macCloseContextMenu = false; 968 | body.addEventListener('contextmenu', function(e){ 969 | e.preventDefault(); 970 | clearMenu(); 971 | if (os == 'mac'){ 972 | macCloseContextMenu = false; 973 | setTimeout(function(){ macCloseContextMenu = true; }, 500); 974 | } 975 | var el = e.target; 976 | if (el.tagName == 'A'){ 977 | currentContext = el; 978 | var active = body.querySelector('.active'); 979 | if (active) active.removeClass('active'); 980 | el.addClass('active'); 981 | var bookmarkMenuWidth = $bookmarkContextMenu.offsetWidth; 982 | var bookmarkMenuHeight = $bookmarkContextMenu.offsetHeight; 983 | var pageX = rtl ? Math.max(0, e.pageX - bookmarkMenuWidth) : Math.min(e.pageX, body.offsetWidth - bookmarkMenuWidth); 984 | var pageY = e.pageY; 985 | var boundY = window.innerHeight - bookmarkMenuHeight; 986 | if (pageY > boundY) pageY -= bookmarkMenuHeight; 987 | if (pageY < 0) pageY = boundY; 988 | pageY = Math.max(0, pageY); 989 | $bookmarkContextMenu.style.left = pageX + 'px'; 990 | $bookmarkContextMenu.style.top = pageY + 'px'; 991 | $bookmarkContextMenu.style.opacity = 1; 992 | $bookmarkContextMenu.focus(); 993 | } else if (el.tagName == 'SPAN'){ 994 | currentContext = el; 995 | var active = body.querySelector('.active'); 996 | if (active) active.removeClass('active'); 997 | el.addClass('active'); 998 | if (el.parentNode.dataset.parentid == '0'){ 999 | $folderContextMenu.addClass('hide-editables'); 1000 | } else { 1001 | $folderContextMenu.removeClass('hide-editables'); 1002 | } 1003 | var folderMenuWidth = $folderContextMenu.offsetWidth; 1004 | var folderMenuHeight = $folderContextMenu.offsetHeight; 1005 | var pageX = rtl ? Math.max(0, e.pageX - folderMenuWidth) : Math.min(e.pageX, body.offsetWidth - folderMenuWidth); 1006 | var pageY = e.pageY; 1007 | var boundY = window.innerHeight - folderMenuHeight; 1008 | if (pageY > boundY) pageY -= folderMenuHeight; 1009 | if (pageY < 0) pageY = boundY; 1010 | $folderContextMenu.style.left = pageX + 'px'; 1011 | $folderContextMenu.style.top = pageY + 'px'; 1012 | $folderContextMenu.style.opacity = 1; 1013 | $folderContextMenu.focus(); 1014 | } 1015 | }); 1016 | // on Mac, holding down right-click for a period of time closes the context menu 1017 | // Not a complete implementation, but it works :) 1018 | if (os == 'mac') body.addEventListener('mouseup', function(e){ 1019 | if (e.button == 2 && macCloseContextMenu){ 1020 | macCloseContextMenu = false; 1021 | clearMenu(); 1022 | } 1023 | }); 1024 | 1025 | var bookmarkContextHandler = function(e){ 1026 | e.stopPropagation(); 1027 | if (!currentContext) return; 1028 | var el = e.target; 1029 | // if (el.tagName != 'COMMAND') return; 1030 | var url = currentContext.href; 1031 | switch (el.id){ 1032 | case 'bookmark-new-tab': 1033 | actions.openBookmarkNewTab(url); 1034 | break; 1035 | case 'bookmark-new-window': 1036 | actions.openBookmarkNewWindow(url); 1037 | break; 1038 | case 'bookmark-new-incognito-window': 1039 | actions.openBookmarkNewWindow(url, true); 1040 | break; 1041 | case 'bookmark-edit': 1042 | var li = currentContext.parentNode; 1043 | var id = li.id.replace(/(neat\-tree|results)\-item\-/, ''); 1044 | actions.editBookmarkFolder(id); 1045 | break; 1046 | case 'bookmark-delete': 1047 | var li = currentContext.parentNode; 1048 | var id = li.id.replace(/(neat\-tree|results)\-item\-/, ''); 1049 | actions.deleteBookmark(id); 1050 | break; 1051 | } 1052 | clearMenu(); 1053 | }; 1054 | // On Mac, all three mouse clicks work; on Windows, middle-click doesn't work 1055 | $bookmarkContextMenu.addEventListener('mouseup', function(e){ 1056 | e.stopPropagation(); 1057 | if (e.button == 0 || (os == 'mac' && e.button == 1)) bookmarkContextHandler(e); 1058 | }); 1059 | $bookmarkContextMenu.addEventListener('contextmenu', bookmarkContextHandler); 1060 | $bookmarkContextMenu.addEventListener('click', function(e){ 1061 | e.stopPropagation(); 1062 | }); 1063 | 1064 | var folderContextHandler = function(e){ 1065 | if (!currentContext) return; 1066 | var el = e.target; 1067 | // if (el.tagName != 'COMMAND') return; 1068 | var li = currentContext.parentNode; 1069 | var id = li.id.replace('neat-tree-item-', ''); 1070 | chrome.bookmarks.getChildren(id, function(children){ 1071 | var urls = Array.map(function(c){ 1072 | return c.url; 1073 | }, children).clean(); 1074 | var urlsLen = urls.length; 1075 | var noURLS = !urlsLen; 1076 | switch (el.id){ 1077 | case 'folder-window': 1078 | if (noURLS) return; 1079 | actions.openBookmarks(urls); 1080 | break; 1081 | case 'folder-new-window': 1082 | if (noURLS) return; 1083 | actions.openBookmarksNewWindow(urls); 1084 | break; 1085 | case 'folder-new-incognito-window': 1086 | if (noURLS) return; 1087 | actions.openBookmarksNewWindow(urls, true); 1088 | break; 1089 | case 'folder-edit': 1090 | actions.editBookmarkFolder(id); 1091 | break; 1092 | case 'folder-delete': 1093 | actions.deleteBookmarks(id, urlsLen, children.length-urlsLen); 1094 | break; 1095 | } 1096 | }); 1097 | clearMenu(); 1098 | }; 1099 | $folderContextMenu.addEventListener('mouseup', function(e){ 1100 | e.stopPropagation(); 1101 | if (e.button == 0 || (os == 'mac' && e.button == 1)) folderContextHandler(e); 1102 | }); 1103 | $folderContextMenu.addEventListener('contextmenu', folderContextHandler); 1104 | $folderContextMenu.addEventListener('click', function(e){ 1105 | e.stopPropagation(); 1106 | }); 1107 | 1108 | // Keyboard navigation 1109 | var keyBuffer = '', keyBufferTimer; 1110 | var treeKeyDown = function(e){ 1111 | var item = document.activeElement; 1112 | if (!/^(a|span)$/i.test(item.tagName)) item = $tree.querySelector('.focus') || $tree.querySelector('li:first-child>span'); 1113 | var li = item.parentNode; 1114 | var keyCode = e.keyCode; 1115 | var metaKey = e.metaKey; 1116 | if (keyCode == 40 && metaKey) keyCode = 35; // cmd + down (Mac) 1117 | if (keyCode == 38 && metaKey) keyCode = 36; // cmd + up (Mac) 1118 | switch (keyCode){ 1119 | case 40: // down 1120 | e.preventDefault(); 1121 | var liChild = li.querySelector('ul>li:first-child'); 1122 | if (li.hasClass('open') && liChild){ 1123 | liChild.querySelector('a, span').focus(); 1124 | } else { 1125 | var nextLi = li.nextElementSibling; 1126 | if (nextLi){ 1127 | nextLi.querySelector('a, span').focus(); 1128 | } else { 1129 | do { 1130 | li = li.parentNode.parentNode; 1131 | if (li.tagName === 'LI') nextLi = li.nextElementSibling; 1132 | if (nextLi) nextLi.querySelector('a, span').focus(); 1133 | } while (li.tagName === 'LI' && !nextLi); 1134 | } 1135 | } 1136 | break; 1137 | case 38: // up 1138 | e.preventDefault(); 1139 | var prevLi = li.previousElementSibling; 1140 | if (prevLi){ 1141 | while (prevLi.hasClass('open') && prevLi.querySelector('ul>li:last-child')){ 1142 | var lis = prevLi.querySelectorAll('ul>li:last-child'); 1143 | prevLi = Array.filter(function(li){ 1144 | return !!li.parentNode.offsetHeight; 1145 | }, lis).getLast(); 1146 | }; 1147 | prevLi.querySelector('a, span').focus(); 1148 | } else { 1149 | var parentPrevLi = li.parentNode.parentNode; 1150 | if (parentPrevLi && parentPrevLi.tagName == 'LI'){ 1151 | parentPrevLi.querySelector('a, span').focus(); 1152 | } else { 1153 | searchInput.focus(); 1154 | } 1155 | } 1156 | break; 1157 | case 39: // right (left for RTL) 1158 | e.preventDefault(); 1159 | if (li.hasClass('parent') && ((!rtl && !li.hasClass('open')) || (rtl && li.hasClass('open')))){ 1160 | var event = document.createEvent('MouseEvents'); 1161 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 1162 | li.firstElementChild.dispatchEvent(event); 1163 | } else if (rtl){ 1164 | var parentID = li.dataset.parentid; 1165 | if (parentID == '0') return; 1166 | document.querySelector('#neat-tree-item-' + parentID).querySelector('span').focus(); 1167 | } 1168 | break; 1169 | case 37: // left (right for RTL) 1170 | e.preventDefault(); 1171 | if (li.hasClass('parent') && ((!rtl && li.hasClass('open')) || (rtl && !li.hasClass('open')))){ 1172 | var event = document.createEvent('MouseEvents'); 1173 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 1174 | li.firstElementChild.dispatchEvent(event); 1175 | } else if (!rtl){ 1176 | var parentID = li.dataset.parentid; 1177 | if (parentID == '0') return; 1178 | document.querySelector('#neat-tree-item-' + parentID).querySelector('span').focus(); 1179 | } 1180 | break; 1181 | case 32: // space 1182 | case 13: // enter 1183 | e.preventDefault(); 1184 | var event = document.createEvent('MouseEvents'); 1185 | event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, e.ctrlKey, false, e.shiftKey, e.metaKey, 0, null); 1186 | li.firstElementChild.dispatchEvent(event); 1187 | break; 1188 | case 35: // end 1189 | if (searchMode){ 1190 | this.querySelector('li:last-child a').focus(); 1191 | } else { 1192 | var lis = this.querySelectorAll('ul>li:last-child'); 1193 | var li = Array.filter(function(li){ 1194 | return !!li.parentNode.offsetHeight; 1195 | }, lis).getLast(); 1196 | li.querySelector('span, a').focus(); 1197 | } 1198 | break; 1199 | case 36: // home 1200 | if (searchMode){ 1201 | this.querySelector('ul>li:first-child a').focus(); 1202 | } else { 1203 | this.querySelector('ul>li:first-child').querySelector('span, a').focus(); 1204 | } 1205 | break; 1206 | case 34: // page down 1207 | var self = this; 1208 | var getLastItem = function(){ 1209 | var bound = self.offsetHeight + self.scrollTop; 1210 | var items = self.querySelectorAll('a, span'); 1211 | return Array.filter(function(item){ 1212 | return !!item.parentElement.offsetHeight && item.offsetTop < bound; 1213 | }, items).getLast(); 1214 | }; 1215 | var item = getLastItem(); 1216 | if (item != document.activeElement){ 1217 | e.preventDefault(); 1218 | item.focus(); 1219 | } else { 1220 | setTimeout(function(){ 1221 | getLastItem().focus(); 1222 | }, 0); 1223 | } 1224 | break; 1225 | case 33: // page up 1226 | var self = this; 1227 | var getFirstItem = function(){ 1228 | var bound = self.scrollTop; 1229 | var items = self.querySelectorAll('a, span'); 1230 | return Array.filter(function(item){ 1231 | return !!item.parentElement.offsetHeight && ((item.offsetTop + item.offsetHeight) > bound); 1232 | }, items)[0]; 1233 | }; 1234 | var item = getFirstItem(); 1235 | if (item != document.activeElement){ 1236 | e.preventDefault(); 1237 | item.focus(); 1238 | } else { 1239 | setTimeout(function(){ 1240 | getFirstItem().focus(); 1241 | }, 0); 1242 | } 1243 | break; 1244 | case 113: // F2, not for Mac 1245 | if (os == 'mac') break; 1246 | var id = li.id.replace(/(neat\-tree|results)\-item\-/, ''); 1247 | actions.editBookmarkFolder(id); 1248 | break; 1249 | case 46: // delete 1250 | break; // don't run 'default' 1251 | default: 1252 | var key = String.fromCharCode(keyCode).trim(); 1253 | if (!key) return; 1254 | if (key != keyBuffer) keyBuffer += key; 1255 | clearTimeout(keyBufferTimer); 1256 | keyBufferTimer = setTimeout(function(){ keyBuffer = ''; }, 500); 1257 | var lis = this.querySelectorAll('ul>li'); 1258 | var items = []; 1259 | for (var i = 0, l = lis.length; i < l; i++){ 1260 | var li = lis[i]; 1261 | if (li.parentNode.offsetHeight) items.push(li.firstElementChild); 1262 | } 1263 | var pattern = new RegExp('^' + keyBuffer.escapeRegExp(), 'i'); 1264 | var batch = []; 1265 | var startFind = false; 1266 | var found = false; 1267 | var activeElement = document.activeElement; 1268 | for (var i = 0, l = items.length; i < l; i++){ 1269 | var item = items[i]; 1270 | if (item == activeElement){ 1271 | startFind = true; 1272 | } else if (startFind){ 1273 | if (pattern.test(item.textContent.trim())){ 1274 | found = true; 1275 | item.focus(); 1276 | break; 1277 | } 1278 | } else { 1279 | batch.push(item); 1280 | } 1281 | } 1282 | if (!found){ 1283 | for (var i = 0, l = batch.length; i < l; i++){ 1284 | var item = batch[i]; 1285 | if (pattern.test(item.textContent.trim())){ 1286 | item.focus(); 1287 | break; 1288 | } 1289 | } 1290 | } 1291 | } 1292 | }; 1293 | $tree.addEventListener('keydown', treeKeyDown); 1294 | $results.addEventListener('keydown', treeKeyDown); 1295 | 1296 | var treeKeyUp = function(e){ 1297 | var item = document.activeElement; 1298 | if (!/^(a|span)$/i.test(item.tagName)) item = $tree.querySelector('.focus') || $tree.querySelector('li:first-child>span'); 1299 | var li = item.parentNode; 1300 | switch (e.keyCode){ 1301 | case 8: // backspace 1302 | if (os != 'mac') break; // somehow delete button on mac gives backspace 1303 | case 46: // delete 1304 | e.preventDefault(); 1305 | var id = li.id.replace(/(neat\-tree|results)\-item\-/, ''); 1306 | if (li.hasClass('parent')){ 1307 | chrome.bookmarks.getChildren(id, function(children){ 1308 | var urlsLen = Array.map(function(c){ 1309 | return c.url; 1310 | }, children).clean().length; 1311 | actions.deleteBookmarks(id, urlsLen, children.length-urlsLen); 1312 | }); 1313 | } else { 1314 | actions.deleteBookmark(id); 1315 | } 1316 | break; 1317 | } 1318 | }; 1319 | $tree.addEventListener('keyup', treeKeyUp); 1320 | $results.addEventListener('keyup', treeKeyUp); 1321 | 1322 | var contextKeyDown = function(e){ 1323 | var menu = this; 1324 | var item = document.activeElement; 1325 | var metaKey = e.metaKey; 1326 | switch (e.keyCode){ 1327 | case 40: // down 1328 | e.preventDefault(); 1329 | if (metaKey){ // cmd + down (Mac) 1330 | menu.lastElementChild.focus(); 1331 | } else { 1332 | if (item.tagName == 'A'){ 1333 | var nextItem = item.nextElementSibling; 1334 | if (nextItem && nextItem.tagName == 'HR') nextItem = nextItem.nextElementSibling; 1335 | if (nextItem){ 1336 | nextItem.focus(); 1337 | } else if (os != 'mac'){ 1338 | menu.firstElementChild.focus(); 1339 | } 1340 | } else { 1341 | item.firstElementChild.focus(); 1342 | } 1343 | } 1344 | break; 1345 | case 38: // up 1346 | e.preventDefault(); 1347 | if (metaKey){ // cmd + up (Mac) 1348 | menu.firstElementChild.focus(); 1349 | } else { 1350 | if (item.tagName == 'A'){ 1351 | var prevItem = item.previousElementSibling; 1352 | if (prevItem && prevItem.tagName == 'HR') prevItem = prevItem.previousElementSibling; 1353 | if (prevItem){ 1354 | prevItem.focus(); 1355 | } else if (os != 'mac'){ 1356 | menu.lastElementChild.focus(); 1357 | } 1358 | } else { 1359 | item.lastElementChild.focus(); 1360 | } 1361 | } 1362 | break; 1363 | case 32: // space 1364 | if (os != 'mac') break; 1365 | case 13: // enter 1366 | e.preventDefault(); 1367 | var event = document.createEvent('MouseEvents'); 1368 | event.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 1369 | item.dispatchEvent(event); 1370 | case 27: // esc 1371 | e.preventDefault(); 1372 | var active = body.querySelector('.active'); 1373 | if (active) active.removeClass('active').focus(); 1374 | clearMenu(); 1375 | } 1376 | }; 1377 | $bookmarkContextMenu.addEventListener('keydown', contextKeyDown); 1378 | $folderContextMenu.addEventListener('keydown', contextKeyDown); 1379 | 1380 | var contextMouseMove = function(e){ 1381 | e.target.focus(); 1382 | }; 1383 | $bookmarkContextMenu.addEventListener('mousemove', contextMouseMove); 1384 | $folderContextMenu.addEventListener('mousemove', contextMouseMove); 1385 | 1386 | var contextMouseOut = function(){ 1387 | if (this.style.opacity.toInt()) this.focus(); 1388 | }; 1389 | $bookmarkContextMenu.addEventListener('mouseout', contextMouseOut); 1390 | $folderContextMenu.addEventListener('mouseout', contextMouseOut); 1391 | 1392 | // Drag and drop 1393 | var draggedBookmark = null; 1394 | var draggedOut = false; 1395 | var canDrop = false; 1396 | var zoomLevel = 1; 1397 | var bookmarkClone = document.querySelector('#bookmark-clone'); 1398 | var dropOverlay = document.querySelector('#drop-overlay'); 1399 | $tree.addEventListener('mousedown', function(e){ 1400 | if (e.button != 0) return; 1401 | var el = e.target; 1402 | var elParent = el.parentNode; 1403 | // can move any bookmarks/folders except the default root folders 1404 | if ((el.tagName == 'A' && elParent.hasClass('child')) || (el.tagName == 'SPAN' && elParent.hasClass('parent') && elParent.dataset.parentid != '0')){ 1405 | e.preventDefault(); 1406 | draggedOut = false; 1407 | draggedBookmark = el; 1408 | if (localStorage.zoom) zoomLevel = (localStorage.zoom.toInt() / 100); 1409 | bookmarkClone.innerHTML = el.innerHTML; 1410 | el.focus(); 1411 | } 1412 | }); 1413 | var scrollTree, scrollTreeInterval = 100, scrollTreeSpot = 10; 1414 | var scrollTreeSpeed = 20; 1415 | var stopScrollTree = function(){ 1416 | clearInterval(scrollTree); 1417 | scrollTree = null; 1418 | }; 1419 | document.addEventListener('mousemove', function(e){ 1420 | if (e.button != 0) return; 1421 | if (!draggedBookmark) return; 1422 | e.preventDefault(); 1423 | var el = e.target; 1424 | var clientX = e.clientX; 1425 | var clientY = e.clientY + document.body.scrollTop; 1426 | if (el == draggedBookmark){ 1427 | bookmarkClone.style.left = '-999px'; 1428 | dropOverlay.style.left = '-999px'; 1429 | canDrop = false; 1430 | return; 1431 | } 1432 | draggedOut = true; 1433 | // if hovering over the dragged element itself or cursor move outside the tree 1434 | var treeTop = $tree.offsetTop, treeBottom = window.innerHeight; 1435 | if (clientX < 0 || clientY < treeTop || clientX > $tree.offsetWidth || clientY > treeBottom){ 1436 | bookmarkClone.style.left = '-999px'; 1437 | dropOverlay.style.left = '-999px'; 1438 | canDrop = false; 1439 | } 1440 | // if hovering over the top or bottom edges of the tree, scroll the tree 1441 | var treeScrollHeight = $tree.scrollHeight, treeOffsetHeight = $tree.offsetHeight; 1442 | if (treeScrollHeight > treeOffsetHeight){ // only scroll when it's scrollable 1443 | var treeScrollTop = $tree.scrollTop; 1444 | if (clientY <= treeTop + scrollTreeSpot){ 1445 | if (treeScrollTop == 0){ 1446 | stopScrollTree(); 1447 | } else if (!scrollTree) scrollTree = setInterval(function(){ 1448 | $tree.scrollTop -= scrollTreeSpeed; 1449 | dropOverlay.style.left = '-999px'; 1450 | }, scrollTreeInterval); 1451 | } else if (clientY >= treeBottom - scrollTreeSpot){ 1452 | if (treeScrollTop == (treeScrollHeight - treeOffsetHeight)){ 1453 | stopScrollTree(); 1454 | } else if (!scrollTree) scrollTree = setInterval(function(){ 1455 | $tree.scrollTop += scrollTreeSpeed; 1456 | dropOverlay.style.left = '-999px'; 1457 | }, scrollTreeInterval); 1458 | } else { 1459 | stopScrollTree(); 1460 | } 1461 | } 1462 | // collapse the folder before moving it 1463 | var draggedBookmarkParent = draggedBookmark.parentNode; 1464 | if (draggedBookmark.tagName == 'SPAN' && draggedBookmarkParent.hasClass('open')){ 1465 | draggedBookmarkParent.removeClass('open').setAttribute('aria-expanded', false); 1466 | } 1467 | clientX /= zoomLevel; 1468 | clientY /= zoomLevel; 1469 | if (el.tagName == 'A'){ 1470 | canDrop = true; 1471 | bookmarkClone.style.top = clientY + 'px'; 1472 | bookmarkClone.style.left = (rtl ? (clientX - bookmarkClone.offsetWidth) : clientX) + 'px'; 1473 | var elRect = el.getBoundingClientRect(); 1474 | var elRectTop = elRect.top + document.body.scrollTop; 1475 | var elRectBottom = elRect.bottom + document.body.scrollTop; 1476 | var top = (clientY >= elRectTop + elRect.height / 2) ? elRectBottom : elRectTop; 1477 | dropOverlay.className = 'bookmark'; 1478 | dropOverlay.style.top = top + 'px'; 1479 | dropOverlay.style.left = rtl ? '0px' : el.style.paddingInlineStart.toInt() + 16 + 'px'; 1480 | dropOverlay.style.width = (el.getComputedStyle('width').toInt() - 12) + 'px'; 1481 | dropOverlay.style.height = null; 1482 | } else if (el.tagName == 'SPAN'){ 1483 | canDrop = true; 1484 | bookmarkClone.style.top = clientY + 'px'; 1485 | bookmarkClone.style.left = clientX + 'px'; 1486 | var elRect = el.getBoundingClientRect(); 1487 | var top = null; 1488 | var elRectTop = elRect.top + document.body.scrollTop; 1489 | var elRectHeight = elRect.height; 1490 | var elRectBottom = elRect.bottom + document.body.scrollTop; 1491 | var elParent = el.parentNode; 1492 | if (elParent.dataset.parentid != '0'){ 1493 | if (clientY < elRectTop + elRectHeight * .3){ 1494 | top = elRectTop; 1495 | } else if (clientY > (elRectTop + elRectHeight * .7) && !elParent.hasClass('open')){ 1496 | top = elRectBottom; 1497 | } 1498 | } 1499 | if (top == null){ 1500 | dropOverlay.className = 'folder'; 1501 | dropOverlay.style.top = elRectTop + 'px'; 1502 | dropOverlay.style.left = '0px'; 1503 | dropOverlay.style.width = elRect.width + 'px'; 1504 | dropOverlay.style.height = elRect.height + 'px'; 1505 | } else { 1506 | dropOverlay.className = 'bookmark'; 1507 | dropOverlay.style.top = top + 'px'; 1508 | dropOverlay.style.left = el.style.paddingInlineStart.toInt() + 16 + 'px'; 1509 | console.log(el); 1510 | dropOverlay.style.width = (el.getComputedStyle('width').toInt() - 12) + 'px'; 1511 | dropOverlay.style.height = null; 1512 | } 1513 | } 1514 | }); 1515 | var onDrop = function(){ 1516 | draggedBookmark = null; 1517 | bookmarkClone.style.left = '-999px'; 1518 | dropOverlay.style.left = '-999px'; 1519 | canDrop = false; 1520 | }; 1521 | document.addEventListener('mouseup', function(e){ 1522 | if (e.button != 0) return; 1523 | if (!draggedBookmark) return; 1524 | stopScrollTree(); 1525 | if (!canDrop){ 1526 | if (draggedOut) noOpenBookmark = true; 1527 | draggedOut = false; 1528 | onDrop(); 1529 | return; 1530 | }; 1531 | var el = e.target; 1532 | var elParent = el.parentNode; 1533 | var id = elParent.id.replace('neat-tree-item-', ''); 1534 | if (!id){ 1535 | onDrop(); 1536 | return; 1537 | } 1538 | var draggedBookmarkParent = draggedBookmark.parentNode; 1539 | var draggedID = draggedBookmarkParent.id.replace('neat-tree-item-', ''); 1540 | var clientY = (e.clientY + document.body.scrollTop) / zoomLevel; 1541 | if (el.tagName == 'A'){ 1542 | var elRect = el.getBoundingClientRect(); 1543 | var elRectTop = elRect.top + document.body.scrollTop; 1544 | var moveBottom = (clientY >= elRectTop + elRect.height / 2); 1545 | chrome.bookmarks.get(id, function(node){ 1546 | if (!node || !node.length) return; 1547 | node = node[0]; 1548 | var index = node.index; 1549 | var parentId = node.parentId; 1550 | if (draggedID){ 1551 | chrome.bookmarks.move(draggedID, { 1552 | parentId: parentId, 1553 | index: moveBottom ? ++index : index 1554 | }, function(){ 1555 | draggedBookmarkParent.inject(elParent, moveBottom ? 'after' : 'before'); 1556 | draggedBookmark.style.paddingInlineStart = el.style.paddingInlineStart; 1557 | draggedBookmark.focus(); 1558 | onDrop(); 1559 | }); 1560 | } 1561 | }); 1562 | } else if (el.tagName == 'SPAN'){ 1563 | var elRect = el.getBoundingClientRect(); 1564 | var move = 0; // 0 = middle, 1 = top, 2 = bottom 1565 | var elRectTop = elRect.top, elRectHeight = elRect.height; 1566 | var elParent = el.parentNode; 1567 | if (elParent.dataset.parentid != '0'){ 1568 | if (clientY < elRectTop + elRectHeight * .3){ 1569 | move = 1; 1570 | } else if (clientY > elRectTop + elRectHeight * .7 && !elParent.hasClass('open')){ 1571 | move = 2; 1572 | } 1573 | } 1574 | if (move > 0){ 1575 | var moveBottom = (move == 2); 1576 | chrome.bookmarks.get(id, function(node){ 1577 | if (!node || !node.length) return; 1578 | node = node[0]; 1579 | var index = node.index; 1580 | var parentId = node.parentId; 1581 | chrome.bookmarks.move(draggedID, { 1582 | parentId: parentId, 1583 | index: moveBottom ? ++index : index 1584 | }, function(){ 1585 | draggedBookmarkParent.inject(elParent, moveBottom ? 'after' : 'before'); 1586 | draggedBookmark.style.paddingInlineStart = el.style.paddingInlineStart; 1587 | draggedBookmark.focus(); 1588 | onDrop(); 1589 | }); 1590 | }); 1591 | } else { 1592 | chrome.bookmarks.move(draggedID, { 1593 | parentId: id 1594 | }, function(){ 1595 | var ul = elParent.querySelector('ul'); 1596 | var level = parseInt(elParent.parentNode.dataset.level) + 1; 1597 | draggedBookmark.style.paddingInlineStart = (14 * level) + 'px'; 1598 | if (ul){ 1599 | draggedBookmarkParent.inject(ul); 1600 | } else { 1601 | draggedBookmarkParent.destroy(); 1602 | } 1603 | el.focus(); 1604 | onDrop(); 1605 | }); 1606 | } 1607 | } else { 1608 | onDrop(); 1609 | } 1610 | }); 1611 | 1612 | // Resizer 1613 | var $resizer = document.querySelector('#resizer'); 1614 | var resizerDown = false; 1615 | var bodyWidth, screenX; 1616 | $resizer.addEventListener('mousedown', function(e){ 1617 | e.preventDefault(); 1618 | e.stopPropagation(); 1619 | resizerDown = true; 1620 | bodyWidth = body.offsetWidth; 1621 | screenX = e.screenX; 1622 | }); 1623 | document.addEventListener('mousemove', function(e){ 1624 | if (!resizerDown) return; 1625 | e.preventDefault(); 1626 | var changedWidth = rtl ? (e.screenX - screenX) : (screenX - e.screenX); 1627 | var width = bodyWidth + changedWidth; 1628 | width = Math.min(640, Math.max(320, width)); 1629 | body.style.width = width + 'px'; 1630 | localStorage.popupWidth = width; 1631 | clearMenu(); // messes the context menu 1632 | }); 1633 | document.addEventListener('mouseup', function(e){ 1634 | if (!resizerDown) return; 1635 | e.preventDefault(); 1636 | resizerDown = false; 1637 | adaptBookmarkTooltips(); 1638 | }); 1639 | 1640 | // Closing dialogs on escape 1641 | var closeDialogs = function(){ 1642 | if (body.hasClass('needConfirm')) ConfirmDialog.fn2(); ConfirmDialog.close(); 1643 | if (body.hasClass('needEdit')) EditDialog.close(); 1644 | if (body.hasClass('needAlert')) AlertDialog.close(); 1645 | }; 1646 | document.addEventListener('keydown', function(e){ 1647 | if (e.keyCode == 27 && (body.hasClass('needConfirm') || body.hasClass('needEdit') || body.hasClass('needAlert'))){ // esc 1648 | e.preventDefault(); 1649 | closeDialogs(); 1650 | } else if ((e.metaKey || e.ctrlKey) && e.keyCode == 70){ // cmd/ctrl + f 1651 | searchInput.focus(); 1652 | searchInput.select(); 1653 | } 1654 | }); 1655 | document.querySelector('#cover').addEventListener('click', closeDialogs); 1656 | 1657 | // Make webkit transitions work only after elements are settled down 1658 | setTimeout(function(){ 1659 | body.addClass('transitional'); 1660 | }, 10); 1661 | 1662 | // Zoom 1663 | // if (localStorage.zoom){ 1664 | // body.dataset.zoom = localStorage.zoom; 1665 | // } 1666 | // var zoom = function(val){ 1667 | // if (draggedBookmark) return; // prevent zooming when drag-n-droppping 1668 | // var dataZoom = body.dataset.zoom; 1669 | // var currentZoom = dataZoom ? dataZoom.toInt() : 100; 1670 | // if (val == 0){ 1671 | // delete body.dataset.zoom; 1672 | // localStorage.removeItem('zoom'); 1673 | // } else { 1674 | // var z = (val>0) ? currentZoom + 10 : currentZoom - 10; 1675 | // z = Math.min(150, Math.max(90, z)); 1676 | // body.dataset.zoom = z; 1677 | // localStorage.zoom = z; 1678 | // } 1679 | // body.addClass('dummy').removeClass('dummy'); // force redraw 1680 | // resetHeight(); 1681 | // }; 1682 | // document.addEventListener('mousewheel', function(e){ 1683 | // if (!e.metaKey && !e.ctrlKey) return; 1684 | // e.preventDefault(); 1685 | // zoom(e.wheelDelta); 1686 | // }); 1687 | // document.addEventListener('keydown', function(e){ 1688 | // if (!e.metaKey && !e.ctrlKey) return; 1689 | // switch (e.keyCode){ 1690 | // case 187: // + (plus) 1691 | // e.preventDefault(); 1692 | // zoom(1); 1693 | // break; 1694 | // case 189: // - (minus) 1695 | // e.preventDefault(); 1696 | // zoom(-1); 1697 | // break; 1698 | // case 48: // 0 (zero) 1699 | // e.preventDefault(); 1700 | // zoom(0); 1701 | // break; 1702 | // } 1703 | // }); 1704 | 1705 | // Fix stupid wrong offset of the page on Mac 1706 | if (os == 'mac'){ 1707 | setTimeout(function(){ 1708 | var top = body.scrollTop; 1709 | if (top != 0) body.scrollTop = 0; 1710 | }, 1500); 1711 | } 1712 | } 1713 | 1714 | onerror = function(){ 1715 | chrome.runtime.sendMessage({error: [].slice.call(arguments)}); 1716 | }; 1717 | --------------------------------------------------------------------------------