├── README.md ├── _locales ├── en │ └── messages.json └── zh │ └── messages.json ├── background.js ├── common.js ├── manifest.json └── options ├── options.css ├── options.html └── options.js /README.md: -------------------------------------------------------------------------------- 1 | # Move unloaded tabs for Tree Style Tab 2 | 3 | This is a Firfox extension that allows tabs to be moved without becoming active when using drag and drop in the Firefox extension "Tree Stlye Tab". 4 | 5 | Link: [Move unloaded tabs for Tree Style Tab on addons.mozilla.org](https://addons.mozilla.org/firefox/addon/move-unloaded-tabs-for-tst) 6 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "options_section_LongPress": { 3 | "message": "Long pressed tabs" 4 | }, 5 | "options_section_LongPress_PreventDragAndDrop": { 6 | "message": "Prevent drag and drop" 7 | }, 8 | "options_section_DragAndDrop": { 9 | "message": "Drag and dropped tabs" 10 | }, 11 | "options_section_CustomDrag": { 12 | "message": "Custom dragged tabs" 13 | }, 14 | "options_PreventOnlyUnloadedTabs": { 15 | "message": "Ignore clicks on loaded tabs and only prevent unloaded tabs from becoming active." 16 | }, 17 | "options_DetectLongPress": { 18 | "message": "Detect if a tab is long pressed." 19 | }, 20 | "options_longPressTimeInMilliseconds": { 21 | "message": "Time between \"mouse down\" and \"mouse up\" events for a click to register as a long press: " 22 | }, 23 | "options_preventDragAndDropAfterLongPress": { 24 | "message": "Prevent long pressed tabs from being drag and dropped." 25 | }, 26 | "options_preventDragAndDropAfterLongPress_Legacy": { 27 | "message": "Use older method of stopping long pressed tabs from being drag and dropped. This should be used if the Tree Style Tab version is 2.7.7 or earlier. When this method is used the time between the \"mouse down\" event and the \"mouse up\" event is controller by Tree Style Tab and it defaults to 400ms." 28 | }, 29 | "options_PreventOnLongPress": { 30 | "message": "Prevent long pressed tabs from becoming active. Otherwise long pressed tabs should always become active." 31 | }, 32 | "options_detectDragAndDrop": { 33 | "message": "Detect if a drag and drop operation is started for the clicked tab. (Requires Tree Style Tab version 2.7.8 or later.)" 34 | }, 35 | "options_preventDragAndDroppedTabs": { 36 | "message": "Prevent drag and dropped tabs from becoming active. Otherwise drag and dropped tabs should always become active." 37 | }, 38 | "options_detectCustomDrag": { 39 | "message": "Detect if a drag operation is started by another addon for the the clicked tab. The operation is started when the mouse is moved over to another tab and an addon overrides normal drag and drop behavior. An addon that can trigger this event is Multiple Tab Handler which allows selecting multiple tabs by long pressing a tab and then dragging to select more." 40 | }, 41 | "options_preventCustomDraggedTabs": { 42 | "message": "Prevent custom dragged tabs from becoming active. Otherwise custom dragged tabs should always become active." 43 | }, 44 | "options_Milliseconds": { 45 | "message": "milliseconds." 46 | }, 47 | "options_resetSettings": { 48 | "message": "Reset Settings" 49 | }, 50 | "options_resetSettings_Prompt": { 51 | "message": "Do you want to reset all settings to default values?" 52 | } 53 | } -------------------------------------------------------------------------------- /_locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "options_section_LongPress": { 3 | "message": "长按标签页", 4 | "hash": "8d206e574c351e7a16b2af37943e4c3b" 5 | }, 6 | "options_section_LongPress_PreventDragAndDrop": { 7 | "message": "阻止拖放", 8 | "hash": "e64a9cda2e5a6ff4d0c1997317e2bdd9" 9 | }, 10 | "options_section_DragAndDrop": { 11 | "message": "拖放标签页", 12 | "hash": "5d11752662d240d2b28f117f292f5d22" 13 | }, 14 | "options_section_CustomDrag": { 15 | "message": "自定义拖动标签页", 16 | "hash": "68d217ba06d8d1d88155b6ed33095a04" 17 | }, 18 | "options_PreventOnlyUnloadedTabs": { 19 | "message": "忽略对已加载标签页的点击,只阻止未加载标签页变为活动状态。", 20 | "hash": "cfee34dbed940793b645f9d174f2ad96" 21 | }, 22 | "options_DetectLongPress": { 23 | "message": "检测标签页是否被长按", 24 | "hash": "4ae7fa3c85852347dc5a2b7bd9ef2867" 25 | }, 26 | "options_longPressTimeInMilliseconds": { 27 | "message": "当一次点击中 “鼠标按下” 和 “鼠标松开” 事件的间隔时间大于该时间时,其会被当做一次长按触发相应操作:", 28 | "hash": "d94347c33d327639b47c28abef224446" 29 | }, 30 | "options_preventDragAndDropAfterLongPress": { 31 | "message": "阻止长按状态下的标签页被拖放。", 32 | "hash": "2b124eca37ddca4d7b7ff0cdb05dfcca" 33 | }, 34 | "options_preventDragAndDropAfterLongPress_Legacy": { 35 | "message": "使用较旧的方法来阻止长按状态下的标签页被拖放。如果 树状标签页 的版本为 2.7.7 或者更早,则此项应该被使用。当该方法被使用时, “鼠标按下” 和 “鼠标松开” 事件的间隔时间,会交由 树状标签页 来控制,默认会被设置到 400 毫秒。", 36 | "hash": "4b095d3bc86fcfc5b69d492407c63551" 37 | }, 38 | "options_PreventOnLongPress": { 39 | "message": "阻止标签页在被长按时变为活动状态。否则这些标签页将总会在被长按时变为活动状态。", 40 | "hash": "ea93ba3446f529b4ff4f47a9368a25fd" 41 | }, 42 | "options_detectDragAndDrop": { 43 | "message": "当一个标签页被点击时,检测其是否已开始拖放操作。(这需要 树状标签页 版本为 2.7.8 或者更高)", 44 | "hash": "ad19c4740516985a3f0d815240e99f83" 45 | }, 46 | "options_preventDragAndDroppedTabs": { 47 | "message": "阻止标签页在被拖放时变为活动状态。否则这些标签页将总会在被拖放时变为活动状态。", 48 | "hash": "ddd4f6c8b169b8df445cc65a1db53d47" 49 | }, 50 | "options_detectCustomDrag": { 51 | "message": "当标签页被点击时,检测是否有其它附加组件对其开始了拖动操作。当鼠标被移到另一个标签页,且有附加组件覆盖了正常的拖放行为时,该操作会被开启。一个可以触发此事件的附加组件为 多标签页处理 ,其允许通过长按一个标签页然后进行拖动来选中多个标签页。", 52 | "hash": "4c55ec5afa6862e468daf2aba7997dcf" 53 | }, 54 | "options_preventCustomDraggedTabs": { 55 | "message": "阻止标签页在被自定义拖动时变为活动状态。否则这些标签页将在总会在被自定义拖动时变为活动状态。", 56 | "hash": "2080dcce8e579d2e6f6cc0c612833f11" 57 | }, 58 | "options_Milliseconds": { 59 | "message": "毫秒。", 60 | "hash": "8764b81b03b30c5b729f931277c7b4d7" 61 | }, 62 | "options_resetSettings": { 63 | "message": "重置设置", 64 | "hash": "f5545c85577b03ed93d451abf748ce8d" 65 | }, 66 | "options_resetSettings_Prompt": { 67 | "message": "您想要重置所有设置到默认值吗?", 68 | "hash": "efb0732b8fbad8ba229adacec8e59350" 69 | }, 70 | 71 | "__WET_LOCALE__": { "message": "zh" } 72 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | 2 | const kTST_ID = 'treestyletab@piro.sakura.ne.jp'; 3 | 4 | 5 | // #region Settings 6 | 7 | const settings = getDefaultSettings(); 8 | let changed = {}; 9 | function applySettingChanges(target, changes, fallbackToDefault = true) { 10 | try { 11 | let defaultSettings = null; 12 | for (const [key, value] of Object.entries(changes)) { 13 | if ('newValue' in value) { 14 | target[key] = value.newValue; 15 | } else { 16 | if (fallbackToDefault) { 17 | if (!defaultSettings) { 18 | defaultSettings = getDefaultSettings(); 19 | } 20 | target[key] = defaultSettings[key]; 21 | } else { 22 | delete target[key]; 23 | } 24 | } 25 | } 26 | } catch (error) { 27 | console.error('Failed to update settings!\n', error); 28 | } 29 | } 30 | browser.storage.onChanged.addListener((changes, areaName) => { 31 | applySettingChanges(settings, changes); 32 | if (changed) { 33 | // Settings not loaded yet: 34 | applySettingChanges(changed, changes); 35 | } else { 36 | if ( 37 | changes.detectDragAndDrop || 38 | changes.detectCustomDrag || 39 | changes.detectLongPressedTabs || 40 | changes.preventDragAndDropAfterLongPress || 41 | changes.preventDragAndDropAfterLongPress_Legacy 42 | ) { 43 | registerToTST(); 44 | } 45 | } 46 | }); 47 | const settingsLoaded = browser.storage.local.get(null).then((value) => { 48 | let changedKeys = Object.keys(changed); 49 | for (let key of Object.keys(value)) { 50 | if (!changedKeys.includes(key)) { 51 | settings[key] = value[key]; 52 | } 53 | } 54 | changed = null; 55 | }); 56 | 57 | // #endregion Settings 58 | 59 | 60 | // #region Tree Style Tab 61 | 62 | async function registerToTST() { 63 | try { 64 | await unregisterFromTST(); 65 | 66 | const listeningTypes = ['ready', 'tab-mousedown', 'tab-mouseup']; 67 | 68 | if (settings.detectDragAndDrop) { 69 | listeningTypes.push('native-tab-dragstart'); // Drag and drop of tab started. 70 | } 71 | if (settings.detectCustomDrag) { 72 | listeningTypes.push('tab-dragstart'); // Drag and drop of tab was prevented in favour of custom drag handling by some addon. 73 | } 74 | if (settings.detectLongPressedTabs && settings.preventDragAndDropAfterLongPress && settings.preventDragAndDropAfterLongPress_Legacy) { 75 | listeningTypes.push('tab-dragready'); // If tab is long pressed (time is configured from TST's hidden debug settings and defaults to 400ms) than prevent drag and drop in favour of custom drag handling. 76 | } 77 | 78 | const registrationDetails = { 79 | type: 'register-self', 80 | name: browser.runtime.getManifest().name, 81 | listeningTypes, 82 | }; 83 | await browser.runtime.sendMessage(kTST_ID, registrationDetails); 84 | } catch (error) { return false; } 85 | return true; 86 | } 87 | async function unregisterFromTST() { 88 | try { 89 | await browser.runtime.sendMessage(kTST_ID, { 90 | type: 'unregister-self' 91 | }); 92 | } 93 | catch (e) { 94 | // TST is not available 95 | return false; 96 | } 97 | return true; 98 | } 99 | 100 | // #endregion Tree Style Tab 101 | 102 | 103 | // #region Handle Tree Style Tab Event 104 | 105 | let lastResolve = null; 106 | let longPressTimeoutId = null; 107 | function resolveAs(value) { 108 | if (lastResolve && typeof lastResolve === 'function') { 109 | lastResolve(value); 110 | } 111 | lastResolve = null; 112 | 113 | if (longPressTimeoutId !== null) { 114 | clearTimeout(longPressTimeoutId); 115 | } 116 | longPressTimeoutId = null; 117 | } 118 | 119 | let lastMessage; 120 | let lastPromise; 121 | function handleLongPress(preventDragAndDrop, preventActive) { 122 | if (preventDragAndDrop) { 123 | browser.runtime.sendMessage(kTST_ID, { type: 'start-custom-drag', windowId: lastMessage.windowId }); 124 | } 125 | if (preventActive) { 126 | const resolve = lastResolve; 127 | if (resolve) { 128 | lastResolve = () => resolve(true); 129 | } 130 | } else { 131 | resolveAs(false); 132 | } 133 | } 134 | 135 | browser.runtime.onMessageExternal.addListener((message, sender) => { 136 | if (sender.id !== kTST_ID) { 137 | return; 138 | } 139 | switch (message.type) { 140 | case 'ready': { 141 | // passive registration for secondary (or after) startup: 142 | registerToTST(); 143 | return Promise.resolve(true); 144 | } break; 145 | case 'tab-mousedown': { 146 | if (message.button !== 0) { 147 | break; 148 | } 149 | lastPromise = null; 150 | resolveAs(true); // Ensure last click doesn't select a tab. 151 | if (message.closebox || message.soundButton || message.twisty) { 152 | break; 153 | } 154 | if (settings.preventOnlyForUnloadedTabs && !message.tab.discarded) { 155 | if ('discarded' in message.tab) { 156 | break; 157 | } else { 158 | console.error("Tree Style Tab didn't provide 'discarded' property in tab info."); 159 | } 160 | } 161 | const aPromise = new Promise((resolve, reject) => { 162 | resolveAs(true); 163 | lastResolve = (value) => { 164 | if (value) { 165 | // If block tab select then wait to notify TST about it since that can cause issues with users of the drag APIs such as Multiple Tab Handler. 166 | // This shouldn't make a difference to the user since tab select is blocked while TST waits for the response anyway. 167 | setTimeout(() => resolve(value), 5000); 168 | } else { 169 | resolve(value); 170 | } 171 | }; 172 | lastMessage = message; 173 | if ( 174 | settings.detectLongPressedTabs && 175 | // Not using legacy long press detection: 176 | !(settings.preventDragAndDropAfterLongPress && settings.preventDragAndDropAfterLongPress_Legacy) 177 | ) { 178 | longPressTimeoutId = setTimeout(() => { 179 | longPressTimeoutId = null; 180 | if (settings.detectLongPressedTabs) { 181 | handleLongPress(settings.preventDragAndDropAfterLongPress && !settings.preventDragAndDropAfterLongPress_Legacy, settings.preventLongPressedTabs); 182 | } 183 | }, settings.longPressTimeInMilliseconds); 184 | } 185 | }); 186 | lastPromise = aPromise; 187 | return aPromise; 188 | } break; 189 | case 'tab-mouseup': { 190 | if (message.button !== 0) { 191 | break; 192 | } 193 | const releasedWithoutMove = lastMessage && message.tab.id === lastMessage.tab.id; 194 | const aPromise = lastPromise; 195 | lastPromise = null; 196 | resolveAs(!releasedWithoutMove); 197 | if (aPromise) { 198 | return aPromise; 199 | } 200 | } break; 201 | case 'native-tab-dragstart': { 202 | // Drag and drop of tab started (available in TST 2.7.8 and later): 203 | if (!settings.detectDragAndDrop) { 204 | break; 205 | } 206 | resolveAs(settings.preventDragAndDroppedTabs); // Prevent tab from becoming active. 207 | } break; 208 | case 'tab-dragready': { 209 | if (!settings.detectLongPressedTabs || !settings.preventDragAndDropAfterLongPress || !settings.preventDragAndDropAfterLongPress_Legacy) { 210 | break; 211 | } 212 | handleLongPress(true, settings.preventLongPressedTabs); 213 | } break; 214 | case 'tab-dragstart': { 215 | // Drag and drop of tab was prevented in favour of custom drag handling by some addon and mouse has moved over to another tab: 216 | if (!settings.detectCustomDrag) { 217 | break; 218 | } 219 | resolveAs(settings.preventCustomDraggedTabs); // Prevent tab from becoming active. 220 | } break; 221 | } 222 | return Promise.resolve(false); 223 | }); 224 | 225 | // #endregion Handle Tree Style Tab Event 226 | 227 | 228 | settingsLoaded.finally(() => { 229 | // aggressive registration on initial installation: 230 | if (!registerToTST()) { 231 | setTimeout(registerToTST, 5000); 232 | } 233 | }); 234 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | function getDefaultSettings() { 2 | return { 3 | preventOnlyForUnloadedTabs: false, 4 | 5 | detectLongPressedTabs: false, 6 | longPressTimeInMilliseconds: 400, 7 | preventLongPressedTabs: true, 8 | preventDragAndDropAfterLongPress: false, 9 | preventDragAndDropAfterLongPress_Legacy: false, 10 | 11 | detectDragAndDrop: true, 12 | preventDragAndDroppedTabs: true, 13 | 14 | detectCustomDrag: true, 15 | preventCustomDraggedTabs: true, 16 | }; 17 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Move unloaded tabs for Tree Style Tab", 4 | "version": "2.4", 5 | "applications": { 6 | "gecko": { 7 | "strict_min_version": "57.0", 8 | "id": "{731bf636-c808-4c86-b02f-af462eccc963}" 9 | } 10 | }, 11 | "default_locale": "en", 12 | "permissions": [ 13 | "storage" 14 | ], 15 | "options_ui": { 16 | "page": "options/options.html" 17 | }, 18 | "background": { 19 | "scripts": [ 20 | "common.js", 21 | "background.js" 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /options/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 1.2em; 3 | } 4 | 5 | 6 | *[class*='message'] { 7 | white-space: pre-line; 8 | } 9 | .textSelectable { 10 | -moz-user-select: text; 11 | } 12 | .textNotSelectable { 13 | -moz-user-select: none; 14 | } 15 | 16 | .tstInstructions { 17 | margin-bottom: 30px; 18 | } 19 | 20 | .disabled { 21 | opacity: 0.6; 22 | } 23 | .disabled .disabled { 24 | opacity: 1; 25 | } 26 | 27 | #resetSettingsButton { 28 | padding: 10px; 29 | } -------------------------------------------------------------------------------- /options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 24 |
25 | 26 |
27 |

