From 7563e02872abec1b76debba4636cc840f557d799 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Thu, 4 Jun 2026 13:21:49 +0200 Subject: [PATCH 1/3] quick create --- frontend/app.js | 115 ++++++++++++++++++++++++++++++++++++++++++-- frontend/index.html | 46 +++++++++++++++++- locales/de-DE.json | 11 ++++- locales/en-GB.json | 11 ++++- locales/en-US.json | 11 ++++- locales/es-ES.json | 11 ++++- locales/fr-FR.json | 11 ++++- locales/hu-HU.json | 11 ++++- locales/it-IT.json | 11 ++++- locales/ja-JP.json | 11 ++++- locales/ru-RU.json | 11 ++++- locales/sl-SI.json | 11 ++++- locales/zh-CN.json | 11 ++++- 13 files changed, 264 insertions(+), 18 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 627790d..0d1b3fa 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -43,8 +43,23 @@ const LOCAL_SETTINGS = { hideUnderscoreFolders: { key: 'hideUnderscoreFolders', type: 'boolean', default: false }, tabInsertsTab: { key: 'tabInsertsTab', type: 'boolean', default: false }, sidebarPanelCollapsed: { key: 'sidebarPanelCollapsed', type: 'boolean', default: false }, + // When true, the New Note name modal opens with a yyyymmddHHMMSS title pre-selected + // (Zettelkasten-style). Press Enter to accept, or type to replace. See issue #217. + autoFillNoteTitle: { key: 'autoFillNoteTitle', type: 'boolean', default: false }, // String settings sortMode: { key: 'sortMode', type: 'string', default: 'a-z' }, + // What clicking + (or the top-level New button) does. 'chooser' preserves the + // current dropdown-with-4-options behaviour; the other values dispatch straight + // to that creation flow. Shift+click always falls back to 'chooser'. See #217. + newButtonAction: { + key: 'newButtonAction', type: 'string', default: 'chooser', + valid: ['chooser', 'note', 'folder', 'template', 'drawing'] + }, + // Name of the last template that successfully created a note. Used to pre-select + // it the next time the "Create from Template" modal opens, so the common case of + // "I always use the same template" is one click less. Cleared / ignored silently + // if the template no longer exists. + lastUsedTemplate: { key: 'lastUsedTemplate', type: 'string', default: '' }, // Number settings with validation sidebarWidth: { key: 'sidebarWidth', type: 'number', default: CONFIG.DEFAULT_SIDEBAR_WIDTH, min: 200, max: 600 }, editorWidth: { key: 'editorWidth', type: 'number', default: 50, min: 20, max: 80 }, @@ -408,6 +423,12 @@ function noteApp() { availableTemplates: [], selectedTemplate: '', newTemplateNoteName: '', + // Persisted via LOCAL_SETTINGS; hydrated by loadLocalSettings() at app init. + // Inline defaults below mirror the LOCAL_SETTINGS defaults so the reactive + // proxy has the right type/shape before hydration runs. See issue #217. + newButtonAction: 'chooser', + autoFillNoteTitle: false, + lastUsedTemplate: '', // New note / folder name modal (replaces window.prompt) showCreateNameModal: false, @@ -1742,6 +1763,11 @@ function noteApp() { const data = await response.json(); + // Remember for pre-selection next time openTemplateModal() runs (see #217). + // Captured BEFORE the reset below so the name is still available. + this.lastUsedTemplate = this.selectedTemplate; + try { localStorage.setItem('lastUsedTemplate', this.selectedTemplate); } catch (_) {} + // Close modal and reset state this.showTemplateModal = false; this.selectedTemplate = ''; @@ -4570,16 +4596,49 @@ function noteApp() { // DROPDOWN MENU SYSTEM // ===================================================== + // Entry point for every "+" button and the top-level "New" button. Either opens + // the type-chooser dropdown (default behaviour) or dispatches straight to a + // single creation flow, depending on the user's `newButtonAction` setting. + // + // Shift+click ALWAYS forces the chooser regardless of configured default — a + // single discoverable escape hatch (mentioned in the settings description) that + // saves users from needing to open Settings just to reach the other modes once + // in a while. See issue #217. + // + // Folder context (dropdownTargetFolder) is set by the calling site BEFORE this + // method runs; each dispatched action reads it via inferredNewItemTargetFolder() + // and clears the dropdown state itself, so we don't call closeDropdown() here. toggleNewDropdown(event) { - this.showNewDropdown = true; // Always open (or keep open) + const wantsChooser = + (event && event.shiftKey) || + !this.newButtonAction || + this.newButtonAction === 'chooser'; + + if (wantsChooser) { + this._openNewDropdownChooser(event); + return; + } + + switch (this.newButtonAction) { + case 'note': this.createNote(); break; + case 'folder': this.createFolder(); break; + case 'template': this.openTemplateModal(); break; + case 'drawing': this.createNewDrawing(); break; + default: this._openNewDropdownChooser(event); + } + }, + + // Open the type-chooser dropdown anchored next to the clicking element. Extracted + // verbatim from the original toggleNewDropdown body so existing positioning logic + // is preserved bit-for-bit; the new routing layer above is a thin wrapper. + _openNewDropdownChooser(event) { + this.showNewDropdown = true; if (event && event.target) { const rect = event.target.getBoundingClientRect(); - // Position dropdown next to the clicked element - let top = rect.bottom + 4; // 4px spacing + let top = rect.bottom + 4; let left = rect.left; - // Keep dropdown on screen const dropdownWidth = 200; const dropdownHeight = 150; if (left + dropdownWidth > window.innerWidth) { @@ -4593,6 +4652,37 @@ function noteApp() { } }, + // Open "Create from Template" modal. Shared by: + // - the type-chooser dropdown's "from template" button + // - the quick-dispatch path from toggleNewDropdown() + // so both paths consistently pre-select the last-used template (if it still + // exists) AND pre-fill the name when autoFillNoteTitle is on, matching the + // plain New Note popup behaviour. dropdownTargetFolder is intentionally NOT + // cleared — it's read at submit time by createNoteFromTemplate() via + // inferredNewItemTargetFolder(). See issue #217. + openTemplateModal() { + this.showNewDropdown = false; + this.mobileSidebarOpen = false; + const hasLastUsed = !!this.lastUsedTemplate && + this.availableTemplates.some(t => t.name === this.lastUsedTemplate); + this.selectedTemplate = hasLastUsed ? this.lastUsedTemplate : ''; + this.newTemplateNoteName = this.autoFillNoteTitle ? this._autoTitleTimestamp() : ''; + this.showTemplateModal = true; + // If a template was pre-selected the modal can be submitted with Enter + // alone, so focus the name input and select its contents (so typing + // immediately replaces). When no template is pre-selected the user has + // to pick one first, so we don't steal focus from the dropdown. + if (hasLastUsed) { + this.$nextTick(() => { + const el = document.getElementById('template-note-name-input'); + if (el) { + el.focus(); + el.select(); + } + }); + } + }, + closeDropdown() { this.showNewDropdown = false; this.dropdownTargetFolder = null; // Reset folder context @@ -4653,7 +4743,12 @@ function noteApp() { this.mobileSidebarOpen = false; this.createNameModalKind = kind; this.createNameModalTargetFolder = targetFolder; - this.createNameModalInput = ''; + // Auto-fill title with a yyyymmddHHMMSS timestamp when the user opted in + // (notes only; doesn't make sense for folders). The input gets .select()'d + // below, so typing anything immediately replaces it — Enter accepts it as + // a Zettelkasten-style filename. See issue #217. + this.createNameModalInput = + (kind === 'note' && this.autoFillNoteTitle) ? this._autoTitleTimestamp() : ''; this.showCreateNameModal = true; this.$nextTick(() => { const el = document.getElementById('create-name-modal-input'); @@ -4664,6 +4759,16 @@ function noteApp() { }); }, + // Zettelkasten convention: yyyymmddHHMMSS — compact, sorts chronologically, + // and contains no characters that need escaping on any filesystem. Local + // time deliberately (matches what users see on the clock). + _autoTitleTimestamp() { + const d = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` + + `${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; + }, + closeCreateNameModal() { this.showCreateNameModal = false; this.createNameModalInput = ''; diff --git a/frontend/index.html b/frontend/index.html index a2b9f06..00d60a6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2227,6 +2227,46 @@

+ + +
+ + +

+
+ + +
+ +

+
@@ -3449,7 +3489,7 @@