diff --git a/Makefile b/Makefile index 260a7572..955fb47c 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ DEST_BUNDLE_2 = tabfern/src/view/bundle_tree.js .PHONY: all bundle clean all: bundle + ./check-version.sh ./check-webstore.sh bundle: $(DEST_BUNDLE_1) $(DEST_BUNDLE_2) diff --git a/check-version.sh b/check-version.sh new file mode 100755 index 00000000..bc1155cd --- /dev/null +++ b/check-version.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# check-version.sh: report version numbers in TabFern +function check() { + ack_opts=( -m 1 --nocolor --nopager --output '$line_no: $line' ) + # $filename is also available + echo -n "$1: " + ack "${ack_opts[@]}" VERSION "$1" || + ack "${ack_opts[@]}" version "$1" +} + +files=(package.json package-lock.json) + +for tree in tabfern webstore ; do + files+=(${tree}/manifest.json ${tree}/src/common/common.js) +done + +for f in "${files[@]}" ; do + check "$f" +done + +if [[ $1 = '-e' ]]; then + vi "${files[@]}" +fi +# vi: set ts=4 sts=4 sw=4 et ai: # diff --git a/package-lock.json b/package-lock.json index 9250ee52..470f2dbd 100755 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "tabfern", - "version": "0.1.17.1337", + "version": "0.1.18.1337", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 254c6653..2c158fb8 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tabfern", - "version": "0.1.17.1337", + "version": "0.1.18.1337", "description": "Google Chrome extension for displaying, saving, and managing tabs", "main": "src/view/main.js", "directories": { diff --git a/tabfern/_locales/de/messages.json b/tabfern/_locales/de/messages.json index 8609f660..88f4146a 100755 --- a/tabfern/_locales/de/messages.json +++ b/tabfern/_locales/de/messages.json @@ -214,6 +214,10 @@ "message": "Notiz hinzufügen oder bearbeiten" ,"description":"The context-menu item to add or edit a tab's note" } + , "menuAddEditNoteThisTab": { + "message": "Notiz für den aktuellen Tab hinzufügen oder bearbeiten" + ,"description":"The extension-menu item to add or edit the current tab's note" + } , "menuRename": { "message": "Umbenennen" ,"description":"The context-menu item to rename a window's tree entry" diff --git a/tabfern/_locales/en/messages.json b/tabfern/_locales/en/messages.json index fc30c4a2..ff2a9056 100755 --- a/tabfern/_locales/en/messages.json +++ b/tabfern/_locales/en/messages.json @@ -84,6 +84,14 @@ } } } + , "dlgpTextToReplace": { + "message": "Text to replace? (/.../ for regex)" + ,"description":"Prompt for the user to enter text to replace" + } + , "dlgpReplacementText": { + "message": "Replacement text?" + ,"description":"Prompt for the user to enter text to replace" + } , "dlgYesHTML": { "message": "Yes" @@ -215,6 +223,10 @@ "message": "Add/edit a note" ,"description":"The context-menu item to add or edit a tab's note" } + , "menuAddEditNoteThisTab": { + "message": "Add/edit a note for the current tab" + ,"description":"The extension-menu item to add or edit the current tab's note" + } , "menuRename": { "message": "Rename" ,"description":"The context-menu item to rename a window's tree entry" @@ -243,6 +255,18 @@ "message": "Delete" ,"description":"The context-menu item to delete a window or tab's tree item" } + , "menuURLSubstitute": { + "message": "Replace in URLs" + ,"description":"The context-menu item to make a replacement in the URL of each tab in the window" + } + , "menuttURLSubstitute": { + "message": "Make a text replacement in all of the URLs in this window" + ,"description":"The context-menu tooltip for menuURLSubstitute" + } + , "menuMoveToTop": { + "message": "Move to top" + ,"description":"The context-menu item to move a window's tree item to the top of the tree" + } , "error_text": { "message": "--------------------------------------------" ,"description": "Text for error messages" } diff --git a/tabfern/assets/css/icons.css b/tabfern/assets/css/icons.css index 6c54a6aa..7331aa75 100755 --- a/tabfern/assets/css/icons.css +++ b/tabfern/assets/css/icons.css @@ -60,6 +60,10 @@ background-image: url("/assets/icons/text_padding_top.png"); } +.jstree-themeicon-custom.arrow-switch, .vakata-context .arrow-switch { + background-image: url("/assets/icons/arrow_switch.png"); +} + /* Class for icons with no content. Used in jstree.set_icon() when the * icon is actually being set using CSS. */ @@ -91,12 +95,9 @@ content: url("/assets/icons/cross.png"); } -/* Background sizes in context menu are different. TODO fix this --- it is - * an ugly hack. */ -.vakata-context .fff-pencil, -.vakata-context .fff-cross, -.vakata-context .fff-picture-delete, -.vakata-context .fff-text-padding-top { +/* Background sizes of icons in context menu are different. Note: Need the + * `li a` to make it specific enough. */ +.vakata-context li a i { background-repeat: no-repeat; background-position: center center; } diff --git a/tabfern/assets/icons/arrow_switch.png b/tabfern/assets/icons/arrow_switch.png new file mode 100644 index 00000000..258c16c6 Binary files /dev/null and b/tabfern/assets/icons/arrow_switch.png differ diff --git a/tabfern/conf/require-config.js b/tabfern/conf/require-config.js index a042a965..105623c9 100755 --- a/tabfern/conf/require-config.js +++ b/tabfern/conf/require-config.js @@ -50,10 +50,6 @@ var require = { exports: 'BLAKE2s' } }, - async: { - useHash: true // #callback=x rather than ?callback=x since Chrome - // won't load files with ? - }, }; // vi: set ts=4 sts=4 sw=4 et ai fo-=o fo-=r: // diff --git a/tabfern/js/asq-helpers.js b/tabfern/js/asq-helpers.js index 7f7b667b..970ec07d 100755 --- a/tabfern/js/asq-helpers.js +++ b/tabfern/js/asq-helpers.js @@ -1,25 +1,15 @@ // asq-helpers.js: Helpers for asynquence and Chrome callbacks. (function (root, factory) { - let imports=['asynquence-contrib']; - if (typeof define === 'function' && define.amd) { // AMD - define('asq-helpers',imports, factory); + define('asq-helpers', ['asynquence-contrib'], factory); } else if (typeof exports === 'object') { // Node, CommonJS-like - let requirements = []; - for(let modulename of imports) { - requirements.push(require(modulename)); - } - module.exports = factory(...requirements); + module.exports = factory(require('asynquence-contrib')); } else { // Browser globals (root is `window`) - let requirements = []; - for(let modulename of imports) { - requirements.push(root[modulename]); - } - root.ASQH = factory(...requirements); + root.ASQH = factory(root.ASQ); } }(this, function (ASQ) { "use strict"; diff --git a/tabfern/js/async.js b/tabfern/js/async.js deleted file mode 100644 index 4786c106..00000000 --- a/tabfern/js/async.js +++ /dev/null @@ -1,46 +0,0 @@ -/** @license - * RequireJS plugin for async dependency load like JSONP and Google Maps - * Author: Miller Medeiros - * Version: 0.1.2 (2014/02/24) - * Released under the MIT license - */ -define(function(){ - - var DEFAULT_PARAM_NAME = 'callback', - _uid = 0; - - function injectScript(src){ - var s, t; - s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = src; - t = document.getElementsByTagName('script')[0]; t.parentNode.insertBefore(s,t); - } - - function formatUrl(name, id, useHash){ - var separ = (useHash ? '#' : '?'), - paramRegex = /!(.+)/, - url = name.replace(paramRegex, ''), - param = (paramRegex.test(name)) ? name.replace(/.+!/, '') : DEFAULT_PARAM_NAME; - url += (url.indexOf(separ) < 0) ? separ : '&'; - return url + param +'='+ id; - } - - function uid() { - _uid += 1; - return '__async_req_'+ _uid +'__'; - } - - return{ - load : function(name, req, onLoad, config){ - if(config.isBuild){ - onLoad(null); //avoid errors on the optimizer - }else{ - var id = uid(); - //create a global variable that stores onLoad so callback - //function can define new module after async load - window[id] = onLoad; - injectScript(formatUrl(req.toUrl(name), id, config.async.useHash)); - } - } - }; -}); -// vi: set ts=4 sts=4 sw=4 et ai fo-=o fo-=r: // diff --git a/tabfern/js/buffer.js b/tabfern/js/buffer.js index b84e1e22..c709bcee 100755 --- a/tabfern/js/buffer.js +++ b/tabfern/js/buffer.js @@ -2121,4 +2121,4 @@ module.exports = Array.isArray || function (arr) { /***/ }) -/******/ ]); \ No newline at end of file +/******/ ]); diff --git a/tabfern/js/export-file.js b/tabfern/js/export-file.js index f6fc3fa0..63f700e0 100644 --- a/tabfern/js/export-file.js +++ b/tabfern/js/export-file.js @@ -11,10 +11,9 @@ module.exports = factory(); } else { // Browser globals (root is window) - root.Fileops = root.Fileops || {}; - root.Fileops.Export = factory(); + root.ExportFile = factory(); } -}(this, function ($) { +}(this, function () { /// Save the given text to the given filename. This is what is returned /// by the module loader. diff --git a/tabfern/js/import-file.js b/tabfern/js/import-file.js index ed33bcaa..fc8f2129 100644 --- a/tabfern/js/import-file.js +++ b/tabfern/js/import-file.js @@ -11,8 +11,7 @@ module.exports = factory(); } else { // Browser globals (root is window) - root.Fileops = root.Fileops || {}; - root.Fileops.Importer = factory(); + root.ImportFile = factory(); } }(this, function () { diff --git a/tabfern/js/jstree-multitype.js b/tabfern/js/jstree-multitype.js index 8f76af8a..b0c968f6 100644 --- a/tabfern/js/jstree-multitype.js +++ b/tabfern/js/jstree-multitype.js @@ -20,7 +20,7 @@ else { factory(jQuery, jQuery.jstree); } -}(function ($, jstree, undefined) { +}(function ($, _jstree_unused, undefined) { "use strict"; if($.jstree.plugins.multitype) { return; } diff --git a/tabfern/js/jstree-redraw-event.js b/tabfern/js/jstree-redraw-event.js index cca800cd..17d194de 100644 --- a/tabfern/js/jstree-redraw-event.js +++ b/tabfern/js/jstree-redraw-event.js @@ -24,17 +24,24 @@ if($.jstree.plugins.redraw_event) { return; } $.jstree.plugins.redraw_event = function (options, parent) { - //this._data.redraw_event = {reason: undefined}; + this._data.redraw_event = {suppress: false}; /// Redraw. /// @param {DOM object} obj The node being redrawn /// @return the object, if the parent was able to redraw it. this.redraw_node = function(obj, deep, callback, force_render) { - obj = parent.redraw_node.apply(this, arguments); - this.trigger('redraw_event', {obj: obj}); + if(!this._data.redraw_event.suppress) { + obj = parent.redraw_node.apply(this, arguments); + this.trigger('redraw_event', {obj: obj}); + } return obj; }; //redraw_node + /// Suppress redraw temporarily. EXPERIMENTAL. + this.suppress_redraw = function(whether_to) { + this._data.redraw_event.suppress = !!whether_to; + } + }; })); diff --git a/tabfern/js/justhtmlescape.js b/tabfern/js/justhtmlescape.js index 72fd9b2c..c6fc455c 100644 --- a/tabfern/js/justhtmlescape.js +++ b/tabfern/js/justhtmlescape.js @@ -3,7 +3,7 @@ /// Adapted from https://github.com/janl/mustache.js/blob/master/mustache.js /// MIT license --- see end of file -// Defines HTMLEscaper, which has escape(text) and unescape(text) functions. +// Returns { escape(text), unescape(text) }. (function (root, factory) { if (typeof define === 'function' && define.amd) { @@ -14,7 +14,7 @@ module.exports = factory(); } else { // Browser globals (root is window) - root.HTMLEscaper = factory(); + root.JustHTMLEscape = factory(); } }(this, function () { diff --git a/tabfern/js/management.js b/tabfern/js/management.js deleted file mode 100755 index cafdd8e9..00000000 --- a/tabfern/js/management.js +++ /dev/null @@ -1,57 +0,0 @@ -// management.js: Test of a management module. -// Copyright (c) Chris White 2017. CC-BY-SA 4.0 International. -// Load this using the async plugin, -// https://github.com/millermedeiros/requirejs-plugins/blob/master/src/async.js - -// Code to check development status thanks to -// https://stackoverflow.com/a/12833511/2877364 by -// https://stackoverflow.com/users/1143495/konrad-dzwinel and -// https://stackoverflow.com/users/934239/xan - -(function(root){ - - /// The completion callback - call when the module is fully loaded - let callback; - - /// Our worker function - function with_info(info) - { - let obj = info; - obj.isDevelMode = (info.installType === 'development'); - console.log({'Got info': obj}); - callback(obj); //complete module loading - } //with_info - - // Stash the onload callback for later, when we are done loading - // Thanks to https://stackoverflow.com/a/22745553/2877364 by - // https://stackoverflow.com/users/140264/brice for - // info about document.currentScript. - if(!document.currentScript) - throw new Error("Can't load --- I don't know what script I'm in"); - - // VVV code from here to "^^^" is also available as CC-BY 4.0 International - - let script_url = document.currentScript.src; - - let url = new URL(script_url); - let searchParams = new URLSearchParams(url.hash.slice(1)); - // Using the hash, not the query string, because Chrome won't load - // chrome-extension resources with query strings. - - if(searchParams.has('callback')) { - let cbk_name = searchParams.get('callback'); - callback = root[cbk_name]; - if(!callback) throw new Error( - `Can't load --- I can't find the ${cbk_name} callback`); - - } else { - throw new Error("Can't load --- I can't find a #callback=... param"); - } - - // ^^^ - - // Fire off the loading - chrome.management.getSelf(with_info); - -})(this); -// vi: set ts=4 sts=4 sw=4 et ai fo-=o: // diff --git a/tabfern/js/spin-packed.js b/tabfern/js/spin-packed.js index 8a5c2d05..846afe62 100644 --- a/tabfern/js/spin-packed.js +++ b/tabfern/js/spin-packed.js @@ -292,4 +292,4 @@ function convertOffset(x, y, degrees) { /***/ }) -/******/ ]); \ No newline at end of file +/******/ ]); diff --git a/tabfern/manifest.json b/tabfern/manifest.json index 6d9bacdc..1a8f695a 100755 --- a/tabfern/manifest.json +++ b/tabfern/manifest.json @@ -1,8 +1,8 @@ { "name": "__MSG_wsLongName__", "short_name": "__MSG_wsShortName__", - "version": "0.1.17.1337", - "version_name": "0.1.17", + "version": "0.1.18.1337", + "version_name": "0.1.18", "offline_enabled": true, "manifest_version": 2, "minimum_chrome_version": "54", diff --git a/tabfern/src/bg/background.js b/tabfern/src/bg/background.js index 1ef6e284..7e4a6b83 100755 --- a/tabfern/src/bg/background.js +++ b/tabfern/src/bg/background.js @@ -133,7 +133,7 @@ function editNoteOnClick(info, tab) } //editNoteOnClick chrome.contextMenus.create({ - id: 'editNote', title: 'Add/edit a note for the current tab', + id: 'editNote', title: _T('menuAddEditNoteThisTab'), contexts: ['browser_action'], onclick: editNoteOnClick }); @@ -166,14 +166,6 @@ chrome.runtime.onMessage.addListener(messageListener); // 'sample_setting': 'This is how you use Store.js to remember values' //}); - -////example of using a message handler from the inject scripts -//chrome.extension.onMessage.addListener( -// function(request, sender, sendResponse) { -// chrome.pageAction.show(sender.tab.id); -// sendResponse(); -// }); - ////////////////////////////////////////////////////////////////////////// // MAIN // @@ -181,7 +173,10 @@ chrome.runtime.onMessage.addListener(messageListener); window.addEventListener('load', function() { console.log('TabFern: background window loaded'); - setTimeout(loadView, 500); + if(getBoolSetting(CFG_POPUP_ON_STARTUP)) { + console.log('Opening popup window'); + setTimeout(loadView, 500); + } }, { 'once': true } ); diff --git a/tabfern/src/common/common.js b/tabfern/src/common/common.js index 9e6c98c8..8e56cd11 100755 --- a/tabfern/src/common/common.js +++ b/tabfern/src/common/common.js @@ -2,16 +2,17 @@ // in this file. // ** Not currently a require.js module so that it can be used in contexts // ** where require.js is not available (e.g., background.js). -// ** TODO make this a UMD module? +// ** TODO split this into UMD modules so the pieces can be loaded with or +// without require.js. console.log('TabFern common.js loading'); ////////////////////////////////////////////////////////////////////////// -// General constants // +// General constants // {{{1 /// The TabFern extension friendly version number. Displayed in the /// title bar of the popup window, so lowercase (no shouting!). -const TABFERN_VERSION='0.1.17'; +const TABFERN_VERSION='0.1.18'; // When you change this, also update: // - manifest.json: both the version and version_name // - package.json @@ -20,14 +21,14 @@ const TABFERN_VERSION='0.1.17'; // Design decision: version numbers follow semver.org. // In the Chrome manifest, the version_name attribute tracks the above. // The version attribute, `x.y.z.w`, which is compared in numeric order L-R, -// is as follows: x.y.z track the above. w is the "-pre." number. +// is as follows: x.y.z track the above. w is the "-pre." or "-rc." number. // A release to the Chrome Web Store has w=1337. // E.g., 1.2.3-pre.4 is `version='1.2.3.4'`, and 1.2.3 (release) is // `version='1.2.3.1337'`. // If you get up to -pre.1336, just bump the `z` value and reset `w` :) . -////////////////////////////////////////////////////////////////////////// -// Messages between parts of TabFern // +////////////////////////////////////////////////////////////////////////// }}}1 +// Messages between parts of TabFern // {{{1 // The format of a message is // { msg: [, anything else] } @@ -36,8 +37,8 @@ const TABFERN_VERSION='0.1.17'; const MSG_GET_VIEW_WIN_ID = 'getViewWindowID'; const MSG_EDIT_TAB_NOTE = 'editTabNote'; -////////////////////////////////////////////////////////////////////////// -// Names of settings, and their defaults // +////////////////////////////////////////////////////////////////////////// }}}1 +// Names of settings, and their defaults // {{{1 /// An array to build the defaults in. Every property must have a default, /// since the defaults array is also used to identify properties to be @@ -51,7 +52,11 @@ let _DEF = { __proto__: null }; let _VAL = { __proto__: null }; let _vbool = (v)=>{ return ((typeof v === 'boolean')?v:undefined)}; -// Booleans +// Booleans {{{2 +const CFG_POPUP_ON_STARTUP = 'open-popup-on-chrome-startup'; +_DEF[CFG_POPUP_ON_STARTUP] = true; +_VAL[CFG_POPUP_ON_STARTUP] = _vbool; + const CFG_ENB_CONTEXT_MENU = 'ContextMenu.Enabled'; _DEF[CFG_ENB_CONTEXT_MENU] = true; _VAL[CFG_ENB_CONTEXT_MENU] = _vbool; @@ -122,8 +127,6 @@ const SETTINGS_LOADED_OK = '__settings_loaded_OK'; _DEF[SETTINGS_LOADED_OK] = false; _VAL[SETTINGS_LOADED_OK] = ()=>{return undefined;} - - // Not yet implemented - pending #35. Whether to open closed tabs when // you click on the tree item for a partially-open window. //const CFG_OPEN_REST_ON_CLICK = 'open-rest-on-win-click', @@ -131,7 +134,8 @@ _VAL[SETTINGS_LOADED_OK] = ()=>{return undefined;} // CFG_OROC_DO_NOT = false; //_DEF[CFG_OPEN_REST_ON_CLICK] = CFG_OROC_DO_NOT; -// Strings, including limited-choice controls such as radio buttons and dropdowns. +// }}}2 +// Strings and limited-choice controls such as radio buttons and dropdowns. {{{2 const CFGS_BACKGROUND = 'window-background'; _DEF[CFGS_BACKGROUND] = ''; _VAL[CFGS_BACKGROUND] = (v)=>{ @@ -155,12 +159,13 @@ _VAL[CFGS_SCROLLBAR_COLOR] = (v)=>{ return ((Validation.isValidColor(v)) ? v : undefined); }; +// }}}2 /// The default values for the configuration settings. const CFG_DEFAULTS = Object.seal(_DEF); const CFG_VALIDATORS = Object.seal(_VAL); -////////////////////////////////////////////////////////////////////////// -// Test for Firefox // +////////////////////////////////////////////////////////////////////////// }}}1 +// Test for Firefox // {{{1 // Not sure if I need this, but I'm playing it safe for now. Firefox returns // null rather than undefined in chrome.runtime.lastError when there is // no error. This is to test for null in Firefox without changing my @@ -197,8 +202,8 @@ BROWSER_TYPE=null; // unknown } })(window); -////////////////////////////////////////////////////////////////////////// -// Setting-related functions // +////////////////////////////////////////////////////////////////////////// }}}1 +// Setting-related functions // {{{1 const SETTING_PREFIX = 'store.settings.'; @@ -288,8 +293,8 @@ function getThemeName() else return CFG_DEFAULTS[CFGS_THEME_NAME]; } //getThemeName -////////////////////////////////////////////////////////////////////////// -// DOM-related functions // +////////////////////////////////////////////////////////////////////////// }}}1 +// DOM-related functions // {{{1 /// Append a - + + + + + + + - - - - - - - - + + diff --git a/tabfern/src/view/tree.js b/tabfern/src/view/tree.js index 21659db7..dae5eb0f 100755 --- a/tabfern/src/view/tree.js +++ b/tabfern/src/view/tree.js @@ -2,7 +2,9 @@ // Copyright (c) cxw42, 2017--2018 // See /doc/design.md for information about notation and organization. -// TODO break more of this into separate modules +// This is not a module. That is so that its internals are available for +// console inspection and debugging. That may change in the future. +// TODO break more of this into separate modules. console.log(`============================================================= Loading TabFern ${TABFERN_VERSION}`); @@ -47,7 +49,7 @@ let Module_dependencies = [ 'loglevel', 'hamburger', 'bypasser', 'multidex', 'justhtmlescape', 'signals', 'export-file', 'import-file', 'asynquence-contrib', 'asq-helpers', 'rmodal', - 'tinycolor', + 'tinycolor', 'spin-packed', // Shimmed modules. Refer to these via Modules or *without* the // `window.` prefix so it will be easier to refactor references @@ -181,15 +183,6 @@ function local_init() M = Modules['view/model']; ASQ = Modules['asynquence-contrib']; ASQH = Modules['asq-helpers']; - - // Check development status. Thanks to - // https://stackoverflow.com/a/12833511/2877364 by - // https://stackoverflow.com/users/1143495/konrad-dzwinel and - // https://stackoverflow.com/users/934239/xan - chrome.management.getSelf(function(info){ - if(info.installType === 'development') is_devel_mode = true; - }); - } //init() /// Copy properties named #property_names from #source to #dest. @@ -207,6 +200,12 @@ function copyTruthyProperties(dest, source, property_names, modifier) return dest; } //copyTruthyProperties +/// Escape text for use in a regex. By Mozilla Contributors (CC-BY-SA 2.5+), from +/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} //escapeRegExp + ////////////////////////////////////////////////////////////////////////// }}}1 // DOM Manipulation // {{{1 @@ -424,6 +423,7 @@ function showConfirmationModalDialog(message_html) { afterOpen: function() { $('#confirm-dialog .btn-primary').focus(); + // Note: focus is set but not visible in Chrome 69 - #142 //console.log('opened'); }, @@ -513,7 +513,7 @@ function saveTree(save_ephemeral_windows = true, cbk = undefined) // Get the raw data for the whole tree. Can't use $(...) because closed // tree nodes aren't in the DOM. - let root_node = T.treeobj.get_node($.jstree.root); //from get_json() src + let root_node = T.root_node(); if(!root_node || !root_node.children) { if(typeof cbk === 'function') cbk(new Error("Can't get root node")); return; @@ -592,6 +592,65 @@ function saveTree(save_ephemeral_windows = true, cbk = undefined) ); //storage.local.set } //saveTree() +////////////////////////////////////////////////////////////////////////// }}}1 +// Other actions // {{{1 + +/// Make a string replacement on the URLs of all the tabs in a window +/// @param node_id {string} the ID of the window's node +/// @param node {Object} the window's node +function actionURLSubstitute(node_id, node, unused_action_id, unused_action_el) +{ + let win_val = D.windows.by_node_id(node_id); + if(!win_val) return; + + // TODO replace window.prompt with an in-DOM GUI. + let old_text = window.prompt(_T('dlgpTextToReplace')); + if(old_text === null) return; // user cancelled + + let new_text = window.prompt(_T('dlgpReplacementText')); + if(new_text === null) return; // user cancelled + + if(!old_text) return; // search pattern is required + + // TODO URL escaping of new_text? + + let findregex; + if(old_text.length > 1 && old_text[0]==='/' && + old_text[old_text.length-1]==='/') { // Regex + findregex = new RegExp(old_text.slice(1, -1)); // drop the slashes + // TODO support flags as well + } else { // Literal + findregex = new RegExp(escapeRegExp(old_text)); + } + + for(let tab_node_id of node.children) { + let tab_val = D.tabs.by_node_id(tab_node_id); + if(!tab_val || !tab_val.tab_id) continue; + let new_url = tab_val.raw_url.replace(findregex, new_text); + // TODO URL escaping? + // TODO also replace in favicon URL? + if(new_url === tab_val.raw_url) continue; + + if(win_val.isOpen) { + // Make the change and let tabOnUpdated update the model. + ASQH.NowCC((cc)=>{ // ASQ for error reporting + chrome.tabs.update(tab_val.tab_id, {url: new_url}, cc); + }); + } else { + tab_val.raw_url = new_url; + M.refresh_label(tab_val); // do what tabOnUpdated() would + M.refresh_icon(tab_val); + M.refresh_tooltip(tab_val); + } + } // foreach child + + // If the window is closed, update the hash. Otherwise, tabOnUpdated() + // handled it. + if(!win_val.isOpen) { + M.updateOrderedURLHash(win_val); + } +} //actionURLSubstitute + ////////////////////////////////////////////////////////////////////////// }}}1 // jstree-action callbacks // {{{1 @@ -832,6 +891,14 @@ function actionDeleteWindow(node_id, node, unused_action_id, unused_action_el, } //endif confirmation required } //actionDeleteWindow +/// Move a window to the top of the tree. +function actionMoveWinToTop(node_id, node, unused_action_id, unused_action_el) +{ + if(!node) return; + T.treeobj.move_node(node, T.root_node(), 1); + // 1 => after the holding pen +} //actionMoveWinToTop + /// Toggle the top border on a node. This is a hack until I can add /// dividers. function actionToggleTabTopBorder(node_id, node, unused_action_id, unused_action_el) @@ -1502,11 +1569,32 @@ var loadSavedWindowsFromData = (function(){ // Load it if(vernum in versionLoaders) { - loader_retval = versionLoaders[vernum](data); - } else { + + try { +/* // TEMPORARILY REMOVED + T.treeobj.suppress_redraw(true); // EXPERIMENTAL +*/ + loader_retval = versionLoaders[vernum](data); + + } catch(e) { + log.error( + `Error loading version-${vernum} save data: ${e}`); + loader_retval = false; + // Continue out of the catch block so the + // suppress_redraw(false) will be called. + } + +/* // TEMPORARILY REMOVED + T.treeobj.suppress_redraw(false); // EXPERIMENTAL + T.treeobj.redraw(true); // Just in case the experiment + // had different results than + // we expected! +*/ + + } else { // unknown version log.error("I don't know how to load save data from version " + vernum); break READIT; - } + } //endif known version else if(loader_retval === false) { log.error("There was a problem loading save data of version " + vernum); @@ -2060,8 +2148,8 @@ function initFocusHandler() else if(old_win_id === WINID_NONE) change_from = FC_FROM_NONE; else change_from = FC_FROM_OPEN; - // Uncomment if you are debugging focus-change behaviour - //log.info({change_from, old_win_id, change_to, win_id}); + // Uncomment if you are debugging focus-change behaviour TODO RESUME HERE + log.info({change_from, old_win_id, change_to, win_id}); let same_window = (old_win_id === win_id); previously_focused_winid = win_id; @@ -2371,6 +2459,11 @@ function tabOnUpdated(tabid, changeinfo, ctab) // Caution: changeinfo doesn't always have all the changed information. // Therefore, we check changeinfo and ctab. + // TODO refactor the following into a separate routine that can be + // used to update closed or open tabs' tree items. Maybe move it + // to model.js as well. This will reduce code duplication, e.g., in + // actionURLSubstitute. + // URL let new_raw_url = changeinfo.url || ctab.url || 'about:blank'; if(new_raw_url !== tab_node_val.raw_url) { @@ -2810,7 +2903,7 @@ function hamRestoreLastDeleted() if(typeof wins_loaded === 'number' && wins_loaded > 0) { // We loaded the window successfully. Open it, if the user wishes. if(getBoolSetting(CFG_RESTORE_ON_LAST_DELETED, false)) { - let root = T.treeobj.get_node($.jstree.root); + let root = T.root_node(); let node_id = root.children[root.children.length-1]; T.treeobj.select_node(node_id); } @@ -2833,7 +2926,7 @@ function hamCollapseAll() function hamSorter(compare_fn) { return function() { - let arr = T.treeobj.get_node($.jstree.root).children; + let arr = T.root_node().children; Modules['view/sorts'].stable_sort(arr, compare_fn); // children[] holds node IDs, so compare_fn will always get strings. T.treeobj.redraw(true); // true => full redraw @@ -3043,7 +3136,6 @@ function getMainContextMenuItems(node, _unused_proxyfunc, e) // }; // } - return tabItems; } //endif K.IT_TAB @@ -3089,6 +3181,18 @@ function getMainContextMenuItems(node, _unused_proxyfunc, e) }; } + { // If not the first item, add "Move to top" + let parent_node = T.treeobj.get_node(node.parent); + if(parent_node.children[1] !== node.id) { + // children[1], not [0], because [0] is the holding pen. + winItems.toTopItem = { + label: _T('menuMoveToTop'), + icon: 'fff-text-padding-top', + action: ()=>{actionMoveWinToTop(node.id,node,null,null);}, + }; + } + } + winItems.deleteItem = { label: _T('menuDelete'), icon: 'fff-cross', @@ -3097,6 +3201,17 @@ function getMainContextMenuItems(node, _unused_proxyfunc, e) function(){actionDeleteWindow(node.id, node, null, null);} }; +/* // TEMPORARILY REMOVED + winItems.urlSubstituteItem = { + label: _T('menuURLSubstitute'), + title: _T('menuttURLSubstitute'), + icon: 'arrow-switch', + separator_before: true, + action: + function(){actionURLSubstitute(node.id, node, null, null);} + }; +*/ + return winItems; } //endif K.IT_WIN @@ -3755,7 +3870,7 @@ function delete_all_closed_nodes(are_you_sure) { if(!are_you_sure) return; - let root = T.treeobj.get_node($.jstree.root); + let root = T.root_node(); if(!root) return for(let i=root.children.length-1; i>0; --i) { @@ -3854,7 +3969,22 @@ function preLoadInit() } //preLoadInit -/// Beginning of the onload initialization. +// Beginning of the onload initialization. + +/// Check development status in an ASQ step. Thanks to +/// https://stackoverflow.com/a/12833511/2877364 by +/// https://stackoverflow.com/users/1143495/konrad-dzwinel and +/// https://stackoverflow.com/users/934239/xan +function determine_devel_mode(done) +{ + ASQH.NowCC((cc)=>{ chrome.management.getSelf(cc); }) + .val((info)=>{ + is_devel_mode = (info.installType === 'development'); + }) + .pipe(done); +} //determine_devel_mode() + +/// Initialization we can do before we have our window ID function basicInit(done) { next_init_step('basic initialization'); @@ -4188,6 +4318,8 @@ function initIncompleteWarning() //////////////////////////////////////////////////////////////////////// }}}1 // MAIN // {{{1 +/// The main function. Called once RequireJS has loaded all the +/// dependencies. function main(...args) { // Hack: Copy the loaded modules into our Modules global @@ -4219,11 +4351,22 @@ function main(...args) // Run the main init steps once the page has loaded let s = ASQ(); - callbackOnLoad(s.errfcb()); + callbackOnLoad(s.errfcb()); // Just using errfcb() to kick off s. // Note: on one test on Firefox, the rest of the chain never fired. // Not sure why. - s.then(basicInit) + // Start a spinner if loading takes more than 1 s + let spinner = new Spinner(); + let spin_starter = function() { + if(spinner) spinner.spin($('#tabfern-container')[0]); + }; + //let spin_timer = window.setTimeout(spin_starter, 1000); + + s.then(determine_devel_mode) + .then(basicInit) + + .val(spin_starter) + // for now, always start --- loadSavedWindowsIntoTree is synchronous .try((done)=>{ // Get our Chrome-extensions-API window ID from the background page. @@ -4252,6 +4395,13 @@ function main(...args) .val(check_init_step_count) + // Stop the spinner, if it started + .val(()=>{ + spinner.stop(); + spinner = null; + //clearTimeout(spin_timer); + }) + .or((err)=>{ $(K.INIT_MSG_SEL).text( $(K.INIT_MSG_SEL).text() + "\n" + err diff --git a/webstore/_locales/de/messages.json b/webstore/_locales/de/messages.json index 8609f660..88f4146a 100755 --- a/webstore/_locales/de/messages.json +++ b/webstore/_locales/de/messages.json @@ -214,6 +214,10 @@ "message": "Notiz hinzufügen oder bearbeiten" ,"description":"The context-menu item to add or edit a tab's note" } + , "menuAddEditNoteThisTab": { + "message": "Notiz für den aktuellen Tab hinzufügen oder bearbeiten" + ,"description":"The extension-menu item to add or edit the current tab's note" + } , "menuRename": { "message": "Umbenennen" ,"description":"The context-menu item to rename a window's tree entry" diff --git a/webstore/_locales/en/messages.json b/webstore/_locales/en/messages.json index fc30c4a2..ff2a9056 100755 --- a/webstore/_locales/en/messages.json +++ b/webstore/_locales/en/messages.json @@ -84,6 +84,14 @@ } } } + , "dlgpTextToReplace": { + "message": "Text to replace? (/.../ for regex)" + ,"description":"Prompt for the user to enter text to replace" + } + , "dlgpReplacementText": { + "message": "Replacement text?" + ,"description":"Prompt for the user to enter text to replace" + } , "dlgYesHTML": { "message": "Yes" @@ -215,6 +223,10 @@ "message": "Add/edit a note" ,"description":"The context-menu item to add or edit a tab's note" } + , "menuAddEditNoteThisTab": { + "message": "Add/edit a note for the current tab" + ,"description":"The extension-menu item to add or edit the current tab's note" + } , "menuRename": { "message": "Rename" ,"description":"The context-menu item to rename a window's tree entry" @@ -243,6 +255,18 @@ "message": "Delete" ,"description":"The context-menu item to delete a window or tab's tree item" } + , "menuURLSubstitute": { + "message": "Replace in URLs" + ,"description":"The context-menu item to make a replacement in the URL of each tab in the window" + } + , "menuttURLSubstitute": { + "message": "Make a text replacement in all of the URLs in this window" + ,"description":"The context-menu tooltip for menuURLSubstitute" + } + , "menuMoveToTop": { + "message": "Move to top" + ,"description":"The context-menu item to move a window's tree item to the top of the tree" + } , "error_text": { "message": "--------------------------------------------" ,"description": "Text for error messages" } diff --git a/webstore/assets/css/icons.css b/webstore/assets/css/icons.css index 6c54a6aa..7331aa75 100755 --- a/webstore/assets/css/icons.css +++ b/webstore/assets/css/icons.css @@ -60,6 +60,10 @@ background-image: url("/assets/icons/text_padding_top.png"); } +.jstree-themeicon-custom.arrow-switch, .vakata-context .arrow-switch { + background-image: url("/assets/icons/arrow_switch.png"); +} + /* Class for icons with no content. Used in jstree.set_icon() when the * icon is actually being set using CSS. */ @@ -91,12 +95,9 @@ content: url("/assets/icons/cross.png"); } -/* Background sizes in context menu are different. TODO fix this --- it is - * an ugly hack. */ -.vakata-context .fff-pencil, -.vakata-context .fff-cross, -.vakata-context .fff-picture-delete, -.vakata-context .fff-text-padding-top { +/* Background sizes of icons in context menu are different. Note: Need the + * `li a` to make it specific enough. */ +.vakata-context li a i { background-repeat: no-repeat; background-position: center center; } diff --git a/webstore/assets/icons/arrow_switch.png b/webstore/assets/icons/arrow_switch.png new file mode 100644 index 00000000..258c16c6 Binary files /dev/null and b/webstore/assets/icons/arrow_switch.png differ diff --git a/webstore/conf/require-config.js b/webstore/conf/require-config.js index a042a965..105623c9 100755 --- a/webstore/conf/require-config.js +++ b/webstore/conf/require-config.js @@ -50,10 +50,6 @@ var require = { exports: 'BLAKE2s' } }, - async: { - useHash: true // #callback=x rather than ?callback=x since Chrome - // won't load files with ? - }, }; // vi: set ts=4 sts=4 sw=4 et ai fo-=o fo-=r: // diff --git a/webstore/js/asq-helpers.js b/webstore/js/asq-helpers.js index 7f7b667b..970ec07d 100755 --- a/webstore/js/asq-helpers.js +++ b/webstore/js/asq-helpers.js @@ -1,25 +1,15 @@ // asq-helpers.js: Helpers for asynquence and Chrome callbacks. (function (root, factory) { - let imports=['asynquence-contrib']; - if (typeof define === 'function' && define.amd) { // AMD - define('asq-helpers',imports, factory); + define('asq-helpers', ['asynquence-contrib'], factory); } else if (typeof exports === 'object') { // Node, CommonJS-like - let requirements = []; - for(let modulename of imports) { - requirements.push(require(modulename)); - } - module.exports = factory(...requirements); + module.exports = factory(require('asynquence-contrib')); } else { // Browser globals (root is `window`) - let requirements = []; - for(let modulename of imports) { - requirements.push(root[modulename]); - } - root.ASQH = factory(...requirements); + root.ASQH = factory(root.ASQ); } }(this, function (ASQ) { "use strict"; diff --git a/webstore/js/async.js b/webstore/js/async.js deleted file mode 100644 index 4786c106..00000000 --- a/webstore/js/async.js +++ /dev/null @@ -1,46 +0,0 @@ -/** @license - * RequireJS plugin for async dependency load like JSONP and Google Maps - * Author: Miller Medeiros - * Version: 0.1.2 (2014/02/24) - * Released under the MIT license - */ -define(function(){ - - var DEFAULT_PARAM_NAME = 'callback', - _uid = 0; - - function injectScript(src){ - var s, t; - s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = src; - t = document.getElementsByTagName('script')[0]; t.parentNode.insertBefore(s,t); - } - - function formatUrl(name, id, useHash){ - var separ = (useHash ? '#' : '?'), - paramRegex = /!(.+)/, - url = name.replace(paramRegex, ''), - param = (paramRegex.test(name)) ? name.replace(/.+!/, '') : DEFAULT_PARAM_NAME; - url += (url.indexOf(separ) < 0) ? separ : '&'; - return url + param +'='+ id; - } - - function uid() { - _uid += 1; - return '__async_req_'+ _uid +'__'; - } - - return{ - load : function(name, req, onLoad, config){ - if(config.isBuild){ - onLoad(null); //avoid errors on the optimizer - }else{ - var id = uid(); - //create a global variable that stores onLoad so callback - //function can define new module after async load - window[id] = onLoad; - injectScript(formatUrl(req.toUrl(name), id, config.async.useHash)); - } - } - }; -}); -// vi: set ts=4 sts=4 sw=4 et ai fo-=o fo-=r: // diff --git a/webstore/js/buffer.js b/webstore/js/buffer.js index b84e1e22..c709bcee 100755 --- a/webstore/js/buffer.js +++ b/webstore/js/buffer.js @@ -2121,4 +2121,4 @@ module.exports = Array.isArray || function (arr) { /***/ }) -/******/ ]); \ No newline at end of file +/******/ ]); diff --git a/webstore/js/export-file.js b/webstore/js/export-file.js index f6fc3fa0..63f700e0 100644 --- a/webstore/js/export-file.js +++ b/webstore/js/export-file.js @@ -11,10 +11,9 @@ module.exports = factory(); } else { // Browser globals (root is window) - root.Fileops = root.Fileops || {}; - root.Fileops.Export = factory(); + root.ExportFile = factory(); } -}(this, function ($) { +}(this, function () { /// Save the given text to the given filename. This is what is returned /// by the module loader. diff --git a/webstore/js/import-file.js b/webstore/js/import-file.js index ed33bcaa..fc8f2129 100644 --- a/webstore/js/import-file.js +++ b/webstore/js/import-file.js @@ -11,8 +11,7 @@ module.exports = factory(); } else { // Browser globals (root is window) - root.Fileops = root.Fileops || {}; - root.Fileops.Importer = factory(); + root.ImportFile = factory(); } }(this, function () { diff --git a/webstore/js/jstree-multitype.js b/webstore/js/jstree-multitype.js index 8f76af8a..b0c968f6 100644 --- a/webstore/js/jstree-multitype.js +++ b/webstore/js/jstree-multitype.js @@ -20,7 +20,7 @@ else { factory(jQuery, jQuery.jstree); } -}(function ($, jstree, undefined) { +}(function ($, _jstree_unused, undefined) { "use strict"; if($.jstree.plugins.multitype) { return; } diff --git a/webstore/js/jstree-redraw-event.js b/webstore/js/jstree-redraw-event.js index cca800cd..17d194de 100644 --- a/webstore/js/jstree-redraw-event.js +++ b/webstore/js/jstree-redraw-event.js @@ -24,17 +24,24 @@ if($.jstree.plugins.redraw_event) { return; } $.jstree.plugins.redraw_event = function (options, parent) { - //this._data.redraw_event = {reason: undefined}; + this._data.redraw_event = {suppress: false}; /// Redraw. /// @param {DOM object} obj The node being redrawn /// @return the object, if the parent was able to redraw it. this.redraw_node = function(obj, deep, callback, force_render) { - obj = parent.redraw_node.apply(this, arguments); - this.trigger('redraw_event', {obj: obj}); + if(!this._data.redraw_event.suppress) { + obj = parent.redraw_node.apply(this, arguments); + this.trigger('redraw_event', {obj: obj}); + } return obj; }; //redraw_node + /// Suppress redraw temporarily. EXPERIMENTAL. + this.suppress_redraw = function(whether_to) { + this._data.redraw_event.suppress = !!whether_to; + } + }; })); diff --git a/webstore/js/justhtmlescape.js b/webstore/js/justhtmlescape.js index 72fd9b2c..c6fc455c 100644 --- a/webstore/js/justhtmlescape.js +++ b/webstore/js/justhtmlescape.js @@ -3,7 +3,7 @@ /// Adapted from https://github.com/janl/mustache.js/blob/master/mustache.js /// MIT license --- see end of file -// Defines HTMLEscaper, which has escape(text) and unescape(text) functions. +// Returns { escape(text), unescape(text) }. (function (root, factory) { if (typeof define === 'function' && define.amd) { @@ -14,7 +14,7 @@ module.exports = factory(); } else { // Browser globals (root is window) - root.HTMLEscaper = factory(); + root.JustHTMLEscape = factory(); } }(this, function () { diff --git a/webstore/js/management.js b/webstore/js/management.js deleted file mode 100755 index cafdd8e9..00000000 --- a/webstore/js/management.js +++ /dev/null @@ -1,57 +0,0 @@ -// management.js: Test of a management module. -// Copyright (c) Chris White 2017. CC-BY-SA 4.0 International. -// Load this using the async plugin, -// https://github.com/millermedeiros/requirejs-plugins/blob/master/src/async.js - -// Code to check development status thanks to -// https://stackoverflow.com/a/12833511/2877364 by -// https://stackoverflow.com/users/1143495/konrad-dzwinel and -// https://stackoverflow.com/users/934239/xan - -(function(root){ - - /// The completion callback - call when the module is fully loaded - let callback; - - /// Our worker function - function with_info(info) - { - let obj = info; - obj.isDevelMode = (info.installType === 'development'); - console.log({'Got info': obj}); - callback(obj); //complete module loading - } //with_info - - // Stash the onload callback for later, when we are done loading - // Thanks to https://stackoverflow.com/a/22745553/2877364 by - // https://stackoverflow.com/users/140264/brice for - // info about document.currentScript. - if(!document.currentScript) - throw new Error("Can't load --- I don't know what script I'm in"); - - // VVV code from here to "^^^" is also available as CC-BY 4.0 International - - let script_url = document.currentScript.src; - - let url = new URL(script_url); - let searchParams = new URLSearchParams(url.hash.slice(1)); - // Using the hash, not the query string, because Chrome won't load - // chrome-extension resources with query strings. - - if(searchParams.has('callback')) { - let cbk_name = searchParams.get('callback'); - callback = root[cbk_name]; - if(!callback) throw new Error( - `Can't load --- I can't find the ${cbk_name} callback`); - - } else { - throw new Error("Can't load --- I can't find a #callback=... param"); - } - - // ^^^ - - // Fire off the loading - chrome.management.getSelf(with_info); - -})(this); -// vi: set ts=4 sts=4 sw=4 et ai fo-=o: // diff --git a/webstore/js/spin-packed.js b/webstore/js/spin-packed.js index 8a5c2d05..846afe62 100644 --- a/webstore/js/spin-packed.js +++ b/webstore/js/spin-packed.js @@ -292,4 +292,4 @@ function convertOffset(x, y, degrees) { /***/ }) -/******/ ]); \ No newline at end of file +/******/ ]); diff --git a/webstore/manifest.json b/webstore/manifest.json index 6d9bacdc..1a8f695a 100755 --- a/webstore/manifest.json +++ b/webstore/manifest.json @@ -1,8 +1,8 @@ { "name": "__MSG_wsLongName__", "short_name": "__MSG_wsShortName__", - "version": "0.1.17.1337", - "version_name": "0.1.17", + "version": "0.1.18.1337", + "version_name": "0.1.18", "offline_enabled": true, "manifest_version": 2, "minimum_chrome_version": "54", diff --git a/webstore/src/bg/background.js b/webstore/src/bg/background.js index 1ef6e284..7e4a6b83 100755 --- a/webstore/src/bg/background.js +++ b/webstore/src/bg/background.js @@ -133,7 +133,7 @@ function editNoteOnClick(info, tab) } //editNoteOnClick chrome.contextMenus.create({ - id: 'editNote', title: 'Add/edit a note for the current tab', + id: 'editNote', title: _T('menuAddEditNoteThisTab'), contexts: ['browser_action'], onclick: editNoteOnClick }); @@ -166,14 +166,6 @@ chrome.runtime.onMessage.addListener(messageListener); // 'sample_setting': 'This is how you use Store.js to remember values' //}); - -////example of using a message handler from the inject scripts -//chrome.extension.onMessage.addListener( -// function(request, sender, sendResponse) { -// chrome.pageAction.show(sender.tab.id); -// sendResponse(); -// }); - ////////////////////////////////////////////////////////////////////////// // MAIN // @@ -181,7 +173,10 @@ chrome.runtime.onMessage.addListener(messageListener); window.addEventListener('load', function() { console.log('TabFern: background window loaded'); - setTimeout(loadView, 500); + if(getBoolSetting(CFG_POPUP_ON_STARTUP)) { + console.log('Opening popup window'); + setTimeout(loadView, 500); + } }, { 'once': true } ); diff --git a/webstore/src/common/common.js b/webstore/src/common/common.js index 9e6c98c8..8e56cd11 100755 --- a/webstore/src/common/common.js +++ b/webstore/src/common/common.js @@ -2,16 +2,17 @@ // in this file. // ** Not currently a require.js module so that it can be used in contexts // ** where require.js is not available (e.g., background.js). -// ** TODO make this a UMD module? +// ** TODO split this into UMD modules so the pieces can be loaded with or +// without require.js. console.log('TabFern common.js loading'); ////////////////////////////////////////////////////////////////////////// -// General constants // +// General constants // {{{1 /// The TabFern extension friendly version number. Displayed in the /// title bar of the popup window, so lowercase (no shouting!). -const TABFERN_VERSION='0.1.17'; +const TABFERN_VERSION='0.1.18'; // When you change this, also update: // - manifest.json: both the version and version_name // - package.json @@ -20,14 +21,14 @@ const TABFERN_VERSION='0.1.17'; // Design decision: version numbers follow semver.org. // In the Chrome manifest, the version_name attribute tracks the above. // The version attribute, `x.y.z.w`, which is compared in numeric order L-R, -// is as follows: x.y.z track the above. w is the "-pre." number. +// is as follows: x.y.z track the above. w is the "-pre." or "-rc." number. // A release to the Chrome Web Store has w=1337. // E.g., 1.2.3-pre.4 is `version='1.2.3.4'`, and 1.2.3 (release) is // `version='1.2.3.1337'`. // If you get up to -pre.1336, just bump the `z` value and reset `w` :) . -////////////////////////////////////////////////////////////////////////// -// Messages between parts of TabFern // +////////////////////////////////////////////////////////////////////////// }}}1 +// Messages between parts of TabFern // {{{1 // The format of a message is // { msg: [, anything else] } @@ -36,8 +37,8 @@ const TABFERN_VERSION='0.1.17'; const MSG_GET_VIEW_WIN_ID = 'getViewWindowID'; const MSG_EDIT_TAB_NOTE = 'editTabNote'; -////////////////////////////////////////////////////////////////////////// -// Names of settings, and their defaults // +////////////////////////////////////////////////////////////////////////// }}}1 +// Names of settings, and their defaults // {{{1 /// An array to build the defaults in. Every property must have a default, /// since the defaults array is also used to identify properties to be @@ -51,7 +52,11 @@ let _DEF = { __proto__: null }; let _VAL = { __proto__: null }; let _vbool = (v)=>{ return ((typeof v === 'boolean')?v:undefined)}; -// Booleans +// Booleans {{{2 +const CFG_POPUP_ON_STARTUP = 'open-popup-on-chrome-startup'; +_DEF[CFG_POPUP_ON_STARTUP] = true; +_VAL[CFG_POPUP_ON_STARTUP] = _vbool; + const CFG_ENB_CONTEXT_MENU = 'ContextMenu.Enabled'; _DEF[CFG_ENB_CONTEXT_MENU] = true; _VAL[CFG_ENB_CONTEXT_MENU] = _vbool; @@ -122,8 +127,6 @@ const SETTINGS_LOADED_OK = '__settings_loaded_OK'; _DEF[SETTINGS_LOADED_OK] = false; _VAL[SETTINGS_LOADED_OK] = ()=>{return undefined;} - - // Not yet implemented - pending #35. Whether to open closed tabs when // you click on the tree item for a partially-open window. //const CFG_OPEN_REST_ON_CLICK = 'open-rest-on-win-click', @@ -131,7 +134,8 @@ _VAL[SETTINGS_LOADED_OK] = ()=>{return undefined;} // CFG_OROC_DO_NOT = false; //_DEF[CFG_OPEN_REST_ON_CLICK] = CFG_OROC_DO_NOT; -// Strings, including limited-choice controls such as radio buttons and dropdowns. +// }}}2 +// Strings and limited-choice controls such as radio buttons and dropdowns. {{{2 const CFGS_BACKGROUND = 'window-background'; _DEF[CFGS_BACKGROUND] = ''; _VAL[CFGS_BACKGROUND] = (v)=>{ @@ -155,12 +159,13 @@ _VAL[CFGS_SCROLLBAR_COLOR] = (v)=>{ return ((Validation.isValidColor(v)) ? v : undefined); }; +// }}}2 /// The default values for the configuration settings. const CFG_DEFAULTS = Object.seal(_DEF); const CFG_VALIDATORS = Object.seal(_VAL); -////////////////////////////////////////////////////////////////////////// -// Test for Firefox // +////////////////////////////////////////////////////////////////////////// }}}1 +// Test for Firefox // {{{1 // Not sure if I need this, but I'm playing it safe for now. Firefox returns // null rather than undefined in chrome.runtime.lastError when there is // no error. This is to test for null in Firefox without changing my @@ -197,8 +202,8 @@ BROWSER_TYPE=null; // unknown } })(window); -////////////////////////////////////////////////////////////////////////// -// Setting-related functions // +////////////////////////////////////////////////////////////////////////// }}}1 +// Setting-related functions // {{{1 const SETTING_PREFIX = 'store.settings.'; @@ -288,8 +293,8 @@ function getThemeName() else return CFG_DEFAULTS[CFGS_THEME_NAME]; } //getThemeName -////////////////////////////////////////////////////////////////////////// -// DOM-related functions // +////////////////////////////////////////////////////////////////////////// }}}1 +// DOM-related functions // {{{1 /// Append a - + + + + + + + - - - - - - - - + + diff --git a/webstore/src/view/tree.js b/webstore/src/view/tree.js index 21659db7..dae5eb0f 100755 --- a/webstore/src/view/tree.js +++ b/webstore/src/view/tree.js @@ -2,7 +2,9 @@ // Copyright (c) cxw42, 2017--2018 // See /doc/design.md for information about notation and organization. -// TODO break more of this into separate modules +// This is not a module. That is so that its internals are available for +// console inspection and debugging. That may change in the future. +// TODO break more of this into separate modules. console.log(`============================================================= Loading TabFern ${TABFERN_VERSION}`); @@ -47,7 +49,7 @@ let Module_dependencies = [ 'loglevel', 'hamburger', 'bypasser', 'multidex', 'justhtmlescape', 'signals', 'export-file', 'import-file', 'asynquence-contrib', 'asq-helpers', 'rmodal', - 'tinycolor', + 'tinycolor', 'spin-packed', // Shimmed modules. Refer to these via Modules or *without* the // `window.` prefix so it will be easier to refactor references @@ -181,15 +183,6 @@ function local_init() M = Modules['view/model']; ASQ = Modules['asynquence-contrib']; ASQH = Modules['asq-helpers']; - - // Check development status. Thanks to - // https://stackoverflow.com/a/12833511/2877364 by - // https://stackoverflow.com/users/1143495/konrad-dzwinel and - // https://stackoverflow.com/users/934239/xan - chrome.management.getSelf(function(info){ - if(info.installType === 'development') is_devel_mode = true; - }); - } //init() /// Copy properties named #property_names from #source to #dest. @@ -207,6 +200,12 @@ function copyTruthyProperties(dest, source, property_names, modifier) return dest; } //copyTruthyProperties +/// Escape text for use in a regex. By Mozilla Contributors (CC-BY-SA 2.5+), from +/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} //escapeRegExp + ////////////////////////////////////////////////////////////////////////// }}}1 // DOM Manipulation // {{{1 @@ -424,6 +423,7 @@ function showConfirmationModalDialog(message_html) { afterOpen: function() { $('#confirm-dialog .btn-primary').focus(); + // Note: focus is set but not visible in Chrome 69 - #142 //console.log('opened'); }, @@ -513,7 +513,7 @@ function saveTree(save_ephemeral_windows = true, cbk = undefined) // Get the raw data for the whole tree. Can't use $(...) because closed // tree nodes aren't in the DOM. - let root_node = T.treeobj.get_node($.jstree.root); //from get_json() src + let root_node = T.root_node(); if(!root_node || !root_node.children) { if(typeof cbk === 'function') cbk(new Error("Can't get root node")); return; @@ -592,6 +592,65 @@ function saveTree(save_ephemeral_windows = true, cbk = undefined) ); //storage.local.set } //saveTree() +////////////////////////////////////////////////////////////////////////// }}}1 +// Other actions // {{{1 + +/// Make a string replacement on the URLs of all the tabs in a window +/// @param node_id {string} the ID of the window's node +/// @param node {Object} the window's node +function actionURLSubstitute(node_id, node, unused_action_id, unused_action_el) +{ + let win_val = D.windows.by_node_id(node_id); + if(!win_val) return; + + // TODO replace window.prompt with an in-DOM GUI. + let old_text = window.prompt(_T('dlgpTextToReplace')); + if(old_text === null) return; // user cancelled + + let new_text = window.prompt(_T('dlgpReplacementText')); + if(new_text === null) return; // user cancelled + + if(!old_text) return; // search pattern is required + + // TODO URL escaping of new_text? + + let findregex; + if(old_text.length > 1 && old_text[0]==='/' && + old_text[old_text.length-1]==='/') { // Regex + findregex = new RegExp(old_text.slice(1, -1)); // drop the slashes + // TODO support flags as well + } else { // Literal + findregex = new RegExp(escapeRegExp(old_text)); + } + + for(let tab_node_id of node.children) { + let tab_val = D.tabs.by_node_id(tab_node_id); + if(!tab_val || !tab_val.tab_id) continue; + let new_url = tab_val.raw_url.replace(findregex, new_text); + // TODO URL escaping? + // TODO also replace in favicon URL? + if(new_url === tab_val.raw_url) continue; + + if(win_val.isOpen) { + // Make the change and let tabOnUpdated update the model. + ASQH.NowCC((cc)=>{ // ASQ for error reporting + chrome.tabs.update(tab_val.tab_id, {url: new_url}, cc); + }); + } else { + tab_val.raw_url = new_url; + M.refresh_label(tab_val); // do what tabOnUpdated() would + M.refresh_icon(tab_val); + M.refresh_tooltip(tab_val); + } + } // foreach child + + // If the window is closed, update the hash. Otherwise, tabOnUpdated() + // handled it. + if(!win_val.isOpen) { + M.updateOrderedURLHash(win_val); + } +} //actionURLSubstitute + ////////////////////////////////////////////////////////////////////////// }}}1 // jstree-action callbacks // {{{1 @@ -832,6 +891,14 @@ function actionDeleteWindow(node_id, node, unused_action_id, unused_action_el, } //endif confirmation required } //actionDeleteWindow +/// Move a window to the top of the tree. +function actionMoveWinToTop(node_id, node, unused_action_id, unused_action_el) +{ + if(!node) return; + T.treeobj.move_node(node, T.root_node(), 1); + // 1 => after the holding pen +} //actionMoveWinToTop + /// Toggle the top border on a node. This is a hack until I can add /// dividers. function actionToggleTabTopBorder(node_id, node, unused_action_id, unused_action_el) @@ -1502,11 +1569,32 @@ var loadSavedWindowsFromData = (function(){ // Load it if(vernum in versionLoaders) { - loader_retval = versionLoaders[vernum](data); - } else { + + try { +/* // TEMPORARILY REMOVED + T.treeobj.suppress_redraw(true); // EXPERIMENTAL +*/ + loader_retval = versionLoaders[vernum](data); + + } catch(e) { + log.error( + `Error loading version-${vernum} save data: ${e}`); + loader_retval = false; + // Continue out of the catch block so the + // suppress_redraw(false) will be called. + } + +/* // TEMPORARILY REMOVED + T.treeobj.suppress_redraw(false); // EXPERIMENTAL + T.treeobj.redraw(true); // Just in case the experiment + // had different results than + // we expected! +*/ + + } else { // unknown version log.error("I don't know how to load save data from version " + vernum); break READIT; - } + } //endif known version else if(loader_retval === false) { log.error("There was a problem loading save data of version " + vernum); @@ -2060,8 +2148,8 @@ function initFocusHandler() else if(old_win_id === WINID_NONE) change_from = FC_FROM_NONE; else change_from = FC_FROM_OPEN; - // Uncomment if you are debugging focus-change behaviour - //log.info({change_from, old_win_id, change_to, win_id}); + // Uncomment if you are debugging focus-change behaviour TODO RESUME HERE + log.info({change_from, old_win_id, change_to, win_id}); let same_window = (old_win_id === win_id); previously_focused_winid = win_id; @@ -2371,6 +2459,11 @@ function tabOnUpdated(tabid, changeinfo, ctab) // Caution: changeinfo doesn't always have all the changed information. // Therefore, we check changeinfo and ctab. + // TODO refactor the following into a separate routine that can be + // used to update closed or open tabs' tree items. Maybe move it + // to model.js as well. This will reduce code duplication, e.g., in + // actionURLSubstitute. + // URL let new_raw_url = changeinfo.url || ctab.url || 'about:blank'; if(new_raw_url !== tab_node_val.raw_url) { @@ -2810,7 +2903,7 @@ function hamRestoreLastDeleted() if(typeof wins_loaded === 'number' && wins_loaded > 0) { // We loaded the window successfully. Open it, if the user wishes. if(getBoolSetting(CFG_RESTORE_ON_LAST_DELETED, false)) { - let root = T.treeobj.get_node($.jstree.root); + let root = T.root_node(); let node_id = root.children[root.children.length-1]; T.treeobj.select_node(node_id); } @@ -2833,7 +2926,7 @@ function hamCollapseAll() function hamSorter(compare_fn) { return function() { - let arr = T.treeobj.get_node($.jstree.root).children; + let arr = T.root_node().children; Modules['view/sorts'].stable_sort(arr, compare_fn); // children[] holds node IDs, so compare_fn will always get strings. T.treeobj.redraw(true); // true => full redraw @@ -3043,7 +3136,6 @@ function getMainContextMenuItems(node, _unused_proxyfunc, e) // }; // } - return tabItems; } //endif K.IT_TAB @@ -3089,6 +3181,18 @@ function getMainContextMenuItems(node, _unused_proxyfunc, e) }; } + { // If not the first item, add "Move to top" + let parent_node = T.treeobj.get_node(node.parent); + if(parent_node.children[1] !== node.id) { + // children[1], not [0], because [0] is the holding pen. + winItems.toTopItem = { + label: _T('menuMoveToTop'), + icon: 'fff-text-padding-top', + action: ()=>{actionMoveWinToTop(node.id,node,null,null);}, + }; + } + } + winItems.deleteItem = { label: _T('menuDelete'), icon: 'fff-cross', @@ -3097,6 +3201,17 @@ function getMainContextMenuItems(node, _unused_proxyfunc, e) function(){actionDeleteWindow(node.id, node, null, null);} }; +/* // TEMPORARILY REMOVED + winItems.urlSubstituteItem = { + label: _T('menuURLSubstitute'), + title: _T('menuttURLSubstitute'), + icon: 'arrow-switch', + separator_before: true, + action: + function(){actionURLSubstitute(node.id, node, null, null);} + }; +*/ + return winItems; } //endif K.IT_WIN @@ -3755,7 +3870,7 @@ function delete_all_closed_nodes(are_you_sure) { if(!are_you_sure) return; - let root = T.treeobj.get_node($.jstree.root); + let root = T.root_node(); if(!root) return for(let i=root.children.length-1; i>0; --i) { @@ -3854,7 +3969,22 @@ function preLoadInit() } //preLoadInit -/// Beginning of the onload initialization. +// Beginning of the onload initialization. + +/// Check development status in an ASQ step. Thanks to +/// https://stackoverflow.com/a/12833511/2877364 by +/// https://stackoverflow.com/users/1143495/konrad-dzwinel and +/// https://stackoverflow.com/users/934239/xan +function determine_devel_mode(done) +{ + ASQH.NowCC((cc)=>{ chrome.management.getSelf(cc); }) + .val((info)=>{ + is_devel_mode = (info.installType === 'development'); + }) + .pipe(done); +} //determine_devel_mode() + +/// Initialization we can do before we have our window ID function basicInit(done) { next_init_step('basic initialization'); @@ -4188,6 +4318,8 @@ function initIncompleteWarning() //////////////////////////////////////////////////////////////////////// }}}1 // MAIN // {{{1 +/// The main function. Called once RequireJS has loaded all the +/// dependencies. function main(...args) { // Hack: Copy the loaded modules into our Modules global @@ -4219,11 +4351,22 @@ function main(...args) // Run the main init steps once the page has loaded let s = ASQ(); - callbackOnLoad(s.errfcb()); + callbackOnLoad(s.errfcb()); // Just using errfcb() to kick off s. // Note: on one test on Firefox, the rest of the chain never fired. // Not sure why. - s.then(basicInit) + // Start a spinner if loading takes more than 1 s + let spinner = new Spinner(); + let spin_starter = function() { + if(spinner) spinner.spin($('#tabfern-container')[0]); + }; + //let spin_timer = window.setTimeout(spin_starter, 1000); + + s.then(determine_devel_mode) + .then(basicInit) + + .val(spin_starter) + // for now, always start --- loadSavedWindowsIntoTree is synchronous .try((done)=>{ // Get our Chrome-extensions-API window ID from the background page. @@ -4252,6 +4395,13 @@ function main(...args) .val(check_init_step_count) + // Stop the spinner, if it started + .val(()=>{ + spinner.stop(); + spinner = null; + //clearTimeout(spin_timer); + }) + .or((err)=>{ $(K.INIT_MSG_SEL).text( $(K.INIT_MSG_SEL).text() + "\n" + err