From dd1fc23fe8aa2961ce579356e1976c4b16234612 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 14 May 2019 22:29:47 +0200 Subject: [PATCH] work on hash & history --- .../0134__create_openTabs_option.sql | 2 +- src/public/javascripts/mobile.js | 2 - src/public/javascripts/services/branches.js | 4 - .../services/frontend_script_api.js | 8 +- .../javascripts/services/note_detail.js | 36 +++++++-- .../javascripts/services/search_notes.js | 2 +- .../javascripts/services/tab_context.js | 41 ++++++++-- src/public/javascripts/services/tab_row.js | 20 ++--- src/public/javascripts/services/tree.js | 78 ++++++++++--------- src/public/stylesheets/desktop.css | 5 -- src/public/stylesheets/style.css | 2 + src/routes/api/recent_notes.js | 1 - 12 files changed, 123 insertions(+), 78 deletions(-) diff --git a/db/migrations/0134__create_openTabs_option.sql b/db/migrations/0134__create_openTabs_option.sql index 289e27a54..be75f1e0f 100644 --- a/db/migrations/0134__create_openTabs_option.sql +++ b/db/migrations/0134__create_openTabs_option.sql @@ -1,4 +1,4 @@ INSERT INTO options (name, value, utcDateCreated, utcDateModified, isSynced) - SELECT 'openTabs', '[{"notePath":"' || value || '","active": true}]', '2019-05-01T18:31:00.874Z', '2019-05-01T18:31:00.874Z', 0 FROM options WHERE name = 'startNotePath'; + SELECT 'openTabs', '[{"notePath":"' || value || '","active": true,"tabId":"1111"}]', '2019-05-01T18:31:00.874Z', '2019-05-01T18:31:00.874Z', 0 FROM options WHERE name = 'startNotePath'; DELETE FROM options WHERE name = 'startNotePath'; \ No newline at end of file diff --git a/src/public/javascripts/mobile.js b/src/public/javascripts/mobile.js index 575d4e90d..d6b2ba226 100644 --- a/src/public/javascripts/mobile.js +++ b/src/public/javascripts/mobile.js @@ -47,8 +47,6 @@ async function showTree() { treeService.clearSelectedNodes(); - treeService.setCurrentNotePathToHash(node); - showDetailPane(); const notePath = await treeUtils.getNotePath(node); diff --git a/src/public/javascripts/services/branches.js b/src/public/javascripts/services/branches.js index 12e773134..1d4089c7b 100644 --- a/src/public/javascripts/services/branches.js +++ b/src/public/javascripts/services/branches.js @@ -103,8 +103,6 @@ async function deleteNodes(nodes) { if (next) { // activate next element after this one is deleted so we don't lose focus next.setActive(); - - treeService.setCurrentNotePathToHash(next); } await treeService.loadTreeCache(); @@ -163,8 +161,6 @@ async function changeNode(func, node, beforeNoteId = null, afterNoteId = null) { await treeCache.moveNote(childNoteId, thisOldParentNode.data.noteId, thisNewParentNode.data.noteId, beforeNoteId, afterNoteId); - treeService.setCurrentNotePathToHash(node); - await treeService.checkFolderStatus(thisOldParentNode); await treeService.checkFolderStatus(thisNewParentNode); diff --git a/src/public/javascripts/services/frontend_script_api.js b/src/public/javascripts/services/frontend_script_api.js index 6b5a950b7..caca3c1e6 100644 --- a/src/public/javascripts/services/frontend_script_api.js +++ b/src/public/javascripts/services/frontend_script_api.js @@ -268,9 +268,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null) { /** * @method - * @returns {Promise} returns note path of active note + * @returns {Promise} returns note path of active note or null if there isn't active note */ - this.getActiveNotePath = treeService.getActiveNotePath; + this.getActiveNotePath = () => { + const activeTabContext = noteDetailService.getActiveTabContext(); + + return activeTabContext ? activeTabContext.notePath : null; + }; /** * This method checks whether user navigated away from the note from which the scripts has been started. diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 9beaec6e9..ba2adb749 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -102,9 +102,27 @@ function getActiveTabContext() { return tabContexts.find(tc => tc.tabId === tabId); } +function isActive(tabContext) { + return tabContext.$tab[0] === tabRow.activateTab; +} + +async function activateTabContext(tabContext) { + await tabRow.activateTab(tabContext.$tab[0]); +} + +/** @returns {TabContext} */ +function getTabContext(tabId) { + return tabContexts.find(tc => tc.tabId === tabId); +} + async function showTab(tabId) { for (const ctx of tabContexts) { - ctx.$tabContent.toggle(ctx.tabId === tabId); + if (ctx.tabId === tabId) { + ctx.show(); + } + else { + ctx.hide(); + } } const oldActiveNode = treeService.getActiveNode(); @@ -207,9 +225,9 @@ async function loadNoteDetail(origNotePath, options = {}) { const loadedNote = await loadNote(noteId); let ctx; - if (tabContexts.length === 0 || newTab) { + if (!getActiveTabContext() || newTab) { // if it's a new tab explicitly by user then it's in background - ctx = new TabContext(tabRow); + ctx = new TabContext(tabRow, options.tabId); tabContexts.push(ctx); } else { @@ -229,7 +247,7 @@ async function loadNoteDetail(origNotePath, options = {}) { if (activate) { // will also trigger showTab via event - tabRow.setCurrentTab(ctx.$tab[0]); + await tabRow.activateTab(ctx.$tab[0]); } } @@ -316,7 +334,7 @@ async function openEmptyTab() { await renderComponent(ctx); - await tabRow.setCurrentTab(ctx.tab); + await tabRow.activateTab(ctx.$tab[0]); } tabRow.addListener('newTab', openEmptyTab); @@ -380,7 +398,7 @@ if (utils.isElectron()) { const nextTab = tabRow.nextTabEl; if (nextTab) { - tabRow.setCurrentTab(nextTab); + tabRow.activateTab(nextTab); } }); @@ -388,7 +406,7 @@ if (utils.isElectron()) { const prevTab = tabRow.previousTabEl; if (prevTab) { - tabRow.setCurrentTab(prevTab); + tabRow.activateTab(prevTab); } }); } @@ -424,6 +442,7 @@ async function saveOpenTabs() { if (tabContext && tabContext.notePath) { openTabs.push({ + tabId: tabContext.tabId, notePath: tabContext.notePath, active: activeTabEl === tabEl }); @@ -457,8 +476,11 @@ export default { saveNotesIfChanged, onNoteChange, addDetailLoadedListener, + getTabContext, getTabContexts, getActiveTabContext, + isActive, + activateTabContext, getActiveComponent, clearOpenTabsTask, filterTabs diff --git a/src/public/javascripts/services/search_notes.js b/src/public/javascripts/services/search_notes.js index c8d726b9c..a9817a9c4 100644 --- a/src/public/javascripts/services/search_notes.js +++ b/src/public/javascripts/services/search_notes.js @@ -141,7 +141,7 @@ async function refreshSearch() { } function init() { - const hashValue = treeService.getHashValueFromAddress(); + const hashValue = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial # if (hashValue.startsWith("search=")) { showSearch(); diff --git a/src/public/javascripts/services/tab_context.js b/src/public/javascripts/services/tab_context.js index 74406c6e9..9a40630a4 100644 --- a/src/public/javascripts/services/tab_context.js +++ b/src/public/javascripts/services/tab_context.js @@ -36,10 +36,10 @@ class TabContext { /** * @param {TabRow} tabRow */ - constructor(tabRow) { + constructor(tabRow, tabId = null) { this.tabRow = tabRow; - this.$tab = $(this.tabRow.addTab()); - this.tabId = this.$tab.attr('data-tab-id'); + this.tabId = tabId || utils.randomString(4); + this.$tab = $(this.tabRow.addTab(this.tabId)); this.$tabContent = $(".note-tab-content-template").clone(); this.$tabContent.removeClass('note-tab-content-template'); @@ -96,9 +96,40 @@ class TabContext { this.setupClasses(); + this.setCurrentNotePathToHash(); + + setTimeout(async () => { + // we include the note into recent list only if the user stayed on the note at least 5 seconds + if (notePath && notePath === await this.notePath) { + await server.post('recent-notes', { notePath }); + } + }, 5000); + console.log(`Switched tab ${this.tabId} to ${this.noteId}`); } + show() { + this.$tabContent.show(); + this.setCurrentNotePathToHash(); + + document.title = "Trilium Notes"; + + if (this.note) { + // it helps navigating in history if note title is included in the title + document.title += " - " + this.note.title; + } + } + + hide() { + this.$tabContent.hide(); + } + + setCurrentNotePathToHash() { + if (this.$tab[0] === this.tabRow.activeTabEl) { + document.location.hash = (this.notePath || "") + "-" + this.tabId; + } + } + setupClasses() { for (const clazz of Array.from(this.$tab[0].classList)) { // create copy to safely iterate over while removing classes if (clazz !== 'note-tab') { @@ -205,13 +236,11 @@ class TabContext { this.$childrenOverview.empty(); - const notePath = await treeService.getActiveNotePath(); - for (const childBranch of await this.note.getChildBranches()) { const link = $('', { href: 'javascript:', text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId) - }).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId); + }).attr('data-action', 'note').attr('data-note-path', this.notePath + '/' + childBranch.noteId); const childEl = $('
').html(link); this.$childrenOverview.append(childEl); diff --git a/src/public/javascripts/services/tab_row.js b/src/public/javascripts/services/tab_row.js index a2bec4e0b..eaa4e16d1 100644 --- a/src/public/javascripts/services/tab_row.js +++ b/src/public/javascripts/services/tab_row.js @@ -6,6 +6,7 @@ */ import optionsInit from './options_init.js'; +import utils from './utils.js'; !function(i,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(t){return e(i,t)}):"object"==typeof module&&module.exports?module.exports=e(i,require("jquery")):i.jQueryBridget=e(i,i.jQuery)}(window,function(t,i){"use strict";var c=Array.prototype.slice,e=t.console,p=void 0===e?function(){}:function(t){e.error(t)};function n(d,o,u){(u=u||i||t.jQuery)&&(o.prototype.option||(o.prototype.option=function(t){u.isPlainObject(t)&&(this.options=u.extend(!0,this.options,t))}),u.fn[d]=function(t){if("string"==typeof t){var i=c.call(arguments,1);return s=i,a="$()."+d+'("'+(r=t)+'")',(e=this).each(function(t,i){var e=u.data(i,d);if(e){var n=e[r];if(n&&"_"!=r.charAt(0)){var o=n.apply(e,s);h=void 0===h?o:h}else p(a+" is not a valid method")}else p(d+" not initialized. Cannot call methods, i.e. "+a)}),void 0!==h?h:e}var e,r,s,h,a,n;return n=t,this.each(function(t,i){var e=u.data(i,d);e?(e.option(n),e._init()):(e=new o(i,n),u.data(i,d,e))}),this},r(u))}function r(t){!t||t&&t.bridget||(t.bridget=n)}return r(i||t.jQuery),n}),function(t,i){"use strict";"function"==typeof define&&define.amd?define("get-size/get-size",[],function(){return i()}):"object"==typeof module&&module.exports?module.exports=i():t.getSize=i()}(window,function(){"use strict";function m(t){var i=parseFloat(t);return-1==t.indexOf("%")&&!isNaN(i)&&i}var e="undefined"==typeof console?function(){}:function(t){console.error(t)},y=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],b=y.length;function E(t){var i=getComputedStyle(t);return i||e("Style returned "+i+". Are you running this code in a hidden iframe on Firefox? See http://bit.ly/getsizebug1"),i}var _,x=!1;function P(t){if(function(){if(!x){x=!0;var t=document.createElement("div");t.style.width="200px",t.style.padding="1px 2px 3px 4px",t.style.borderStyle="solid",t.style.borderWidth="1px 2px 3px 4px",t.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(t);var e=E(t);P.isBoxSizeOuter=_=200==m(e.width),i.removeChild(t)}}(),"string"==typeof t&&(t=document.querySelector(t)),t&&"object"==typeof t&&t.nodeType){var i=E(t);if("none"==i.display)return function(){for(var t={width:0,height:0,innerWidth:0,innerHeight:0,outerWidth:0,outerHeight:0},i=0;i+
`; -const defaultTapProperties = { - title: 'New tab' -}; - class TabRow { constructor(el) { this.draggabillies = []; @@ -158,20 +155,19 @@ class TabRow { this.styleEl.innerHTML = styleHTML; } - addTab(tabProperties) { + addTab(tabId) { const div = document.createElement('div'); div.innerHTML = tabTemplate; const tabEl = div.firstElementChild; - tabEl.setAttribute('data-tab-id', "t" + this.tabIdCounter++); + tabEl.setAttribute('data-tab-id', tabId); tabEl.classList.add('note-tab-was-just-added'); setTimeout(() => tabEl.classList.remove('note-tab-was-just-added'), 500); - tabProperties = Object.assign({}, defaultTapProperties, tabProperties); this.newTabEl.before(tabEl); this.setVisibility(); this.setTabCloseEventListener(tabEl); - this.updateTab(tabEl, tabProperties); + this.updateTab(tabEl, {title: 'New tab'}); this.cleanUpPreviouslyDraggedTabs(); this.layoutTabs(); this.setupDraggabilly(); @@ -231,7 +227,7 @@ class TabRow { return !!this.activeTabEl; } - async setCurrentTab(tabEl) { + async activateTab(tabEl) { const activeTabEl = this.activeTabEl; if (activeTabEl === tabEl) return; if (activeTabEl) activeTabEl.removeAttribute('active'); @@ -242,9 +238,9 @@ class TabRow { async removeTab(tabEl) { if (tabEl === this.activeTabEl) { if (tabEl.nextElementSibling && tabEl.nextElementSibling.classList.contains("note-tab")) { - await this.setCurrentTab(tabEl.nextElementSibling) + await this.activateTab(tabEl.nextElementSibling) } else if (tabEl.previousElementSibling && tabEl.previousElementSibling.classList.contains("note-tab")) { - await this.setCurrentTab(tabEl.previousElementSibling) + await this.activateTab(tabEl.previousElementSibling) } } tabEl.parentNode.removeChild(tabEl); @@ -300,7 +296,7 @@ class TabRow { this.draggabillies.push(draggabilly); draggabilly.on('pointerDown', _ => { - this.setCurrentTab(tabEl) + this.activateTab(tabEl) }); draggabilly.on('dragStart', _ => { diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index ea8d04fc1..35c5a0a6c 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -38,10 +38,6 @@ function getActiveNode() { return $tree.fancytree("getActiveNode"); } -async function getActiveNotePath() { - return getHashValueFromAddress(); -} - async function getNodesByBranchId(branchId) { utils.assertArguments(branchId); @@ -135,6 +131,12 @@ async function activateNote(notePath, noteLoadedListener) { // notePath argument can contain only noteId which is not good when hoisted since // then we need to check the whole note path const runNotePath = await getRunPath(notePath); + + if (!runNotePath) { + console.log("Cannot activate " + notePath); + return; + } + const hoistedNoteId = await hoistedNoteService.getHoistedNoteId(); if (hoistedNoteId !== 'root' && !runNotePath.includes(hoistedNoteId)) { @@ -172,7 +174,7 @@ async function activateNote(notePath, noteLoadedListener) { async function resolveNotePath(notePath) { const runPath = await getRunPath(notePath); - return runPath.join("/"); + return runPath ? runPath.join("/") : null; } /** @@ -205,7 +207,8 @@ async function getRunPath(notePath) { const child = await treeCache.getNote(childNoteId); if (!child) { - console.log("Can't find " + childNoteId); + console.log("Can't find note " + childNoteId); + return; } const parents = await child.getParentNotes(); @@ -331,26 +334,6 @@ async function setExpandedToServer(branchId, isExpanded) { await server.put('branches/' + branchId + '/expanded/' + expandedNum); } -function addRecentNote(branchId, notePath) { - setTimeout(async () => { - // we include the note into recent list only if the user stayed on the note at least 5 seconds - if (notePath && notePath === await getActiveNotePath()) { - await server.post('recent-notes', { branchId, notePath }); - } - }, 1500); -} - -async function setCurrentNotePathToHash(node) { - utils.assertArguments(node); - - const activeNotePath = await treeUtils.getNotePath(node); - const currentBranchId = node.data.branchId; - - document.location.hash = activeNotePath; - - addRecentNote(currentBranchId, activeNotePath); -} - function getSelectedNodes(stopOnParents = false) { return getTree().getSelectedNodes(stopOnParents); } @@ -402,8 +385,13 @@ async function treeInitialized() { }); } + if (!filteredTabs.find(tab => tab.active)) { + filteredTabs[0].active = true; + } + for (const tab of filteredTabs) { await noteDetailService.loadNoteDetail(tab.notePath, { + tabId: tab.tabId, newTab: true, activate: tab.active }); @@ -465,8 +453,6 @@ function initFancyTree(tree) { // click event won't propagate so let's close context menu manually contextMenuWidget.hideContextMenu(); - await setCurrentNotePathToHash(node); - const notePath = await treeUtils.getNotePath(node); noteDetailService.switchToNote(notePath); @@ -554,11 +540,17 @@ async function reload() { } function isNotePathInAddress() { - return getHashValueFromAddress().startsWith("root"); + const [notePath, tabId] = getHashValueFromAddress(); + + return notePath.startsWith("root") + // empty string is for empty/uninitialized tab + || (notePath === '' && !!tabId); } function getHashValueFromAddress() { - return document.location.hash ? document.location.hash.substr(1) : ""; // strip initial # + const str = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial # + + return str.split("-"); } async function loadTreeCache() { @@ -835,13 +827,27 @@ utils.bindShortcut('ctrl+.', scrollToActiveNote); $(window).bind('hashchange', async function() { if (isNotePathInAddress()) { - const notePath = getHashValueFromAddress(); - const noteId = notePath.split("/").pop(); + const [notePath, tabId] = getHashValueFromAddress(); - if (noteId !== '-' && noteId !== getActiveNode().data.noteId) { - console.debug("Switching to " + notePath + " because of hash change"); + console.debug(`Switching to ${notePath} on tab ${tabId} because of hash change`); - activateNote(notePath); + let tabContext = noteDetailService.getTabContext(tabId); + + if (!tabContext) { + noteDetailService.loadNoteDetail(notePath, { + newTab: true, + tabId: tabId, + activate: true + }); + } + else { + if (!noteDetailService.isActive(tabContext)) { + noteDetailService.activateTabContext(tabContext); + } + + if (notePath && tabContext.notePath !== notePath) { + noteDetailService.loadNoteDetail(notePath); + } } } }); @@ -860,8 +866,6 @@ export default { activateNote, getFocusedNode, getActiveNode, - getActiveNotePath, - setCurrentNotePathToHash, setNoteTitle, setPrefix, createNote, diff --git a/src/public/stylesheets/desktop.css b/src/public/stylesheets/desktop.css index 69892935d..ca9d4bb5a 100644 --- a/src/public/stylesheets/desktop.css +++ b/src/public/stylesheets/desktop.css @@ -134,11 +134,6 @@ li.dropdown-submenu:hover > ul.dropdown-menu { border-style: solid; } -.note-tab-content { - font-family: var(--detail-font-family); - font-size: var(--detail-font-size); -} - ::-webkit-scrollbar { width: 12px; } diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 63b2c4ca3..2200206df 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -118,6 +118,8 @@ ul.fancytree-container { flex-direction: column; min-height: 200px; word-break: break-all; /* otherwise CKEditor fails miserably on super long lines */ + font-family: var(--detail-font-family); + font-size: var(--detail-font-size); } .note-detail-component { diff --git a/src/routes/api/recent_notes.js b/src/routes/api/recent_notes.js index 375eb4f2f..280dadbad 100644 --- a/src/routes/api/recent_notes.js +++ b/src/routes/api/recent_notes.js @@ -1,6 +1,5 @@ "use strict"; -const optionService = require('../../services/options'); const RecentNote = require('../../entities/recent_note'); async function addRecentNote(req) {