From 316d2e6edb8476e0073625c52174dfd7ebcce131 Mon Sep 17 00:00:00 2001 From: tconfrey Date: Sun, 2 Jun 2024 14:06:14 -0400 Subject: [PATCH] - Added webnavigation event to support sticky tabs - Some error handling - Bookmarker more responsive sizing - handle &'s in page titles and in topic names - added setting for stickyTabs - gave BTNode an explicit topicName fn to stop re-using displayName or title (confusingly) - updated popup to allow saving a navigated sticky node - fixed bug saving notes to all tabs of the tg not just selected one --- app/BTAppNode.js | 11 +-- app/BTNode.js | 56 ++++-------- app/bt.css | 2 +- app/bt.js | 71 +++++++++++---- app/configManager.js | 14 ++- app/index.html | 17 +++- extension/background.js | 34 ++++++-- extension/manifest.json | 2 +- extension/popup.css | 5 +- extension/popup.html | 1 + extension/popup.js | 18 ++-- extension/topicCard.js | 18 +++- extension/topicSelector.js | 4 +- versions/Release-Candidate/RCextension.zip | Bin 1427773 -> 1428991 bytes versions/Release-Candidate/app/BTAppNode.js | 11 +-- versions/Release-Candidate/app/BTNode.js | 67 ++++++-------- versions/Release-Candidate/app/bt.css | 12 ++- versions/Release-Candidate/app/bt.js | 82 ++++++++++++++---- .../Release-Candidate/app/configManager.js | 25 +++++- versions/Release-Candidate/app/index.html | 25 +++++- .../Release-Candidate/extension/background.js | 45 ++++++++-- .../Release-Candidate/extension/manifest.json | 2 +- .../Release-Candidate/extension/popup.css | 15 +++- .../Release-Candidate/extension/popup.html | 9 ++ versions/Release-Candidate/extension/popup.js | 32 +++++-- .../Release-Candidate/extension/topicCard.js | 29 ++++++- .../extension/topicSelector.js | 15 +++- 27 files changed, 452 insertions(+), 170 deletions(-) diff --git a/app/BTAppNode.js b/app/BTAppNode.js index dae2f80..48185ea 100644 --- a/app/BTAppNode.js +++ b/app/BTAppNode.js @@ -528,13 +528,14 @@ class BTAppNode extends BTNode { }); window.postMessage({'function': 'groupAndPositionTabs', 'tabGroupId': this.tabGroupId, 'windowId': this.windowId, 'tabInfo': tabInfo, - 'groupName': BTAppNode.displayNameFromTitle(this.displayTopic)}); + 'groupName': this.topicName(), // BTAppNode.displayNameFromTitle(this.displayTopic), + }); } putInGroup() { // wrap this one nodes tab in a group if (!this.tabId || !this.windowId || (GroupingMode != 'TABGROUP')) return; - const groupName = this.isTopic() ? this.displayTopic : AllNodes[this.parentId]?.displayTopic; + const groupName = this.isTopic() ? this.topicName() : AllNodes[this.parentId]?.topicName(); const tgId = this.tabGroupId || AllNodes[this.parentId]?.tabGroupId; window.postMessage({'function': 'groupAndPositionTabs', 'tabGroupId': tgId, 'windowId': this.windowId, 'tabInfo': [{'nodeId': this.id, 'tabId': this.tabId, 'tabIndex': this.tabIndex}], @@ -556,7 +557,7 @@ class BTAppNode extends BTNode { let rsp; if (this.tabGroupId && this.isTopic()) rsp = callBackground({'function': 'updateGroup', 'tabGroupId': this.tabGroupId, - 'collapsed': this.folded, 'title': this.title}); + 'collapsed': this.folded, 'title': this.topicName()}); return rsp; } @@ -774,7 +775,7 @@ class BTAppNode extends BTNode { tabGroupTabs.push({'nodeId': id, 'url': node.URL}); }); const me = tabGroupTabs.length ? - {'tabGroupId': this.tabGroupId, 'windowId': this.windowId, 'groupName': this.displayTopic, + {'tabGroupId': this.tabGroupId, 'windowId': this.windowId, 'groupName': this.topicName(), 'tabGroupTabs': tabGroupTabs} : []; const subtopics = this.childIds.flatMap(id => AllNodes[id].listOpenableTabGroups()); return [me, ...subtopics].flat(); @@ -891,7 +892,7 @@ class BTAppNode extends BTNode { const topTopic = (components && components.length) ? components[0] : topic; // Find or create top node - let topNode = AllNodes.find(node => node && node.displayTopic == topTopic); + let topNode = AllNodes.find(node => node && node.topicName() == topTopic); if (!topNode) { topNode = new BTAppNode(topTopic, null, "", 1); topNode.createDisplayNode(); diff --git a/app/BTNode.js b/app/BTNode.js index bc8c89b..0af591d 100644 --- a/app/BTNode.js +++ b/app/BTNode.js @@ -31,7 +31,6 @@ class BTNode { this._displayTopic = BTNode.displayNameFromTitle(_title); this._childIds = []; this._topicPath = ''; - this.generateUniqueTopicPath(); if (parentId && AllNodes[parentId]) { AllNodes[parentId].addChild(this._id, false, firstChild); // add to parent, index not passed, firstChild => front or back } @@ -99,7 +98,7 @@ class BTNode { findChild(childTopic) { // does this topic node have this sub topic - const childId = this.childIds.find(id => AllNodes[id].displayTopic == childTopic); + const childId = this.childIds.find(id => AllNodes[id].topicName() == childTopic); return childId ? AllNodes[childId] : null; } @@ -133,6 +132,13 @@ class BTNode { // Is this node used as a topic => has webLinked children return (this.level == 1) || (!this.URL) || this.childIds.some(id => AllNodes[id]._hasWebLinks); } + + topicName () { + // return the topic name for this node + if (this.isTopic()) + return (this.URL) ? BTNode.editableTopicFromTitle(this.title) : this.title; + return AllNodes[this.parentId].topicName(); + } isTopicTree() { // Does this nodes url match a pointer to a web .org resource that can be loaded @@ -287,42 +293,13 @@ class BTNode { fullTopicPath() { // distinguished name for this node - const myTopic = this.isTopic() ? this.title : ''; + const myTopic = this.isTopic() ? this.topicName() : ''; if (this.parentId && AllNodes[this.parentId]) return AllNodes[this.parentId].fullTopicPath() + ':' + myTopic; else - return myTopic; - } - - generateUniqueTopicPath() { - // same topic can be under multiple parents, generate a unique topicPath - // only called from ctor. suplanted by below. can't really amke uniquee without looking at all topics - - if (!this.isTopic()) { - if (this.parentId && AllNodes[this.parentId]) - this._topicPath = AllNodes[this.parentId].topicPath; - else - this._topicPath = this._displayTopic; - return; - } - - if (this.displayTopic == "") { - this._topicPath = this._displayTopic; - return; - } - const sameTopic = AllNodes.filter(nn => nn && nn.isTopic() && nn.displayTopic == this.displayTopic); - if (sameTopic.length == 1) { - // unique - this._topicPath = this._displayTopic; - return; - } - sameTopic.forEach(function(nn) { - const parentTag = AllNodes[nn.parentId] ? AllNodes[nn.parentId].displayTopic : ""; - nn._topicPath = parentTag + ":" + nn.displayTopic; - }); + return myTopic; } - static generateUniqueTopicPaths() { // same topic can be under multiple parents, generate a unique topic Path for each node @@ -332,13 +309,14 @@ class BTNode { let level = 1; AllNodes.forEach((n) => { if (!n) return; + const topicName = n.topicName(); if (n.isTopic()) { - if (topics[n.displayTopic]) { - topics[n.displayTopic].push(n.id); + if (topics[topicName]) { + topics[topicName].push(n.id); flat = false; } else - topics[n.displayTopic] = Array(1).fill(n.id); + topics[topicName] = Array(1).fill(n.id); }}); // !flat => dup topic names (<99 to prevent infinite loop @@ -349,11 +327,11 @@ class BTNode { // replace dups w DN of increasing levels until flat delete topics[topic]; ids.forEach(id => { - let tpath = AllNodes[id].displayTopic; + let tpath = AllNodes[id].topicName(); let parent = AllNodes[id].parentId; for (let i = 1; i < level; i++) { if (parent && AllNodes[parent]) { - tpath = AllNodes[parent].displayTopic + ":" + tpath; + tpath = AllNodes[parent].topicName() + ":" + tpath; parent = AllNodes[parent].parentId; } } @@ -379,7 +357,7 @@ class BTNode { if (node.parentId && AllNodes[node.parentId]) node._topicPath = AllNodes[node.parentId].topicPath; else - node._topicPath = node._displayTopic; + node._topicPath = BTNode.editableTopicFromTitle(node.title); // no parent but not topic, use [[][title part]] } }); } diff --git a/app/bt.css b/app/bt.css index 2e77816..e0f2c0c 100644 --- a/app/bt.css +++ b/app/bt.css @@ -480,7 +480,7 @@ textarea:focus, input[type="text"]:focus { #youShallNotPass { position: absolute; width: 98%; - height: 400px; + height: 450px; top: 242px;left: 3px; z-index: 5; background-color: #888; diff --git a/app/bt.js b/app/bt.js index 6f27614..8f4b0a6 100644 --- a/app/bt.js +++ b/app/bt.js @@ -753,6 +753,7 @@ function tabClosed(data) { node.tabIndex = 0; node.windowId = 0; node.opening = false; + node.navigated = false; tabActivated(data); // update ui and animate parent to indicate change @@ -790,17 +791,25 @@ function saveTabs(data) { // Handle existing node case: update and return const existingNode = BTAppNode.findFromTab(tab.tabId); - if (existingNode) { + if (existingNode && !existingNode.navigated) { if (note) { existingNode.text = note; existingNode.redisplay(); } + if (close) existingNode.closeTab(); return; // already saved, ignore other than making any note update } - // Deal with Topic - const [topicDN, keyword] = BTNode.processTopicString(tab.topic || "📝 Scratch"); - const topicNode = BTAppNode.findOrCreateFromTopicDN(topicDN); + // Find or create topic, use existingNodes topic if it exists + let topicNode, keyword, topicDN; + if (existingNode) { + tabClosed({"tabId": tab.tabId}); // tab navigated away, clean up + topicNode = AllNodes[existingNode.parentId]; + topicDN = topicNode.topicPath; + } else { + [topicDN, keyword] = BTNode.processTopicString(tab.topic || "📝 Scratch"); + topicNode = BTAppNode.findOrCreateFromTopicDN(topicDN); + } changedTopicNodes.add(topicNode); // Create and populate node @@ -832,7 +841,7 @@ function saveTabs(data) { BTAppNode.generateTopics(); let lastTopicNode = Array.from(changedTopicNodes).pop(); window.postMessage({'function': 'localStore', - 'data': { 'topics': Topics, 'mruTopics': MRUTopicPerWindow, 'currentTopic': lastTopicNode?.title || '', 'currentText': note}}); + 'data': { 'topics': Topics, 'mruTopics': MRUTopicPerWindow, 'currentTopic': lastTopicNode?.topicName() || '', 'currentText': note}}); window.postMessage({'function' : 'brainZoom', 'tabId' : data.tabs[0].tabId}); initializeUI(); @@ -863,7 +872,23 @@ function tabPositioned(data, highlight = false) { } function tabNavigated(data) { - // tab updated event, could be nav away or to a BT node + // tab updated event, could be nav away or to a BT node or even between two btnodes + // if tabs are sticky, tab sticks w original BTnode, as long as: + // 1) it was a result of a link click or server redirect (ie not a new use of the tab like typing in the address bar) or + // 2) its not a nav to a different btnode whose topic is open in the same window + + function stickyTab() { + // Should the tab stay associated with the BT node + if (configManager.getProp('BTStickyTabs') =='NOTSTICKY') return false; + if (['link', 'reload'].includes(transitionType)) return true; + if (transitionQualifiers.length && !transitionQualifiers.includes('from_address_bar')) return true; + return false; + } + function closeAndUngroup() { + data['nodeId'] = tabNode.id; + tabClosed(data); + callBackground({'function' : 'ungroup', 'tabIds' : [tabId]}); + } const tabId = data.tabId; const tabUrl = data.tabURL; @@ -871,6 +896,10 @@ function tabNavigated(data) { const windowId = data.windowId; const tabNode = BTAppNode.findFromTab(tabId); const urlNode = BTAppNode.findFromURLTGWin(tabUrl, groupId, windowId); + const parentsWindow = urlNode?.parentId ? AllNodes[urlNode.parentId]?.windowId : null; + const transitionType = data?.transitionData?.transitionType; + const transitionQualifiers = data?.transitionData?.transitionQualifiers || []; + const sticky = stickyTab(); if (tabNode) { // activity was on managed active tab @@ -883,18 +912,26 @@ function tabNavigated(data) { tabNode.URL = tabUrl; } else { - // nav away from BT tab - data['nodeId'] = tabNode.id; - tabClosed(data); - callBackground({'function' : 'ungroup', 'tabIds' : [tabId]}); + // Might be nav away from BT tab or maybe the tab sticks with the BT node + if (sticky) { + tabNode.navigated = true; + tabActivated(data); // handles updating localstorage/popup with current topic etc + } + else closeAndUngroup(); } } tabNode.opening = false; } - const parentsWindow = urlNode?.parentId ? AllNodes[urlNode.parentId]?.windowId : null; - if (urlNode && (!parentsWindow || (parentsWindow == windowId))) { - // nav into a bt node from an open tab, ignore if parent/TG open elsewhere else - handle like tab open + if (urlNode && (parentsWindow == windowId)) { + // nav into a bt node from an open tab, ignore if parent/TG open elsewhere else handle like tab open + if (tabNode && sticky) closeAndUngroup(); // if sticky we won't have closed above but if urlnode is in same window we should + data['nodeId'] = urlNode.id; + tabOpened(data, true); + return; + } + if (urlNode && !parentsWindow && (!tabNode || !sticky)) { + // nav into a bt node from an open tab, set open if not open elsewhere and url has not stuck to stick tabnode data['nodeId'] = urlNode.id; tabOpened(data, true); return; @@ -924,12 +961,12 @@ function tabActivated(data) { let m1, m2 = {'windowTopic': winNode ? winNode.topicPath : '', 'groupTopic': groupNode ? groupNode.topicPath : '', 'currentTabId' : tabId}; if (node) { - node.topicPath || node.generateUniqueTopicPath(); + node.topicPath || BTNode.generateUniqueTopicPaths(); changeSelected(node); // select in tree - m1 = {'currentTopic': node.topicPath, 'currentText': node.text, 'currentTitle': node.displayTopic}; + m1 = {'currentTopic': node.topicPath, 'currentText': node.text, 'currentTitle': node.displayTopic, 'tabNavigated': node.navigated}; } else { - m1 = {'currentTopic': '', 'currentText': '', 'currentTitle': ''}; + m1 = {'currentTopic': '', 'currentText': '', 'currentTitle': '', 'tabNavigated': false}; clearSelected(); } window.postMessage({'function': 'localStore', 'data': {...m1, ...m2}}); @@ -993,7 +1030,9 @@ function tabJoinedTG(data) { $("table.treetable").treetable("loadBranch", topicNode.getTTNode(), tabNode.HTML()); tabNode.populateFavicon(); initializeUI(); + tabActivated(data); // handles setting topic etc into local storage for popup changeSelected(tabNode); + setNodeOpen(tabNode); positionInTopic(topicNode, tabNode, index, indices, winId); } diff --git a/app/configManager.js b/app/configManager.js index a0ec67d..03c1986 100644 --- a/app/configManager.js +++ b/app/configManager.js @@ -26,7 +26,7 @@ const configManager = (() => { const Properties = { 'keys': ['CLIENT_ID', 'API_KEY', 'FB_KEY', 'STRIPE_KEY'], - 'localStorageProps': ['BTId', 'BTTimestamp', 'BTFileID', 'BTGDriveConnected', 'BTStats', 'BTLastShownMessageIndex', 'BTManagerHome', + 'localStorageProps': ['BTId', 'BTTimestamp', 'BTFileID', 'BTGDriveConnected', 'BTStats', 'BTLastShownMessageIndex', 'BTManagerHome', 'BTStickyTabs', 'BTTheme', 'BTFavicons', 'BTNotes', 'BTDense', 'BTSize', 'BTTooltips', 'BTGroupingMode', 'BTDontShowIntro', 'BTExpiry'], 'orgProps': ['BTCohort', 'BTVersion', 'BTId'], 'stats': ['BTNumTabOperations', 'BTNumSaves', 'BTNumLaunches', 'BTInstallDate', 'BTSessionStartTime', 'BTLastActivityTime', 'BTSessionStartSaves', 'BTSessionStartOps', 'BTDaysOfUse'], @@ -173,6 +173,12 @@ const configManager = (() => { $radio.filter(`[value=${notes}]`).prop('checked', true); checkCompactMode((notes == 'NONOTES')); // turn off if needed + // Sticky Tabs? + const sticky = configManager.getProp('BTStickyTabs') || 'STICKY'; + $radio = $('#stickyToggle :radio[name=sticky]'); + $radio.filter(`[value=${sticky}]`).prop('checked', true); + configManager.setProp('BTStickyTabs', sticky); + // Dense? const dense = configManager.getProp('BTDense') || 'NOTDENSE'; $radio = $('#denseToggle :radio[name=dense]'); @@ -235,6 +241,12 @@ const configManager = (() => { checkCompactMode((newN == 'NONOTES')); saveBT(); }); + $('#stickyToggle :radio').change(function () { + const newN = $(this).val(); + configManager.setProp('BTStickyTabs', newN); + // No immediate action, take effect on next tabNavigated event + saveBT(); + }); $('#denseToggle :radio').change(function () { const newD = $(this).val(); configManager.setProp('BTDense', newD); diff --git a/app/index.html b/app/index.html index 3489165..65f6ede 100644 --- a/app/index.html +++ b/app/index.html @@ -106,7 +106,7 @@ -
* Close and re-open the Topic Manager to apply this setting
+
* Applies next time the Topic Manager is opened.

@@ -156,6 +156,21 @@
+
+
Sticky Tabs?
+
+ + + + + + + + +
+
+
+
Dark Mode?
diff --git a/extension/background.js b/extension/background.js index a605cca..c32a446 100644 --- a/extension/background.js +++ b/extension/background.js @@ -218,6 +218,22 @@ chrome.tabs.onRemoved.addListener(async (tabId, otherInfo) => { btSendMessage(BTTab, {'function': 'tabClosed', 'tabId': tabId, 'indices': indices}); }); +const tabTransitionData = {}; // map of tabId: {transitionType: "", transitionQualifiers: [""..]} + + +// Listen for webNav events to know if the user was clicking a link or typing in the URL bar etc. +// Seems like some sites (g Reddit) trigger the history instead of Committed event. Don't know why +chrome.webNavigation.onCommitted.addListener(async (details) => { + if (details?.frameId !== 0) return; + console.log('webNavigation.onCommitted fired:', JSON.stringify(details)); + tabTransitionData[details.tabId] = {transitionType: details.transitionType, transitionQualifiers: details.transitionQualifiers}; +}); +chrome.webNavigation.onHistoryStateUpdated.addListener(async (details) => { + if (details?.frameId !== 0) return; + console.log('webNavigation.onHistoryStateUpdated fired:', JSON.stringify(details)); + tabTransitionData[details.tabId] = {transitionType: details.transitionType, transitionQualifiers: details.transitionQualifiers}; +}); + chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { // listen for tabs navigating to and from BT URLs or being moved to/from TGs if (Awaiting) return; // ignore events while we're awaiting 'synchronous' commands to take effect @@ -226,12 +242,14 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { const [BTTab, BTWin] = await getBTTabWin(); if (!tabId || !BTTab || (tabId == BTTab)) return; // not set up yet or don't care - const indices = await tabIndices(); + const indices = await tabIndices(); // keep indicies in sync if (changeInfo.status == 'complete') { - // tab navigated to/from url + // tab navigated to/from url, add in transition info from Web Nav event, above + const transitionData = tabTransitionData[tabId] || {}; // set in webNavigation.onCommitted event above + setTimeout (() => delete tabTransitionData[tabId], 1000); // clear out for next event btSendMessage( BTTab, {'function': 'tabNavigated', 'tabId': tabId, 'groupId': tab.groupId, 'tabIndex': tab.index, - 'tabURL': tab.url, 'windowId': tab.windowId, 'indices': indices}); + 'tabURL': tab.url, 'windowId': tab.windowId, 'indices': indices, 'transitionData': transitionData,}); setTimeout(function() {setBadge(tabId);}, 200); return; } @@ -244,7 +262,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { 'tabId': tabId, 'groupId': tab.groupId, 'tabIndex': tab.index, 'windowId': tab.windowId, 'indices': indices, 'tab': tab}); - }, 100); + }, 250); setTimeout(function() {setBadge(tabId);}, 200); } }); @@ -608,7 +626,8 @@ async function groupAndPositionTabs(msg, sender) { chrome.tabs.group(groupArgs, async (groupId) => { // then group appropriately. NB this order cos move drops the tabgroup check('groupAndPositionTabs-group'); - await chrome.tabGroups.update(groupId, {'title' : groupName}); + if (!groupId) console.log('Error: groupId not returned from tabs.group call.'); + else await chrome.tabGroups.update(groupId, {'title' : groupName}); const theTabs = Array.isArray(tabs) ? tabs : [tabs]; // single tab? theTabs.forEach(t => { const nodeInfo = tabInfo.find(ti => ti.tabId == t.id); @@ -727,8 +746,9 @@ function setBadge(tabId) { chrome.action.setBadgeText({'text' : "", 'tabId' : tabId}, () => check('Resetting badge text:')); chrome.action.setTitle({'title' : 'BrainTool'}); - } else { - marquee(data.currentTopic, 0); + } else { + let title = data.currentTopic.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); + marquee(title, 0); chrome.action.setTitle({'title' : data.currentText || 'BrainTool'}); chrome.action.setBadgeBackgroundColor({'color' : '#59718C'}); } diff --git a/extension/manifest.json b/extension/manifest.json index 31581c1..307bd39 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -4,7 +4,7 @@ "description": "__MSG_appDesc__", "default_locale": "en", "version": "1.0.0", - "permissions": ["tabs", "storage", "tabGroups"], + "permissions": ["tabs", "storage", "tabGroups", "webNavigation"], "optional_permissions": ["bookmarks"], "background": { "service_worker": "background.js" diff --git a/extension/popup.css b/extension/popup.css index 3fa5c9e..a59a204 100644 --- a/extension/popup.css +++ b/extension/popup.css @@ -53,7 +53,7 @@ body { width: 440px; - height:550px; + min-height:240px; max-height: 600px; padding-bottom: 50px; overflow: hidden; @@ -122,6 +122,9 @@ label { #saveSession { float: right; } +#saveAs { + color: #58BA00; +} #topicSelector, #saveCheckboxes, #editCard, #buttonDiv { width: 360px; diff --git a/extension/popup.html b/extension/popup.html index 3460c82..c972450 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -42,6 +42,7 @@

BrainTool Bookmarker

Page Title

+

Note this will save as a new page.

Multiple Pages

diff --git a/extension/popup.js b/extension/popup.js index 40eae0c..0a813ae 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -224,7 +224,7 @@ function updateForSelection() { } async function popupOpen(tab) { - // Get data from storage and launch popup w card editor, either existing node or new + // Get data from storage and launch popup w card editor, either existing node or new, or existing but navigated CurrentTab = tab; const tg = (tab.groupId > 0) ? await chrome.tabGroups.get(tab.groupId) : null; const messageElt = document.getElementById('message'); @@ -234,10 +234,12 @@ async function popupOpen(tab) { const saveTG = document.getElementById('saveTG'); const saveTab = document.getElementById('saveTab'); const saveWindow = document.getElementById('saveWindowSpan'); + const saveAs = document.getElementById('saveAs'); saverDiv.style.display = 'block'; + saveAs.style.display = 'none'; messageElt.style.display = 'none'; if (tg) { - // tab is part of a TG => set the saveTg checkbox to be checked and run the update function + // tab is part of a TG => set the saveTg checkbox to be checked document.getElementById('tgName').textContent = tg.title; saveWindow.style.display = 'none'; saveTG.checked = true; @@ -248,10 +250,9 @@ async function popupOpen(tab) { // Pull data from local storage, prepopulate and open saver chrome.storage.local.get( - ['topics', 'currentTabId', 'currentTopic', 'currentText', + ['topics', 'currentTabId', 'currentTopic', 'currentText', 'tabNavigated', 'currentTitle', 'mruTopics', 'saveAndClose'], data => { - console.log(`title [${tab.title}], len: ${tab.title.length}, substr:[${tab.title.substr(0, 100)}]`); let title = (tab.title.length < 150) ? tab.title : tab.title.substr(0, 150) + "..."; titleH2.textContent = title; @@ -268,7 +269,7 @@ async function popupOpen(tab) { document.getElementById('topicSelector').style.display = 'none'; document.getElementById('saveCheckboxes').style.display = 'none'; TopicCard.setupExisting(tab, data.currentText, - data.currentTitle, saveCB); + data.currentTitle, data.tabNavigated, saveCB); return; } @@ -300,7 +301,12 @@ async function saveCB(close) { const title = TopicCard.title(); const note = TopicCard.note(); const newTopic = OldTopic || TopicSelector.topic(); - const saveType = SaveTab.checked ? 'Tab' : (SaveTG.checked ? 'TG' : (SaveWindow.checked ? 'Window' : 'Session')); + const saverDiv = document.getElementById("saveCheckboxes"); + let saveType; + + // Is the savetype selector is hidden we're editng a single tab. Otherwise use the selector + if (window.getComputedStyle(saverDiv).display == 'none') saveType = 'Tab'; + else saveType = SaveTab.checked ? 'Tab' : (SaveTG.checked ? 'TG' : (SaveWindow.checked ? 'Window' : 'Session')); await chrome.runtime.sendMessage({'from': 'popup', 'function': 'saveTabs', 'type': saveType, 'currentWindowId': CurrentTab.windowId, 'close': close, 'topic': newTopic, 'note': note, 'title': title}); diff --git a/extension/topicCard.js b/extension/topicCard.js index 35ba17e..e335a2f 100644 --- a/extension/topicCard.js +++ b/extension/topicCard.js @@ -22,14 +22,22 @@ const TopicCard = (() => { const NoteElt = document.querySelector('#note'); const TitleElt = document.querySelector('#titleInput'); const NoteHint = document.getElementById("newNoteHint"); + const SaveAs = document.getElementById('saveAs'); let SaveCB; let ExistingCard = false; - function setupExisting(tab, note, title, saveCB) { + function setupExisting(tab, note, title, tabNavigated, saveCB) { // entry point when existing page is selected. - TitleElt.value = title; // value, cos its a text input - if (note) NoteElt.value = note; + TitleElt.value = tab.title; //title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); // value, cos its a text input + if (tabNavigated) { + NoteElt.value = ""; + SaveAs.style.display = "block"; + NoteHint.style.display = "block"; // show hint + } else { + if (note) NoteElt.value = note; + SaveAs.style.display = "none"; + } ExistingCard = true; SaveCB = saveCB; @@ -40,12 +48,14 @@ const TopicCard = (() => { NoteElt.setSelectionRange(NoteElt.value.length, NoteElt.value.length); } + function setupNew(title, tab, saveCB) { // entry point for new page - TitleElt.value = title; // value, cos its a text input + TitleElt.value = title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); // value, cos its a text input SaveCB = saveCB; NoteHint.addEventListener('click', (e) => NoteElt.focus()); + SaveAs.style.display = "none"; } NoteElt.onkeydown = function(e) { diff --git a/extension/topicSelector.js b/extension/topicSelector.js index 170bab2..309f81c 100644 --- a/extension/topicSelector.js +++ b/extension/topicSelector.js @@ -80,7 +80,7 @@ const TopicSelector = (() => { let fullPath = []; // keep track of parentage topicsArray.forEach(topic => { const level = topic.level; - const name = topic.name; + const name = topic.name.replace(/&/g, "&").replace(//g, ">"); const visible = (level > 2) ? "display:none;" : ""; const bg = (level == 1) ? "lightgrey" : ""; const nextTopic = topicsArray[index++]; @@ -130,7 +130,7 @@ const TopicSelector = (() => { AwesomeWidget.evaluate(); // might be >1 item matching, find right one. let index = 0; - while (AwesomeWidget.suggestions[index].value != text) index++; + while (AwesomeWidget.suggestions[index]?.value != text) index++; AwesomeWidget.goto(index); AwesomeWidget.select(); TopicHint.style.display = "none"; // hide hint diff --git a/versions/Release-Candidate/RCextension.zip b/versions/Release-Candidate/RCextension.zip index 0dad5d18d7c2784a52f336ce5bb0d15d236d8d3b..5bc88bacba52dfae55cdcfbb746ca8a545b16eb7 100644 GIT binary patch delta 27248 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 delta 25838 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 diff --git a/versions/Release-Candidate/app/BTAppNode.js b/versions/Release-Candidate/app/BTAppNode.js index dae2f80..48185ea 100644 --- a/versions/Release-Candidate/app/BTAppNode.js +++ b/versions/Release-Candidate/app/BTAppNode.js @@ -528,13 +528,14 @@ class BTAppNode extends BTNode { }); window.postMessage({'function': 'groupAndPositionTabs', 'tabGroupId': this.tabGroupId, 'windowId': this.windowId, 'tabInfo': tabInfo, - 'groupName': BTAppNode.displayNameFromTitle(this.displayTopic)}); + 'groupName': this.topicName(), // BTAppNode.displayNameFromTitle(this.displayTopic), + }); } putInGroup() { // wrap this one nodes tab in a group if (!this.tabId || !this.windowId || (GroupingMode != 'TABGROUP')) return; - const groupName = this.isTopic() ? this.displayTopic : AllNodes[this.parentId]?.displayTopic; + const groupName = this.isTopic() ? this.topicName() : AllNodes[this.parentId]?.topicName(); const tgId = this.tabGroupId || AllNodes[this.parentId]?.tabGroupId; window.postMessage({'function': 'groupAndPositionTabs', 'tabGroupId': tgId, 'windowId': this.windowId, 'tabInfo': [{'nodeId': this.id, 'tabId': this.tabId, 'tabIndex': this.tabIndex}], @@ -556,7 +557,7 @@ class BTAppNode extends BTNode { let rsp; if (this.tabGroupId && this.isTopic()) rsp = callBackground({'function': 'updateGroup', 'tabGroupId': this.tabGroupId, - 'collapsed': this.folded, 'title': this.title}); + 'collapsed': this.folded, 'title': this.topicName()}); return rsp; } @@ -774,7 +775,7 @@ class BTAppNode extends BTNode { tabGroupTabs.push({'nodeId': id, 'url': node.URL}); }); const me = tabGroupTabs.length ? - {'tabGroupId': this.tabGroupId, 'windowId': this.windowId, 'groupName': this.displayTopic, + {'tabGroupId': this.tabGroupId, 'windowId': this.windowId, 'groupName': this.topicName(), 'tabGroupTabs': tabGroupTabs} : []; const subtopics = this.childIds.flatMap(id => AllNodes[id].listOpenableTabGroups()); return [me, ...subtopics].flat(); @@ -891,7 +892,7 @@ class BTAppNode extends BTNode { const topTopic = (components && components.length) ? components[0] : topic; // Find or create top node - let topNode = AllNodes.find(node => node && node.displayTopic == topTopic); + let topNode = AllNodes.find(node => node && node.topicName() == topTopic); if (!topNode) { topNode = new BTAppNode(topTopic, null, "", 1); topNode.createDisplayNode(); diff --git a/versions/Release-Candidate/app/BTNode.js b/versions/Release-Candidate/app/BTNode.js index 5b4b122..0af591d 100644 --- a/versions/Release-Candidate/app/BTNode.js +++ b/versions/Release-Candidate/app/BTNode.js @@ -1,3 +1,14 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + + /*** * * Base model for a BrainTool node. Keeps track of containment and relationships @@ -20,7 +31,6 @@ class BTNode { this._displayTopic = BTNode.displayNameFromTitle(_title); this._childIds = []; this._topicPath = ''; - this.generateUniqueTopicPath(); if (parentId && AllNodes[parentId]) { AllNodes[parentId].addChild(this._id, false, firstChild); // add to parent, index not passed, firstChild => front or back } @@ -88,7 +98,7 @@ class BTNode { findChild(childTopic) { // does this topic node have this sub topic - const childId = this.childIds.find(id => AllNodes[id].displayTopic == childTopic); + const childId = this.childIds.find(id => AllNodes[id].topicName() == childTopic); return childId ? AllNodes[childId] : null; } @@ -122,6 +132,13 @@ class BTNode { // Is this node used as a topic => has webLinked children return (this.level == 1) || (!this.URL) || this.childIds.some(id => AllNodes[id]._hasWebLinks); } + + topicName () { + // return the topic name for this node + if (this.isTopic()) + return (this.URL) ? BTNode.editableTopicFromTitle(this.title) : this.title; + return AllNodes[this.parentId].topicName(); + } isTopicTree() { // Does this nodes url match a pointer to a web .org resource that can be loaded @@ -276,42 +293,13 @@ class BTNode { fullTopicPath() { // distinguished name for this node - const myTopic = this.isTopic() ? this.title : ''; + const myTopic = this.isTopic() ? this.topicName() : ''; if (this.parentId && AllNodes[this.parentId]) return AllNodes[this.parentId].fullTopicPath() + ':' + myTopic; else - return myTopic; + return myTopic; } - generateUniqueTopicPath() { - // same topic can be under multiple parents, generate a unique topicPath - // only called from ctor. suplanted by below. can't really amke uniquee without looking at all topics - - if (!this.isTopic()) { - if (this.parentId && AllNodes[this.parentId]) - this._topicPath = AllNodes[this.parentId].topicPath; - else - this._topicPath = this._displayTopic; - return; - } - - if (this.displayTopic == "") { - this._topicPath = this._displayTopic; - return; - } - const sameTopic = AllNodes.filter(nn => nn && nn.isTopic() && nn.displayTopic == this.displayTopic); - if (sameTopic.length == 1) { - // unique - this._topicPath = this._displayTopic; - return; - } - sameTopic.forEach(function(nn) { - const parentTag = AllNodes[nn.parentId] ? AllNodes[nn.parentId].displayTopic : ""; - nn._topicPath = parentTag + ":" + nn.displayTopic; - }); - } - - static generateUniqueTopicPaths() { // same topic can be under multiple parents, generate a unique topic Path for each node @@ -321,13 +309,14 @@ class BTNode { let level = 1; AllNodes.forEach((n) => { if (!n) return; + const topicName = n.topicName(); if (n.isTopic()) { - if (topics[n.displayTopic]) { - topics[n.displayTopic].push(n.id); + if (topics[topicName]) { + topics[topicName].push(n.id); flat = false; } else - topics[n.displayTopic] = Array(1).fill(n.id); + topics[topicName] = Array(1).fill(n.id); }}); // !flat => dup topic names (<99 to prevent infinite loop @@ -338,11 +327,11 @@ class BTNode { // replace dups w DN of increasing levels until flat delete topics[topic]; ids.forEach(id => { - let tpath = AllNodes[id].displayTopic; + let tpath = AllNodes[id].topicName(); let parent = AllNodes[id].parentId; for (let i = 1; i < level; i++) { if (parent && AllNodes[parent]) { - tpath = AllNodes[parent].displayTopic + ":" + tpath; + tpath = AllNodes[parent].topicName() + ":" + tpath; parent = AllNodes[parent].parentId; } } @@ -368,7 +357,7 @@ class BTNode { if (node.parentId && AllNodes[node.parentId]) node._topicPath = AllNodes[node.parentId].topicPath; else - node._topicPath = node._displayTopic; + node._topicPath = BTNode.editableTopicFromTitle(node.title); // no parent but not topic, use [[][title part]] } }); } diff --git a/versions/Release-Candidate/app/bt.css b/versions/Release-Candidate/app/bt.css index e14aa1b..e0f2c0c 100644 --- a/versions/Release-Candidate/app/bt.css +++ b/versions/Release-Candidate/app/bt.css @@ -1,3 +1,13 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + /* First define the basic theme variables */ :root { --btRowHeight: 30px; @@ -470,7 +480,7 @@ textarea:focus, input[type="text"]:focus { #youShallNotPass { position: absolute; width: 98%; - height: 400px; + height: 450px; top: 242px;left: 3px; z-index: 5; background-color: #888; diff --git a/versions/Release-Candidate/app/bt.js b/versions/Release-Candidate/app/bt.js index 9091068..8f4b0a6 100644 --- a/versions/Release-Candidate/app/bt.js +++ b/versions/Release-Candidate/app/bt.js @@ -1,3 +1,14 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + + /*** * * Manages the App window UI and associated logic. @@ -742,6 +753,7 @@ function tabClosed(data) { node.tabIndex = 0; node.windowId = 0; node.opening = false; + node.navigated = false; tabActivated(data); // update ui and animate parent to indicate change @@ -779,17 +791,25 @@ function saveTabs(data) { // Handle existing node case: update and return const existingNode = BTAppNode.findFromTab(tab.tabId); - if (existingNode) { + if (existingNode && !existingNode.navigated) { if (note) { existingNode.text = note; existingNode.redisplay(); } + if (close) existingNode.closeTab(); return; // already saved, ignore other than making any note update } - // Deal with Topic - const [topicDN, keyword] = BTNode.processTopicString(tab.topic || "📝 Scratch"); - const topicNode = BTAppNode.findOrCreateFromTopicDN(topicDN); + // Find or create topic, use existingNodes topic if it exists + let topicNode, keyword, topicDN; + if (existingNode) { + tabClosed({"tabId": tab.tabId}); // tab navigated away, clean up + topicNode = AllNodes[existingNode.parentId]; + topicDN = topicNode.topicPath; + } else { + [topicDN, keyword] = BTNode.processTopicString(tab.topic || "📝 Scratch"); + topicNode = BTAppNode.findOrCreateFromTopicDN(topicDN); + } changedTopicNodes.add(topicNode); // Create and populate node @@ -821,7 +841,7 @@ function saveTabs(data) { BTAppNode.generateTopics(); let lastTopicNode = Array.from(changedTopicNodes).pop(); window.postMessage({'function': 'localStore', - 'data': { 'topics': Topics, 'mruTopics': MRUTopicPerWindow, 'currentTopic': lastTopicNode?.title || '', 'currentText': note}}); + 'data': { 'topics': Topics, 'mruTopics': MRUTopicPerWindow, 'currentTopic': lastTopicNode?.topicName() || '', 'currentText': note}}); window.postMessage({'function' : 'brainZoom', 'tabId' : data.tabs[0].tabId}); initializeUI(); @@ -852,7 +872,23 @@ function tabPositioned(data, highlight = false) { } function tabNavigated(data) { - // tab updated event, could be nav away or to a BT node + // tab updated event, could be nav away or to a BT node or even between two btnodes + // if tabs are sticky, tab sticks w original BTnode, as long as: + // 1) it was a result of a link click or server redirect (ie not a new use of the tab like typing in the address bar) or + // 2) its not a nav to a different btnode whose topic is open in the same window + + function stickyTab() { + // Should the tab stay associated with the BT node + if (configManager.getProp('BTStickyTabs') =='NOTSTICKY') return false; + if (['link', 'reload'].includes(transitionType)) return true; + if (transitionQualifiers.length && !transitionQualifiers.includes('from_address_bar')) return true; + return false; + } + function closeAndUngroup() { + data['nodeId'] = tabNode.id; + tabClosed(data); + callBackground({'function' : 'ungroup', 'tabIds' : [tabId]}); + } const tabId = data.tabId; const tabUrl = data.tabURL; @@ -860,6 +896,10 @@ function tabNavigated(data) { const windowId = data.windowId; const tabNode = BTAppNode.findFromTab(tabId); const urlNode = BTAppNode.findFromURLTGWin(tabUrl, groupId, windowId); + const parentsWindow = urlNode?.parentId ? AllNodes[urlNode.parentId]?.windowId : null; + const transitionType = data?.transitionData?.transitionType; + const transitionQualifiers = data?.transitionData?.transitionQualifiers || []; + const sticky = stickyTab(); if (tabNode) { // activity was on managed active tab @@ -872,18 +912,26 @@ function tabNavigated(data) { tabNode.URL = tabUrl; } else { - // nav away from BT tab - data['nodeId'] = tabNode.id; - tabClosed(data); - callBackground({'function' : 'ungroup', 'tabIds' : [tabId]}); + // Might be nav away from BT tab or maybe the tab sticks with the BT node + if (sticky) { + tabNode.navigated = true; + tabActivated(data); // handles updating localstorage/popup with current topic etc + } + else closeAndUngroup(); } } tabNode.opening = false; } - const parentsWindow = urlNode?.parentId ? AllNodes[urlNode.parentId]?.windowId : null; - if (urlNode && (!parentsWindow || (parentsWindow == windowId))) { - // nav into a bt node from an open tab, ignore if parent/TG open elsewhere else - handle like tab open + if (urlNode && (parentsWindow == windowId)) { + // nav into a bt node from an open tab, ignore if parent/TG open elsewhere else handle like tab open + if (tabNode && sticky) closeAndUngroup(); // if sticky we won't have closed above but if urlnode is in same window we should + data['nodeId'] = urlNode.id; + tabOpened(data, true); + return; + } + if (urlNode && !parentsWindow && (!tabNode || !sticky)) { + // nav into a bt node from an open tab, set open if not open elsewhere and url has not stuck to stick tabnode data['nodeId'] = urlNode.id; tabOpened(data, true); return; @@ -913,12 +961,12 @@ function tabActivated(data) { let m1, m2 = {'windowTopic': winNode ? winNode.topicPath : '', 'groupTopic': groupNode ? groupNode.topicPath : '', 'currentTabId' : tabId}; if (node) { - node.topicPath || node.generateUniqueTopicPath(); + node.topicPath || BTNode.generateUniqueTopicPaths(); changeSelected(node); // select in tree - m1 = {'currentTopic': node.topicPath, 'currentText': node.text, 'currentTitle': node.displayTopic}; + m1 = {'currentTopic': node.topicPath, 'currentText': node.text, 'currentTitle': node.displayTopic, 'tabNavigated': node.navigated}; } else { - m1 = {'currentTopic': '', 'currentText': '', 'currentTitle': ''}; + m1 = {'currentTopic': '', 'currentText': '', 'currentTitle': '', 'tabNavigated': false}; clearSelected(); } window.postMessage({'function': 'localStore', 'data': {...m1, ...m2}}); @@ -982,7 +1030,9 @@ function tabJoinedTG(data) { $("table.treetable").treetable("loadBranch", topicNode.getTTNode(), tabNode.HTML()); tabNode.populateFavicon(); initializeUI(); + tabActivated(data); // handles setting topic etc into local storage for popup changeSelected(tabNode); + setNodeOpen(tabNode); positionInTopic(topicNode, tabNode, index, indices, winId); } diff --git a/versions/Release-Candidate/app/configManager.js b/versions/Release-Candidate/app/configManager.js index da0392d..03c1986 100644 --- a/versions/Release-Candidate/app/configManager.js +++ b/versions/Release-Candidate/app/configManager.js @@ -1,3 +1,14 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + + /*** * * Handles configuration/actions/help getting/setting and associated displays. @@ -15,7 +26,7 @@ const configManager = (() => { const Properties = { 'keys': ['CLIENT_ID', 'API_KEY', 'FB_KEY', 'STRIPE_KEY'], - 'localStorageProps': ['BTId', 'BTTimestamp', 'BTFileID', 'BTGDriveConnected', 'BTStats', 'BTLastShownMessageIndex', 'BTManagerHome', + 'localStorageProps': ['BTId', 'BTTimestamp', 'BTFileID', 'BTGDriveConnected', 'BTStats', 'BTLastShownMessageIndex', 'BTManagerHome', 'BTStickyTabs', 'BTTheme', 'BTFavicons', 'BTNotes', 'BTDense', 'BTSize', 'BTTooltips', 'BTGroupingMode', 'BTDontShowIntro', 'BTExpiry'], 'orgProps': ['BTCohort', 'BTVersion', 'BTId'], 'stats': ['BTNumTabOperations', 'BTNumSaves', 'BTNumLaunches', 'BTInstallDate', 'BTSessionStartTime', 'BTLastActivityTime', 'BTSessionStartSaves', 'BTSessionStartOps', 'BTDaysOfUse'], @@ -162,6 +173,12 @@ const configManager = (() => { $radio.filter(`[value=${notes}]`).prop('checked', true); checkCompactMode((notes == 'NONOTES')); // turn off if needed + // Sticky Tabs? + const sticky = configManager.getProp('BTStickyTabs') || 'STICKY'; + $radio = $('#stickyToggle :radio[name=sticky]'); + $radio.filter(`[value=${sticky}]`).prop('checked', true); + configManager.setProp('BTStickyTabs', sticky); + // Dense? const dense = configManager.getProp('BTDense') || 'NOTDENSE'; $radio = $('#denseToggle :radio[name=dense]'); @@ -224,6 +241,12 @@ const configManager = (() => { checkCompactMode((newN == 'NONOTES')); saveBT(); }); + $('#stickyToggle :radio').change(function () { + const newN = $(this).val(); + configManager.setProp('BTStickyTabs', newN); + // No immediate action, take effect on next tabNavigated event + saveBT(); + }); $('#denseToggle :radio').change(function () { const newD = $(this).val(); configManager.setProp('BTDense', newD); diff --git a/versions/Release-Candidate/app/index.html b/versions/Release-Candidate/app/index.html index d7bb848..65f6ede 100644 --- a/versions/Release-Candidate/app/index.html +++ b/versions/Release-Candidate/app/index.html @@ -1,3 +1,11 @@ + @@ -98,7 +106,7 @@
-
* Close and re-open the Topic Manager to apply this setting
+
* Applies next time the Topic Manager is opened.

@@ -148,6 +156,21 @@
+
+
Sticky Tabs?
+
+ + + + + + + + +
+
+
+
Dark Mode?
diff --git a/versions/Release-Candidate/extension/background.js b/versions/Release-Candidate/extension/background.js index 7605b43..c32a446 100644 --- a/versions/Release-Candidate/extension/background.js +++ b/versions/Release-Candidate/extension/background.js @@ -1,3 +1,14 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + + /*** * * Main entry point for all window and tab manipulation. Listens for messages from app @@ -207,6 +218,22 @@ chrome.tabs.onRemoved.addListener(async (tabId, otherInfo) => { btSendMessage(BTTab, {'function': 'tabClosed', 'tabId': tabId, 'indices': indices}); }); +const tabTransitionData = {}; // map of tabId: {transitionType: "", transitionQualifiers: [""..]} + + +// Listen for webNav events to know if the user was clicking a link or typing in the URL bar etc. +// Seems like some sites (g Reddit) trigger the history instead of Committed event. Don't know why +chrome.webNavigation.onCommitted.addListener(async (details) => { + if (details?.frameId !== 0) return; + console.log('webNavigation.onCommitted fired:', JSON.stringify(details)); + tabTransitionData[details.tabId] = {transitionType: details.transitionType, transitionQualifiers: details.transitionQualifiers}; +}); +chrome.webNavigation.onHistoryStateUpdated.addListener(async (details) => { + if (details?.frameId !== 0) return; + console.log('webNavigation.onHistoryStateUpdated fired:', JSON.stringify(details)); + tabTransitionData[details.tabId] = {transitionType: details.transitionType, transitionQualifiers: details.transitionQualifiers}; +}); + chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { // listen for tabs navigating to and from BT URLs or being moved to/from TGs if (Awaiting) return; // ignore events while we're awaiting 'synchronous' commands to take effect @@ -215,12 +242,14 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { const [BTTab, BTWin] = await getBTTabWin(); if (!tabId || !BTTab || (tabId == BTTab)) return; // not set up yet or don't care - const indices = await tabIndices(); + const indices = await tabIndices(); // keep indicies in sync if (changeInfo.status == 'complete') { - // tab navigated to/from url + // tab navigated to/from url, add in transition info from Web Nav event, above + const transitionData = tabTransitionData[tabId] || {}; // set in webNavigation.onCommitted event above + setTimeout (() => delete tabTransitionData[tabId], 1000); // clear out for next event btSendMessage( BTTab, {'function': 'tabNavigated', 'tabId': tabId, 'groupId': tab.groupId, 'tabIndex': tab.index, - 'tabURL': tab.url, 'windowId': tab.windowId, 'indices': indices}); + 'tabURL': tab.url, 'windowId': tab.windowId, 'indices': indices, 'transitionData': transitionData,}); setTimeout(function() {setBadge(tabId);}, 200); return; } @@ -233,7 +262,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { 'tabId': tabId, 'groupId': tab.groupId, 'tabIndex': tab.index, 'windowId': tab.windowId, 'indices': indices, 'tab': tab}); - }, 100); + }, 250); setTimeout(function() {setBadge(tabId);}, 200); } }); @@ -597,7 +626,8 @@ async function groupAndPositionTabs(msg, sender) { chrome.tabs.group(groupArgs, async (groupId) => { // then group appropriately. NB this order cos move drops the tabgroup check('groupAndPositionTabs-group'); - await chrome.tabGroups.update(groupId, {'title' : groupName}); + if (!groupId) console.log('Error: groupId not returned from tabs.group call.'); + else await chrome.tabGroups.update(groupId, {'title' : groupName}); const theTabs = Array.isArray(tabs) ? tabs : [tabs]; // single tab? theTabs.forEach(t => { const nodeInfo = tabInfo.find(ti => ti.tabId == t.id); @@ -716,8 +746,9 @@ function setBadge(tabId) { chrome.action.setBadgeText({'text' : "", 'tabId' : tabId}, () => check('Resetting badge text:')); chrome.action.setTitle({'title' : 'BrainTool'}); - } else { - marquee(data.currentTopic, 0); + } else { + let title = data.currentTopic.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); + marquee(title, 0); chrome.action.setTitle({'title' : data.currentText || 'BrainTool'}); chrome.action.setBadgeBackgroundColor({'color' : '#59718C'}); } diff --git a/versions/Release-Candidate/extension/manifest.json b/versions/Release-Candidate/extension/manifest.json index e89eb81..761c6db 100644 --- a/versions/Release-Candidate/extension/manifest.json +++ b/versions/Release-Candidate/extension/manifest.json @@ -4,7 +4,7 @@ "description": "__MSG_appDesc__", "default_locale": "en", "version": "1.0.0", - "permissions": ["tabs", "storage", "tabGroups"], + "permissions": ["tabs", "storage", "tabGroups", "webNavigation"], "optional_permissions": ["bookmarks"], "background": { "service_worker": "background.js" diff --git a/versions/Release-Candidate/extension/popup.css b/versions/Release-Candidate/extension/popup.css index f36ac2f..a59a204 100644 --- a/versions/Release-Candidate/extension/popup.css +++ b/versions/Release-Candidate/extension/popup.css @@ -1,3 +1,13 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + /* First define the basic theme variables */ :root { --btFont: Roboto; @@ -43,7 +53,7 @@ body { width: 440px; - height:550px; + min-height:240px; max-height: 600px; padding-bottom: 50px; overflow: hidden; @@ -112,6 +122,9 @@ label { #saveSession { float: right; } +#saveAs { + color: #58BA00; +} #topicSelector, #saveCheckboxes, #editCard, #buttonDiv { width: 360px; diff --git a/versions/Release-Candidate/extension/popup.html b/versions/Release-Candidate/extension/popup.html index 20f8762..c972450 100644 --- a/versions/Release-Candidate/extension/popup.html +++ b/versions/Release-Candidate/extension/popup.html @@ -1,3 +1,11 @@ + @@ -34,6 +42,7 @@

BrainTool Bookmarker

Page Title

+

Note this will save as a new page.

Multiple Pages

diff --git a/versions/Release-Candidate/extension/popup.js b/versions/Release-Candidate/extension/popup.js index 83a5c42..0a813ae 100644 --- a/versions/Release-Candidate/extension/popup.js +++ b/versions/Release-Candidate/extension/popup.js @@ -1,3 +1,14 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + + /*** * * This code runs under the popup and controls the topic entry for adding a page to BT. @@ -118,8 +129,7 @@ async function windowOpen(home = 'PANEL', location) { // Create window, remember it and highlight it const version = chrome.runtime.getManifest().version; // const url = "https://BrainTool.org/app/"; - //const url = "http://localhost:8000/app/"; - const url = "https://BrainTool.org/versions/Release-Candidate/app/"; + const url = "http://localhost:8000/app/"; // versions/"+version+"/app/"; // const url = "https://BrainTool.org/versions/"+version+'/app/'; console.log('loading from ', url); @@ -214,7 +224,7 @@ function updateForSelection() { } async function popupOpen(tab) { - // Get data from storage and launch popup w card editor, either existing node or new + // Get data from storage and launch popup w card editor, either existing node or new, or existing but navigated CurrentTab = tab; const tg = (tab.groupId > 0) ? await chrome.tabGroups.get(tab.groupId) : null; const messageElt = document.getElementById('message'); @@ -224,10 +234,12 @@ async function popupOpen(tab) { const saveTG = document.getElementById('saveTG'); const saveTab = document.getElementById('saveTab'); const saveWindow = document.getElementById('saveWindowSpan'); + const saveAs = document.getElementById('saveAs'); saverDiv.style.display = 'block'; + saveAs.style.display = 'none'; messageElt.style.display = 'none'; if (tg) { - // tab is part of a TG => set the saveTg checkbox to be checked and run the update function + // tab is part of a TG => set the saveTg checkbox to be checked document.getElementById('tgName').textContent = tg.title; saveWindow.style.display = 'none'; saveTG.checked = true; @@ -238,10 +250,9 @@ async function popupOpen(tab) { // Pull data from local storage, prepopulate and open saver chrome.storage.local.get( - ['topics', 'currentTabId', 'currentTopic', 'currentText', + ['topics', 'currentTabId', 'currentTopic', 'currentText', 'tabNavigated', 'currentTitle', 'mruTopics', 'saveAndClose'], data => { - console.log(`title [${tab.title}], len: ${tab.title.length}, substr:[${tab.title.substr(0, 100)}]`); let title = (tab.title.length < 150) ? tab.title : tab.title.substr(0, 150) + "..."; titleH2.textContent = title; @@ -258,7 +269,7 @@ async function popupOpen(tab) { document.getElementById('topicSelector').style.display = 'none'; document.getElementById('saveCheckboxes').style.display = 'none'; TopicCard.setupExisting(tab, data.currentText, - data.currentTitle, saveCB); + data.currentTitle, data.tabNavigated, saveCB); return; } @@ -290,7 +301,12 @@ async function saveCB(close) { const title = TopicCard.title(); const note = TopicCard.note(); const newTopic = OldTopic || TopicSelector.topic(); - const saveType = SaveTab.checked ? 'Tab' : (SaveTG.checked ? 'TG' : (SaveWindow.checked ? 'Window' : 'Session')); + const saverDiv = document.getElementById("saveCheckboxes"); + let saveType; + + // Is the savetype selector is hidden we're editng a single tab. Otherwise use the selector + if (window.getComputedStyle(saverDiv).display == 'none') saveType = 'Tab'; + else saveType = SaveTab.checked ? 'Tab' : (SaveTG.checked ? 'TG' : (SaveWindow.checked ? 'Window' : 'Session')); await chrome.runtime.sendMessage({'from': 'popup', 'function': 'saveTabs', 'type': saveType, 'currentWindowId': CurrentTab.windowId, 'close': close, 'topic': newTopic, 'note': note, 'title': title}); diff --git a/versions/Release-Candidate/extension/topicCard.js b/versions/Release-Candidate/extension/topicCard.js index 67bada9..e335a2f 100644 --- a/versions/Release-Candidate/extension/topicCard.js +++ b/versions/Release-Candidate/extension/topicCard.js @@ -1,3 +1,14 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + + /*** * * This code runs under the popup and controls the topic card @@ -11,14 +22,22 @@ const TopicCard = (() => { const NoteElt = document.querySelector('#note'); const TitleElt = document.querySelector('#titleInput'); const NoteHint = document.getElementById("newNoteHint"); + const SaveAs = document.getElementById('saveAs'); let SaveCB; let ExistingCard = false; - function setupExisting(tab, note, title, saveCB) { + function setupExisting(tab, note, title, tabNavigated, saveCB) { // entry point when existing page is selected. - TitleElt.value = title; // value, cos its a text input - if (note) NoteElt.value = note; + TitleElt.value = tab.title; //title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); // value, cos its a text input + if (tabNavigated) { + NoteElt.value = ""; + SaveAs.style.display = "block"; + NoteHint.style.display = "block"; // show hint + } else { + if (note) NoteElt.value = note; + SaveAs.style.display = "none"; + } ExistingCard = true; SaveCB = saveCB; @@ -29,12 +48,14 @@ const TopicCard = (() => { NoteElt.setSelectionRange(NoteElt.value.length, NoteElt.value.length); } + function setupNew(title, tab, saveCB) { // entry point for new page - TitleElt.value = title; // value, cos its a text input + TitleElt.value = title.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); // value, cos its a text input SaveCB = saveCB; NoteHint.addEventListener('click', (e) => NoteElt.focus()); + SaveAs.style.display = "none"; } NoteElt.onkeydown = function(e) { diff --git a/versions/Release-Candidate/extension/topicSelector.js b/versions/Release-Candidate/extension/topicSelector.js index 60de23c..309f81c 100644 --- a/versions/Release-Candidate/extension/topicSelector.js +++ b/versions/Release-Candidate/extension/topicSelector.js @@ -1,3 +1,14 @@ +/*** + * + * Copyright (c) 2019-2024 Tony Confrey, DataFoundries LLC + * + * This file is part of the BrainTool browser manager extension, open source licensed under the GNU AGPL license. + * See the LICENSE file contained with this project. + * + ***/ + + + /*** * * This code runs under the popup and controls the topic seclection for adding a page to BT. @@ -69,7 +80,7 @@ const TopicSelector = (() => { let fullPath = []; // keep track of parentage topicsArray.forEach(topic => { const level = topic.level; - const name = topic.name; + const name = topic.name.replace(/&/g, "&").replace(//g, ">"); const visible = (level > 2) ? "display:none;" : ""; const bg = (level == 1) ? "lightgrey" : ""; const nextTopic = topicsArray[index++]; @@ -119,7 +130,7 @@ const TopicSelector = (() => { AwesomeWidget.evaluate(); // might be >1 item matching, find right one. let index = 0; - while (AwesomeWidget.suggestions[index].value != text) index++; + while (AwesomeWidget.suggestions[index]?.value != text) index++; AwesomeWidget.goto(index); AwesomeWidget.select(); TopicHint.style.display = "none"; // hide hint