7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/background/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | /*eslint-env commonjs*/
7 | /*eslint quote-props: ['error', "always"] */
8 |
9 | 'use strict';
10 |
11 | module.exports = {
12 | 'extends': [
13 | '../tools/eslint/for-module.js',
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/common/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | /*eslint-env commonjs*/
7 | /*eslint quote-props: ['error', "always"] */
8 |
9 | 'use strict';
10 |
11 | module.exports = {
12 | 'extends': [
13 | '../tools/eslint/for-module.js',
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/options/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | /*eslint-env commonjs*/
7 | /*eslint quote-props: ['error', "always"] */
8 |
9 | 'use strict';
10 |
11 | module.exports = {
12 | 'extends': [
13 | '../tools/eslint/for-module.js',
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "submodules/webextensions-lib-l10n"]
2 | path = submodules/webextensions-lib-l10n
3 | url = https://github.com/piroor/webextensions-lib-l10n.git
4 | [submodule "submodules/webextensions-lib-options"]
5 | path = submodules/webextensions-lib-options
6 | url = https://github.com/piroor/webextensions-lib-options.git
7 | [submodule "submodules/webextensions-lib-configs"]
8 | path = submodules/webextensions-lib-configs
9 | url = https://github.com/piroor/webextensions-lib-configs.git
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tst-more-tree-commands",
3 | "version": "0.0.0",
4 | "engines": {
5 | "node": ">=8.6.0"
6 | },
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "dependencies": {
11 | "babel-core": "^6.0.0",
12 | "babel-plugin-module-resolver": "^3.0.0",
13 | "eslint": "^5.16.0",
14 | "eslint-import-resolver-babel-module": "^4.0.0",
15 | "eslint-plugin-import": "^2.17.2",
16 | "jsonlint-cli": "*",
17 | "tunnel-agent": ">=0.6.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI/CD
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: prepare manifest.json with a revision number
13 | run: |
14 | cp manifest.json manifest.json.bak
15 | version=$(cat manifest.json.bak | jq -r ".version" | sed -r -e "s/$/.$(git log --oneline | wc -l)/")
16 | cat manifest.json.bak | jq ".version |= \"$version\"" > manifest.json
17 | - name: build xpi
18 | run: make
19 | - uses: actions/upload-artifact@v4
20 | with:
21 | name: tst-more-tree-commands.xpi
22 | path: tst-more-tree-commands.xpi
23 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /*eslint-env commonjs*/
2 | /*eslint quote-props: ['error', "always"] */
3 |
4 | module.exports = {
5 | 'root': true,
6 |
7 | 'parserOptions': {
8 | 'ecmaVersion': 2018,
9 | },
10 |
11 | 'env': {
12 | 'browser': true,
13 | 'es6': true,
14 | 'webextensions': true,
15 | },
16 |
17 | 'settings': {
18 | 'import/resolver': {
19 | 'babel-module': {
20 | 'root': ['./'],
21 | }
22 | }
23 | },
24 |
25 | 'rules': {
26 | // stylisitc problem
27 | 'indent': ['warn', 2, {
28 | 'SwitchCase': 1,
29 | 'MemberExpression': 1,
30 | 'CallExpression': {
31 | 'arguments': 'first',
32 | },
33 | 'VariableDeclarator': {
34 | 'var': 2,
35 | 'let': 2,
36 | 'const': 3,
37 | }
38 | }],
39 | 'quotes': ['warn', 'single', {
40 | 'avoidEscape': true,
41 | 'allowTemplateLiterals': true,
42 | }],
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/options/options.css:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 |
7 | :root > * {
8 | transition: opacity 0.25s ease-out;
9 | }
10 | :root:not(.initialized) > * {
11 | opacity: 0;
12 | }
13 |
14 | :root.rtl {
15 | direction: rtl;
16 | }
17 |
18 | body {
19 | background: var(--bg-color);
20 | color: var(--text-color);
21 | }
22 |
23 | :root:not(.debugging) #debug-configs {
24 | max-height: 0;
25 | overflow: hidden;
26 | }
27 |
28 | :root:not(.debugging) #debug-configs * {
29 | -moz-user-focus: ignore;
30 | -moz-user-input: disabled;
31 | }
32 |
33 |
34 | p, ul {
35 | margin: 0 0 0.5em 0;
36 | padding: 0;
37 | }
38 |
39 | ul,
40 | ul li {
41 | list-style: none;
42 | }
43 |
44 | p.sub {
45 | margin-left: 2em;
46 | }
47 |
48 | ul p.sub {
49 | margin-top: 0;
50 | margin-bottom: 0;
51 | }
52 |
--------------------------------------------------------------------------------
/options/init.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | 'use strict';
7 |
8 | import {
9 | configs,
10 | isRTL,
11 | } from '/common/common.js';
12 | import Options from '/extlib/Options.js';
13 | import '/extlib/l10n.js';
14 |
15 | /*const options = */new Options(configs);
16 |
17 | /*
18 | function onConfigChanged(key) {
19 | switch (key) {
20 | case 'debug':
21 | if (configs.debug)
22 | document.documentElement.classList.add('debugging');
23 | else
24 | document.documentElement.classList.remove('debugging');
25 | break;
26 | }
27 | }
28 |
29 | configs.$addObserver(onConfigChanged);
30 | */
31 |
32 | window.addEventListener('DOMContentLoaded', async () => {
33 | document.documentElement.classList.toggle('rtl', isRTL());
34 | await configs.$loaded;
35 |
36 | //options.buildUIForAllConfigs(document.querySelector('#debug-configs'));
37 | //onConfigChanged('debug');
38 |
39 | document.documentElement.classList.add('initialized');
40 | }, { once: true });
41 |
--------------------------------------------------------------------------------
/history.ja.md:
--------------------------------------------------------------------------------
1 | # 更新履歴
2 |
3 | - master/HEAD
4 | - 1.5.1 (2024.3.26)
5 | * Waterfox G6.0.10以降に含まれる「タブサイドバー」に対応
6 | - 1.5 (2022.09.12)
7 | * 選択されたタブの中に既存のグループタブが含まれている場合に「これらのタブを新しいグループにする」を使用できなくなっていたのを修正([by emvaized](https://github.com/piroor/tst-more-tree-commands/pull/18), thanks!)
8 | - 1.4 (2022.04.22)
9 | * プライベートウィンドウのタブの完全な情報をAPI経由で受信できるようにするために、自身をTSTに登録するようにした
10 | * 「これらのタブを新しいグループにする」を単独のタブに対しても実行できるようにした([by emvaized](https://github.com/piroor/tst-more-tree-commands/pull/18), thanks!)
11 | * `ru`ロケール更新(by [wvxwxvw](https://github.com/wvxwxvw), thanks!)
12 | - 1.3 (2021.05.05)
13 | * スクロール量とアニメーションにかける時間をカスタマイズ可能な、サイドバーを指定行数だけ上下にスクロールするショートカットコマンドを追加
14 | * 進行中のスクロールを中断するショートカットコマンドを追加
15 | - 1.2 (2021.02.12)
16 | * [「次の同階層のタブの後に移動する」コマンドでの不要なツリーの展開を抑制するようにした](https://github.com/piroor/tst-more-tree-commands/pull/7)([by @spinda](https://github.com/spinda). Thanks!)
17 | * `ru`ロケール更新(by [wvxwxvw](https://github.com/wvxwxvw), thanks!)
18 | - 1.1 (2021.02.09)
19 | * [「ツリーを展開する/折りたたむ」](https://github.com/piroor/tst-more-tree-commands/pull/6)、[「現在のタブを同じ階層の前のタブの手前に移動」および「現在のタブを同じ階層の次のタブの後に移動」](https://github.com/piroor/tst-more-tree-commands/pull/5)をコマンドに追加([by @spinda](https://github.com/spinda). Thanks!)
20 | * `ru`ロケール追加(by [wvxwxvw](https://github.com/wvxwxvw), thanks!)
21 | - 1.0 (2020.11.05)
22 | * 最初のリリース
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | NPM_MOD_DIR := $(CURDIR)/node_modules
2 | NPM_BIN_DIR := $(NPM_MOD_DIR)/.bin
3 |
4 | .PHONY: xpi install_dependency install_hook lint format init_extlib update_extlib install_extlib
5 |
6 | all: xpi
7 |
8 | install_dependency:
9 | [ -e "$(NPM_BIN_DIR)/eslint" -a -e "$(NPM_BIN_DIR)/jsonlint-cli" ] || npm install
10 |
11 | install_hook:
12 | echo '#!/bin/sh\nmake lint' > "$(CURDIR)/.git/hooks/pre-commit" && chmod +x "$(CURDIR)/.git/hooks/pre-commit"
13 |
14 | lint: install_dependency
15 | "$(NPM_BIN_DIR)/eslint" . --ext=.js --report-unused-disable-directives
16 | find . -type d -name node_modules -prune -o -type f -name '*.json' -print | xargs "$(NPM_BIN_DIR)/jsonlint-cli"
17 |
18 | format: install_dependency
19 | "$(NPM_BIN_DIR)/eslint" . --ext=.js --report-unused-disable-directives --fix
20 |
21 | xpi: init_extlib install_extlib lint
22 | rm -f ./*.xpi
23 | zip -r -9 tst-more-tree-commands.xpi manifest.json _locales extlib common background options -x '*/.*' >/dev/null 2>/dev/null
24 |
25 | init_extlib:
26 | git submodule update --init
27 |
28 | update_extlib:
29 | git submodule foreach 'git checkout trunk || git checkout main || git checkout master && git pull'
30 |
31 | install_extlib:
32 | rm -f extlib/*.js
33 | cp submodules/webextensions-lib-configs/Configs.js extlib/; echo 'export default Configs;' >> extlib/Configs.js
34 | cp submodules/webextensions-lib-options/Options.js extlib/; echo 'export default Options;' >> extlib/Options.js
35 | cp submodules/webextensions-lib-l10n/l10n.js extlib/; echo 'export default l10n;' >> extlib/l10n.js
36 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_extensionName__",
4 | "version": "1.5.1",
5 | "author": "YUKI \"Piro\" Hiroshi",
6 | "description": "__MSG_extensionDescription__",
7 | "permissions": [
8 | "menus",
9 | "tabs",
10 | "storage"
11 | ],
12 | "background": {
13 | "page": "/background/background.html"
14 | },
15 | "commands": {
16 | "group": {
17 | "description": "__MSG_command_group__"
18 | },
19 | "ungroup": {
20 | "description": "__MSG_command_ungroup__"
21 | },
22 | "flatten": {
23 | "description": "__MSG_command_flatten__"
24 | },
25 | "indent": {
26 | "description": "__MSG_command_indent__"
27 | },
28 | "outdent": {
29 | "description": "__MSG_command_outdent__"
30 | },
31 | "moveBeforePreviousSibling": {
32 | "description": "__MSG_command_moveBeforePreviousSibling__"
33 | },
34 | "moveAfterNextSibling": {
35 | "description": "__MSG_command_moveAfterNextSibling__"
36 | },
37 | "tabbarLinesDown": {
38 | "description": "__MSG_command_tabbarLinesDown__"
39 | },
40 | "tabbarLinesUp": {
41 | "description": "__MSG_command_tabbarLinesUp__"
42 | },
43 | "tabbarStopScroll": {
44 | "description": "__MSG_command_tabbarStopScroll__"
45 | },
46 | "toggle": {
47 | "description": "__MSG_command_toggle__"
48 | }
49 | },
50 | "options_ui": {
51 | "page": "/options/options.html"
52 | },
53 | "default_locale": "en",
54 | "applications": {
55 | "gecko": {
56 | "id": "tst-more-tree-commands@piro.sakura.ne.jp"
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/history.en.md:
--------------------------------------------------------------------------------
1 | # History
2 |
3 | - master/HEAD
4 | - 1.5.1 (2024.3.26)
5 | * Add support for "Tabs Sidebar" of Watefox G6.0.10 and later.
6 | - 1.5 (2022.09.12)
7 | * Activate "Create group" command for tabs even if any other group tab is in the selection ([by emvaized](https://github.com/piroor/tst-more-tree-commands/pull/18), thanks!)
8 | - 1.4 (2022.04.22)
9 | * Register self to TST to allow receiving full tab information on private windows via API.
10 | * Activate "Create group" command for single tab also ([by emvaized](https://github.com/piroor/tst-more-tree-commands/pull/18), thanks!)
11 | * Update `ru` locale by [wvxwxvw](https://github.com/wvxwxvw). Thanks!
12 | - 1.3 (2021.05.05)
13 | * Add custom shortcut commands to scroll up/down the sidebar by some lines, with customizable number of lines and animation duration.
14 | * Add a custom shortcut command to stop in-progress scroll of the sidebar.
15 | - 1.2 (2021.02.12)
16 | * [Suppress needless expansion of trees on the "move after next sibling" command](https://github.com/piroor/tst-more-tree-commands/pull/7) [by @spinda](https://github.com/spinda). Thanks!
17 | * Update `ru` locale by [wvxwxvw](https://github.com/wvxwxvw). Thanks!
18 | - 1.1 (2021.02.09)
19 | * Add new commands ["Toggle Collapsed State of Tree"](https://github.com/piroor/tst-more-tree-commands/pull/6), ["Move Current Tab Before Previous Sibling" and "Move Current Tab After Next Sibling"](https://github.com/piroor/tst-more-tree-commands/pull/5) [by @spinda](https://github.com/spinda). Thanks!
20 | * Add `ru` locale by [wvxwxvw](https://github.com/wvxwxvw). Thanks!
21 | - 1.0 (2020.11.05)
22 | * Initial release.
23 |
--------------------------------------------------------------------------------
/common/common.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | 'use strict';
7 |
8 | import Configs from '/extlib/Configs.js';
9 |
10 | export const configs = new Configs({
11 | contextMenuTopLevelCommand: 'moreTreeCommands',
12 |
13 | tabbarScrollLines: 3,
14 | tabbarScrollDuration: 150,
15 |
16 | cleanupGroupTabsAfterFlattenTree: true,
17 |
18 | TSTID: null,
19 | }, {
20 | localKeys: [
21 | ]
22 | });
23 |
24 |
25 | export const TST_ID = 'treestyletab@piro.sakura.ne.jp';
26 | export const WS_ID = 'sidebar@waterfox.net';
27 |
28 | export async function ensureTSTDetected() {
29 | try {
30 | if (await browser.runtime.sendMessage(TST_ID, { type: 'ping' })) {
31 | configs.TSTID = TST_ID;
32 | return;
33 | }
34 | }
35 | catch(_error) {
36 | }
37 | try {
38 | if (await browser.runtime.sendMessage(WS_ID, { type: 'ping' })) {
39 | configs.TSTID = WS_ID;
40 | return;
41 | }
42 | }
43 | catch(_error) {
44 | }
45 | throw new Error('Missing dependency: you need to install Tree Style Tab addon also');
46 | }
47 |
48 | export async function callTSTAPI(message) {
49 | if (!configs.TSTID)
50 | await ensureTSTDetected();
51 |
52 | try {
53 | return browser.runtime.sendMessage(configs.TSTID, message);
54 | }
55 | catch(error) {
56 | configs.TSTID = null;
57 | throw error;
58 | }
59 | }
60 |
61 | const RTL_LANGUAGES = new Set([
62 | 'ar',
63 | 'he',
64 | 'fa',
65 | 'ur',
66 | 'ps',
67 | 'sd',
68 | 'ckb',
69 | 'prs',
70 | 'rhg',
71 | ]);
72 |
73 | export function isRTL() {
74 | const lang = (
75 | navigator.language ||
76 | navigator.userLanguage ||
77 | //(new Intl.DateTimeFormat()).resolvedOptions().locale ||
78 | ''
79 | ).split('-')[0];
80 | return RTL_LANGUAGES.has(lang);
81 | }
82 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TST More Tree Commands
2 |
3 | 
4 |
5 | This provides extra commands to manipulate tree structure for the Tree Style Tab addon.
6 |
7 | * [Signed package on AMO](https://addons.mozilla.org/firefox/addon/tst-more-tree-commands/)
8 | * [Development builds for each commit are available at "Artifacts" of the CI/CD action](https://github.com/piroor/tst-more-tree-commands/actions?query=workflow%3ACI%2FCD)
9 |
10 | ## Author's comment from @piroor
11 |
12 | I'm finding successor developers of the "TST More Tree Commands" project. I'm ready to transfer the ownership of this project to successors.
13 |
14 | I generally use TST without manual manipulation modifying the tree structure, instead I use TST mainly with automatically constructed tree.
15 | "Indent", "outdent", and more other tree manipulation commands are non-critical for my usecase, and I'm negative about adding more new tree manipulation commands to TST itself.
16 | This is the reason why I decided to separate these features from TST to a helper addon.
17 |
18 | From the same reason, sorry but I have less motivation to develop and maintain this project.
19 | I believe that this project should be owned by someone who really want to use it.
20 | If you are interested to take the ownership of this project, please contact me via the [issue tracker](https://github.com/piroor/tst-more-tree-commands/issues) or [email](mailto:piro.outsider.reflex@gmail.com).
21 |
22 | ## Privacy Policy
23 |
24 | This software does not collect any privacy data automatically, but this includes ability to synchronize options across multiple devices automatically via Firefox Sync.
25 | Any data you input to options may be sent to Mozilla's Sync server, if you configure Firefox to activate Firefox Sync.
26 |
27 | このソフトウェアはいかなるプライバシー情報も自動的に収集しませんが、Firefox Syncを介して自動的に設定情報をデバイス間で同期する機能を含みます。
28 | Firefox Syncを有効化している場合、設定画面に入力されたデータは、Mozillaが運用するSyncサーバーに送信される場合があります。
29 |
30 |
--------------------------------------------------------------------------------
/_locales/ja/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": { "message": "TST More Tree Commands" },
3 | "extensionDescription": { "message": "Tree Style Tabに対し、ツリーの操作のための追加のコマンドを提供します。" },
4 |
5 | "command_group": { "message": "選択中のタブを新しいグループにする" },
6 | "command_ungroup": { "message": "タブのグループ化を解除" },
7 | "command_flatten": { "message": "タブのツリーを解除" },
8 | "command_indent": { "message": "現在のタブを1階層深くする" },
9 | "command_outdent": { "message": "現在のタブを1階層浅くする" },
10 | "command_toggle": { "message": "ツリーを展開する/折りたたむ" },
11 | "command_tabbarLinesUp": { "message": "タブの一覧を数行下にスクロール" },
12 | "command_tabbarLinesDown": { "message": "タブの一覧を数行下にスクロール" },
13 | "command_tabbarStopScroll": { "message": "進行中のタブのスクロールをキャンセルする" },
14 |
15 | "command_moveBeforePreviousSibling": { "message": "現在のタブを同じ階層の前のタブの手前に移動" },
16 | "command_moveAfterNextSibling": { "message": "現在のタブを同じ階層の次のタブの後に移動" },
17 |
18 | "context_moreTreeCommands_label": { "message": "ツリーの操作" },
19 | "context_group_label": { "message": "これらのタブを新しいグループにする(&G)" },
20 | "context_group_command": { "message": "これらのタブを新しいグループにする" },
21 | "context_ungroup_label": { "message": "グループ化を解除(&U)" },
22 | "context_ungroup_command": { "message": "グループ化を解除" },
23 | "context_flatten_label": { "message": "ツリーを解除(&F)" },
24 | "context_flatten_command": { "message": "ツリーを解除" },
25 | "context_indent_label": { "message": "1階層深く(&I)" },
26 | "context_indent_command": { "message": "1階層深く" },
27 | "context_outdent_label": { "message": "1階層浅く(&O)" },
28 | "context_outdent_command": { "message": "1階層浅く" },
29 | "context_moveBeforePreviousSibling_label": { "message": "同じ階層の前のタブの手前に移動(&B)" },
30 | "context_moveBeforePreviousSibling_command": { "message": "同じ階層の前のタブの手前に移動" },
31 | "context_moveAfterNextSibling_label": { "message": "同じ階層の次のタブの後に移動(&A)" },
32 | "context_moveAfterNextSibling_command": { "message": "同じ階層の次のタブの後に移動" },
33 |
34 |
35 | "config_title": { "message": "TST More Tree Commandsの設定" },
36 |
37 | "config_contextMenuTopLevelCommand_label": { "message": "コンテキストメニューで使う機能" },
38 | "config_contextMenuTopLevelCommand_all": { "message": "すべて(サブメニューに表示)" },
39 |
40 | "config_tabbarScrollLines_label_before": { "message": "キーボードショートカットの「タブの一覧を数行上/下にスクロール」で" },
41 | "config_tabbarScrollLines_label_after": { "message": "行分スクロールする(TST本体のショートカットとは別に制御されます)" },
42 |
43 | "config_tabbarScrollDuration_label_before": { "message": "\u200b" },
44 | "config_tabbarScrollDuration_label_after": { "message": "ミリ秒かけてスクロール" },
45 |
46 | "config_cleanupGroupTabsAfterFlattenTree_label": { "message": "ツリーを解除した後、不要なグループタブを自動的に閉じる" }
47 | }
48 |
--------------------------------------------------------------------------------
/_locales/ru/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": { "message": "TST More Tree Commands" },
3 | "extensionDescription": { "message": "Команды группировки вкладок для Tree Style Tab." },
4 |
5 | "command_group": { "message": "Создать группу из выбранных" },
6 | "command_ungroup": { "message": "Разгруппировать" },
7 | "command_flatten": { "message": "Сровнять подуровни" },
8 | "command_indent": { "message": "Отступ для текущей" },
9 | "command_outdent": { "message": "Убрать отступ" },
10 | "command_toggle": { "message": "Свернуть / развернуть" },
11 | "command_tabbarLinesUp": { "message": "Прокрутить вверх" },
12 | "command_tabbarLinesDown": { "message": "Прокрутить вниз" },
13 | "command_tabbarStopScroll": { "message": "Отмена прокрутки в процессе работы" },
14 |
15 | "command_moveBeforePreviousSibling": { "message": "Выше в дереве" },
16 | "command_moveAfterNextSibling": { "message": "Ниже в дереве" },
17 |
18 | "context_moreTreeCommands_label": { "message": "Группировка" },
19 | "context_group_label": { "message": "Создать &группу" },
20 | "context_group_command": { "message": "Создать группу" },
21 | "context_ungroup_label": { "message": "&Разгруппировать" },
22 | "context_ungroup_command": { "message": "Разгруппировать" },
23 | "context_flatten_label": { "message": "&Сровнять подуровни" },
24 | "context_flatten_command": { "message": "Сровнять подуровни" },
25 | "context_indent_label": { "message": "&Отступ для текущей" },
26 | "context_indent_command": { "message": "Отступ для текущей" },
27 | "context_outdent_label": { "message": "&Убрать отступ текущей" },
28 | "context_outdent_command": { "message": "Убрать отступ текущей" },
29 | "context_moveBeforePreviousSibling_label": { "message": "&Выше в дереве" },
30 | "context_moveBeforePreviousSibling_command": { "message": "Выше в дереве" },
31 | "context_moveAfterNextSibling_label": { "message": "&Ниже в дереве" },
32 | "context_moveAfterNextSibling_command": { "message": "Ниже в дереве" },
33 |
34 |
35 | "config_title": { "message": "Настройки TST More Tree Commands" },
36 |
37 | "config_contextMenuTopLevelCommand_label": { "message": "Команды в контекстном меню" },
38 | "config_contextMenuTopLevelCommand_all": { "message": "Все команды в подменю" },
39 |
40 | "config_tabbarScrollLines_label_before": { "message": "Прокручивать вверх или вниз на " },
41 | "config_tabbarScrollLines_label_after": { "message": " строк командами \"Прокрутить вверх / вниз\" ( * Это не влияет на встроенные команды TST)" },
42 |
43 | "config_tabbarScrollDuration_label_before": { "message": "в течение " },
44 | "config_tabbarScrollDuration_label_after": { "message": "мс" },
45 |
46 | "config_cleanupGroupTabsAfterFlattenTree_label": { "message": "Удалять лишние групповые вкладки после сравнивания подуровней" }
47 | }
48 |
--------------------------------------------------------------------------------
/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": { "message": "TST More Tree Commands" },
3 | "extensionDescription": { "message": "Provides extra tree manipulation commands for Tree Style Tab." },
4 |
5 | "command_group": { "message": "Create New Group from Selected Tabs" },
6 | "command_ungroup": { "message": "Ungroup Tabs" },
7 | "command_flatten": { "message": "Flatten Tree" },
8 | "command_indent": { "message": "Indent Current Tab" },
9 | "command_outdent": { "message": "Outdent Current Tab" },
10 | "command_toggle": { "message": "Toggle Collapsed State of Tree" },
11 | "command_tabbarLinesUp": { "message": "Scroll Tabs Up by Some Lines" },
12 | "command_tabbarLinesDown": { "message": "Scroll Tabs Down by Some Lines" },
13 | "command_tabbarStopScroll": { "message": "Cancel In-Progress Scroll of Tabs" },
14 |
15 | "command_moveBeforePreviousSibling": { "message": "Move Current Tab Before Previous Sibling" },
16 | "command_moveAfterNextSibling": { "message": "Move Current Tab After Next Sibling" },
17 |
18 | "context_moreTreeCommands_label": { "message": "Operate Tree" },
19 | "context_group_label": { "message": "Create New &Group from tabs" },
20 | "context_group_command": { "message": "Create New Group from tabs" },
21 | "context_ungroup_label": { "message": "&Ungroup tabs" },
22 | "context_ungroup_command": { "message": "Ungroup tabs" },
23 | "context_flatten_label": { "message": "&Flatten tree" },
24 | "context_flatten_command": { "message": "Flatten tree" },
25 | "context_indent_label": { "message": "&Indent" },
26 | "context_indent_command": { "message": "Indent" },
27 | "context_outdent_label": { "message": "&Outdent" },
28 | "context_outdent_command": { "message": "Outdent" },
29 | "context_moveBeforePreviousSibling_label": { "message": "Move &before previous sibling" },
30 | "context_moveBeforePreviousSibling_command": { "message": "Move before previous sibling" },
31 | "context_moveAfterNextSibling_label": { "message": "Move &after next sibling" },
32 | "context_moveAfterNextSibling_command": { "message": "Move after next sibling" },
33 |
34 |
35 | "config_title": { "message": "TST More Tree Commands Options" },
36 |
37 | "config_contextMenuTopLevelCommand_label": { "message": "Visible command in the context menu" },
38 | "config_contextMenuTopLevelCommand_all": { "message": "All (under a submenu)" },
39 |
40 | "config_tabbarScrollLines_label_before": { "message": "Scroll up or down " },
41 | "config_tabbarScrollLines_label_after": { "message": "lines via \"Scroll Tabs Up/Down by Some Lines\" shortcut commands (*This does not affect to TST's built-in shortcuts)" },
42 |
43 | "config_tabbarScrollDuration_label_before": { "message": "within " },
44 | "config_tabbarScrollDuration_label_after": { "message": "msec." },
45 |
46 | "config_cleanupGroupTabsAfterFlattenTree_label": { "message": "Clean up needless group tabs after a tree is flattened" }
47 | }
48 |
--------------------------------------------------------------------------------
/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 | __MSG_config_title__
11 |
12 |
13 |
16 |
36 |
37 |
42 |
48 |
49 |
52 |
53 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/tools/eslint/for-module.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | /*eslint-env commonjs*/
7 | /*eslint quote-props: ['error', "always"] */
8 |
9 | 'use strict';
10 |
11 | module.exports = {
12 | 'parserOptions': {
13 | 'sourceType': 'module',
14 | },
15 |
16 | 'plugins': [
17 | 'import',
18 | ],
19 |
20 | 'rules': {
21 | 'no-const-assign': 'error',
22 | 'prefer-const': ['warn', {
23 | 'destructuring': 'any',
24 | 'ignoreReadBeforeAssign': false
25 | }],
26 | 'no-var': 'error',
27 | 'no-unused-vars': ['warn', { // Not make an error for debugging.
28 | 'vars': 'all',
29 | 'args': 'after-used',
30 | 'argsIgnorePattern': '^_',
31 | 'caughtErrors': 'all',
32 | 'caughtErrorsIgnorePattern': '^_', // Allow `catch (_e) {...}`
33 | }],
34 | 'no-use-before-define': ['error', { // the measure for Temporary Dead Zone
35 | 'functions': false, // Function declarations are hoisted.
36 | 'classes': true, // Class declarations are not hoisted. We should warn it.
37 | }],
38 | 'no-unused-expressions': 'error',
39 | 'no-unused-labels': 'error',
40 | 'no-undef': ['error', {
41 | 'typeof': true,
42 | }],
43 |
44 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/default.md
45 | 'import/default': 'error',
46 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/namespace.md
47 | 'import/namespace': 'error',
48 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-duplicates.md
49 | 'import/no-duplicates': 'error',
50 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/export.md
51 | 'import/export': 'error',
52 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/extensions.md
53 | 'import/extensions': ['error', 'always'],
54 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/first.md
55 | 'import/first': 'error',
56 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/named.md
57 | 'import/named': 'error',
58 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-named-as-default.md
59 | 'import/no-named-as-default': 'error',
60 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-named-as-default-member.md
61 | 'import/no-named-as-default-member': 'error',
62 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-cycle.md
63 | 'import/no-cycle': ['warn', {
64 | // If we comment out this, `maxDepth` is `Infinity`.
65 | //'maxDepth': 1,
66 | }],
67 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-webpack-loader-syntax.md
68 | 'import/no-self-import': 'error',
69 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unresolved.md
70 | 'import/no-unresolved': ['error', {
71 | 'caseSensitive': true,
72 | }],
73 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-useless-path-segments.md
74 | 'import/no-useless-path-segments': 'error',
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/background/background.js:
--------------------------------------------------------------------------------
1 | /*
2 | # This Source Code Form is subject to the terms of the Mozilla Public
3 | # License, v. 2.0. If a copy of the MPL was not distributed with this
4 | # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 | 'use strict';
7 |
8 | import {
9 | configs,
10 | TST_ID,
11 | WS_ID,
12 | callTSTAPI,
13 | } from '/common/common.js';
14 |
15 |
16 | async function registerToTST() {
17 | try {
18 | //const base = `moz-extension://${location.host}`;
19 | await callTSTAPI({
20 | type: 'register-self',
21 | name: browser.i18n.getMessage('extensionName'),
22 | //icons: browser.runtime.getManifest().icons,
23 | });
24 | }
25 | catch(_error) {
26 | // TST is not available
27 | }
28 | }
29 | registerToTST();
30 |
31 | browser.runtime.onMessageExternal.addListener((message, sender) => {
32 | switch (sender.id) {
33 | case TST_ID:
34 | case WS_ID:
35 | switch (message.type) {
36 | case 'ready':
37 | registerToTST();
38 | break;
39 | }
40 | break;
41 | }
42 | });
43 |
44 |
45 | const menuItemDefinitionsById = {
46 | topLevel_moreTreeCommands: {
47 | title: browser.i18n.getMessage('context_moreTreeCommands_label'),
48 | contexts: ['tab'],
49 | visible: false,
50 | enabled: true
51 | },
52 | group: {
53 | parentId: 'topLevel_moreTreeCommands',
54 | title: browser.i18n.getMessage('context_group_label'),
55 | contexts: ['tab'],
56 | visible: true,
57 | enabled: false
58 | },
59 | ungroup: {
60 | parentId: 'topLevel_moreTreeCommands',
61 | title: browser.i18n.getMessage('context_ungroup_label'),
62 | contexts: ['tab'],
63 | visible: true,
64 | enabled: false
65 | },
66 | flatten: {
67 | parentId: 'topLevel_moreTreeCommands',
68 | title: browser.i18n.getMessage('context_flatten_label'),
69 | contexts: ['tab'],
70 | visible: true,
71 | enabled: false
72 | },
73 | separatorAfterGrouping: {
74 | parentId: 'topLevel_moreTreeCommands',
75 | type: 'separator',
76 | contexts: ['tab'],
77 | visible: true,
78 | enabled: true
79 | },
80 | indent: {
81 | parentId: 'topLevel_moreTreeCommands',
82 | title: browser.i18n.getMessage('context_indent_label'),
83 | contexts: ['tab'],
84 | visible: true,
85 | enabled: true
86 | },
87 | outdent: {
88 | parentId: 'topLevel_moreTreeCommands',
89 | title: browser.i18n.getMessage('context_outdent_label'),
90 | contexts: ['tab'],
91 | visible: true,
92 | enabled: false
93 | },
94 | separatorAfterIndentOutdent: {
95 | parentId: 'topLevel_moreTreeCommands',
96 | type: 'separator',
97 | contexts: ['tab'],
98 | visible: true,
99 | enabled: true
100 | },
101 | moveBeforePreviousSibling: {
102 | parentId: 'topLevel_moreTreeCommands',
103 | title: browser.i18n.getMessage('context_moveBeforePreviousSibling_label'),
104 | contexts: ['tab'],
105 | visible: true,
106 | enabled: false
107 | },
108 | moveAfterNextSibling: {
109 | parentId: 'topLevel_moreTreeCommands',
110 | title: browser.i18n.getMessage('context_moveAfterNextSibling_label'),
111 | contexts: ['tab'],
112 | visible: true,
113 | enabled: false
114 | },
115 |
116 | topLevel_group: {
117 | title: browser.i18n.getMessage('context_group_label'),
118 | contexts: ['tab'],
119 | visible: false,
120 | enabled: false
121 | },
122 | topLevel_ungroup: {
123 | title: browser.i18n.getMessage('context_ungroup_label'),
124 | contexts: ['tab'],
125 | visible: false,
126 | enabled: false
127 | },
128 | topLevel_flatten: {
129 | title: browser.i18n.getMessage('context_flatten_label'),
130 | contexts: ['tab'],
131 | visible: false,
132 | enabled: false
133 | },
134 | topLevel_indent: {
135 | title: browser.i18n.getMessage('context_indent_label'),
136 | contexts: ['tab'],
137 | visible: false,
138 | enabled: true
139 | },
140 | topLevel_outdent: {
141 | title: browser.i18n.getMessage('context_outdent_label'),
142 | contexts: ['tab'],
143 | visible: false,
144 | enabled: false
145 | },
146 | topLevel_moveBeforePreviousSibling: {
147 | title: browser.i18n.getMessage('context_moveBeforePreviousSibling_label'),
148 | contexts: ['tab'],
149 | visible: false,
150 | enabled: false
151 | },
152 | topLevel_moveAfterNextSibling: {
153 | title: browser.i18n.getMessage('context_moveAfterNextSibling_label'),
154 | contexts: ['tab'],
155 | visible: false,
156 | enabled: false
157 | }
158 | };
159 | for (const [id, definition] of Object.entries(menuItemDefinitionsById)) {
160 | const params = {
161 | id,
162 | title: definition.title,
163 | type: definition.type || 'normal',
164 | contexts: definition.contexts,
165 | visible: definition.visible,
166 | enabled: definition.enabled
167 | };
168 | if (definition.parentId)
169 | params.parentId = definition.parentId;
170 | browser.menus.create(params);
171 | }
172 |
173 | browser.menus.onShown.addListener(async (info, tab) => {
174 | const miltiselectedTabs = await getMultiselectedTabs(tab);
175 | const treeItems = await getTreeItems(miltiselectedTabs);
176 | const rootItems = collectRootItems(treeItems);
177 | console.log({miltiselectedTabs, treeItems, rootItems});
178 |
179 | let previousSiblingItem, nextSiblingItem;
180 | // The "Move Before/After Previous/Next Sibling" commands only support single
181 | // selections (or else behavior would be unpredictable depending on the order
182 | // of operations).
183 | if (miltiselectedTabs.length === 1 && treeItems.length === 1) {
184 | [previousSiblingItem, nextSiblingItem] = await Promise.all([
185 | getRelatedTreeItem(tab, 'previousSibling'),
186 | getRelatedTreeItem(tab, 'nextSibling')
187 | ]);
188 | }
189 |
190 | let modified = false;
191 | for (const [id, definition] of Object.entries(menuItemDefinitionsById)) {
192 | const name = id.replace(/^topLevel_/, '');
193 | const visible = id.startsWith('topLevel_') ? configs.contextMenuTopLevelCommand == name : true;
194 | if (!visible && !definition.visible)
195 | continue;
196 |
197 | const changes = {};
198 |
199 | if (definition.visible != visible)
200 | changes.visible = definition.visible = visible;
201 |
202 | let enabled;
203 | switch (name) {
204 | case 'group':
205 | enabled = miltiselectedTabs.length > 1 || !treeItems.some(tab => tab.states.includes('group-tab'));
206 | break;
207 |
208 | case 'ungroup':
209 | enabled = treeItems.some(tab => tab.states.includes('group-tab'));
210 | break;
211 |
212 | case 'flatten':
213 | enabled = treeItems.some(tab => tab.children.length > 0);
214 | break;
215 |
216 | case 'outdent':
217 | enabled = rootItems.some(tab => tab.ancestorTabIds.length > 0);
218 | break;
219 |
220 | case 'moveBeforePreviousSibling':
221 | enabled = previousSiblingItem != null;
222 | break;
223 |
224 | case 'moveAfterNextSibling':
225 | enabled = nextSiblingItem != null;
226 | break;
227 |
228 | default:
229 | break;
230 | }
231 | if (definition.enabled != enabled)
232 | changes.enabled = definition.enabled = enabled;
233 |
234 | if (Object.keys(changes).length == 0)
235 | continue;
236 |
237 | browser.menus.update(id, changes);
238 | modified = true;
239 | }
240 | if (modified)
241 | browser.menus.refresh();
242 | });
243 |
244 | browser.menus.onClicked.addListener(async (info, tab) => {
245 | // Extra context menu commands won't be available on the blank area of the tab bar.
246 | if (!tab)
247 | return;
248 |
249 | const miltiselectedTabs = await getMultiselectedTabs(tab);
250 |
251 | switch (info.menuItemId.replace(/^topLevel_/, '')) {
252 | case 'group': {
253 | if (miltiselectedTabs.length > 1) {
254 | await group(miltiselectedTabs);
255 | } else {
256 | const treeItems = await getTreeItems(miltiselectedTabs);
257 | const descendantItems = collectDescendantItems(treeItems);
258 | await group(descendantItems);
259 | }
260 | return;
261 | }
262 | case 'ungroup':
263 | await ungroup(miltiselectedTabs);
264 | return;
265 | case 'flatten':
266 | await flatten(miltiselectedTabs);
267 | return;
268 |
269 | case 'indent':
270 | await indent(miltiselectedTabs);
271 | return;
272 | case 'outdent':
273 | await outdent(miltiselectedTabs);
274 | return;
275 |
276 | case 'moveBeforePreviousSibling':
277 | if (miltiselectedTabs.length === 1) {
278 | await moveBeforePreviousSibling(tab);
279 | }
280 | return;
281 | case 'moveAfterNextSibling':
282 | if (miltiselectedTabs.length === 1) {
283 | await moveAfterNextSibling(tab);
284 | }
285 | return;
286 |
287 | default:
288 | break;
289 | }
290 | });
291 |
292 | browser.commands.onCommand.addListener(async command => {
293 | const activeTabs = await browser.tabs.query({
294 | active: true,
295 | currentWindow: true
296 | });
297 | const activeTab = activeTabs[0];
298 | const miltiselectedTabs = await getMultiselectedTabs(activeTab);
299 |
300 | switch (command) {
301 | case 'group':
302 | await group(miltiselectedTabs);
303 | return;
304 | case 'ungroup':
305 | await ungroup(miltiselectedTabs);
306 | return;
307 | case 'flatten':
308 | await flatten(miltiselectedTabs);
309 | return;
310 |
311 | case 'indent':
312 | await indent(miltiselectedTabs);
313 | return;
314 | case 'outdent':
315 | await outdent(miltiselectedTabs);
316 | return;
317 |
318 | case 'moveBeforePreviousSibling':
319 | if (miltiselectedTabs.length === 1) {
320 | await moveBeforePreviousSibling(activeTab);
321 | }
322 | return;
323 | case 'moveAfterNextSibling':
324 | if (miltiselectedTabs.length === 1) {
325 | await moveAfterNextSibling(activeTab);
326 | }
327 | return;
328 |
329 | case 'tabbarLinesDown':
330 | callTSTAPI({
331 | type: 'scroll',
332 | window: 'active',
333 | delta: `var(--tab-size) * ${configs.tabbarScrollLines}`,
334 | duration: configs.tabbarScrollDuration,
335 | });
336 | return;
337 |
338 | case 'tabbarLinesUp':
339 | callTSTAPI({
340 | type: 'scroll',
341 | window: 'active',
342 | delta: `0px - var(--tab-size) * ${configs.tabbarScrollLines}`,
343 | duration: configs.tabbarScrollDuration,
344 | });
345 | return;
346 |
347 | case 'tabbarStopScroll':
348 | callTSTAPI({
349 | type: 'stop-scroll',
350 | window: 'active',
351 | });
352 | return;
353 |
354 | case 'toggle':
355 | await toggle(miltiselectedTabs);
356 | return;
357 | }
358 | });
359 |
360 | async function getMultiselectedTabs(tab) {
361 | if (!tab)
362 | return [];
363 | if (tab.highlighted)
364 | return browser.tabs.query({
365 | windowId: tab.windowId,
366 | highlighted: true
367 | });
368 | else
369 | return [tab];
370 | }
371 |
372 | async function getTreeItems(tabs) {
373 | return tabs.length < 1 ? [] : callTSTAPI({
374 | type: 'get-tree',
375 | tabs: tabs.map(tab => tab.id)
376 | });
377 | }
378 |
379 | async function getRelatedTreeItem(tab, relation) {
380 | return callTSTAPI({
381 | type: 'get-tree',
382 | tab: `${relation}-of-${tab.id}`
383 | });
384 | }
385 |
386 | function collectRootItems(tabs) {
387 | const allIds = tabs.map(tab => tab.id);
388 | // extract only top level items
389 | return tabs.filter(tab => new Set([...tab.ancestorTabIds, ...allIds]).size == tab.ancestorTabIds.length + allIds.length);
390 | }
391 |
392 | function collectDescendantItems(treeItems) {
393 | const descendantItems = [...treeItems];
394 | let currentIndex = 0;
395 | while (currentIndex < descendantItems.length) {
396 | const currentItem = descendantItems[currentIndex++];
397 | // Splicing in the children immediately after the current tree item results
398 | // in a depth-first ordering. Pushing all children to the end would instead
399 | // result in a breadth-first ordering. Because various TST commands (like
400 | // move-to-end) expect a depth-first ordering of tabs, we prefer the former
401 | // here.
402 | descendantItems.splice(currentIndex, 0, ...currentItem.children);
403 | }
404 | return descendantItems;
405 | }
406 |
407 | async function group(tabs) {
408 | if (tabs.length >= 1)
409 | await callTSTAPI({
410 | type: 'group-tabs',
411 | tabs: tabs.map(tab => tab.id)
412 | });
413 | }
414 |
415 | function collectTabIds(tabs, { tabIds, includeParent } = {}) {
416 | if (!tabIds)
417 | tabIds = new Set();
418 | for (const tab of tabs) {
419 | tabIds.add(tab.id);
420 | if (includeParent &&
421 | tab.ancestorTabIds.length > 0)
422 | tabIds.add(tab.ancestorTabIds[0]);
423 | if (tab.children)
424 | collectTabIds(tab.children, { tabIds, includeParent });
425 | }
426 | return tabIds;
427 | }
428 |
429 | async function ungroup(tabs) {
430 | const treeItems = await callTSTAPI({
431 | type: 'get-tree',
432 | tabs: tabs.map(tab => tab.id)
433 | });
434 | const parentTabs = treeItems.filter(item => item.children.length > 0);
435 | await flattenInternal(parentTabs, {
436 | shouldDetachAll: treeItems.some(item => item.ancestorTabIds.length == 0),
437 | cleanupGroupTabs: true
438 | });
439 | }
440 |
441 | async function flatten(tabs) {
442 | const treeItems = await callTSTAPI({
443 | type: 'get-tree',
444 | tabs: tabs.map(tab => tab.id)
445 | })
446 | const parentTabs = treeItems.filter(item => item.children.length > 0);
447 | await flattenInternal(parentTabs, {
448 | shouldDetachAll: treeItems.some(item => item.ancestorTabIds.length == 0),
449 | recursively: true,
450 | cleanupGroupTabs: configs.cleanupGroupTabsAfterFlattenTree
451 | });
452 | }
453 | async function flattenInternal(tabs, { targetTabIds, shouldDetachAll, recursively, cleanupGroupTabs, insertBefore, allTabIds }) {
454 | if (!targetTabIds)
455 | targetTabIds = collectTabIds(tabs, { includeParent: true });
456 | if (!insertBefore)
457 | insertBefore = { id: null };
458 | if (!allTabIds)
459 | allTabIds = (await browser.tabs.query({ windowId: tabs[0].windowId })).map(tab => tab.id);
460 |
461 | for (const tab of tabs.slice(0).reverse()) {
462 | if (!tab)
463 | continue;
464 |
465 | const index = allTabIds.indexOf(tab.id);
466 | if (index < allTabIds.length - 1)
467 | insertBefore.id = allTabIds[index + 1];
468 | else
469 | insertBefore.id = null;
470 |
471 | const children = tab.children.slice(0).reverse();
472 | if (recursively)
473 | await flattenInternal(children, { targetTabIds, allTabIds, shouldDetachAll, recursively, insertBefore });
474 | const topLevelParent = !shouldDetachAll && tab.ancestorTabIds.reverse().find(id => targetTabIds.has(id));
475 | for (const child of children) {
476 | if (!child)
477 | continue;
478 | if (topLevelParent) {
479 | await callTSTAPI({
480 | type: 'attach',
481 | parent: topLevelParent,
482 | child: child.id,
483 | insertBefore: insertBefore.id
484 | });
485 | }
486 | else {
487 | await callTSTAPI({
488 | type: 'detach',
489 | tab: child.id
490 | });
491 | }
492 | insertBefore.id = child.id;
493 | }
494 | tab.children = [];
495 | }
496 |
497 | if (cleanupGroupTabs) {
498 | const groupTabs = tabs.filter(tab => tab.states.includes('group-tab'));
499 | if (groupTabs.length > 0)
500 | browser.tabs.remove(groupTabs.map(tab => tab.id));
501 | }
502 | }
503 |
504 | async function indent(tabs) {
505 | await callTSTAPI({
506 | type: 'indent',
507 | tab: tabs[0].id,
508 | followChildren: true
509 | });
510 | }
511 |
512 | async function outdent(tabs) {
513 | await callTSTAPI({
514 | type: 'outdent',
515 | tab: tabs[0].id,
516 | followChildren: true
517 | });
518 | }
519 |
520 | async function toggle(tabs) {
521 | await Promise.all(tabs.map(tab => callTSTAPI({
522 | type: 'toggle-tree-collapsed',
523 | tab: tab.id
524 | })));
525 | }
526 |
527 | async function moveBefore(tab, referenceTab) {
528 | await callTSTAPI({
529 | type: 'move-before',
530 | tab: tab.id,
531 | referenceTabId: referenceTab.id,
532 | followChildren: true
533 | });
534 | }
535 |
536 | async function moveToEnd(tabs) {
537 | await callTSTAPI({
538 | type: 'move-to-end',
539 | tabs: tabs.map(tab => tab.id)
540 | });
541 | }
542 |
543 | async function moveBeforePreviousSibling(tab) {
544 | const previousSiblingItem = await getRelatedTreeItem(tab, 'previousSibling');
545 | if (previousSiblingItem != null) {
546 | await moveBefore(tab, previousSiblingItem);
547 | }
548 | }
549 |
550 | async function moveAfterNextSibling(tab) {
551 | const nextSiblingItem = await getRelatedTreeItem(tab, 'nextSibling');
552 | if (nextSiblingItem == null) {
553 | return;
554 | }
555 |
556 | // Naively telling TST to move the tab after its next sibling produces
557 | // unexpected tree structures.
558 |
559 | // In most cases, we move-before the next sibling of the tab's next sibling
560 | // instead.
561 | const nextNextSiblingItem = await getRelatedTreeItem(nextSiblingItem, 'nextSibling');
562 | if (nextNextSiblingItem != null) {
563 | return moveBefore(tab, nextNextSiblingItem);
564 | }
565 |
566 | let newIndentationLevel = 0;
567 | const desiredIndentationLevel = nextSiblingItem.indent;
568 |
569 | // Otherwise, we look for the next tab after the tab's next sibling's last
570 | // descendant.
571 | let nextSiblingLastDescendantItem = nextSiblingItem;
572 | while (nextSiblingLastDescendantItem.children.length > 0) {
573 | nextSiblingLastDescendantItem = nextSiblingLastDescendantItem.children[nextSiblingLastDescendantItem.children.length - 1];
574 | }
575 | const nextOfNextSiblingLastDescendantItem = await getRelatedTreeItem(nextSiblingLastDescendantItem, 'next');
576 | if (nextOfNextSiblingLastDescendantItem != null) {
577 | await moveBefore(tab, nextOfNextSiblingLastDescendantItem);
578 | newIndentationLevel = nextOfNextSiblingLastDescendantItem.indent;
579 | } else {
580 | // If there's no tab following the tab's next sibling's last descendant, then
581 | // we can do a move-to-end to get what we want.
582 | const treeItems = await getTreeItems([tab]);
583 | await moveToEnd(collectDescendantItems(treeItems));
584 | }
585 |
586 | // Doing a move-before moves the tab to the indentation level of the reference
587 | // tab; similarly, a move-to-end deindents the tab entirely. We do a series of
588 | // indents here to shift the tab back to the indentation level of its next
589 | // sibling.
590 | const deltaIndentationLevel = desiredIndentationLevel - newIndentationLevel;
591 | for (let i = 0; i < deltaIndentationLevel; i++) {
592 | await indent([tab]);
593 | }
594 | }
595 |
--------------------------------------------------------------------------------