mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	tab row widgetizing
This commit is contained in:
		
							parent
							
								
									f25d735b9d
								
							
						
					
					
						commit
						9d81bf030d
					
				| @ -8,7 +8,7 @@ instanceName= | ||||
| [Network] | ||||
| # host setting is relevant only for web deployments - set the host on which the server will listen | ||||
| # host=0.0.0.0 | ||||
| # port setting is relevant only for web deployments, desktop builds run on random free port | ||||
| # port setting is relevant only for web deployments, desktop builds run on a fixed port (changeable with TRILIUM_PORT environment variable) | ||||
| port=8080 | ||||
| # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). | ||||
| https=false | ||||
|  | ||||
| @ -3,7 +3,7 @@ import contextMenu from './services/tree_context_menu.js'; | ||||
| import link from './services/link.js'; | ||||
| import ws from './services/ws.js'; | ||||
| import noteDetailService from './services/note_detail.js'; | ||||
| import noteType from './services/note_type.js'; | ||||
| import noteType from './widgets/note_type.js'; | ||||
| import protectedSessionService from './services/protected_session.js'; | ||||
| import protectedSessionHolder from './services/protected_session_holder.js'; | ||||
| import searchNotesService from './services/search_notes.js'; | ||||
| @ -21,7 +21,7 @@ import bundle from "./services/bundle.js"; | ||||
| import treeCache from "./services/tree_cache.js"; | ||||
| import libraryLoader from "./services/library_loader.js"; | ||||
| import hoistedNoteService from './services/hoisted_note.js'; | ||||
| import noteTypeService from './services/note_type.js'; | ||||
| import noteTypeService from './widgets/note_type.js'; | ||||
| import linkService from './services/link.js'; | ||||
| import noteAutocompleteService from './services/note_autocomplete.js'; | ||||
| import macInit from './services/mac_init.js'; | ||||
|  | ||||
| @ -2,13 +2,12 @@ import GlobalButtonsWidget from "../widgets/global_buttons.js"; | ||||
| import SearchBoxWidget from "../widgets/search_box.js"; | ||||
| import SearchResultsWidget from "../widgets/search_results.js"; | ||||
| import NoteTreeWidget from "../widgets/note_tree.js"; | ||||
| import tabRow from "./tab_row.js"; | ||||
| import treeService from "./tree.js"; | ||||
| import noteDetailService from "./note_detail.js"; | ||||
| import TabContext from "./tab_context.js"; | ||||
| import server from "./server.js"; | ||||
| import keyboardActionService from "./keyboard_actions.js"; | ||||
| import contextMenuService from "./context_menu.js"; | ||||
| import TabRowWidget from "./tab_row.js"; | ||||
| 
 | ||||