28 | 29 | 30 | 31 |

32 | 33 |
34 | 35 |
36 | 40 |
41 |
42 | 43 |
44 | 48 |
49 |
50 |
51 | 52 |
53 | 57 |
58 |
59 |
60 |
61 | 62 |
63 | 64 |
65 | 69 |
70 |
71 | 72 |
73 | 77 |
78 |
79 |
80 | 81 |
82 | 83 |
84 | 88 |
89 |
90 | 91 |
92 | 96 |
97 |
98 |
99 | 100 | 101 |
102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /options/options.js: -------------------------------------------------------------------------------- 1 | 2 | const messagePrefix = 'message-'; 3 | const requiresPrefix = 'requires-'; 4 | 5 | 6 | function setTextMessages(elementsToText = null, { asHTML = true } = {}) { 7 | if (!Array.isArray(elementsToText)) { 8 | let rootElement = document; 9 | if (elementsToText) { 10 | rootElement = elementsToText; 11 | } 12 | elementsToText = Array.from(rootElement.querySelectorAll(`*[class*='${messagePrefix}']`)); 13 | if (rootElement !== document) { 14 | elementsToText.push(rootElement); 15 | } 16 | } 17 | for (const ele of elementsToText) { 18 | for (const c of ele.classList) { 19 | if (c.length > messagePrefix.length && c.startsWith(messagePrefix)) { 20 | const messageId = c.substring(messagePrefix.length); 21 | const message = browser.i18n.getMessage(messageId); 22 | if (asHTML) { 23 | ele.innerHTML = message; 24 | } else { 25 | ele.textContent = message; 26 | } 27 | break; 28 | } 29 | } 30 | } 31 | } 32 | 33 | function bindElementIdsToSettings(settings, createListeners = true) { 34 | for (let key of Object.keys(settings)) { 35 | let element = document.getElementById(key); 36 | if (!element) { 37 | continue; 38 | } 39 | 40 | let propertyName; 41 | if (element.type === 'checkbox') { 42 | propertyName = 'checked'; 43 | } else { 44 | propertyName = 'value'; 45 | } 46 | 47 | element[propertyName] = settings[key]; 48 | if (createListeners) { 49 | element.addEventListener('input', e => { 50 | const keyValue = {}; 51 | let value = e.target[propertyName]; 52 | if (element.type === 'number') { 53 | value = parseInt(value); 54 | if (isNaN(value)) 55 | return; 56 | } 57 | keyValue[key] = value; 58 | browser.storage.local.set(keyValue); 59 | }); 60 | } 61 | } 62 | } 63 | 64 | function bindDependantSettings() { 65 | const requireObjs = []; 66 | const checkRequired = (affectedObject = null) => { 67 | for (const obj of requireObjs) { 68 | if (affectedObject && obj !== affectedObject) { 69 | continue; 70 | } 71 | const changed = obj.checkEnabled(); 72 | if (changed) { 73 | return checkRequired(); 74 | } 75 | } 76 | }; 77 | 78 | const requireAreas = Array.from(document.querySelectorAll(`*[class*='${requiresPrefix}']`)); 79 | for (const ele of requireAreas) { 80 | for (const c of ele.classList) { 81 | if (c.length > requiresPrefix.length && c.startsWith(requiresPrefix)) { 82 | let requireId = c.substring(requiresPrefix.length); 83 | let inverted = false; 84 | if (requireId.startsWith('!')) { 85 | requireId = requireId.slice(1); 86 | inverted = true; 87 | } 88 | 89 | const requiredElement = document.getElementById(requireId); 90 | let obj = { 91 | listener: (e) => { 92 | const changed = obj.checkEnabled(); 93 | if (changed) { 94 | checkRequired(); 95 | } 96 | }, 97 | checkEnabled: () => { 98 | let enabled = false; 99 | if (requiredElement.type === 'checkbox') { 100 | enabled = requiredElement.checked; 101 | } else if (requiredElement.type === 'number') { 102 | let value = parseInt(requiredElement.value); 103 | enabled = !isNaN(value) && value >= 0; 104 | } 105 | if (inverted) { 106 | enabled = !enabled; 107 | } 108 | let eleToCheck = requiredElement; 109 | while (eleToCheck) { 110 | if (enabled) { 111 | break; 112 | } 113 | if (eleToCheck.classList.contains('disabled')) { 114 | enabled = true; 115 | } 116 | eleToCheck = eleToCheck.parentElement; 117 | } 118 | 119 | const was = ele.classList.contains('disabled'); 120 | if (was !== !enabled) { 121 | ele.classList.toggle('disabled', !enabled); 122 | return true; 123 | } 124 | return false; 125 | }, 126 | }; 127 | requireObjs.push(obj); 128 | requiredElement.addEventListener('input', obj.listener); 129 | 130 | break; 131 | } 132 | } 133 | } 134 | return checkRequired; 135 | } 136 | 137 | 138 | async function initiatePage() { 139 | setTextMessages(); 140 | const checkRequired = bindDependantSettings(); 141 | 142 | const defaultSettings = getDefaultSettings(); 143 | const settings = Object.assign({}, getDefaultSettings(), await browser.storage.local.get(null)); 144 | 145 | let firstLoad = true; 146 | const handleLoad = () => { 147 | bindElementIdsToSettings(settings, firstLoad); 148 | checkRequired(); 149 | 150 | firstLoad = false; 151 | }; 152 | handleLoad(); 153 | 154 | browser.storage.onChanged.addListener((changes, areaName) => { 155 | for (const [key, value] of Object.entries(changes)) { 156 | settings[key] = ('newValue' in value) ? value.newValue : defaultSettings[key]; 157 | } 158 | checkRequired(changes); 159 | }); 160 | 161 | document.getElementById('resetSettingsButton').addEventListener('click', async (e) => { 162 | let ok = confirm(browser.i18n.getMessage('options_resetSettings_Prompt')); 163 | if (!ok) { 164 | return; 165 | } 166 | 167 | // Clear settings: 168 | await browser.storage.local.clear(); 169 | 170 | // Reload settings: 171 | setTimeout(handleLoad, 250); 172 | }); 173 | } 174 | initiatePage(); --------------------------------------------------------------------------------