├── .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 |
7 |
8 |
9 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
37 |
38 |
39 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
60 |
61 |
62 |
66 |
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 |
16 |
17 |
18 | Always open bookmarks in new tabs
19 |
20 |
21 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | Open new tabs in background
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Close unused folders
58 |
59 |
60 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Popup stays when opening bookmarks
80 |
81 |
89 |
90 |
91 |
92 | Remember last state when popup open
93 |
94 |
95 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | Icon
115 |
116 |
117 |
118 |
Style
119 |
120 |
152 |
153 |
154 |
155 | Color
156 |
157 |
158 |
159 | Switch color automatically
160 |
161 |
162 |
170 |
171 |
172 |
173 |
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 = '';
273 |
274 | for (var i = 0, l = data.length; i < l; i++){
275 | var d = data[i];
276 | var children = d.children;
277 | var title = d.title.htmlspecialchars();
278 | var url = d.url;
279 | var id = d.id;
280 | var parentID = d.parentId;
281 | var idHTML = id ? ' id="neat-tree-item-' + id + '"': '';
282 | var isFolder = d.dateGroupModified || children || typeof url == 'undefined';
283 | if (isFolder){
284 | var isOpen = false;
285 | var open = '';
286 | if (preserveState){
287 | isOpen = opens.contains(id);
288 | if (isOpen) open = ' open';
289 | console.log('id: ' + id);
290 | console.log('open: ' + open);
291 | }
292 |
293 | /* @ altered */
294 | html += ''
295 | + ' '
296 | + '' + (title || _m('noTitle')) + ' ' + ' ';
297 | /* @ end of altered */
298 |
299 | if (isOpen){
300 |
301 | if (children){
302 | html += generateHTML(children, level + 1);
303 | } else {
304 |
305 | (function(_id){
306 | chrome.bookmarks.getChildren(_id, function(children){
307 | var html = generateHTML(children, level + 1);
308 | var div = document.createElement('div');
309 | div.innerHTML = html;
310 | var ul = div.querySelector('ul');
311 | ul.inject(document.querySelector('#neat-tree-item-' + _id));
312 | div.destroy();
313 | });
314 | })(id);
315 | }
316 | }
317 | } else {
318 | html += ' ' + generateBookmarkHTML(title, url, 'style="padding-inline-start: ' + paddingInlineStart + 'px"');
319 | }
320 | html += ' ';
321 | }
322 | 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 = '';
484 | for (var i = 0, l = results.length; i < l; i++){
485 | var result = results[i];
486 |
487 | if (result.url) {
488 | var id = result.id;
489 | html += ''
490 | + generateBookmarkHTML(result.title, result.url);
491 | }
492 |
493 | }
494 | 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 |
--------------------------------------------------------------------------------