added SHIFT + TAB behavior to decrease indent
This commit is contained in:
parent
bf48d80fd6
commit
c80e164581
|
|
@ -33,7 +33,7 @@
|
||||||
- **Rename anything** - Files and folders, instantly
|
- **Rename anything** - Files and folders, instantly
|
||||||
- **Visual tree view** - Expandable/collapsible navigation
|
- **Visual tree view** - Expandable/collapsible navigation
|
||||||
- **Hide system folders** - Toggle to hide `_attachments`, `_templates` and other underscore-prefixed folders from sidebar
|
- **Hide system folders** - Toggle to hide `_attachments`, `_templates` and other underscore-prefixed folders from sidebar
|
||||||
- **Tab inserts tab** - Toggle to make Tab key insert a tab character in the editor instead of changing focus
|
- **Tab inserts tab** - Toggle so Tab inserts a literal tab in the editor instead of changing focus; when this is on, Shift+Tab outdents (one leading tab or up to four leading spaces per line in the selection), and if nothing is removed, Shift+Tab still moves focus backward
|
||||||
- **List & quote continuation (Enter)** - At the end of a line, Enter continues blockquotes (`>`), bullets (`-` / `*` / `+`), ordered lists (`1.` / `1)`), and task lists (`- [ ]` / `- [x]`); pressing Enter again on an empty continued line exits the list or quote. Disabled inside fenced code blocks and when using Shift+Enter (plain newline)
|
- **List & quote continuation (Enter)** - At the end of a line, Enter continues blockquotes (`>`), bullets (`-` / `*` / `+`), ordered lists (`1.` / `1)`), and task lists (`- [ ]` / `- [x]`); pressing Enter again on an empty continued line exits the list or quote. Disabled inside fenced code blocks and when using Shift+Enter (plain newline)
|
||||||
|
|
||||||
### Export & Print
|
### Export & Print
|
||||||
|
|
|
||||||
103
frontend/app.js
103
frontend/app.js
|
|
@ -162,6 +162,89 @@ const FilenameValidator = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Max leading spaces Shift+Tab removes per line when matching one “tab indent” vs \t inserts. */
|
||||||
|
const EDITOR_TAB_OUTDENT_MAX_SPACES = 4;
|
||||||
|
|
||||||
|
function editorIndentRemoveLen(lineSegment) {
|
||||||
|
if (!lineSegment || lineSegment.length === 0) return 0;
|
||||||
|
if (lineSegment.charCodeAt(0) === 9 /* \t */) return 1;
|
||||||
|
let i = 0;
|
||||||
|
const max = Math.min(EDITOR_TAB_OUTDENT_MAX_SPACES, lineSegment.length);
|
||||||
|
while (i < max && lineSegment[i] === ' ') i++;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editorOutdentChunk(chunk) {
|
||||||
|
if (chunk.length === 0) return { newChunk: chunk, changed: false };
|
||||||
|
const lines = chunk.split('\n');
|
||||||
|
const newLines = lines.map((line) => {
|
||||||
|
const n = editorIndentRemoveLen(line);
|
||||||
|
return n > 0 ? line.slice(n) : line;
|
||||||
|
});
|
||||||
|
const newChunk = newLines.join('\n');
|
||||||
|
return { newChunk, changed: newChunk !== chunk };
|
||||||
|
}
|
||||||
|
|
||||||
|
function editorRemovedBeforeInChunk(chunk, rel) {
|
||||||
|
const target = Math.min(Math.max(rel, 0), chunk.length);
|
||||||
|
let off = 0;
|
||||||
|
let removed = 0;
|
||||||
|
while (off < chunk.length && off < target) {
|
||||||
|
const nl = chunk.indexOf('\n', off);
|
||||||
|
const lineEnd = nl === -1 ? chunk.length : nl;
|
||||||
|
const lineSlice = chunk.slice(off, lineEnd);
|
||||||
|
const n = editorIndentRemoveLen(lineSlice);
|
||||||
|
if (target >= lineEnd) {
|
||||||
|
removed += n;
|
||||||
|
if (nl === -1) break;
|
||||||
|
off = lineEnd + 1;
|
||||||
|
} else {
|
||||||
|
const col = target - off;
|
||||||
|
removed += Math.min(col, n);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editorMapCaretAfterOutdent(chunk, newChunk, rel) {
|
||||||
|
const r = Math.max(0, rel);
|
||||||
|
if (r >= chunk.length) return newChunk.length;
|
||||||
|
return r - editorRemovedBeforeInChunk(chunk, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift+Tab outdent while “tab inserts tab” is enabled: remove one leading \t or up to 4 spaces per affected line.
|
||||||
|
* @returns {{ changed: boolean, text: string, selStart: number, selEnd: number }}
|
||||||
|
*/
|
||||||
|
function applyEditorOutdent(text, selStart, selEnd) {
|
||||||
|
const a = Math.min(selStart, selEnd);
|
||||||
|
const b = Math.max(selStart, selEnd);
|
||||||
|
const blockStart = text.lastIndexOf('\n', Math.max(0, a - 1)) + 1;
|
||||||
|
const ref = b > a ? b - 1 : a;
|
||||||
|
let blockEnd = text.indexOf('\n', ref);
|
||||||
|
if (blockEnd === -1) blockEnd = text.length;
|
||||||
|
|
||||||
|
const chunk = text.slice(blockStart, blockEnd);
|
||||||
|
const { newChunk, changed } = editorOutdentChunk(chunk);
|
||||||
|
if (!changed) {
|
||||||
|
return { changed: false, text, selStart, selEnd };
|
||||||
|
}
|
||||||
|
|
||||||
|
const newText = text.slice(0, blockStart) + newChunk + text.slice(blockEnd);
|
||||||
|
const anchorRel = a - blockStart;
|
||||||
|
const focusRel = b - blockStart;
|
||||||
|
const newA = blockStart + editorMapCaretAfterOutdent(chunk, newChunk, anchorRel);
|
||||||
|
const newB = blockStart + editorMapCaretAfterOutdent(chunk, newChunk, focusRel);
|
||||||
|
|
||||||
|
return {
|
||||||
|
changed: true,
|
||||||
|
text: newText,
|
||||||
|
selStart: Math.min(newA, newB),
|
||||||
|
selEnd: Math.max(newA, newB),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function noteApp() {
|
function noteApp() {
|
||||||
return {
|
return {
|
||||||
// App state
|
// App state
|
||||||
|
|
@ -947,14 +1030,28 @@ function noteApp() {
|
||||||
localStorage.setItem('tabInsertsTab', this.tabInsertsTab);
|
localStorage.setItem('tabInsertsTab', this.tabInsertsTab);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle Tab key in editor (inserts tab if setting enabled)
|
// Handle Tab key in editor (inserts tab if setting enabled; Shift+Tab outdents matching lines)
|
||||||
handleTabKey(event) {
|
handleTabKey(event) {
|
||||||
if (!this.tabInsertsTab) return;
|
if (!this.tabInsertsTab) return;
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
const textarea = event.target;
|
const textarea = event.target;
|
||||||
const start = textarea.selectionStart;
|
const start = textarea.selectionStart;
|
||||||
const end = textarea.selectionEnd;
|
const end = textarea.selectionEnd;
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
const result = applyEditorOutdent(this.noteContent, start, end);
|
||||||
|
if (!result.changed) return;
|
||||||
|
event.preventDefault();
|
||||||
|
this.noteContent = result.text;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
textarea.selectionStart = result.selStart;
|
||||||
|
textarea.selectionEnd = result.selEnd;
|
||||||
|
});
|
||||||
|
this.autoSave();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
this.noteContent = this.noteContent.substring(0, start) + '\t' + this.noteContent.substring(end);
|
this.noteContent = this.noteContent.substring(0, start) + '\t' + this.noteContent.substring(end);
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue