From fe18d95000c6a274d59eb5517bfb0b02539df851 Mon Sep 17 00:00:00 2001 From: Chris White Date: Sun, 4 Mar 2018 16:43:44 -0500 Subject: [PATCH] Updated webstore for v0.1.16 --- webstore/assets/css/icons.css | 78 +++++-- webstore/js/jstree-actions.js | 7 +- webstore/js/jstree.js | 6 +- webstore/manifest.json | 4 +- webstore/src/common/common.js | 4 +- webstore/src/settings/manifest.js | 13 ++ webstore/src/view/bundle_tree.js | 13 +- webstore/src/view/item_tree.js | 5 +- webstore/src/view/model.js | 332 ++++++++++++------------------ webstore/src/view/tree.css | 2 +- webstore/src/view/tree.html | 3 +- webstore/src/view/tree.js | 95 ++++++--- 12 files changed, 298 insertions(+), 264 deletions(-) diff --git a/webstore/assets/css/icons.css b/webstore/assets/css/icons.css index 1949ddf2..390905a1 100755 --- a/webstore/assets/css/icons.css +++ b/webstore/assets/css/icons.css @@ -1,59 +1,95 @@ /* icons.css: Common icon definitions */ -.tf-window.tfs-saved > a > i { - content: url(/assets/icons/folder-from-jstree-default-dark-32px.png); +.jstree-themeicon-custom.tf-window.tfs-saved > a > i { + background-image: url(/assets/icons/folder-from-jstree-default-dark-32px.png); } /* For Chrome */ -.visible-window-icon, .tf-window.tfs-open > a > i { - content: url(/assets/icons/monitor.png); /* or folder.png? */ +.jstree-themeicon-custom.visible-window-icon, +.jstree-themeicon-custom.tf-window.tfs-open > a > i { + background-image: url(/assets/icons/monitor.png); /* or folder.png? */ } -.visible-saved-window-icon, .tf-window.tfs-open.tfs-saved > a > i { - content: url(/assets/icons/monitor_add.png); /* or folder.png? */ +.jstree-themeicon-custom.visible-saved-window-icon, .jstree-themeicon-custom.tf-window.tfs-open.tfs-saved > a > i { + background-image: url(/assets/icons/monitor_add.png); /* or folder.png? */ } /* For Firefox */ +/* .tf-window.tfs-open.tfs-saved > a > i::before { - content: url(/assets/icons/monitor_add.png); /* or folder.png? */ + content: url(/assets/icons/monitor_add.png); * or folder.png? * } .tf-window.tfs-open > a > i::before { - content: url(/assets/icons/monitor.png); /* or folder.png? */ + content: url(/assets/icons/monitor.png); * or folder.png? * } +*/ /* Back to cross-browser */ -.fff-page /*, .tf-tab > i*/ { - content: url(/assets/icons/page_white.png); +.jstree-themeicon-custom.fff-page /*, .tf-tab > i*/ { + background-image: url(/assets/icons/page_white.png); } -.fff-page-white-with-red-banner { - content: url(/assets/icons/page_white_red_banner.png); +.jstree-themeicon-custom.fff-monitor-add { + background-image: url(/assets/icons/monitor_add.png); } -.fff-pencil { - content: url(/assets/icons/pencil.png); +.jstree-themeicon-custom.fff-monitor { + background-image: url(/assets/icons/monitor.png); } -.fff-cross { - content: url(/assets/icons/cross.png); +.jstree-themeicon-custom.fff-page-white-with-red-banner { + background-image: url(/assets/icons/page_white_red_banner.png); } -.fff-picture-delete { - content: url(/assets/icons/picture_delete.png); +.jstree-themeicon-custom.fff-pencil, .vakata-context .fff-pencil { + background-image: url(/assets/icons/pencil.png); +} + +.jstree-themeicon-custom.fff-cross, .vakata-context .fff-cross { + background-image: url(/assets/icons/cross.png); } -.fff-text-padding-top { - content: url(/assets/icons/text_padding_top.png); +.jstree-themeicon-custom.fff-picture-delete, .vakata-context .fff-picture-delete { + background-image: url(/assets/icons/picture_delete.png); +} + +.jstree-themeicon-custom.fff-text-padding-top, +.vakata-context .fff-text-padding-top { + background-image: url(/assets/icons/text_padding_top.png); } /* Class for icons with no content. Used in jstree.set_icon() when the * icon is actually being set using CSS. */ -.clear-icon { +.jstree-themeicon-custom.clear-icon, .vakata-context .clear-icon { /*url(/assets/icons/clear-icon.png);*/ /*content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQI12NgYGBgAAAABQABXvMqOgAAAABJRU5ErkJggg==);*/ } +/* Icons for actions, which do use content rather than background-image. + * That way we do not have to set the size of the manually. */ +.tf-action-button.fff-pencil { + content: url(/assets/icons/pencil.png); +} + +.tf-action-button.fff-picture-delete { + content: url(/assets/icons/picture_delete.png); +} + +.tf-action-button.fff-cross { + 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-repeat: no-repeat; + background-position: center center; +} + /* vi: set ts=4 sts=4 sw=4 et ai: */ diff --git a/webstore/js/jstree-actions.js b/webstore/js/jstree-actions.js index 8da820b9..92a7dc85 100644 --- a/webstore/js/jstree-actions.js +++ b/webstore/js/jstree-actions.js @@ -124,6 +124,7 @@ * The action object can contain the following keys: * id <- string An ID which identifies the action. The same ID can be shared across different nodes * text <- string The action's text + * html <- string The action's html; used in preference to text if both are provided * class <- string (a string containing all the classes you want to add to the action (space separated) * event <- string The event on which the trigger will be called * callback <- function that will be called when the action is clicked @@ -217,7 +218,11 @@ var action_el = document.createElement("i"); action_el.className = action.class; - action_el.textContent = action.text; + if(action.html) { + action_el.innerHTML = action.html; + } else { + action_el.textContent = action.text || ''; + } // Set up element data-* values, if any if( action_el.dataset && action.dataset && diff --git a/webstore/js/jstree.js b/webstore/js/jstree.js index a7269323..62bd9e79 100755 --- a/webstore/js/jstree.js +++ b/webstore/js/jstree.js @@ -2477,7 +2477,7 @@ node.childNodes[1].childNodes[0].style.backgroundImage = 'url("'+obj.icon+'")'; node.childNodes[1].childNodes[0].style.backgroundPosition = 'center center'; node.childNodes[1].childNodes[0].style.backgroundSize = 'auto'; - node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom'; + node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom jstree-url-icon'; } } @@ -4712,7 +4712,7 @@ this.hide_icon(obj); } else if(icon === true || icon === null || icon === undefined || icon === '') { - dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel"); + dom.removeClass('jstree-themeicon-custom jstree-url-icon ' + old).css("background","").removeAttr("rel"); if(old === false) { this.show_icon(obj); } } else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) { @@ -4722,7 +4722,7 @@ } else { dom.removeClass(old).css("background",""); - dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon); + dom.addClass('jstree-themeicon-custom jstree-url-icon').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon); if(old === false) { this.show_icon(obj); } } return true; diff --git a/webstore/manifest.json b/webstore/manifest.json index aea0a709..7ea1213f 100755 --- a/webstore/manifest.json +++ b/webstore/manifest.json @@ -1,8 +1,8 @@ { "name": "TabFern tab manager and backup tool", "short_name": "TabFern", - "version": "0.1.15.1337", - "version_name": "0.1.15", + "version": "0.1.16.1337", + "version_name": "0.1.16", "offline_enabled": true, "manifest_version": 2, "minimum_chrome_version": "54", diff --git a/webstore/src/common/common.js b/webstore/src/common/common.js index d3f3b93c..a301ef75 100755 --- a/webstore/src/common/common.js +++ b/webstore/src/common/common.js @@ -11,7 +11,7 @@ console.log('TabFern common.js loading'); /// The TabFern extension friendly version number. Displayed in the /// title bar of the popup window, so lowercase (no shouting!). -const TABFERN_VERSION='0.1.15' //' alpha \u26a0' +const TABFERN_VERSION='0.1.16' //' alpha \u26a0' // When you change this, also update: // - manifest.json: both the version and version_name // - package.json @@ -70,7 +70,7 @@ const CFG_DEFAULTS = { [CFG_OPEN_TOP_ON_STARTUP]: false, [CFG_HIDE_HORIZONTAL_SCROLLBARS]: true, [CFG_SKINNY_SCROLLBARS]: false, - [CFG_NEW_WINS_AT_TOP]: false, + [CFG_NEW_WINS_AT_TOP]: true, [CFG_SHOW_TREE_LINES]: false, [CFG_CONFIRM_DEL_OF_SAVED]: true, [CFG_CONFIRM_DEL_OF_UNSAVED]: false, diff --git a/webstore/src/settings/manifest.js b/webstore/src/settings/manifest.js index f6c97322..bd2081e0 100755 --- a/webstore/src/settings/manifest.js +++ b/webstore/src/settings/manifest.js @@ -259,6 +259,19 @@ bar (it will start with "file://") // }}}2 // Changelog {{{1 + { + "tab": i18n.get("What's new?"), + "group": `Version 0.1.16${brplain('2018-xx-xx')}`, + 'group_html':true, + "type": "description", + "text": +`` + }, { "tab": i18n.get("What's new?"), "group": `Version 0.1.15${brplain('2018-02-09')}`, diff --git a/webstore/src/view/bundle_tree.js b/webstore/src/view/bundle_tree.js index fe8b91bc..92237267 100644 --- a/webstore/src/view/bundle_tree.js +++ b/webstore/src/view/bundle_tree.js @@ -2480,7 +2480,7 @@ node.childNodes[1].childNodes[0].style.backgroundImage = 'url("'+obj.icon+'")'; node.childNodes[1].childNodes[0].style.backgroundPosition = 'center center'; node.childNodes[1].childNodes[0].style.backgroundSize = 'auto'; - node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom'; + node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom jstree-url-icon'; } } @@ -4715,7 +4715,7 @@ this.hide_icon(obj); } else if(icon === true || icon === null || icon === undefined || icon === '') { - dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel"); + dom.removeClass('jstree-themeicon-custom jstree-url-icon ' + old).css("background","").removeAttr("rel"); if(old === false) { this.show_icon(obj); } } else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) { @@ -4725,7 +4725,7 @@ } else { dom.removeClass(old).css("background",""); - dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon); + dom.addClass('jstree-themeicon-custom jstree-url-icon').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon); if(old === false) { this.show_icon(obj); } } return true; @@ -8588,6 +8588,7 @@ * The action object can contain the following keys: * id <- string An ID which identifies the action. The same ID can be shared across different nodes * text <- string The action's text + * html <- string The action's html; used in preference to text if both are provided * class <- string (a string containing all the classes you want to add to the action (space separated) * event <- string The event on which the trigger will be called * callback <- function that will be called when the action is clicked @@ -8681,7 +8682,11 @@ var action_el = document.createElement("i"); action_el.className = action.class; - action_el.textContent = action.text; + if(action.html) { + action_el.innerHTML = action.html; + } else { + action_el.textContent = action.text || ''; + } // Set up element data-* values, if any if( action_el.dataset && action.dataset && diff --git a/webstore/src/view/item_tree.js b/webstore/src/view/item_tree.js index 98e773e4..d4a32eb9 100755 --- a/webstore/src/view/item_tree.js +++ b/webstore/src/view/item_tree.js @@ -194,7 +194,7 @@ jstreeTypes[K.IT_WIN] = { li_attr: { 'class': WIN_CLASS }, - icon: 'clear-icon', // We will overlay the actual icon in the CSS + //icon: 'clear-icon', // We will overlay the actual icon in the CSS }; jstreeTypes[K.NST_OPEN] = { li_attr: { 'class': OPEN_CLASS } }; @@ -207,6 +207,9 @@ icon: 'fff-page', // per-node icons will override this }; + // TODO add option for users to create divider items between windows - + // e.g.,
+ // The main config let jstreeConfig = { plugins: ['because', 'wholerow', 'actions', diff --git a/webstore/src/view/model.js b/webstore/src/view/model.js index 7d7afbbb..09df4166 100755 --- a/webstore/src/view/model.js +++ b/webstore/src/view/model.js @@ -52,6 +52,67 @@ /// Value returned by vn*() on error. Both members are falsy. module.VN_NONE = {val: null, node_id: ''}; + // Querying the model ////////////////////////////////////////////// {{{1 + + /// Get a {val, node_id} pair (vn) from one of those (vorny). + /// @param val_or_nodey {mixed} If a string, the node ID of the + /// item; otherwise, the details + /// record for the item, or the jstree node + /// record for the node. + /// @param item_type {mixed=} If provided, the type of the item. + /// Otherwise, all types will be checked. + /// @return {Object} {val, node_id}. `val` is falsy if the + /// given vorny was not found. + module.vn_by_vorny = function(val_or_nodey, item_type) { + if(!val_or_nodey) return module.VN_NONE; + + let val, node_id; + if(typeof val_or_nodey === 'string') { // a node_id + node_id = val_or_nodey; + switch(item_type) { + case K.IT_WIN: + val = D.windows.by_node_id(node_id); break; + case K.IT_TAB: + val = D.tabs.by_node_id(node_id); break; + default: + val = D.val_by_node_id(node_id); break; + } + + } else if(typeof val_or_nodey === 'object' && val_or_nodey.id && + val_or_nodey.parent) { // A jstree node + node_id = val_or_nodey.id; + val = D.val_by_node_id(node_id); + + } else if(typeof val_or_nodey === 'object' && // A val (details record) + val_or_nodey.ty) { + val = val_or_nodey; + if(!val.node_id) return module.VN_NONE; + node_id = val.node_id; + } else { // Unknown + return module.VN_NONE; + } + + return {val, node_id}; + } //vn_by_vorny + + /// Determine whether a model has given subtype(s). + /// @param vorny {mixed} The item + /// @param tys {mixed} A single type or array of types + /// @return {Boolean} true if #vorny has all the subtypes in #tys; + /// false otherwise. + module.has_subtype = function(vorny, ...tys) { + if(!vorny || !tys) return false; + if(tys.length < 1) return false; + let {node_id} = module.vn_by_vorny(vorny); + if(!node_id) return false; + + for(let ty of tys) { + if(!T.treeobj.has_multitype(node_id, ty)) return false; + } + return true; + } //has_subtype + + // }}}1 // Data-access routines //////////////////////////////////////////// {{{1 /// Find a node's value in the model, regardless of type. @@ -86,7 +147,7 @@ } }; //get_win_raw_text() - /// Mark window item #val as unsaved. + /// Mark window item #val as unsaved (forget #val). /// @param val {Object} the item /// @param adjust_title {Boolean=true} Add unsaved markers if truthy /// @return {Boolean} true on success; false on error @@ -104,6 +165,7 @@ // so we don't need to manually assign text here. module.refresh_label(val.node_id); + module.refresh_icon(val); return true; }; //mark_as_unsaved() @@ -134,16 +196,24 @@ retval += '📌 '; // PUSHPIN } + let raw_text = module.get_win_raw_text(val); if(val.raw_bullet && typeof val.raw_bullet === 'string') { // The first condition checks for null/undefined/&c., and also for // empty strings. retval += ''; retval += Esc.escape(val.raw_bullet); - retval += ' ✦'; // a dingbat - retval += ' '; + + // Add a dingbat if there is text to go on both sides of it. + if(raw_text && raw_text !== "\ufeff") { + // \ufeff is a special case for the Empty New Tab Page + // extension, which cxw42 has been using for some years now. + retval += ' ✦ '; // the dingbat + } + + retval += ''; } - retval += Esc.escape(module.get_win_raw_text(val)); + retval += Esc.escape(raw_text); return retval; }; @@ -162,27 +232,48 @@ return retval; }; - /// Update the icon of tab item #val. - /// @param val {Object} The details record for this item + /// Update the icon of #vorny + /// @param vorny {Mixed} The item /// @return {Boolean} true on success; false on error - module.refresh_tab_icon = function(val) { - if(!val || !val.node_id) return false; - if(val.ty !== K.IT_TAB) return false; - - let icon = 'fff-page'; - if(val.raw_favicon_url) { - icon = encodeURI(val.raw_favicon_url); - } else if((/\.pdf$/i).test(val.raw_url)) { //special-case PDFs - icon = 'fff-page-white-with-red-banner'; + module.refresh_icon = function(vorny) { + let {val, node_id} = module.vn_by_vorny(vorny); + let node = T.treeobj.get_node(node_id); + if(!val || !node_id || !node) return false; + + let icon; + + switch(val.ty) { + case K.IT_TAB: + icon = 'fff-page'; + if(val.raw_favicon_url) { + icon = encodeURI(val.raw_favicon_url); + } else if((/\.pdf$/i).test(val.raw_url)) { //special-case PDFs + icon = 'fff-page-white-with-red-banner'; + } + break; + + case K.IT_WIN: + icon = true; // default icon for closed windows + if(val.isOpen && val.keep) { // open and saved + icon = 'fff-monitor-add'; + } else if(val.isOpen) { // ephemeral + icon = 'fff-monitor'; + } + break; + + default: + return false; } - T.treeobj.set_icon(val.node_id, icon); + if(!icon) return false; + + T.treeobj.set_icon(node, icon); // TODO? if the favicon doesn't load, replace the icon with the // generic page icon so we don't keep hitting the favIconUrl. return true; - } //refresh_tab_icon + } //refresh_icon /// Mark the window identified by #win_node_id as to be kept. /// @param win_node_id {string} The window node ID @@ -203,114 +294,10 @@ } module.refresh_label(win_node_id); + module.refresh_icon(val); return true; }; //remember() - // }}}1 - // Item creation /////////////////////////////////////////////////// {{{1 - - /// Create a new fern, optionally for an open Chrome window. - /// ** Does not populate any tab nodes --- this is just for a window. - /// @param cwin {Chrome Window record} The open window. - /// If falsy, there is no Chrome window presently. - /// @param keep {boolean} If #cwin is truthy, determines whether the window - /// is (true) open and saved or (false) ephemeral. - /// If #cwin is falsy, #keep is ignored and treated - /// as if it were `true`. - /// @return {object} {node_id (the fern's id), val}. On error, - /// at least one of node_id or val will be falsy. - module.makeItemForWindow = function(cwin, keep) { - if(!cwin) keep = K.WIN_KEEP; //creating item for a closed window => keep - keep = (keep ? K.WIN_KEEP : K.WIN_NOKEEP); //regularize - - let error_return = {node_id:null, val:null}; - - let pos = (!!cwin && getBoolSetting(CFG_NEW_WINS_AT_TOP)) ? 'first' : 'last'; - let win_node_id = T.treeobj.create_node( - $.jstree.root, // parent - { text: 'Window' // node data - , state: { 'opened': !!cwin } - }, - pos - ); - if(win_node_id === false) return error_return; - T.treeobj.add_multitype(win_node_id, K.IT_WIN); - if(cwin) T.treeobj.add_multitype(win_node_id, K.NST_OPEN); - if(keep) T.treeobj.add_multitype(win_node_id, K.NST_SAVED); - - loginfo({'Adding nodeid map for cwinid': cwin ? cwin.id : 'none'}); - let win_val = D.windows.add({ - win_id: (cwin ? cwin.id : K.NONE), - node_id: win_node_id, - win: (cwin ? cwin : undefined), - raw_title: null, // default name - raw_bullet: null, - isOpen: !!cwin, - keep: keep - }); - - T.treeobj.rename_node(win_node_id, module.get_html_label(win_val)); - - return {node_id: win_node_id, val: win_val}; - } //makeItemForWindow - - /// Create a new node for a tab, optionally for an open Chrome tab. - /// @param parent_node_id {string} The parent's node ID (a window) - /// @param ctab {Chrome Tab record} The open tab. - /// If falsy, there is no Chrome tab presently. - /// @param raw_url {string} If #ctab is falsy, the URL of the tab - /// @param raw_title {string} If #ctab is falsy, the title of the tab - /// @param tys {mixed} If provided, add those multitypes to the tab - /// @return {object} {node_id, val}. On error, - /// at least one of node_id or val will be falsy. - module.makeItemForTab = function(parent_node_id, ctab, raw_url, raw_title, - tys) { - let error_return = {node_id:null, val:null}; - if(!parent_node_id) return error_return; - - let tab_node_id = T.treeobj.create_node( - parent_node_id, - { text: 'Tab' } - ); - if(tab_node_id === false) return error_return; - - T.treeobj.add_multitype(tab_node_id, K.IT_TAB); - if(ctab) T.treeobj.add_multitype(tab_node_id, K.NST_OPEN); - - if(tys) { - if(!$.isArray(tys)) tys=[tys]; - for(let ty of tys) T.treeobj.add_multitype(tab_node_id, ty); - } - - let tab_val = D.tabs.add({ - tab_id: (ctab ? ctab.id : K.NONE), - node_id: tab_node_id, - win_id: (ctab ? ctab.windowId : K.NONE), - index: (ctab ? ctab.index : K.NONE), - tab: (ctab || undefined), - raw_url: (ctab ? ctab.url : String(raw_url)), - raw_title: (ctab ? ctab.title : String(raw_title)), - raw_bullet: null, - isOpen: !!ctab, - }); - - T.treeobj.rename_node(tab_node_id, module.get_html_label(tab_val)); - - { // Set icon - let icon = 'fff-page'; - if(ctab && ctab.favIconUrl) { - icon = encodeURI(ctab.favIconUrl); - } else if((/\.pdf$/i).test(tab_val.raw_url)) { //special-case PDFs - icon = 'fff-page-white-with-red-banner'; - } - T.treeobj.set_icon(tab_node_id, icon); - // TODO if the favicon doesn't load, replace the icon with the - // generic page icon so we don't keep hitting the favIconUrl. - } - - return {node_id: tab_node_id, val: tab_val}; - } //makeItemForTab - // }}}1 // ##################################################################### // ##################################################################### @@ -319,7 +306,7 @@ // "Rez" and "Erase" are adding/removing items, to distinguish them // from creating and destroying Chrome widgets. - // Helper routines ///////////////////////////////////////////////// {{{1 + // Hashing routines //////////////////////////////////////////////// {{{1 // Hash the strings in #strs together. All strings are encoded in utf8 // before hashing. @@ -376,75 +363,13 @@ }; //updateOrderedURLHash() // }}}1 - // Querying the model ////////////////////////////////////////////// {{{1 - - /// Get a {val, node_id} pair (vn) from one of those (vorny). - /// @param val_or_nodey {mixed} If a string, the node ID of the - /// item; otherwise, the details - /// record for the item, or the jstree node - /// record for the node. - /// @param item_type {mixed=} If provided, the type of the item. - /// Otherwise, all types will be checked. - /// @return {Object} {val, node_id}. `val` is falsy if the - /// given vorny was not found. - module.vn_by_vorny = function(val_or_nodey, item_type) { - if(!val_or_nodey) return module.VN_NONE; - - let val, node_id; - if(typeof val_or_nodey === 'string') { // a node_id - node_id = val_or_nodey; - switch(item_type) { - case K.IT_WIN: - val = D.windows.by_node_id(node_id); break; - case K.IT_TAB: - val = D.tabs.by_node_id(node_id); break; - default: - val = D.val_by_node_id(node_id); break; - } - - } else if(typeof val_or_nodey === 'object' && val_or_nodey.id && - val_or_nodey.parent) { // A jstree node - node_id = val_or_nodey.id; - val = D.val_by_node_id(node_id); - - } else if(typeof val_or_nodey === 'object' && // A details record - val_or_nodey.ty) { - val = val_or_nodey; - if(!val.node_id) return module.VN_NONE; - node_id = val.node_id; - } else { // Unknown - return module.VN_NONE; - } - - return {val, node_id}; - } //vn_by_vorny - - /// Determine whether a model has given subtype(s). - /// @param vorny {mixed} The item - /// @param tys {mixed} A single type or array of types - /// @return {Boolean} true if #vorny has all the subtypes in #tys; - /// false otherwise. - module.has_subtype = function(vorny, ...tys) { - if(!vorny || !tys) return false; - if(tys.length < 1) return false; - let {node_id} = module.vn_by_vorny(vorny); - if(!node_id) return false; - - for(let ty of tys) { - if(!T.treeobj.has_multitype(node_id, ty)) return false; - } - return true; - } //has_subtype - - // }}}1 - ////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////// // Initializing and shutting down the model // TODO add a function that wraps T.create() so the user of model does // not have to directly access T to kick things off. - ////////////////////////////////////////////////////////////////////// - // Adding model items + // Adding model items ////////////////////////////////////////////// {{{1 /// Add a model node/item for a window. Does not process Chrome /// widgets. Instead, assumes the tab is closed initially. @@ -457,7 +382,8 @@ let node_id = T.treeobj.create_node( $.jstree.root, { text: 'Window' }, - (isFirstChild ? 'first' : 'last') + (isFirstChild ? 1 : 'last') + // 1 => after the holding pen (T.holding_node_id) ); if(node_id === false) return module.VN_NONE; @@ -478,6 +404,9 @@ return module.VN_NONE; } + module.refresh_label(node_id); + module.refresh_icon(val); + return {val, node_id}; } //vnRezWin @@ -520,11 +449,14 @@ return module.VN_NONE; } + module.refresh_label(node_id); + module.refresh_icon(val); + return {val, node_id}; } //vnRezTab - ////////////////////////////////////////////////////////////////////// - // Updating model items + // }}}1 + // Updating model items //////////////////////////////////////////// {{{1 /// Add a subtype (K.NST_*) to an item. /// @param vorny {mixed} The item @@ -560,8 +492,8 @@ return true; } //add_subtype - ////////////////////////////////////////////////////////////////////// - // Attaching Chrome widgets to model items + // }}}1 + // Attaching Chrome widgets to model items ///////////////////////// {{{1 /// Attach a Chrome window to an existing window item. /// Updates the item, but does not touch the Chrome window. @@ -597,7 +529,7 @@ T.treeobj.add_multitype(node_id, K.NST_OPEN); module.refresh_label(node_id); - // TODO refresh icon? + module.refresh_icon(val); return true; } //markWinAsOpen @@ -640,7 +572,7 @@ T.treeobj.add_multitype(node_id, K.NST_OPEN); module.refresh_label(node_id); - module.refresh_tab_icon(val); // since favicon may have changed + module.refresh_icon(val); // since favicon may have changed // Design decision: tree items for open windows always start expanded. // No one has requested any other behaviour, as of the time of writing. @@ -649,8 +581,8 @@ return true; } //markTabAsOpen - ////////////////////////////////////////////////////////////////////// - // Removing Chrome widgets from model items + // }}}1 + // Removing Chrome widgets from model items //////////////////////// {{{1 /// Remove the connection between #win_vorny and its Chrome window. /// Use this when the Chrome window has been closed. @@ -678,7 +610,7 @@ T.treeobj.del_multitype(node_id, K.NST_OPEN); module.refresh_label(node_id); - //TODO refresh icon + module.refresh_icon(val); return true; } //markWinAsClosed @@ -721,8 +653,8 @@ return true; } //markTabAsClosed - ////////////////////////////////////////////////////////////////////// - // Removing model items + // }}}1 + // Removing model items //////////////////////////////////////////// {{{1 /// Delete a tab from the tree and the details. /// ** NOTE ** Does NOT update the parent's val.ordered_url_hash. @@ -774,6 +706,8 @@ return true; } //eraseWin + // }}}1 + return module; })); diff --git a/webstore/src/view/tree.css b/webstore/src/view/tree.css index fa4e2bfc..f852e9f4 100755 --- a/webstore/src/view/tree.css +++ b/webstore/src/view/tree.css @@ -364,7 +364,7 @@ div.jstree-wholerow-hovered > .tf-action-group { /* --- Tweak up jstree -------------------------------------------- {{{1 -- */ /* shrink large favicons to fit, if necessary */ -.jstree-children .jstree-children .jstree-anchor .jstree-icon { +.jstree-children .jstree-children .jstree-anchor .jstree-icon.jstree-url-icon { /* Two jstree-children: don't touch the top-level nodes, only second-level * and lower. */ background-size: cover !important; diff --git a/webstore/src/view/tree.html b/webstore/src/view/tree.html index dbc3de9c..7ab36337 100644 --- a/webstore/src/view/tree.html +++ b/webstore/src/view/tree.html @@ -13,9 +13,10 @@ - + + diff --git a/webstore/src/view/tree.js b/webstore/src/view/tree.js index 7d74eac8..9c0d564b 100755 --- a/webstore/src/view/tree.js +++ b/webstore/src/view/tree.js @@ -478,6 +478,10 @@ function actionRenameWindow(node_id, node, unused_action_id, unused_action_el) // wants to keep it. false => do not change the raw_title, // since the user just specified it. + M.del_subtype(node_id, K.NST_RECOVERED); + // The user has touched the window, so doesn't need the "recovered" + // reminder. + saveTree(); } //actionRenameWindow() @@ -488,7 +492,6 @@ function actionForgetWindow(node_id, node, unused_action_id, unused_action_el) if(!win_val) return; M.mark_win_as_unsaved(win_val); - //M.refresh_label(node_id); if(win_val.isOpen) { // should always be true, but just in case... //T.treeobj.set_type(node, K.NT_WIN_EPHEMERAL); @@ -506,7 +509,6 @@ function actionRememberWindow(node_id, node, unused_action_id, unused_action_el) //if(!win_val) return; M.remember(node_id); // No-op if node_id isn't a window - //M.refresh_label(node_id); saveTree(); } //actionForgetWindow() @@ -741,7 +743,7 @@ function actionDeleteTab(node_id, node, unused_action_id, unused_action_el, function doDeletion() { if(tab_val.tab_id !== K.NONE) { // Remove open tabs chrome.tabs.remove(tab_val.tab_id, ignore_chrome_error); - //tabOnDeleted will do the rest + //tabOnRemoved will do the rest } else { // Remove closed tabs M.eraseTab(tab_val); @@ -759,11 +761,15 @@ function actionDeleteTab(node_id, node, unused_action_id, unused_action_el, // Prompt for confirmation, if necessary let is_keep = (parent_val.keep === K.WIN_KEEP); let is_nokeep = (parent_val.keep === K.WIN_NOKEEP); + let need_confirmation = ( + (is_keep && getBoolSetting(CFG_CONFIRM_DEL_OF_SAVED_TABS)) || + (is_nokeep && getBoolSetting(CFG_CONFIRM_DEL_OF_UNSAVED_TABS)) + ); - if( //is_internal_unimplemented || - (is_keep && !getBoolSetting(CFG_CONFIRM_DEL_OF_SAVED_TABS)) || - (is_nokeep && !getBoolSetting(CFG_CONFIRM_DEL_OF_UNSAVED_TABS)) - ) { // No confirmation required - just do it + if( !need_confirmation || + (/^((chrome:\/\/newtab\/?)|(about:blank))$/i.test(tab_val.raw_url)) + ) { + // No confirmation required - just do it doDeletion(); } else { // Confirmation required @@ -850,7 +856,12 @@ function addTabNodeActions(tab_node_id) T.treeobj.add_action(tab_node_id, { id: 'editBullet', class: 'fff-pencil ' + K.ACTION_BUTTON_WIN_CLASS, - text: ' ', + text: '\xa0', + // I tried this approach but it was a bit ugly. For example, the + // image was in the right place but the border of the was offset + // down a pixel or two. Also, the class was required for the + // "Actually" event check in treeOnSelect. + //html: ``, grouped: true, callback: actionEditTabBullet, dataset: { action: 'editBullet' } @@ -859,7 +870,7 @@ function addTabNodeActions(tab_node_id) T.treeobj.add_action(tab_node_id, { id: 'deleteTab', class: 'fff-cross ' + K.ACTION_BUTTON_WIN_CLASS, - text: ' ', + text: '\xa0', grouped: true, callback: actionDeleteTab, dataset: { action: 'deleteTab' } @@ -897,6 +908,7 @@ function createNodeForClosedTabV1(tab_data_v1, parent_node_id) copyTruthyProperties(val, tab_data_v1, 'isPinned', Boolean); M.refresh_label(node_id); + M.refresh_icon(val); if(tab_data_v1.bordered) M.add_subtype(val, K.NST_TOP_BORDER); @@ -918,7 +930,7 @@ function addWindowNodeActions(win_node_id) T.treeobj.add_action(win_node_id, { id: 'renameWindow', class: 'fff-pencil ' + K.ACTION_BUTTON_WIN_CLASS, - text: ' ', + text: '\xa0', grouped: true, callback: actionRenameWindow, dataset: { action: 'renameWindow' } @@ -927,7 +939,7 @@ function addWindowNodeActions(win_node_id) T.treeobj.add_action(win_node_id, { id: 'closeWindow', class: 'fff-picture-delete ' + K.ACTION_BUTTON_WIN_CLASS, - text: ' ', + text: '\xa0', grouped: true, callback: actionCloseWindowAndSave, dataset: { action: 'closeWindow' } @@ -936,7 +948,7 @@ function addWindowNodeActions(win_node_id) T.treeobj.add_action(win_node_id, { id: 'deleteWindow', class: 'fff-cross ' + K.ACTION_BUTTON_WIN_CLASS, - text: ' ', + text: '\xa0', grouped: true, callback: actionDeleteWindow, dataset: { action: 'deleteWindow' } @@ -1013,6 +1025,7 @@ function createNodeForClosedWindowV1(win_data_v1) val.raw_title = new_title; M.refresh_label(node_id); + M.refresh_icon(val); addWindowNodeActions(node_id); @@ -1053,13 +1066,9 @@ function attachChromeWindowToSavedWindowItem(cwin, existing_win, during_init=fal if(M.has_subtype(existing_win.node.id, K.NST_RECOVERED)) { M.del_subtype(existing_win.node.id, K.NST_RECOVERED); existing_win.val.raw_title = null; //default title - M.mark_win_as_unsaved(existing_win.val, false); // also refreshes the label + M.mark_win_as_unsaved(existing_win.val, false); } - // Do we need these? -// T.treeobj.open_node(existing_win.node); -// T.treeobj.redraw_node(existing_win.node); - if(cwin.tabs.length !== existing_win.node.children.length) { log.error({ 'Mismatched child count': @@ -1481,6 +1490,8 @@ function treeOnSelect(_evt_unused, evt_data) win_val.win = win; //T.treeobj.set_type(win_node.id, K.NT_WIN_ELVISH); M.add_subtype(win_node.id, K.NST_OPEN); + M.refresh_label(win_node.id); + M.refresh_icon(win_node); T.treeobj.open_node(win_node); T.treeobj.redraw_node(win_node); @@ -1909,13 +1920,13 @@ var tabOnCreated = (function(){ } let cwin = cwin_or_err; - let existing_win = winAlreadyExistsInTree(cwin); - MERGE: if(existing_win && existing_win.val && - !existing_win.val.isOpen // don't hijack other open wins + let merge_to_win = winAlreadyExistsInTree(cwin); + MERGE: if(merge_to_win && merge_to_win.val && + !merge_to_win.val.isOpen // don't hijack other open wins ) { log.info({ - [`merge ${cwin.id} Found existing window`]: cwin, - existing_win + [`merge ${cwin.id} Found merge target in tree for`]: cwin, + merge_to_win }); //actionDeleteWindow(win_node_id, T.treeobj.get_node(win_node_id),null,null); log.debug(`merge ${ctab.windowId}==${cwin.id}: start`); @@ -1925,20 +1936,20 @@ var tabOnCreated = (function(){ // could reach this point. // The window we are going to pull from - let old_win_val = D.windows.by_win_id(cwin.id); - if(!old_win_val) { - log.debug(`merge ${cwin.id}: bail - could not get old_win_val`); + let merge_from_win_val = D.windows.by_win_id(cwin.id); + if(!merge_from_win_val) { + log.debug(`merge ${cwin.id}: bail - could not get merge_from_win_val`); break MERGE; } // Detach the existing nodes from their chrome wins/tabs - if(!destroy_subtree_but_not_widgets(old_win_val.node_id)) { - log.debug(`merge ${cwin.id}: bail - could not remove old subtree`); + if(!destroy_subtree_but_not_widgets(merge_from_win_val.node_id)) { + log.debug(`merge ${cwin.id}: bail - could not remove subtree for open window`); break MERGE; } // Attach the old nodes to the wins/tabs - attachChromeWindowToSavedWindowItem(cwin, existing_win, false); + attachChromeWindowToSavedWindowItem(cwin, merge_to_win, false); } //endif existing (MERGE) }); @@ -2119,7 +2130,7 @@ function tabOnUpdated(tabid, changeinfo, ctab) let new_raw_favicon_url = changeinfo.favIconUrl || ctab.favIconUrl || null; if(new_raw_favicon_url !== tab_node_val.raw_favicon_url) { tab_node_val.raw_favicon_url = new_raw_favicon_url; - M.refresh_tab_icon(tab_node_val); + M.refresh_icon(tab_node_val); } saveTree(); @@ -3343,6 +3354,28 @@ function messageListener(request, sender, sendResponse) } //messageListener +////////////////////////////////////////////////////////////////////////// }}}1 +// Helpers // {{{1 + +/// Delete all nodes for closed windows. Meant to be used from the console. +/// TODO migrate this into a user-accessible function (#98) +function delete_all_closed_nodes(are_you_sure) +{ + if(!are_you_sure) return; + + let root = T.treeobj.get_node($.jstree.root); + if(!root) return + + for(let i=root.children.length-1; i>0; --i) { + let child_node_id = root.children[i]; + let isOpen = D.windows.by_node_id(child_node_id, 'isOpen'); + if( !isOpen && child_node_id != T.holding_node_id ) { + actionDeleteWindow(child_node_id, T.treeobj.get_node(child_node_id), + undefined, undefined, undefined, true); + } + } +} //delete_all_closed_nodes + ////////////////////////////////////////////////////////////////////////// }}}1 // Startup / shutdown // {{{1 @@ -3386,6 +3419,10 @@ function preLoadInit() let before = document.getElementById('last-stylesheet'); loadCSS(document, url, before); + // Load our icons after the jstree theme so they can override the theme + url = chrome.runtime.getURL('/assets/css/icons.css'); + loadCSS(document, url, before); + let body = document.querySelector('body'); if(body) { body.classList += ` jstree-${getThemeName()}`;