| class AppContext { | ||||
|     constructor() { | ||||
| @ -16,6 +15,30 @@ class AppContext { | ||||
|         /** @type {TabContext[]} */ | ||||
|         this.tabContexts = []; | ||||
|         this.tabsChangedTaskId = null; | ||||
|         /** @type {TabRowWidget} */ | ||||
|         this.tabRow = null; | ||||
|     } | ||||
| 
 | ||||
|     showWidgets() { | ||||
|         const $leftPane = $("#left-pane"); | ||||
| 
 | ||||
|         this.tabRow = new TabRowWidget(this); | ||||
|         $("#tab-row-container").append(this.tabRow.render()); | ||||
| 
 | ||||
|         this.noteTreeWidget = new NoteTreeWidget(this); | ||||
| 
 | ||||
|         this.widgets = [ | ||||
|             new GlobalButtonsWidget(this), | ||||
|             new SearchBoxWidget(this), | ||||
|             new SearchResultsWidget(this), | ||||
|             this.noteTreeWidget | ||||
|         ]; | ||||
| 
 | ||||
|         for (const widget of this.widgets) { | ||||
|             const $widget = widget.render(); | ||||
| 
 | ||||
|             $leftPane.append($widget); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     trigger(name, data) { | ||||
| @ -31,7 +54,7 @@ class AppContext { | ||||
| 
 | ||||
|     /** @returns {TabContext} */ | ||||
|     getActiveTabContext() { | ||||
|         const activeTabEl = tabRow.activeTabEl; | ||||
|         const activeTabEl = this.tabRow.activeTabEl; | ||||
| 
 | ||||
|         if (!activeTabEl) { | ||||
|             return null; | ||||
| @ -115,25 +138,6 @@ class AppContext { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     showWidgets() { | ||||
|         const $leftPane = $("#left-pane"); | ||||
| 
 | ||||
|         this.noteTreeWidget = new NoteTreeWidget(this); | ||||
| 
 | ||||
|         this.widgets = [ | ||||
|             new GlobalButtonsWidget(this), | ||||
|             new SearchBoxWidget(this), | ||||
|             new SearchResultsWidget(this), | ||||
|             this.noteTreeWidget | ||||
|         ]; | ||||
| 
 | ||||
|         for (const widget of this.widgets) { | ||||
|             const $widget = widget.render(); | ||||
| 
 | ||||
|             $leftPane.append($widget); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @return {NoteTreeWidget} | ||||
|      */ | ||||
| @ -144,7 +148,7 @@ class AppContext { | ||||
|     getTab(newTab, state) { | ||||
|         if (!this.getActiveTabContext() || newTab) { | ||||
|             // if it's a new tab explicitly by user then it's in background
 | ||||
|             const ctx = new TabContext(tabRow, state); | ||||
|             const ctx = new TabContext(this.tabRow, state); | ||||
|             this.tabContexts.push(ctx); | ||||
| 
 | ||||
|             return ctx; | ||||
| @ -174,16 +178,16 @@ class AppContext { | ||||
|     } | ||||
| 
 | ||||
|     async openEmptyTab() { | ||||
|         const ctx = new TabContext(tabRow); | ||||
|         const ctx = new TabContext(this.tabRow); | ||||
|         this.tabContexts.push(ctx); | ||||
| 
 | ||||
|         await tabRow.activateTab(ctx.$tab[0]); | ||||
|         await this.tabRow.activateTab(ctx.$tab[0]); | ||||
|     } | ||||
| 
 | ||||
|     async filterTabs(noteId) { | ||||
|         for (const tc of this.tabContexts) { | ||||
|             if (tc.notePath && !tc.notePath.split("/").includes(noteId)) { | ||||
|                 await tabRow.removeTab(tc.$tab[0]); | ||||
|                 await this.tabRow.removeTab(tc.$tab[0]); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| @ -197,7 +201,7 @@ class AppContext { | ||||
|     async saveOpenTabs() { | ||||
|         const openTabs = []; | ||||
| 
 | ||||
|         for (const tabEl of tabRow.tabEls) { | ||||
|         for (const tabEl of this.tabRow.tabEls) { | ||||
|             const tabId = tabEl.getAttribute('data-tab-id'); | ||||
|             const tabContext = appContext.getTabContexts().find(tc => tc.tabId === tabId); | ||||
| 
 | ||||
| @ -229,81 +233,63 @@ class AppContext { | ||||
| 
 | ||||
|         this.tabsChangedTaskId = setTimeout(() => this.saveOpenTabs(), 1000); | ||||
|     } | ||||
| 
 | ||||
|     newTabListener() { | ||||
|         this.openEmptyTab(); | ||||
|     } | ||||
| 
 | ||||
|     async activeTabChangedListener({tabEl}) { | ||||
|         const tabId = tabEl.getAttribute('data-tab-id'); | ||||
| 
 | ||||
|         await this.showTab(tabId); | ||||
|     } | ||||
| 
 | ||||
|     async tabRemoveListener({tabEl}) { | ||||
|         const tabId = tabEl.getAttribute('data-tab-id'); | ||||
| 
 | ||||
|         this.tabContexts.filter(nc => nc.tabId === tabId) | ||||
|             .forEach(tc => tc.remove()); | ||||
| 
 | ||||
|         this.tabContexts = this.tabContexts.filter(nc => nc.tabId !== tabId); | ||||
| 
 | ||||
|         if (this.tabContexts.length === 0) { | ||||
|             this.openEmptyTab(); | ||||
|         } | ||||
| 
 | ||||
|         this.openTabsChanged(); | ||||
|     } | ||||
| 
 | ||||
|     tabReorderListener() { | ||||
|         this.openTabsChanged(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const appContext = new AppContext(); | ||||
| 
 | ||||
| tabRow.addListener('newTab', () => appContext.openEmptyTab()); | ||||
| 
 | ||||
| tabRow.addListener('activeTabChange', async ({ detail }) => { | ||||
|     const tabId = detail.tabEl.getAttribute('data-tab-id'); | ||||
| 
 | ||||
|     await appContext.showTab(tabId); | ||||
| }); | ||||
| 
 | ||||
| tabRow.addListener('tabRemove', async ({ detail }) => { | ||||
|     const tabId = detail.tabEl.getAttribute('data-tab-id'); | ||||
| 
 | ||||
|     appContext.tabContexts.filter(nc => nc.tabId === tabId) | ||||
|         .forEach(tc => tc.remove()); | ||||
| 
 | ||||
|     appContext.tabContexts = appContext.tabContexts.filter(nc => nc.tabId !== tabId); | ||||
| 
 | ||||
|     if (appContext.tabContexts.length === 0) { | ||||
|         appContext.openEmptyTab(); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| tabRow.addListener('activeTabChange', () => appContext.openTabsChanged()); | ||||
| tabRow.addListener('tabRemove', () => appContext.openTabsChanged()); | ||||
| tabRow.addListener('tabReorder', () => appContext.openTabsChanged()); | ||||
| 
 | ||||
| keyboardActionService.setGlobalActionHandler('OpenNewTab', () => { | ||||
|     appContext.openEmptyTab(); | ||||
| }); | ||||
| 
 | ||||
| keyboardActionService.setGlobalActionHandler('CloseActiveTab', () => { | ||||
|     if (tabRow.activeTabEl) { | ||||
|         tabRow.removeTab(tabRow.activeTabEl); | ||||
|     if (this.tabRow.activeTabEl) { | ||||
|         this.tabRow.removeTab(this.tabRow.activeTabEl); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| keyboardActionService.setGlobalActionHandler('ActivateNextTab', () => { | ||||
|     const nextTab = tabRow.nextTabEl; | ||||
|     const nextTab = this.tabRow.nextTabEl; | ||||
| 
 | ||||
|     if (nextTab) { | ||||
|         tabRow.activateTab(nextTab); | ||||
|         this.tabRow.activateTab(nextTab); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| keyboardActionService.setGlobalActionHandler('ActivatePreviousTab', () => { | ||||
|     const prevTab = tabRow.previousTabEl; | ||||
|     const prevTab = this.tabRow.previousTabEl; | ||||
| 
 | ||||
|     if (prevTab) { | ||||
|         tabRow.activateTab(prevTab); | ||||
|         this.tabRow.activateTab(prevTab); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
| $(tabRow.el).on('contextmenu', '.note-tab', e => { | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     const tab = $(e.target).closest(".note-tab"); | ||||
| 
 | ||||
|     contextMenuService.initContextMenu(e, { | ||||
|         getContextMenuItems: () => { | ||||
|             return [ | ||||
|                 {title: "Close all tabs", cmd: "removeAllTabs", uiIcon: "empty"}, | ||||
|                 {title: "Close all tabs except for this", cmd: "removeAllTabsExceptForThis", uiIcon: "empty"} | ||||
|             ]; | ||||
|         }, | ||||
|         selectContextMenuItem: (e, cmd) => { | ||||
|             if (cmd === 'removeAllTabs') { | ||||
|                 tabRow.removeAllTabs(); | ||||
|             } else if (cmd === 'removeAllTabsExceptForThis') { | ||||
|                 tabRow.removeAllTabsExceptForThis(tab[0]); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| export default appContext; | ||||
| @ -4,10 +4,8 @@ import server from './server.js'; | ||||
| import ws from "./ws.js"; | ||||
| import treeCache from "./tree_cache.js"; | ||||
| import NoteFull from "../entities/note_full.js"; | ||||
| import contextMenuService from "./context_menu.js"; | ||||
| import treeUtils from "./tree_utils.js"; | ||||
| import tabRow from "./tab_row.js"; | ||||
| import keyboardActionService from "./keyboard_actions.js"; | ||||
| import appContext from "./app_context.js"; | ||||
| 
 | ||||
| const $tabContentsContainer = $("#note-tab-container"); | ||||
|  | ||||
| @ -3,13 +3,8 @@ import protectedSessionHolder from "./protected_session_holder.js"; | ||||
| import server from "./server.js"; | ||||
| import bundleService from "./bundle.js"; | ||||
| import Attributes from "./attributes.js"; | ||||
| import treeUtils from "./tree_utils.js"; | ||||
| import utils from "./utils.js"; | ||||
| import NoteTypeContext from "./note_type.js"; | ||||
| import noteDetailService from "./note_detail.js"; | ||||
| import protectedSessionService from "./protected_session.js"; | ||||
| import optionsService from "./options.js"; | ||||
| import linkService from "./link.js"; | ||||
| import Sidebar from "./sidebar.js"; | ||||
| import appContext from "./app_context.js"; | ||||
| 
 | ||||
| @ -36,7 +31,7 @@ optionsService.addLoadListener(options => { | ||||
| 
 | ||||
| class TabContext { | ||||
|     /** | ||||
|      * @param {TabRow} tabRow | ||||
|      * @param {TabRowWidget} tabRow | ||||
|      * @param {object} state | ||||
|      */ | ||||
|     constructor(tabRow, state = {}) { | ||||
| @ -61,13 +56,8 @@ class TabContext { | ||||
| 
 | ||||
|         $tabContentsContainer.append(this.$tabContent); | ||||
| 
 | ||||
|         this.$noteTitle = this.$tabContent.find(".note-title"); | ||||
|         this.$noteTitleRow = this.$tabContent.find(".note-title-row"); | ||||
|         this.$notePathList = this.$tabContent.find(".note-path-list"); | ||||
|         this.$notePathCount = this.$tabContent.find(".note-path-count"); | ||||
|         this.$noteDetailComponents = this.$tabContent.find(".note-detail-component"); | ||||
|         this.$scriptArea = this.$tabContent.find(".note-detail-script-area"); | ||||
|         this.$savedIndicator = this.$tabContent.find(".saved-indicator"); | ||||
|         this.noteChangeDisabled = false; | ||||
|         this.isNoteChanged = false; | ||||
|         this.attributes = new Attributes(this); | ||||
| @ -78,41 +68,10 @@ class TabContext { | ||||
|             }; | ||||
| 
 | ||||
|             this.sidebar = new Sidebar(this, sidebarState); | ||||
|             this.noteType = new NoteTypeContext(this); | ||||
|         } | ||||
| 
 | ||||
|         this.components = {}; | ||||
| 
 | ||||
|         this.$noteTitle.on('input', () => { | ||||
|             if (!this.note) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             this.noteChanged(); | ||||
| 
 | ||||
|             this.note.title = this.$noteTitle.val(); | ||||
| 
 | ||||
|             this.tabRow.updateTab(this.$tab[0], {title: this.note.title}); | ||||
|             treeService.setNoteTitle(this.note.noteId, this.note.title); | ||||
| 
 | ||||
|             this.setTitleBar(); | ||||
|         }); | ||||
| 
 | ||||
|         if (utils.isDesktop()) { | ||||
|             // keyboard plugin is not loaded in mobile
 | ||||
|             utils.bindElShortcut(this.$noteTitle, 'return', () => { | ||||
|                 this.getComponent().focus(); | ||||
| 
 | ||||
|                 return false; // to not propagate the enter into the editor (causes issues with codemirror)
 | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         this.$protectButton = this.$tabContent.find(".protect-button"); | ||||
|         this.$protectButton.on('click', protectedSessionService.protectNoteAndSendToServer); | ||||
| 
 | ||||
|         this.$unprotectButton = this.$tabContent.find(".unprotect-button"); | ||||
|         this.$unprotectButton.on('click', protectedSessionService.unprotectNoteAndSendToServer); | ||||
| 
 | ||||
|         await this.initComponent(); | ||||
|     } | ||||
| 
 | ||||
| @ -156,8 +115,6 @@ class TabContext { | ||||
|         this.noteChangeDisabled = true; | ||||
| 
 | ||||
|         try { | ||||
|             this.$noteTitle.val(this.note.title); | ||||
| 
 | ||||
|             await this.renderComponent(); | ||||
|         } finally { | ||||
|             this.noteChangeDisabled = false; | ||||
| @ -177,12 +134,6 @@ class TabContext { | ||||
|             } | ||||
|         }, 5000); | ||||
| 
 | ||||
|         this.showPaths(); | ||||
| 
 | ||||
|         if (utils.isDesktop()) { | ||||
|             this.noteType.update(); | ||||
|         } | ||||
| 
 | ||||
|         bundleService.executeRelationBundles(this.note, 'runOnNoteView', this); | ||||
| 
 | ||||
|         // after loading new note make sure editor is scrolled to the top
 | ||||
| @ -281,12 +232,7 @@ class TabContext { | ||||
|         this.$tabContent.addClass(utils.getNoteTypeClass(this.note.type)); | ||||
|         this.$tabContent.addClass(utils.getMimeTypeClass(this.note.mime)); | ||||
| 
 | ||||
|         this.$noteTitleRow.show(); // might be hidden from empty detail
 | ||||
|         this.$tabContent.toggleClass("protected", this.note.isProtected); | ||||
|         this.$protectButton.toggleClass("active", this.note.isProtected); | ||||
|         this.$protectButton.prop("disabled", this.note.isProtected); | ||||
|         this.$unprotectButton.toggleClass("active", !this.note.isProtected); | ||||
|         this.$unprotectButton.prop("disabled", !this.note.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()); | ||||
|     } | ||||
| 
 | ||||
|     getComponent() { | ||||
| @ -349,12 +295,11 @@ class TabContext { | ||||
|             protectedSessionHolder.touchProtectedSession(); | ||||
|         } | ||||
| 
 | ||||
|         this.$savedIndicator.fadeIn(); | ||||
|         // FIXME trigger "noteSaved" event so that title indicator is triggered
 | ||||
|         this.eventReceived('noteSaved'); | ||||
| 
 | ||||
|         // run async
 | ||||
|         bundleService.executeRelationBundles(this.note, 'runOnNoteChange', this); | ||||
| 
 | ||||
|         this.eventReceived('noteSaved'); | ||||
|     } | ||||
| 
 | ||||
|     async saveNoteIfChanged() { | ||||
| @ -372,65 +317,10 @@ class TabContext { | ||||
| 
 | ||||
|         this.isNoteChanged = true; | ||||
| 
 | ||||
|         // FIMXE: trigger noteChanged event
 | ||||
|         this.$savedIndicator.fadeOut(); | ||||
|     } | ||||
| 
 | ||||
|     async addPath(notePath, isCurrent) { | ||||
|         const title = await treeUtils.getNotePathTitle(notePath); | ||||
| 
 | ||||
|         const noteLink = await linkService.createNoteLink(notePath, {title}); | ||||
| 
 | ||||
|         noteLink | ||||
|             .addClass("no-tooltip-preview") | ||||
|             .addClass("dropdown-item"); | ||||
| 
 | ||||
|         if (isCurrent) { | ||||
|             noteLink.addClass("current"); | ||||
|         } | ||||
| 
 | ||||
|         this.$notePathList.append(noteLink); | ||||
|     } | ||||
| 
 | ||||
|     async showPaths() { | ||||
|         if (this.note.noteId === 'root') { | ||||
|             // root doesn't have any parent, but it's still technically 1 path
 | ||||
| 
 | ||||
|             this.$notePathCount.html("1 path"); | ||||
| 
 | ||||
|             this.$notePathList.empty(); | ||||
| 
 | ||||
|             await this.addPath('root', true); | ||||
|         } | ||||
|         else { | ||||
|             const parents = await this.note.getParentNotes(); | ||||
| 
 | ||||
|             this.$notePathCount.html(parents.length + " path" + (parents.length > 1 ? "s" : "")); | ||||
|             this.$notePathList.empty(); | ||||
| 
 | ||||
|             const pathSegments = this.notePath.split("/"); | ||||
|             const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent
 | ||||
| 
 | ||||
|             for (const parentNote of parents) { | ||||
|                 const parentNotePath = await treeService.getSomeNotePath(parentNote); | ||||
|                 // this is to avoid having root notes leading '/'
 | ||||
|                 const notePath = parentNotePath ? (parentNotePath + '/' + this.note.noteId) : this.note.noteId; | ||||
|                 const isCurrent = activeNoteParentNoteId === parentNote.noteId; | ||||
| 
 | ||||
|                 await this.addPath(notePath, isCurrent); | ||||
|             } | ||||
| 
 | ||||
|             const cloneLink = $("<div>") | ||||
|                 .addClass("dropdown-item") | ||||
|                 .append( | ||||
|                     $('<button class="btn btn-sm">') | ||||
|                         .text('Clone note to new location...') | ||||
|                         .on('click', () => import("../dialogs/clone_to.js").then(d => d.showDialog([this.note.noteId]))) | ||||
|                 ); | ||||
| 
 | ||||
|             this.$notePathList.append(cloneLink); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async remove() { | ||||
|         if (this.$tabContent) { | ||||
|             // sometimes there are orphan autocompletes after closing the tab
 | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										219
									
								
								src/public/javascripts/widgets/note_title.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/public/javascripts/widgets/note_title.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,219 @@ | ||||
| import TabAwareWidget from "./tab_aware_widget.js"; | ||||
| import treeService from "../services/tree.js"; | ||||
| import utils from "../services/utils.js"; | ||||
| import protectedSessionService from "../services/protected_session.js"; | ||||
| import treeUtils from "../services/tree_utils.js"; | ||||
| import linkService from "../services/link.js"; | ||||
| import protectedSessionHolder from "../services/protected_session_holder.js"; | ||||
| import NoteTypeWidget from "./note_type.js"; | ||||
| 
 | ||||
| const TPL = ` | ||||
| <style> | ||||
| .note-title-row { | ||||
|     flex-grow: 0; | ||||
|     flex-shrink: 0; | ||||
|     padding-top: 2px; | ||||
| } | ||||
| 
 | ||||
| .note-title { | ||||
|     margin-left: 15px; | ||||
|     margin-right: 10px; | ||||
|     font-size: 150%; | ||||
|     border: 0; | ||||
|     width: 5em; | ||||
|     flex-grow: 100; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
| <div class="note-title-row"> | ||||
|     <div style="display: flex; align-items: center;"> | ||||
|         <div class="dropdown hide-in-zen-mode"> | ||||
|             <button class="btn btn-sm dropdown-toggle note-path-list-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                 <span class="note-path-count">1 path</span> | ||||
| 
 | ||||
|                 <span class="caret"></span> | ||||
|             </button> | ||||
|             <ul class="note-path-list dropdown-menu" aria-labelledby="note-path-list-button"> | ||||
|             </ul> | ||||
|         </div> | ||||
| 
 | ||||
|         <input autocomplete="off" value="" class="note-title" tabindex="1"> | ||||
| 
 | ||||
|         <span class="saved-indicator hide-in-zen-mode bx bx-check" title="All changes have been saved"></span> | ||||
| 
 | ||||
|         <div class="hide-in-zen-mode" style="display: flex; align-items: center;"> | ||||
|             <button class="btn btn-sm icon-button bx bx-play-circle render-button" | ||||
|                     style="display: none; margin-right: 10px;" | ||||
|                     title="Render"></button> | ||||
| 
 | ||||
|             <button class="btn btn-sm icon-button bx bx-play-circle execute-script-button" | ||||
|                     style="display: none; margin-right: 10px;" | ||||
|                     title="Execute (Ctrl+Enter)"></button> | ||||
| 
 | ||||
|             <div class="btn-group btn-group-xs"> | ||||
|                 <button type="button" | ||||
|                         class="btn btn-sm icon-button bx bx-check-shield protect-button" | ||||
|                         title="Protected note can be viewed and edited only after entering password"> | ||||
|                 </button> | ||||
| 
 | ||||
|                 <button type="button" | ||||
|                         class="btn btn-sm icon-button bx bx-shield unprotect-button" | ||||
|                         title="Not protected note can be viewed without entering password"> | ||||
|                 </button> | ||||
|             </div> | ||||
| 
 | ||||
|                 | ||||
| 
 | ||||
|             <div style="display: flex;"> | ||||
|                 <!-- note type here --> | ||||
| 
 | ||||
|                 <div class="dropdown note-actions"> | ||||
|                     <button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle"> | ||||
|                         Note actions | ||||
|                         <span class="caret"></span> | ||||
|                     </button> | ||||
|                     <div class="dropdown-menu dropdown-menu-right"> | ||||
|                         <a class="dropdown-item show-note-revisions-button" data-bind="css: { disabled: type() == 'file' || type() == 'image' }">Revisions</a> | ||||
|                         <a class="dropdown-item show-attributes-button"><kbd data-kb-action="ShowAttributes"></kbd> Attributes</a> | ||||
|                         <a class="dropdown-item show-link-map-button"><kbd data-kb-action="ShowLinkMap"></kbd> Link map</a> | ||||
|                         <a class="dropdown-item show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }"> | ||||
|                             <kbd data-kb-action="ShowNoteSource"></kbd> | ||||
|                             Note source | ||||
|                         </a> | ||||
|                         <a class="dropdown-item import-files-button">Import files</a> | ||||
|                         <a class="dropdown-item export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a> | ||||
|                         <a class="dropdown-item print-note-button"><kbd data-kb-action="PrintActiveNote"></kbd> Print note</a> | ||||
|                         <a class="dropdown-item show-note-info-button"><kbd data-kb-action="ShowNoteInfo"></kbd> Note info</a> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div>`; | ||||
| 
 | ||||
| export default class NoteTitleWidget extends TabAwareWidget { | ||||
|     constructor(appContext) { | ||||
|         super(appContext); | ||||
| 
 | ||||
|         this.tree = null; | ||||
|     } | ||||
| 
 | ||||
|     async doRender($widget) { | ||||
|         $widget.append($(TPL)); | ||||
| 
 | ||||
|         this.$noteTitle = this.$tabContent.find(".note-title"); | ||||
|         this.$noteTitleRow = this.$tabContent.find(".note-title-row"); | ||||
|         this.$notePathList = this.$tabContent.find(".note-path-list"); | ||||
|         this.$notePathCount = this.$tabContent.find(".note-path-count"); | ||||
| 
 | ||||
|         this.$protectButton = this.$tabContent.find(".protect-button"); | ||||
|         this.$protectButton.on('click', protectedSessionService.protectNoteAndSendToServer); | ||||
| 
 | ||||
|         this.$unprotectButton = this.$tabContent.find(".unprotect-button"); | ||||
|         this.$unprotectButton.on('click', protectedSessionService.unprotectNoteAndSendToServer); | ||||
| 
 | ||||
|         this.$savedIndicator = this.$tabContent.find(".saved-indicator"); | ||||
| 
 | ||||
|         this.noteType = new NoteTypeWidget(this); | ||||
| 
 | ||||
|         this.$noteTitle.on('input', () => { | ||||
|             if (!this.note) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // FIXME event not used
 | ||||
|             this.trigger(`activeNoteChanged`); | ||||
| 
 | ||||
|             this.note.title = this.$noteTitle.val(); | ||||
| 
 | ||||
|             this.tabRow.updateTab(this.$tab[0], {title: this.note.title}); | ||||
|             treeService.setNoteTitle(this.note.noteId, this.note.title); | ||||
| 
 | ||||
|             this.setTitleBar(); | ||||
|         }); | ||||
| 
 | ||||
|         if (utils.isDesktop()) { | ||||
|             // keyboard plugin is not loaded in mobile
 | ||||
|             utils.bindElShortcut(this.$noteTitle, 'return', () => { | ||||
|                 this.getComponent().focus(); | ||||
| 
 | ||||
|                 return false; // to not propagate the enter into the editor (causes issues with codemirror)
 | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async activeTabChanged() { | ||||
|         const note = this.tabContext.note; | ||||
| 
 | ||||
|         this.$noteTitle.val(this.note.title); | ||||
| 
 | ||||
|         this.$protectButton.toggleClass("active", note.isProtected); | ||||
|         this.$protectButton.prop("disabled", note.isProtected); | ||||
|         this.$unprotectButton.toggleClass("active", !note.isProtected); | ||||
|         this.$unprotectButton.prop("disabled", !note.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()); | ||||
| 
 | ||||
|         await this.showPaths(); | ||||
|     } | ||||
| 
 | ||||
|     async showPaths() { | ||||
|         const note = this.tabContext.note; | ||||
| 
 | ||||
|         if (note.noteId === 'root') { | ||||
|             // root doesn't have any parent, but it's still technically 1 path
 | ||||
| 
 | ||||
|             this.$notePathCount.html("1 path"); | ||||
| 
 | ||||
|             this.$notePathList.empty(); | ||||
| 
 | ||||
|             await this.addPath('root', true); | ||||
|         } | ||||
|         else { | ||||
|             const parents = await note.getParentNotes(); | ||||
| 
 | ||||
|             this.$notePathCount.html(parents.length + " path" + (parents.length > 1 ? "s" : "")); | ||||
|             this.$notePathList.empty(); | ||||
| 
 | ||||
|             const pathSegments = this.notePath.split("/"); | ||||
|             const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent
 | ||||
| 
 | ||||
|             for (const parentNote of parents) { | ||||
|                 const parentNotePath = await treeService.getSomeNotePath(parentNote); | ||||
|                 // this is to avoid having root notes leading '/'
 | ||||
|                 const notePath = parentNotePath ? (parentNotePath + '/' + note.noteId) : note.noteId; | ||||
|                 const isCurrent = activeNoteParentNoteId === parentNote.noteId; | ||||
| 
 | ||||
|                 await this.addPath(notePath, isCurrent); | ||||
|             } | ||||
| 
 | ||||
|             const cloneLink = $("<div>") | ||||
|                 .addClass("dropdown-item") | ||||
|                 .append( | ||||
|                     $('<button class="btn btn-sm">') | ||||
|                         .text('Clone note to new location...') | ||||
|                         .on('click', () => import("../dialogs/clone_to.js").then(d => d.showDialog([note.noteId]))) | ||||
|                 ); | ||||
| 
 | ||||
|             this.$notePathList.append(cloneLink); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async addPath(notePath, isCurrent) { | ||||
|         const title = await treeUtils.getNotePathTitle(notePath); | ||||
| 
 | ||||
|         const noteLink = await linkService.createNoteLink(notePath, {title}); | ||||
| 
 | ||||
|         noteLink | ||||
|             .addClass("no-tooltip-preview") | ||||
|             .addClass("dropdown-item"); | ||||
| 
 | ||||
|         if (isCurrent) { | ||||
|             noteLink.addClass("current"); | ||||
|         } | ||||
| 
 | ||||
|         this.$notePathList.append(noteLink); | ||||
|     } | ||||
| 
 | ||||
|     noteSavedListener() { | ||||
|         this.$savedIndicator.fadeIn(); | ||||
|     } | ||||
| } | ||||
| @ -1,7 +1,8 @@ | ||||
| import treeService from './tree.js'; | ||||
| import noteDetailService from './note_detail.js'; | ||||
| import server from './server.js'; | ||||
| import mimeTypesService from './mime_types.js'; | ||||
| import treeService from '../services/tree.js'; | ||||
| import noteDetailService from '../services/note_detail.js'; | ||||
| import server from '../services/server.js'; | ||||
| import mimeTypesService from '../services/mime_types.js'; | ||||
| import TabAwareWidget from "./tab_aware_widget.js"; | ||||
| 
 | ||||
| const NOTE_TYPES = [ | ||||
|     { type: "file", title: "File", selectable: false }, | ||||
| @ -15,20 +16,37 @@ const NOTE_TYPES = [ | ||||
|     { type: "code", mime: 'text/plain', title: "Code", selectable: true } | ||||
| ]; | ||||
| 
 | ||||
| export default class NoteTypeContext { | ||||
|     /** | ||||
|      * @param {TabContext} ctx | ||||
|      */ | ||||
|     constructor(ctx) { | ||||
|         this.ctx = ctx; | ||||
| const TPL = ` | ||||
| <style> | ||||
| .note-type-dropdown { | ||||
|     max-height: 500px; | ||||
|     overflow-y: auto; | ||||
|     overflow-x: hidden; | ||||
| } | ||||
| </style> | ||||
| 
 | ||||
|         ctx.$tabContent.find('.note-type').on('show.bs.dropdown', () => this.renderDropdown()); | ||||
| <div class="dropdown note-type"> | ||||
|     <button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle note-type-button"> | ||||
|         Type: <span class="note-type-desc"></span> | ||||
|         <span class="caret"></span> | ||||
|     </button> | ||||
|     <div class="note-type-dropdown dropdown-menu dropdown-menu-right"></div> | ||||
| </div> | ||||
| `;
 | ||||
| 
 | ||||
|         this.$noteTypeDropdown = ctx.$tabContent.find(".note-type-dropdown"); | ||||
|         this.$noteTypeButton = ctx.$tabContent.find(".note-type-button"); | ||||
|         this.$noteTypeDesc = ctx.$tabContent.find(".note-type-desc"); | ||||
|         this.$executeScriptButton = ctx.$tabContent.find(".execute-script-button"); | ||||
|         this.$renderButton = ctx.$tabContent.find('.render-button'); | ||||
| export default class NoteTypeWidget extends TabAwareWidget { | ||||
|     async doRender($widget) { | ||||
|         $widget.append($(TPL)); | ||||
| 
 | ||||
|         $widget.find('.note-type').on('show.bs.dropdown', () => this.renderDropdown()); | ||||
| 
 | ||||
|         this.$noteTypeDropdown = $widget.find(".note-type-dropdown"); | ||||
|         this.$noteTypeButton = $widget.find(".note-type-button"); | ||||
|         this.$noteTypeDesc = $widget.find(".note-type-desc"); | ||||
|         this.$executeScriptButton = $widget.find(".execute-script-button"); | ||||
|         this.$renderButton = $widget.find('.render-button'); | ||||
| 
 | ||||
|         return $widget; | ||||
|     } | ||||
| 
 | ||||
|     async update() { | ||||
| @ -41,6 +59,10 @@ export default class NoteTypeContext { | ||||
|         this.$renderButton.toggle(this.ctx.note.type === 'render'); | ||||
|     } | ||||
| 
 | ||||
|     async activeTabChanged() { | ||||
|         this.update(); | ||||
|     } | ||||
| 
 | ||||
|     /** actual body is rendered lazily on note-type button click */ | ||||
|     async renderDropdown() { | ||||
|         this.$noteTypeDropdown.empty(); | ||||
							
								
								
									
										19
									
								
								src/public/javascripts/widgets/tab_aware_widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/public/javascripts/widgets/tab_aware_widget.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| import BasicWidget from "./basic_widget.js"; | ||||
| 
 | ||||
| export default class TabAwareWidget extends BasicWidget { | ||||
|     constructor(appContext) { | ||||
|         super(appContext); | ||||
| 
 | ||||
|         /** @var {TabContext} */ | ||||
|         this.tabContext = null; | ||||
|     } | ||||
| 
 | ||||
|     // to override
 | ||||
|     activeTabChanged() {} | ||||
| 
 | ||||
|     activeTabChangedListener() { | ||||
|         this.tabContext = this.appContext.getActiveTabContext(); | ||||
| 
 | ||||
|         this.activeTabChanged(); | ||||
|     } | ||||
| } | ||||
| @ -175,21 +175,6 @@ body { | ||||
|     border-color: var(--button-border-color); | ||||
| } | ||||
| 
 | ||||
| .note-title-row { | ||||
|     flex-grow: 0; | ||||
|     flex-shrink: 0; | ||||
|     padding-top: 2px; | ||||
| } | ||||
| 
 | ||||
| .note-title { | ||||
|     margin-left: 15px; | ||||
|     margin-right: 10px; | ||||
|     font-size: 150%; | ||||
|     border: 0; | ||||
|     width: 5em; | ||||
|     flex-grow: 100; | ||||
| } | ||||
| 
 | ||||
| .note-tab-row { | ||||
|     box-sizing: border-box; | ||||
|     position: relative; | ||||
|  | ||||
| @ -381,12 +381,6 @@ div.ui-tooltip { | ||||
|     border-right: none; | ||||
| } | ||||
| 
 | ||||
| .note-type-dropdown { | ||||
|     max-height: 500px; | ||||
|     overflow-y: auto; | ||||
|     overflow-x: hidden; | ||||
| } | ||||
| 
 | ||||
| .cm-matchhighlight { | ||||
|     background-color: #eeeeee | ||||
| } | ||||
|  | ||||
| @ -2,8 +2,6 @@ | ||||
|     <div id="note-tab-container"> | ||||
|         <div class="note-tab-content note-tab-content-template"> | ||||
|             <div class="note-detail-content"> | ||||
|                 <% include title.ejs %> | ||||
| 
 | ||||
|                 <div class="note-detail-script-area"></div> | ||||
| 
 | ||||
|                 <table class="note-detail-promoted-attributes"></table> | ||||
|  | ||||
| @ -85,9 +85,7 @@ | ||||
|             </div> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="note-tab-row"> | ||||
|             <div class="note-tab-row-content"></div> | ||||
|         </div> | ||||
|         <div id="tab-row-container"></div> | ||||
| 
 | ||||
|         <div id="title-bar-buttons" style="display: none;"> | ||||
|             <button class="btn icon-action bx bx-minus" id="minimize-btn"></button> | ||||
|  | ||||
| @ -1,71 +0,0 @@ | ||||
| <div class="note-title-row"> | ||||
|     <div style="display: flex; align-items: center;"> | ||||
|         <div class="dropdown hide-in-zen-mode"> | ||||
|             <button class="btn btn-sm dropdown-toggle note-path-list-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                 <span class="note-path-count">1 path</span> | ||||
| 
 | ||||
|                 <span class="caret"></span> | ||||
|             </button> | ||||
|             <ul class="note-path-list dropdown-menu" aria-labelledby="note-path-list-button"> | ||||
|             </ul> | ||||
|         </div> | ||||
| 
 | ||||
|         <input autocomplete="off" value="" class="note-title" tabindex="1"> | ||||
| 
 | ||||
|         <span class="saved-indicator hide-in-zen-mode bx bx-check" title="All changes have been saved"></span> | ||||
| 
 | ||||
|         <div class="hide-in-zen-mode" style="display: flex; align-items: center;"> | ||||
|             <button class="btn btn-sm icon-button bx bx-play-circle render-button" | ||||
|                     style="display: none; margin-right: 10px;" | ||||
|                     title="Render"></button> | ||||
| 
 | ||||
|             <button class="btn btn-sm icon-button bx bx-play-circle execute-script-button" | ||||
|                     style="display: none; margin-right: 10px;" | ||||
|                     title="Execute (Ctrl+Enter)"></button> | ||||
| 
 | ||||
|             <div class="btn-group btn-group-xs"> | ||||
|                 <button type="button" | ||||
|                         class="btn btn-sm icon-button bx bx-check-shield protect-button" | ||||
|                         title="Protected note can be viewed and edited only after entering password"> | ||||
|                 </button> | ||||
| 
 | ||||
|                 <button type="button" | ||||
|                         class="btn btn-sm icon-button bx bx-shield unprotect-button" | ||||
|                         title="Not protected note can be viewed without entering password"> | ||||
|                 </button> | ||||
|             </div> | ||||
| 
 | ||||
|                 | ||||
| 
 | ||||
|             <div style="display: flex;"> | ||||
|                 <div class="dropdown note-type"> | ||||
|                     <button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle note-type-button"> | ||||
|                         Type: <span class="note-type-desc"></span> | ||||
|                         <span class="caret"></span> | ||||
|                     </button> | ||||
|                     <div class="note-type-dropdown dropdown-menu dropdown-menu-right"></div> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <div class="dropdown note-actions"> | ||||
|                     <button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle"> | ||||
|                         Note actions | ||||
|                         <span class="caret"></span> | ||||
|                     </button> | ||||
|                     <div class="dropdown-menu dropdown-menu-right"> | ||||
|                         <a class="dropdown-item show-note-revisions-button" data-bind="css: { disabled: type() == 'file' || type() == 'image' }">Revisions</a> | ||||
|                         <a class="dropdown-item show-attributes-button"><kbd data-kb-action="ShowAttributes"></kbd> Attributes</a> | ||||
|                         <a class="dropdown-item show-link-map-button"><kbd data-kb-action="ShowLinkMap"></kbd> Link map</a> | ||||
|                         <a class="dropdown-item show-source-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' && type() != 'relation-map' && type() != 'search' }"> | ||||
|                             <kbd data-kb-action="ShowNoteSource"></kbd> | ||||
|                             Note source | ||||
|                         </a> | ||||
|                         <a class="dropdown-item import-files-button">Import files</a> | ||||
|                         <a class="dropdown-item export-note-button" data-bind="css: { disabled: type() != 'text' }">Export note</a> | ||||
|                         <a class="dropdown-item print-note-button"><kbd data-kb-action="PrintActiveNote"></kbd> Print note</a> | ||||
|                         <a class="dropdown-item show-note-info-button"><kbd data-kb-action="ShowNoteInfo"></kbd> Note info</a> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam