dsh-tiddlywiki 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -212
- package/lib/client.bundle.js +27490 -2173
- package/lib/client.js +27500 -2183
- package/lib/index.js +2436 -2424
- package/lib/index.js.map +1 -1
- package/package.json +85 -79
- package/src/client/markdown-editor.ts +123 -0
- package/src/client/note-widget.ts +36 -76
- package/src/client/styles.ts +50 -60
- package/src/host/routes.ts +16 -8
- package/src/index.ts +3 -1
- package/src/client/markdown.ts +0 -137
package/package.json
CHANGED
|
@@ -1,79 +1,85 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-tiddlywiki",
|
|
3
|
-
"description": "TiddlyWiki 5 as the DSH persistent knowledge base: tiddlywiki_* agent tools (search/get/put/delete + git sync), a full TiddlyWiki editor embedded in the GUI center column, a floating quick-note widget in the chat area, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"type": "module",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"dsh",
|
|
8
|
-
"dsh-plugin",
|
|
9
|
-
"tiddlywiki",
|
|
10
|
-
"knowledge-base",
|
|
11
|
-
"note-taking",
|
|
12
|
-
"notes",
|
|
13
|
-
"wiki",
|
|
14
|
-
"git-sync",
|
|
15
|
-
"agent-tools",
|
|
16
|
-
"plugin"
|
|
17
|
-
],
|
|
18
|
-
"author": {
|
|
19
|
-
"name": "bbqisbbq",
|
|
20
|
-
"url": "https://github.com/bbqisbbq"
|
|
21
|
-
},
|
|
22
|
-
"main": "lib/index.js",
|
|
23
|
-
"exports": {
|
|
24
|
-
".": "./lib/index.js",
|
|
25
|
-
"./client": "./lib/client.js",
|
|
26
|
-
"./package.json": "./package.json"
|
|
27
|
-
},
|
|
28
|
-
"dsh": {
|
|
29
|
-
"bundle": {
|
|
30
|
-
"patch": "./cordis.patch.yml"
|
|
31
|
-
},
|
|
32
|
-
"client": {
|
|
33
|
-
"inject": [
|
|
34
|
-
"slots"
|
|
35
|
-
],
|
|
36
|
-
"platform": "web"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
"files": [
|
|
40
|
-
"lib/**/*.js",
|
|
41
|
-
"lib/**/*.js.map",
|
|
42
|
-
"src",
|
|
43
|
-
"cordis.patch.yml",
|
|
44
|
-
"LICENSE",
|
|
45
|
-
"README.md"
|
|
46
|
-
],
|
|
47
|
-
"license": "MIT",
|
|
48
|
-
"homepage": "https://github.com/bbqisbbq/dsh-tiddlywiki#readme",
|
|
49
|
-
"repository": {
|
|
50
|
-
"type": "git",
|
|
51
|
-
"url": "https://github.com/bbqisbbq/dsh-tiddlywiki.git"
|
|
52
|
-
},
|
|
53
|
-
"bugs": {
|
|
54
|
-
"url": "https://github.com/bbqisbbq/dsh-tiddlywiki/issues"
|
|
55
|
-
},
|
|
56
|
-
"engines": {
|
|
57
|
-
"node": ">=22"
|
|
58
|
-
},
|
|
59
|
-
"dependencies": {
|
|
60
|
-
"tiddlywiki": "^5.4.1"
|
|
61
|
-
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"@
|
|
64
|
-
"@
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-tiddlywiki",
|
|
3
|
+
"description": "TiddlyWiki 5 as the DSH persistent knowledge base: tiddlywiki_* agent tools (search/get/put/delete + git sync), a full TiddlyWiki editor embedded in the GUI center column, a floating quick-note widget in the chat area, and git-based sync with auto-commit. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"dsh",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"tiddlywiki",
|
|
10
|
+
"knowledge-base",
|
|
11
|
+
"note-taking",
|
|
12
|
+
"notes",
|
|
13
|
+
"wiki",
|
|
14
|
+
"git-sync",
|
|
15
|
+
"agent-tools",
|
|
16
|
+
"plugin"
|
|
17
|
+
],
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "bbqisbbq",
|
|
20
|
+
"url": "https://github.com/bbqisbbq"
|
|
21
|
+
},
|
|
22
|
+
"main": "lib/index.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./lib/index.js",
|
|
25
|
+
"./client": "./lib/client.js",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
},
|
|
32
|
+
"client": {
|
|
33
|
+
"inject": [
|
|
34
|
+
"slots"
|
|
35
|
+
],
|
|
36
|
+
"platform": "web"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"lib/**/*.js",
|
|
41
|
+
"lib/**/*.js.map",
|
|
42
|
+
"src",
|
|
43
|
+
"cordis.patch.yml",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"homepage": "https://github.com/bbqisbbq/dsh-tiddlywiki#readme",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/bbqisbbq/dsh-tiddlywiki.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/bbqisbbq/dsh-tiddlywiki/issues"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=22"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"tiddlywiki": "^5.4.1"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@codemirror/commands": "^6.11.0",
|
|
64
|
+
"@codemirror/lang-markdown": "^6.5.2",
|
|
65
|
+
"@codemirror/language": "^6.12.4",
|
|
66
|
+
"@codemirror/state": "^6.7.2",
|
|
67
|
+
"@codemirror/view": "^6.43.10",
|
|
68
|
+
"@lezer/highlight": "^1.2.3",
|
|
69
|
+
"@types/node": "^22.20.1",
|
|
70
|
+
"@types/react": "^19.1.0",
|
|
71
|
+
"react": "^19.1.0",
|
|
72
|
+
"tsdown": "0.22.2",
|
|
73
|
+
"tsx": "^4.19.2",
|
|
74
|
+
"typescript": "~5.7.2",
|
|
75
|
+
"unrun": "*"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "node scripts/clean-lib.mjs && npm run build:host && npm run build:client",
|
|
79
|
+
"build:host": "tsdown -c tsdown.host.config.ts",
|
|
80
|
+
"build:client": "tsdown -c tsdown.client.config.ts && node scripts/wrap-client.mjs",
|
|
81
|
+
"watch": "tsdown -c tsdown.host.config.ts --watch",
|
|
82
|
+
"typecheck": "tsc --noEmit",
|
|
83
|
+
"selftest": "node scripts/selftest.mjs"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeMirror 6 Markdown editor for the quick-note widget (replaces the
|
|
3
|
+
* hand-rolled regex highlighter in the old markdown.ts — real Lezer syntax
|
|
4
|
+
* tree, proper GFM coverage, undo/redo, and a native editing surface).
|
|
5
|
+
*
|
|
6
|
+
* The widget stays a plain-DOM product: CodeMirror is framework-agnostic, so
|
|
7
|
+
* no React is pulled in. The editor is built into a wrapper `<div>`
|
|
8
|
+
* (`.dsh-tw-note-editor`) that the widget can append, drag-drop onto, and
|
|
9
|
+
* re-read via `getValue()`/`setValue()`.
|
|
10
|
+
*
|
|
11
|
+
* Theming follows the DSH design tokens (`--dsw-alias-*`) through a
|
|
12
|
+
* `HighlightStyle` that maps Markdown tags onto the same palette the old
|
|
13
|
+
* `.md-*` rules used, so light/dark both look right.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-tiddlywiki/client/markdown-editor
|
|
16
|
+
*/
|
|
17
|
+
import { EditorView, keymap, placeholder, drawSelection } from '@codemirror/view'
|
|
18
|
+
import { EditorState } from '@codemirror/state'
|
|
19
|
+
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
|
20
|
+
import { syntaxHighlighting, HighlightStyle } from '@codemirror/language'
|
|
21
|
+
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
|
|
22
|
+
import { tags as t } from '@lezer/highlight'
|
|
23
|
+
|
|
24
|
+
/** Design-token aliases (same values as styles.ts / the old .md-* rules). */
|
|
25
|
+
const BRAND = 'var(--dsw-alias-brand-primary, #3e63dd)'
|
|
26
|
+
const BRAND_DIM = 'color-mix(in srgb, var(--dsw-alias-brand-primary, #3e63dd) 55%, transparent)'
|
|
27
|
+
const SECONDARY = 'var(--dsw-alias-label-secondary, #888)'
|
|
28
|
+
const DIMMED = 'var(--dsw-alias-label-dimmed, #999)'
|
|
29
|
+
const MONO = 'ui-monospace, "Cascadia Mono", Consolas, "SF Mono", Menlo, monospace'
|
|
30
|
+
const CODE_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 12%, transparent)'
|
|
31
|
+
const CODE_BLOCK_BG = 'color-mix(in srgb, var(--dsw-alias-label-secondary, #888) 8%, transparent)'
|
|
32
|
+
|
|
33
|
+
/** Markdown token → design-token style map (mirrors the old .md-* palette). */
|
|
34
|
+
const mdHighlight = HighlightStyle.define([
|
|
35
|
+
// Headings (# … ######) — brand + bold.
|
|
36
|
+
{ tag: [t.heading1, t.heading2, t.heading3, t.heading4, t.heading5, t.heading6], color: BRAND, fontWeight: '700' },
|
|
37
|
+
// Markers: #, >, -, *, `, [ ] etc.
|
|
38
|
+
{ tag: t.processingInstruction, color: BRAND_DIM },
|
|
39
|
+
// Inline code + fenced code text — monospace, tinted background.
|
|
40
|
+
{ tag: t.monospace, fontFamily: MONO, backgroundColor: CODE_BG, borderRadius: '4px', padding: '0 3px' },
|
|
41
|
+
// Code block content (inside a fence) gets a slightly wider tint.
|
|
42
|
+
{ tag: t.content, fontFamily: MONO, backgroundColor: CODE_BLOCK_BG },
|
|
43
|
+
{ tag: t.strong, fontWeight: '700' },
|
|
44
|
+
{ tag: t.emphasis, fontStyle: 'italic' },
|
|
45
|
+
{ tag: t.strikethrough, textDecoration: 'line-through', opacity: '.75' },
|
|
46
|
+
{ tag: t.link, color: BRAND, textDecoration: 'underline' },
|
|
47
|
+
{ tag: t.url, color: BRAND_DIM, textDecoration: 'underline dotted' },
|
|
48
|
+
{ tag: t.quote, fontStyle: 'italic', color: SECONDARY },
|
|
49
|
+
{ tag: t.contentSeparator, color: DIMMED, textDecoration: 'line-through' },
|
|
50
|
+
{ tag: t.comment, color: DIMMED },
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
/** Public editor surface the quick-note widget consumes. */
|
|
54
|
+
export interface MarkdownEditor {
|
|
55
|
+
/** Wrapper element (append/drag-target). */
|
|
56
|
+
el: HTMLDivElement
|
|
57
|
+
/** The CodeMirror EditorView (advanced use / future extensions). */
|
|
58
|
+
view: EditorView
|
|
59
|
+
getValue(): string
|
|
60
|
+
setValue(value: string): void
|
|
61
|
+
/** Insert a Markdown line at the current caret (used by file upload). */
|
|
62
|
+
insertAtCaret(markdown: string): void
|
|
63
|
+
focus(): void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface MarkdownEditorOptions {
|
|
67
|
+
/** Placeholder text shown when the doc is empty. */
|
|
68
|
+
placeholder?: string
|
|
69
|
+
/** Called on Ctrl/Cmd+Enter (wired after save is defined by the widget). */
|
|
70
|
+
onSave?: () => void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a CodeMirror 6 Markdown editor inside `.dsh-tw-note-editor`.
|
|
75
|
+
* GFM base language (strikethrough, tables, task lists, autolinks) — strictly
|
|
76
|
+
* more coverage than the old regex highlighter.
|
|
77
|
+
*/
|
|
78
|
+
export function buildMarkdownEditor(opts: MarkdownEditorOptions = {}): MarkdownEditor {
|
|
79
|
+
const wrap = document.createElement('div')
|
|
80
|
+
wrap.className = 'dsh-tw-note-editor'
|
|
81
|
+
|
|
82
|
+
const view = new EditorView({
|
|
83
|
+
parent: wrap,
|
|
84
|
+
state: EditorState.create({
|
|
85
|
+
doc: '',
|
|
86
|
+
extensions: [
|
|
87
|
+
EditorView.lineWrapping,
|
|
88
|
+
history(),
|
|
89
|
+
markdown({ base: markdownLanguage }),
|
|
90
|
+
syntaxHighlighting(mdHighlight),
|
|
91
|
+
drawSelection(),
|
|
92
|
+
placeholder(opts.placeholder ?? ''),
|
|
93
|
+
keymap.of([
|
|
94
|
+
...defaultKeymap,
|
|
95
|
+
...historyKeymap,
|
|
96
|
+
{ key: 'Mod-Enter', run: () => { opts.onSave?.(); return true } },
|
|
97
|
+
]),
|
|
98
|
+
],
|
|
99
|
+
}),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
el: wrap,
|
|
104
|
+
view,
|
|
105
|
+
getValue: () => view.state.doc.toString(),
|
|
106
|
+
setValue(value: string) {
|
|
107
|
+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } })
|
|
108
|
+
},
|
|
109
|
+
insertAtCaret(markdownLine: string) {
|
|
110
|
+
const { from } = view.state.selection.main
|
|
111
|
+
const line = view.state.doc.lineAt(from)
|
|
112
|
+
const atLineStart = from === line.from
|
|
113
|
+
const insert = `${atLineStart ? '' : '\n'}${markdownLine}\n`
|
|
114
|
+
view.dispatch({
|
|
115
|
+
changes: { from, insert },
|
|
116
|
+
selection: { anchor: from + insert.length },
|
|
117
|
+
scrollIntoView: true,
|
|
118
|
+
})
|
|
119
|
+
view.focus()
|
|
120
|
+
},
|
|
121
|
+
focus: () => view.focus(),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
* (GET /dsh-tiddlywiki/tags). Enter/`,` commits the typed value; Backspace on
|
|
15
15
|
* an empty field removes the last chip; the dropdown filters on typing.
|
|
16
16
|
*
|
|
17
|
-
* Markdown editing: a
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Markdown editing: a CodeMirror 6 editor (see markdown-editor.ts) with real
|
|
18
|
+
* Lezer-based syntax highlighting (GFM: headings, lists, code, links, tables,
|
|
19
|
+
* task lists, strikethrough…) plus undo/redo, wired into the same
|
|
20
|
+
* `.dsh-tw-note-editor` surface the widget reads/writes.
|
|
20
21
|
*
|
|
21
22
|
* File upload: the body accepts files via the 📎 button or drag-and-drop;
|
|
22
23
|
* each file is POSTed raw to /dsh-tiddlywiki/upload (saved under the wiki's
|
|
@@ -27,7 +28,8 @@
|
|
|
27
28
|
*/
|
|
28
29
|
import { toast } from './toast.ts'
|
|
29
30
|
import { openEditorPopup } from './editor-popup.ts'
|
|
30
|
-
import {
|
|
31
|
+
import { buildMarkdownEditor, type MarkdownEditor } from './markdown-editor.ts'
|
|
32
|
+
import type { EditorView } from '@codemirror/view'
|
|
31
33
|
|
|
32
34
|
const NOTE_ENDPOINT = '/dsh-tiddlywiki/note'
|
|
33
35
|
const EDIT_ENDPOINT = '/dsh-tiddlywiki/edit'
|
|
@@ -195,54 +197,23 @@ function buildTagEditor(): {
|
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
/**
|
|
198
|
-
* Markdown editor
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* changes the user's characters.
|
|
200
|
+
* Build the CodeMirror 6 Markdown editor for the quick-note widget. The editor
|
|
201
|
+
* lives inside `.dsh-tw-note-editor`; the widget reads/writes through
|
|
202
|
+
* getValue/setValue and inserts uploaded files at the caret via insertAtCaret.
|
|
203
|
+
* Mod-Enter (save) is routed through `onSave`.
|
|
203
204
|
*/
|
|
204
|
-
function buildEditor(): {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} {
|
|
210
|
-
const wrap = document.createElement('div')
|
|
211
|
-
wrap.className = 'dsh-tw-note-editor'
|
|
212
|
-
|
|
213
|
-
const hl = document.createElement('pre')
|
|
214
|
-
hl.className = 'dsh-tw-note-hl'
|
|
215
|
-
hl.setAttribute('aria-hidden', 'true')
|
|
216
|
-
|
|
217
|
-
const textarea = document.createElement('textarea')
|
|
218
|
-
textarea.className = 'dsh-tw-note-text'
|
|
219
|
-
textarea.placeholder = '写点东西… Markdown 高亮,可 📎/拖入文件\nCtrl+Enter 保存'
|
|
220
|
-
|
|
221
|
-
wrap.append(hl, textarea)
|
|
222
|
-
|
|
223
|
-
const render = (): void => {
|
|
224
|
-
hl.innerHTML = `${highlightMarkdown(textarea.value)}\n`
|
|
225
|
-
hl.scrollTop = textarea.scrollTop
|
|
226
|
-
hl.scrollLeft = textarea.scrollLeft
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const syncScroll = (): void => {
|
|
230
|
-
hl.scrollTop = textarea.scrollTop
|
|
231
|
-
hl.scrollLeft = textarea.scrollLeft
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
textarea.addEventListener('input', render)
|
|
235
|
-
textarea.addEventListener('scroll', syncScroll)
|
|
236
|
-
render()
|
|
237
|
-
|
|
238
|
-
return { el: wrap, textarea, render, focus: () => textarea.focus() }
|
|
205
|
+
function buildEditor(onSave: () => void): MarkdownEditor {
|
|
206
|
+
return buildMarkdownEditor({
|
|
207
|
+
placeholder: '写点东西… Markdown 高亮,可 📎/拖入文件\nCtrl+Enter 保存',
|
|
208
|
+
onSave,
|
|
209
|
+
})
|
|
239
210
|
}
|
|
240
211
|
|
|
241
212
|
/**
|
|
242
213
|
* Upload one file to the wiki (raw body, name in ?name=), then insert a
|
|
243
214
|
* Markdown image/link line at the caret of the given editor.
|
|
244
215
|
*/
|
|
245
|
-
async function uploadInto(file: File, editor:
|
|
216
|
+
async function uploadInto(file: File, editor: MarkdownEditor): Promise<void> {
|
|
246
217
|
if (file.size > MAX_UPLOAD_BYTES) {
|
|
247
218
|
toast(`文件过大(≤ ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB)`)
|
|
248
219
|
return
|
|
@@ -263,26 +234,15 @@ async function uploadInto(file: File, editor: { textarea: HTMLTextAreaElement; r
|
|
|
263
234
|
const markdown = file.type.startsWith('image/')
|
|
264
235
|
? ``
|
|
265
236
|
: `[${name}](${payload.url})`
|
|
266
|
-
|
|
267
|
-
const start = ta.selectionStart ?? ta.value.length
|
|
268
|
-
const end = ta.selectionEnd ?? start
|
|
269
|
-
const before = ta.value.slice(0, start)
|
|
270
|
-
const after = ta.value.slice(end)
|
|
271
|
-
const needsLead = start > 0 && !before.endsWith('\n')
|
|
272
|
-
const insert = `${needsLead ? '\n' : ''}${markdown}\n`
|
|
273
|
-
ta.value = `${before}${insert}${after}`
|
|
274
|
-
const caret = (before + insert).length
|
|
275
|
-
ta.setSelectionRange(caret, caret)
|
|
276
|
-
ta.focus()
|
|
277
|
-
editor.render()
|
|
237
|
+
editor.insertAtCaret(markdown)
|
|
278
238
|
toast(`已上传「${name}」并插入链接`)
|
|
279
239
|
} catch (err) {
|
|
280
240
|
toast(`上传失败:${err instanceof Error ? err.message : String(err)}`)
|
|
281
241
|
}
|
|
282
242
|
}
|
|
283
243
|
|
|
284
|
-
/** Build the whole widget DOM and wire it up (returns
|
|
285
|
-
function buildWidget(): HTMLDivElement {
|
|
244
|
+
/** Build the whole widget DOM and wire it up (returns root + editor view). */
|
|
245
|
+
function buildWidget(): { root: HTMLDivElement; editorView: EditorView } {
|
|
286
246
|
const root = document.createElement('div')
|
|
287
247
|
root.className = 'dsh-tw-note'
|
|
288
248
|
|
|
@@ -310,8 +270,10 @@ function buildWidget(): HTMLDivElement {
|
|
|
310
270
|
const tagEditor = buildTagEditor()
|
|
311
271
|
fields.append(titleInput, tagEditor.el)
|
|
312
272
|
|
|
313
|
-
|
|
314
|
-
|
|
273
|
+
// Mod-Enter save routes through doSave, which is assigned below (the editor
|
|
274
|
+
// keymap only runs after the widget is fully wired, so this is safe).
|
|
275
|
+
let doSave: () => Promise<void> | undefined
|
|
276
|
+
const editor = buildEditor(() => { void doSave?.() })
|
|
315
277
|
|
|
316
278
|
// ── file upload (button + drag & drop onto the editor) ────────────────
|
|
317
279
|
const uploadBtn = document.createElement('button')
|
|
@@ -407,8 +369,8 @@ function buildWidget(): HTMLDivElement {
|
|
|
407
369
|
toggle.addEventListener('click', () => { void (opened ? close() : open()) })
|
|
408
370
|
closeBtn.addEventListener('click', close)
|
|
409
371
|
|
|
410
|
-
|
|
411
|
-
const text =
|
|
372
|
+
doSave = async (): Promise<void> => {
|
|
373
|
+
const text = editor.getValue().trim()
|
|
412
374
|
if (text.length === 0) {
|
|
413
375
|
toast('内容为空,未保存')
|
|
414
376
|
return
|
|
@@ -431,8 +393,7 @@ function buildWidget(): HTMLDivElement {
|
|
|
431
393
|
toast(`保存失败:${payload?.error ?? `HTTP ${res.status}`}`)
|
|
432
394
|
return
|
|
433
395
|
}
|
|
434
|
-
|
|
435
|
-
editor.render()
|
|
396
|
+
editor.setValue('')
|
|
436
397
|
resetTitle()
|
|
437
398
|
toast(`已保存「${payload.title ?? titleInput.value}」`)
|
|
438
399
|
close()
|
|
@@ -444,18 +405,12 @@ function buildWidget(): HTMLDivElement {
|
|
|
444
405
|
}
|
|
445
406
|
}
|
|
446
407
|
|
|
447
|
-
save.addEventListener('click', () => { void doSave() })
|
|
448
|
-
textarea.addEventListener('keydown', (event) => {
|
|
449
|
-
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
|
|
450
|
-
event.preventDefault()
|
|
451
|
-
void doSave()
|
|
452
|
-
}
|
|
453
|
-
})
|
|
408
|
+
save.addEventListener('click', () => { void doSave?.() })
|
|
454
409
|
|
|
455
410
|
/** Save (if non-empty) and open the tiddler in TW's native editor. */
|
|
456
411
|
const doEdit = async (): Promise<void> => {
|
|
457
412
|
const title = titleInput.value.trim().length > 0 ? titleInput.value.trim() : timestampTitle()
|
|
458
|
-
const text =
|
|
413
|
+
const text = editor.getValue()
|
|
459
414
|
const tags = tagEditor.getTags()
|
|
460
415
|
edit.disabled = true
|
|
461
416
|
edit.textContent = '打开中…'
|
|
@@ -489,19 +444,22 @@ function buildWidget(): HTMLDivElement {
|
|
|
489
444
|
|
|
490
445
|
edit.addEventListener('click', () => { void doEdit() })
|
|
491
446
|
|
|
492
|
-
return root
|
|
447
|
+
return { root, editorView: editor.view }
|
|
493
448
|
}
|
|
494
449
|
|
|
495
450
|
/**
|
|
496
451
|
* Mount the floating quick-note widget. Fetches /status first: when
|
|
497
452
|
* `ui.showQuickNote` is off the widget is never created (no DOM side effects).
|
|
498
|
-
* Returns a disposer that removes it.
|
|
453
|
+
* Returns a disposer that removes it (and destroys the CodeMirror view).
|
|
499
454
|
*/
|
|
500
455
|
export function mountNoteWidget(): () => void {
|
|
501
456
|
let disposed = false
|
|
502
457
|
let root: HTMLDivElement | undefined
|
|
458
|
+
let editorView: EditorView | undefined
|
|
503
459
|
const disposer = (): void => {
|
|
504
460
|
disposed = true
|
|
461
|
+
// CodeMirror holds a ResizeObserver + timers; destroy it, not just the DOM.
|
|
462
|
+
editorView?.destroy()
|
|
505
463
|
root?.remove()
|
|
506
464
|
const toastEl = document.querySelector<HTMLElement>('.dsh-tw-toast')
|
|
507
465
|
toastEl?.remove()
|
|
@@ -509,7 +467,9 @@ export function mountNoteWidget(): () => void {
|
|
|
509
467
|
void (async () => {
|
|
510
468
|
const ui = await fetchUiOptions()
|
|
511
469
|
if (disposed || !ui.showQuickNote) return
|
|
512
|
-
|
|
470
|
+
const widget = buildWidget()
|
|
471
|
+
root = widget.root
|
|
472
|
+
editorView = widget.editorView
|
|
513
473
|
})()
|
|
514
474
|
return disposer
|
|
515
475
|
}
|