@lemoncat7/dsh-knowledge 2.3.2 → 2.3.4
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 +1 -1
- package/docs/ui-maintenance.md +97 -0
- package/lib/activity-control.d.ts +6 -0
- package/lib/activity-control.d.ts.map +1 -0
- package/lib/activity-control.js +118 -0
- package/lib/activity-control.js.map +1 -0
- package/lib/client.d.ts +17 -0
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +4030 -196
- package/lib/client.js.map +4 -4
- package/lib/design-tokens.d.ts +12 -0
- package/lib/design-tokens.d.ts.map +1 -0
- package/lib/design-tokens.js +58 -0
- package/lib/design-tokens.js.map +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/knowledge-activity-api.d.ts +35 -0
- package/lib/knowledge-activity-api.d.ts.map +1 -0
- package/lib/knowledge-activity-api.js +115 -0
- package/lib/knowledge-activity-api.js.map +1 -0
- package/lib/knowledge-activity-controller.d.ts +20 -0
- package/lib/knowledge-activity-controller.d.ts.map +1 -0
- package/lib/knowledge-activity-controller.js +138 -0
- package/lib/knowledge-activity-controller.js.map +1 -0
- package/lib/knowledge-activity-notes.d.ts +9 -0
- package/lib/knowledge-activity-notes.d.ts.map +1 -0
- package/lib/knowledge-activity-notes.js +150 -0
- package/lib/knowledge-activity-notes.js.map +1 -0
- package/lib/knowledge-activity-panel.d.ts +8 -0
- package/lib/knowledge-activity-panel.d.ts.map +1 -0
- package/lib/knowledge-activity-panel.js +245 -0
- package/lib/knowledge-activity-panel.js.map +1 -0
- package/lib/knowledge-activity-presentation.d.ts +14 -0
- package/lib/knowledge-activity-presentation.d.ts.map +1 -0
- package/lib/knowledge-activity-presentation.js +77 -0
- package/lib/knowledge-activity-presentation.js.map +1 -0
- package/lib/knowledge-activity-state.d.ts +17 -0
- package/lib/knowledge-activity-state.d.ts.map +1 -0
- package/lib/knowledge-activity-state.js +14 -0
- package/lib/knowledge-activity-state.js.map +1 -0
- package/lib/latest-request.d.ts +7 -0
- package/lib/latest-request.d.ts.map +1 -0
- package/lib/latest-request.js +14 -0
- package/lib/latest-request.js.map +1 -0
- package/lib/local-provider.d.ts +0 -2
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +2 -323
- package/lib/local-provider.js.map +1 -1
- package/lib/notes/share-page-styles.d.ts +2 -0
- package/lib/notes/share-page-styles.d.ts.map +1 -0
- package/lib/notes/share-page-styles.js +11 -0
- package/lib/notes/share-page-styles.js.map +1 -0
- package/lib/notes/share-page.d.ts.map +1 -1
- package/lib/notes/share-page.js +1 -8
- package/lib/notes/share-page.js.map +1 -1
- package/lib/storage/migrations.d.ts +5 -0
- package/lib/storage/migrations.d.ts.map +1 -0
- package/lib/storage/migrations.js +325 -0
- package/lib/storage/migrations.js.map +1 -0
- package/lib/theme-bridge.d.ts +9 -7
- package/lib/theme-bridge.d.ts.map +1 -1
- package/lib/theme-bridge.js +3 -23
- package/lib/theme-bridge.js.map +1 -1
- package/lib/web-note-editor-outline.js +2 -2
- package/lib/web-note-editor-outline.js.map +1 -1
- package/lib/web-note-editor-selection.d.ts.map +1 -1
- package/lib/web-note-editor-selection.js +51 -16
- package/lib/web-note-editor-selection.js.map +1 -1
- package/lib/web-note-editor.d.ts.map +1 -1
- package/lib/web-note-editor.js +15 -1
- package/lib/web-note-editor.js.map +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +4 -0
- package/lib/web.js.map +1 -1
- package/package.json +4 -1
- package/web/app.js +111 -191
- package/web/design-tokens.css +1 -0
- package/web/dialogs.js +94 -0
- package/web/document-actions.js +68 -0
- package/web/index.html +1 -0
- package/web/model-catalog.js +22 -0
- package/web/note-editor.js +57 -57
- package/web/select-control.js +193 -0
- package/web/styles.css +162 -203
- package/web/ui-primitives.js +5 -0
package/web/app.js
CHANGED
|
@@ -3,14 +3,20 @@ const AUTH_MODE = document.querySelector('meta[name="dsh-knowledge-auth-mode"]')
|
|
|
3
3
|
const WEB_PATH = document.querySelector('meta[name="dsh-knowledge-web"]')?.content || '/knowledge'
|
|
4
4
|
const ASSET_VERSION = document.querySelector('meta[name="dsh-knowledge-asset-version"]')?.content || ''
|
|
5
5
|
const moduleUrl = name => `./${name}.js${ASSET_VERSION ? `?v=${encodeURIComponent(ASSET_VERSION)}` : ''}`
|
|
6
|
-
const [apiModule, themeModule, uiModule] = await Promise.all([
|
|
6
|
+
const [apiModule, themeModule, uiModule, modelCatalogModule, dialogModule, selectModule, documentActionsModule] = await Promise.all([
|
|
7
7
|
import(moduleUrl('api-client')),
|
|
8
8
|
import(moduleUrl('host-theme')),
|
|
9
9
|
import(moduleUrl('ui-primitives')),
|
|
10
|
+
import(moduleUrl('model-catalog')),
|
|
11
|
+
import(moduleUrl('dialogs')),
|
|
12
|
+
import(moduleUrl('select-control')),
|
|
13
|
+
import(moduleUrl('document-actions')),
|
|
10
14
|
])
|
|
11
15
|
const { createApiClient } = apiModule
|
|
12
16
|
const { installHostThemeBridge } = themeModule
|
|
13
17
|
const { actionButton, badge, createToastPresenter, element, interfaceIcon, paneToggleButton } = uiModule
|
|
18
|
+
const { renderMenu: renderDocumentMenu, closeMenus: closeDocumentMenus } = documentActionsModule.createDocumentMenuPresenter(uiModule)
|
|
19
|
+
const readModelCatalog = modelCatalogModule.createModelCatalogLoader()
|
|
14
20
|
const TOKEN_KEY = 'dsh-knowledge.session-token'
|
|
15
21
|
const TYPES = ['preference', 'fact', 'decision', 'procedure', 'lesson']
|
|
16
22
|
const TYPE_LABELS = { preference: '偏好', fact: '事实', decision: '决策', procedure: '流程', lesson: '经验' }
|
|
@@ -26,6 +32,8 @@ const NOTE_MAX_FILE_SIZE = 64 * 1024 * 1024
|
|
|
26
32
|
const pageParams = new URLSearchParams(location.search)
|
|
27
33
|
const initialKnowledgeBaseId = pageParams.get('knowledgeBaseId')?.trim() || ''
|
|
28
34
|
const initialDocumentId = pageParams.get('documentId')?.trim() || ''
|
|
35
|
+
const initialView = pageParams.get('view') === 'notes' ? 'notes' : 'entries'
|
|
36
|
+
const initialNoteId = pageParams.get('noteId')?.trim() || ''
|
|
29
37
|
const mountContext = {
|
|
30
38
|
sessionId: pageParams.get('sessionId')?.trim() || '',
|
|
31
39
|
projectId: pageParams.get('projectId')?.trim() || '',
|
|
@@ -33,6 +41,8 @@ const mountContext = {
|
|
|
33
41
|
const app = document.querySelector('#app')
|
|
34
42
|
const toastRegion = document.querySelector('#toast-region')
|
|
35
43
|
const showToast = createToastPresenter(toastRegion)
|
|
44
|
+
const { openConfirm, openSheet, openModal } = dialogModule.createDialogPresenter({ element, actionButton, interfaceIcon, showToast, friendlyError })
|
|
45
|
+
selectModule.installSelectControls()
|
|
36
46
|
const savedDocumentLayout = readDocumentLayout()
|
|
37
47
|
|
|
38
48
|
function createDocumentViewState(overrides = {}) {
|
|
@@ -46,7 +56,7 @@ function createDocumentViewState(overrides = {}) {
|
|
|
46
56
|
|
|
47
57
|
const state = {
|
|
48
58
|
token: sessionStorage.getItem(TOKEN_KEY) || '',
|
|
49
|
-
view:
|
|
59
|
+
view: initialView,
|
|
50
60
|
menuOpen: false,
|
|
51
61
|
stats: null,
|
|
52
62
|
overview: null,
|
|
@@ -88,9 +98,9 @@ const state = {
|
|
|
88
98
|
tokens: [],
|
|
89
99
|
notes: {
|
|
90
100
|
children: new Map(), loadedFolders: new Set(), expandedFolders: new Set(),
|
|
91
|
-
selectedId:
|
|
101
|
+
selectedId: initialNoteId, selectedNode: null, currentFolderId: null, breadcrumbs: [],
|
|
92
102
|
content: '', draft: '', dirty: false, assetUrl: '', query: '', searchResults: [],
|
|
93
|
-
transfer: null, loadingNodeId: '', shares: [], shareError: '',
|
|
103
|
+
transfer: null, loadingNodeId: '', shares: [], shareError: '', browserOpen: false,
|
|
94
104
|
},
|
|
95
105
|
service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1', remote: false },
|
|
96
106
|
scrollPositions: new Map(),
|
|
@@ -195,7 +205,7 @@ async function boot() {
|
|
|
195
205
|
state.token = ''
|
|
196
206
|
await Promise.all([
|
|
197
207
|
api('service').then(service => { state.service = service }).catch(() => {}),
|
|
198
|
-
navigate(
|
|
208
|
+
navigate(initialView),
|
|
199
209
|
])
|
|
200
210
|
renderShell()
|
|
201
211
|
return
|
|
@@ -206,7 +216,7 @@ async function boot() {
|
|
|
206
216
|
}
|
|
207
217
|
try {
|
|
208
218
|
await api('entries?limit=1')
|
|
209
|
-
await navigate(
|
|
219
|
+
await navigate(initialView)
|
|
210
220
|
} catch (error) {
|
|
211
221
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
212
222
|
state.token = ''
|
|
@@ -233,7 +243,7 @@ function renderLogin(message = '') {
|
|
|
233
243
|
try {
|
|
234
244
|
await api('entries?limit=1')
|
|
235
245
|
sessionStorage.setItem(TOKEN_KEY, token)
|
|
236
|
-
await navigate(
|
|
246
|
+
await navigate(initialView)
|
|
237
247
|
} catch (requestError) {
|
|
238
248
|
state.token = ''
|
|
239
249
|
error.textContent = requestError.status === 401 ? '令牌无效或已被撤销。' : '连接失败,请检查服务状态后重试。'
|
|
@@ -263,7 +273,7 @@ function signOut() {
|
|
|
263
273
|
releaseNoteAsset()
|
|
264
274
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
265
275
|
Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], candidateTargets: new Map(), settings: { writebackPolicy: 'conservative', updatedAt: '' }, tokens: [] })
|
|
266
|
-
state.notes = { children: new Map(), loadedFolders: new Set(), expandedFolders: new Set(), selectedId: '', selectedNode: null, currentFolderId: null, breadcrumbs: [], content: '', draft: '', dirty: false, assetUrl: '', query: '', searchResults: [], transfer: null, loadingNodeId: '', shares: [], shareError: '' }
|
|
276
|
+
state.notes = { children: new Map(), loadedFolders: new Set(), expandedFolders: new Set(), selectedId: '', selectedNode: null, currentFolderId: null, breadcrumbs: [], content: '', draft: '', dirty: false, assetUrl: '', query: '', searchResults: [], transfer: null, loadingNodeId: '', shares: [], shareError: '', browserOpen: false }
|
|
267
277
|
if (AUTH_MODE === 'same-origin') void boot()
|
|
268
278
|
else renderLogin()
|
|
269
279
|
}
|
|
@@ -333,7 +343,10 @@ function updateLoadingPhase(request, label, progress) {
|
|
|
333
343
|
if (request !== navigationRequest || !state.loading) return
|
|
334
344
|
state.loadingPhase = label
|
|
335
345
|
state.loadingProgress = Math.max(0, Math.min(1, progress))
|
|
336
|
-
|
|
346
|
+
const progressNode = document.querySelector('.route-progress')
|
|
347
|
+
progressNode?.setAttribute('aria-label', label)
|
|
348
|
+
progressNode?.setAttribute('aria-valuenow', String(Math.round(state.loadingProgress * 100)))
|
|
349
|
+
progressNode?.firstElementChild?.style.setProperty('--route-progress', String(state.loadingProgress))
|
|
337
350
|
}
|
|
338
351
|
|
|
339
352
|
async function saveBeforeNavigation() {
|
|
@@ -484,7 +497,7 @@ async function loadNotes(signal, onPhase = () => {}) {
|
|
|
484
497
|
? state.notes.selectedNode
|
|
485
498
|
: await api(`notes/${encodeURIComponent(state.notes.selectedId)}`, { signal }).catch(() => null)
|
|
486
499
|
if (!selected) clearNoteSelection()
|
|
487
|
-
else
|
|
500
|
+
else await selectNoteNode(selected)
|
|
488
501
|
}
|
|
489
502
|
}
|
|
490
503
|
|
|
@@ -937,6 +950,7 @@ async function loadTokens(signal) {
|
|
|
937
950
|
}
|
|
938
951
|
|
|
939
952
|
function renderShell() {
|
|
953
|
+
closeDocumentMenus()
|
|
940
954
|
releaseNoteEditors()
|
|
941
955
|
captureScrollPosition()
|
|
942
956
|
const titles = {
|
|
@@ -960,25 +974,20 @@ function renderShell() {
|
|
|
960
974
|
},
|
|
961
975
|
renderSidebar(),
|
|
962
976
|
renderAppSidebarResizer(),
|
|
963
|
-
|
|
964
|
-
type: 'button', class: 'app-sidebar-scrim',
|
|
977
|
+
element('button', {
|
|
978
|
+
type: 'button', class: 'app-sidebar-scrim', hidden: !state.menuOpen,
|
|
965
979
|
'aria-label': '关闭导航菜单',
|
|
966
|
-
onClick: () =>
|
|
967
|
-
})
|
|
980
|
+
onClick: () => setMenuOpen(false),
|
|
981
|
+
}),
|
|
968
982
|
element('main', { class: 'main' },
|
|
969
983
|
element('header', { class: 'topbar' },
|
|
970
984
|
element('div', { class: 'topbar-title' },
|
|
971
|
-
actionButton('
|
|
985
|
+
actionButton(interfaceIcon('menu'), () => setMenuOpen(!state.menuOpen), 'ghost mobile-menu', {
|
|
972
986
|
'aria-label': state.menuOpen ? '关闭导航菜单' : '打开导航菜单',
|
|
973
987
|
'aria-expanded': String(state.menuOpen),
|
|
974
988
|
}),
|
|
975
989
|
paneToggleButton('main', !state.documentView.sidebarHidden, () => setSidebarHidden(!state.documentView.sidebarHidden), '主导航栏'),
|
|
976
|
-
|
|
977
|
-
const workspace = activeDocumentWorkspace()
|
|
978
|
-
if (!workspace) return
|
|
979
|
-
workspace.view.treeOpen = !workspace.view.treeOpen
|
|
980
|
-
renderShell()
|
|
981
|
-
}, '知识目录') : null,
|
|
990
|
+
renderContextPaneToggle(),
|
|
982
991
|
element('div', { class: 'topbar-heading' },
|
|
983
992
|
element('span', { class: 'topbar-kicker' }, `KNOWLEDGE / ${viewIndexes[state.view] || '00'}`),
|
|
984
993
|
element('h1', {}, title),
|
|
@@ -997,6 +1006,21 @@ function renderShell() {
|
|
|
997
1006
|
restoreScrollPosition(currentScrollState())
|
|
998
1007
|
}
|
|
999
1008
|
|
|
1009
|
+
function renderContextPaneToggle() {
|
|
1010
|
+
if (state.view === 'notes') {
|
|
1011
|
+
return paneToggleButton('library', state.notes.browserOpen, () => {
|
|
1012
|
+
state.notes.browserOpen = !state.notes.browserOpen
|
|
1013
|
+
renderShell()
|
|
1014
|
+
}, '笔记目录')
|
|
1015
|
+
}
|
|
1016
|
+
const workspace = activeDocumentWorkspace()
|
|
1017
|
+
if (!workspace) return null
|
|
1018
|
+
return paneToggleButton('library', workspace.view.treeOpen, () => {
|
|
1019
|
+
workspace.view.treeOpen = !workspace.view.treeOpen
|
|
1020
|
+
renderShell()
|
|
1021
|
+
}, '知识目录')
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1000
1024
|
function renderAppSidebarResizer() {
|
|
1001
1025
|
const minimum = 190
|
|
1002
1026
|
const maximum = 340
|
|
@@ -1019,24 +1043,38 @@ function startSidebarResize(event, minimum, maximum) {
|
|
|
1019
1043
|
if (!shell) return
|
|
1020
1044
|
const startX = event.clientX
|
|
1021
1045
|
const startWidth = state.documentView.sidebarWidth
|
|
1046
|
+
let pendingWidth = startWidth
|
|
1047
|
+
let frame = 0
|
|
1022
1048
|
handle.setPointerCapture?.(event.pointerId)
|
|
1023
1049
|
handle.classList.add('is-dragging')
|
|
1024
1050
|
document.body.classList.add('is-resizing-columns')
|
|
1025
1051
|
const move = moveEvent => {
|
|
1026
|
-
|
|
1027
|
-
|
|
1052
|
+
if (moveEvent.pointerId !== event.pointerId) return
|
|
1053
|
+
pendingWidth = clampNumber(startWidth + moveEvent.clientX - startX, minimum, maximum, startWidth)
|
|
1054
|
+
if (!frame) frame = window.requestAnimationFrame(() => {
|
|
1055
|
+
frame = 0
|
|
1056
|
+
if (shell.isConnected) setSidebarWidth(pendingWidth, shell, handle)
|
|
1057
|
+
})
|
|
1028
1058
|
}
|
|
1029
|
-
const finish =
|
|
1059
|
+
const finish = endEvent => {
|
|
1060
|
+
if (endEvent.pointerId !== event.pointerId) return
|
|
1061
|
+
if (frame) window.cancelAnimationFrame(frame)
|
|
1062
|
+
frame = 0
|
|
1063
|
+
if (endEvent.type === 'pointerup') pendingWidth = clampNumber(startWidth + endEvent.clientX - startX, minimum, maximum, startWidth)
|
|
1064
|
+
if (shell.isConnected) setSidebarWidth(pendingWidth, shell, handle)
|
|
1030
1065
|
handle.classList.remove('is-dragging')
|
|
1031
1066
|
document.body.classList.remove('is-resizing-columns')
|
|
1032
1067
|
handle.removeEventListener('pointermove', move)
|
|
1033
1068
|
handle.removeEventListener('pointerup', finish)
|
|
1034
1069
|
handle.removeEventListener('pointercancel', finish)
|
|
1070
|
+
handle.removeEventListener('lostpointercapture', finish)
|
|
1071
|
+
if (handle.hasPointerCapture?.(event.pointerId)) handle.releasePointerCapture(event.pointerId)
|
|
1035
1072
|
saveDocumentLayout()
|
|
1036
1073
|
}
|
|
1037
1074
|
handle.addEventListener('pointermove', move)
|
|
1038
1075
|
handle.addEventListener('pointerup', finish)
|
|
1039
1076
|
handle.addEventListener('pointercancel', finish)
|
|
1077
|
+
handle.addEventListener('lostpointercapture', finish)
|
|
1040
1078
|
}
|
|
1041
1079
|
|
|
1042
1080
|
function resizeSidebarWithKeyboard(event, minimum, maximum) {
|
|
@@ -1056,6 +1094,18 @@ function setSidebarWidth(width, shell, handle) {
|
|
|
1056
1094
|
handle?.setAttribute('aria-valuenow', String(width))
|
|
1057
1095
|
}
|
|
1058
1096
|
|
|
1097
|
+
function setMenuOpen(open) {
|
|
1098
|
+
state.menuOpen = open
|
|
1099
|
+
const shell = app.querySelector('.app-shell')
|
|
1100
|
+
if (!shell) return
|
|
1101
|
+
shell.dataset.menuOpen = String(open)
|
|
1102
|
+
shell.querySelector('.app-sidebar-scrim')?.toggleAttribute('hidden', !open)
|
|
1103
|
+
const toggle = shell.querySelector('.mobile-menu')
|
|
1104
|
+
toggle?.setAttribute('aria-expanded', String(open))
|
|
1105
|
+
toggle?.setAttribute('aria-label', open ? '关闭导航菜单' : '打开导航菜单')
|
|
1106
|
+
applySidebarVisibility(shell, state.documentView.sidebarHidden)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1059
1109
|
function setSidebarHidden(hidden) {
|
|
1060
1110
|
state.documentView.sidebarHidden = hidden
|
|
1061
1111
|
saveDocumentLayout()
|
|
@@ -1069,7 +1119,7 @@ function applySidebarVisibility(shell, hidden) {
|
|
|
1069
1119
|
const resizer = shell.querySelector(':scope > .app-sidebar-resizer')
|
|
1070
1120
|
const main = shell.querySelector(':scope > .main')
|
|
1071
1121
|
const toggle = shell.querySelector('.pane-toggle-button[data-pane="main"]')
|
|
1072
|
-
const compact = window.matchMedia('(max-width:
|
|
1122
|
+
const compact = window.matchMedia('(max-width: 1120px), (hover: none) and (pointer: coarse) and (max-width: 1400px)').matches
|
|
1073
1123
|
const sidebarUnavailable = compact ? !state.menuOpen : hidden
|
|
1074
1124
|
sidebar?.toggleAttribute('inert', sidebarUnavailable)
|
|
1075
1125
|
resizer?.toggleAttribute('inert', hidden)
|
|
@@ -1722,13 +1772,16 @@ function renderNoteEditor(workspace, editor, base) {
|
|
|
1722
1772
|
element('div', { class: 'note-editor-actions' },
|
|
1723
1773
|
finalized ? badge(DOCUMENT_STATE_LABELS[editor.documentState] || '已结束', 'success') : readOnly ? badge('只读') : null,
|
|
1724
1774
|
element('span', { class: 'editor-save-status', role: 'status' }, editor.saveState),
|
|
1725
|
-
!readOnly && !editor.isNew && documentWorkspaceBases(workspace).some(item => item.id !== editor.knowledgeBaseId)
|
|
1726
|
-
? actionButton('移动到…', () => openMoveKnowledgeDocument(workspace, editor), 'ghost small')
|
|
1727
|
-
: null,
|
|
1728
1775
|
finalized && !readOnly ? actionButton('重新打开', () => reopenDocument(workspace, editor), 'small') : null,
|
|
1729
|
-
!readOnly && !editor.isNew && !finalized ? actionButton('标记结束', () => openFinalizeDocument(workspace, editor), 'small') : null,
|
|
1730
|
-
!readOnly && !editor.isNew && !finalized ? actionButton('删除', () => confirmDeleteDocument(workspace, editor), 'ghost small') : null,
|
|
1731
1776
|
!readOnly && !finalized ? actionButton(editor.isNew ? '创建文档' : '保存', () => { void saveDocumentEditor(workspace).then(saved => { if (saved) renderShell() }) }, 'primary small') : null,
|
|
1777
|
+
!readOnly && !editor.isNew ? renderDocumentMenu(editor.title || '知识文档', [
|
|
1778
|
+
[
|
|
1779
|
+
...(documentWorkspaceBases(workspace).some(item => item.id !== editor.knowledgeBaseId)
|
|
1780
|
+
? [{ label: '移动到…', icon: 'move', run: () => openMoveKnowledgeDocument(workspace, editor) }] : []),
|
|
1781
|
+
...(!finalized ? [{ label: '标记结束', icon: 'check', run: () => openFinalizeDocument(workspace, editor) }] : []),
|
|
1782
|
+
],
|
|
1783
|
+
!finalized ? [{ label: '删除文档', icon: 'trash', danger: true, run: () => confirmDeleteDocument(workspace, editor) }] : [],
|
|
1784
|
+
]) : null,
|
|
1732
1785
|
),
|
|
1733
1786
|
),
|
|
1734
1787
|
element('div', { class: 'note-editor-scroll', 'data-scroll-key': 'note-editor' },
|
|
@@ -1972,6 +2025,7 @@ function renderNotes() {
|
|
|
1972
2025
|
const tree = state.notes.query.trim() ? renderNoteSearchResults() : renderNoteTreeBranch(null)
|
|
1973
2026
|
const workspace = element('section', {
|
|
1974
2027
|
class: 'notes-workspace', 'data-drop-active': 'false',
|
|
2028
|
+
'data-browser-open': String(state.notes.browserOpen),
|
|
1975
2029
|
onDragEnter: noteWorkspaceDragEnter, onDragOver: noteWorkspaceDragEnter,
|
|
1976
2030
|
onDragLeave: noteWorkspaceDragLeave,
|
|
1977
2031
|
onDrop: event => {
|
|
@@ -1981,6 +2035,10 @@ function renderNotes() {
|
|
|
1981
2035
|
void importDroppedNotes(event.dataTransfer, state.notes.currentFolderId)
|
|
1982
2036
|
},
|
|
1983
2037
|
},
|
|
2038
|
+
state.notes.browserOpen ? element('button', {
|
|
2039
|
+
type: 'button', class: 'notes-browser-scrim', 'aria-label': '关闭笔记目录',
|
|
2040
|
+
onClick: () => { state.notes.browserOpen = false; renderShell() },
|
|
2041
|
+
}) : null,
|
|
1984
2042
|
element('aside', { class: 'notes-browser', 'aria-label': '笔记目录' },
|
|
1985
2043
|
element('header', { class: 'notes-browser-header' },
|
|
1986
2044
|
element('div', {}, element('h2', {}, '目录'), element('span', {}, `${rootNodes.length} 个根目录项目`)),
|
|
@@ -2643,86 +2701,34 @@ function renderNoteFile(node) {
|
|
|
2643
2701
|
|
|
2644
2702
|
function renderNoteFileToolbar(node, options = {}) {
|
|
2645
2703
|
return element('header', { class: 'notes-document-toolbar' },
|
|
2646
|
-
element('div', { class: 'notes-toolbar-leading' },
|
|
2647
|
-
renderNoteDocumentBreadcrumb(node),
|
|
2648
|
-
),
|
|
2704
|
+
element('div', { class: 'notes-toolbar-leading' }, renderNoteDocumentBreadcrumb(node)),
|
|
2649
2705
|
element('div', { class: 'notes-document-actions' },
|
|
2650
2706
|
options.editable ? element('span', { class: 'notes-save-state', role: 'status', 'data-note-save-state': '', 'data-dirty': String(state.notes.dirty) }, state.notes.dirty ? '未保存' : '已保存') : null,
|
|
2651
|
-
options.enhanced ? actionButton('查找', () => markdownEditorHandle?.openFind(), 'ghost small', { 'data-note-find': '', 'data-note-mobile-overflow': '', 'aria-keyshortcuts': 'Control+F Meta+F', 'aria-pressed': 'false' }) : null,
|
|
2652
2707
|
options.enhanced ? actionButton([
|
|
2653
|
-
interfaceIcon('outline', 'notes-toolbar-action-icon'),
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
options.editable ? actionButton('历史', () => { void openNoteHistory(node) }, 'ghost small', { 'data-note-history': '', 'data-note-mobile-overflow': '' }) : null,
|
|
2657
|
-
noteDownloadButton(node, 'ghost small', '下载', { 'data-note-mobile-overflow': '' }),
|
|
2658
|
-
actionButton(noteShareForNode(node.id) ? '分享中' : '分享', () => { void openNoteShare(node) }, 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2659
|
-
actionButton('复制引用', () => { void copyNoteReference(node) }, 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2660
|
-
actionButton('重命名', () => openRenameNoteNode(node), 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2661
|
-
options.editable ? actionButton('保存', () => { void saveNoteDocument() }, 'primary small', { 'data-note-save': '', 'data-note-mobile-overflow': '', disabled: !state.notes.dirty }) : null,
|
|
2708
|
+
interfaceIcon('outline', 'notes-toolbar-action-icon'), element('span', {}, '大纲'),
|
|
2709
|
+
], () => markdownEditorHandle?.toggleOutline(), 'ghost small notes-outline-action', { 'data-note-outline': '', 'aria-label': '打开标题大纲', title: '标题大纲', 'aria-controls': 'dsh-note-outline', 'aria-expanded': 'false', 'aria-pressed': 'false' }) : null,
|
|
2710
|
+
options.editable ? actionButton('保存', () => { void saveNoteDocument() }, 'primary small', { 'data-note-save': '', disabled: !state.notes.dirty, 'aria-keyshortcuts': 'Control+S Meta+S' }) : null,
|
|
2662
2711
|
renderNoteFileOverflowMenu(node, options),
|
|
2663
2712
|
),
|
|
2664
2713
|
)
|
|
2665
2714
|
}
|
|
2666
2715
|
|
|
2667
2716
|
function renderNoteFileOverflowMenu(node, options) {
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
interfaceIcon('more', 'notes-document-more-icon'),
|
|
2684
|
-
)
|
|
2685
|
-
details.addEventListener('toggle', () => {
|
|
2686
|
-
summary.setAttribute('aria-expanded', String(details.open))
|
|
2687
|
-
summary.setAttribute('aria-label', `${details.open ? '关闭' : '打开'} ${node.name} 的更多操作`)
|
|
2688
|
-
})
|
|
2689
|
-
const closeThen = action => event => {
|
|
2690
|
-
details.open = false
|
|
2691
|
-
action(event)
|
|
2692
|
-
}
|
|
2693
|
-
const menuItems = []
|
|
2694
|
-
if (options.editable && state.notes.dirty) {
|
|
2695
|
-
menuItems.push(noteMenuAction('保存修改', 'save', closeThen(() => { void saveNoteDocument() })))
|
|
2696
|
-
menuItems.push(noteMenuDivider())
|
|
2697
|
-
}
|
|
2698
|
-
if (options.enhanced) {
|
|
2699
|
-
menuItems.push(noteMenuAction('查找', 'search', closeThen(() => markdownEditorHandle?.openFind())))
|
|
2700
|
-
}
|
|
2701
|
-
if (options.editable) menuItems.push(noteMenuAction('页面历史', 'history', closeThen(() => { void openNoteHistory(node) })))
|
|
2702
|
-
if (options.enhanced || options.editable) menuItems.push(noteMenuDivider())
|
|
2703
|
-
menuItems.push(
|
|
2704
|
-
noteMenuAction('下载', 'download', closeThen(event => { void downloadNoteFile(node, event.currentTarget) })),
|
|
2705
|
-
noteMenuAction(noteShareForNode(node.id) ? '管理分享' : '创建分享', 'link', closeThen(() => { void openNoteShare(node) })),
|
|
2706
|
-
noteMenuAction('复制引用', 'link', closeThen(() => { void copyNoteReference(node) })),
|
|
2707
|
-
noteMenuAction('重命名', 'rename', closeThen(() => openRenameNoteNode(node))),
|
|
2708
|
-
noteMenuDivider(),
|
|
2709
|
-
noteMenuAction('查看已分享', 'outline', closeThen(() => { void navigate('shares') })),
|
|
2710
|
-
)
|
|
2711
|
-
const menu = element('div', { class: 'notes-document-more-menu', role: 'menu', 'aria-label': `${node.name} 的更多操作` }, menuItems)
|
|
2712
|
-
summary.setAttribute('aria-expanded', 'false')
|
|
2713
|
-
details.append(summary, menu)
|
|
2714
|
-
return details
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
|
-
function noteMenuAction(label, icon, onClick, attributes = {}) {
|
|
2718
|
-
return actionButton([
|
|
2719
|
-
interfaceIcon(icon, 'notes-document-menu-icon'),
|
|
2720
|
-
element('span', { class: 'notes-document-menu-label' }, label),
|
|
2721
|
-
], onClick, 'ghost small notes-document-menu-item', { role: 'menuitem', ...attributes })
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
function noteMenuDivider() {
|
|
2725
|
-
return element('div', { class: 'notes-document-menu-divider', role: 'separator' })
|
|
2717
|
+
return renderDocumentMenu(node.name, [
|
|
2718
|
+
[
|
|
2719
|
+
...(options.enhanced ? [{ label: '查找', icon: 'search', run: () => markdownEditorHandle?.openFind(), attributes: { 'data-note-find': '', 'aria-keyshortcuts': 'Control+F Meta+F' } }] : []),
|
|
2720
|
+
...(options.editable ? [{ label: '页面历史', icon: 'history', run: () => { void openNoteHistory(node) }, attributes: { 'data-note-history': '' } }] : []),
|
|
2721
|
+
],
|
|
2722
|
+
[
|
|
2723
|
+
{ label: '下载', icon: 'download', run: event => { void downloadNoteFile(node, event.currentTarget) } },
|
|
2724
|
+
{ label: noteShareForNode(node.id) ? '管理分享' : '创建分享', icon: 'link', run: () => { void openNoteShare(node) } },
|
|
2725
|
+
{ label: '复制引用', icon: 'link', run: () => { void copyNoteReference(node) } },
|
|
2726
|
+
],
|
|
2727
|
+
[
|
|
2728
|
+
{ label: '重命名', icon: 'rename', run: () => openRenameNoteNode(node) },
|
|
2729
|
+
{ label: '查看已分享', icon: 'outline', run: () => { void navigate('shares') } },
|
|
2730
|
+
],
|
|
2731
|
+
])
|
|
2726
2732
|
}
|
|
2727
2733
|
|
|
2728
2734
|
function renderNoteFileStatusbar(node) {
|
|
@@ -2767,6 +2773,7 @@ async function selectNoteNode(node, options = {}) {
|
|
|
2767
2773
|
releaseNoteAsset()
|
|
2768
2774
|
state.notes.selectedId = node.id
|
|
2769
2775
|
state.notes.selectedNode = node
|
|
2776
|
+
state.notes.browserOpen = false
|
|
2770
2777
|
Object.assign(state.notes, { content: '', draft: '', dirty: false })
|
|
2771
2778
|
state.notes.loadingNodeId = node.id
|
|
2772
2779
|
renderShell()
|
|
@@ -4184,19 +4191,12 @@ function selectField(label, options, value) {
|
|
|
4184
4191
|
}
|
|
4185
4192
|
|
|
4186
4193
|
async function loadModelCatalog() {
|
|
4187
|
-
if (state.modelCatalog !== null) return state.modelCatalog
|
|
4188
4194
|
try {
|
|
4189
|
-
|
|
4190
|
-
headers: { accept: 'application/json', 'x-dsh-knowledge-client': 'management-web' },
|
|
4191
|
-
})
|
|
4192
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
4193
|
-
const payload = await response.json()
|
|
4194
|
-
state.modelCatalog = Array.isArray(payload.providers) ? payload.providers : []
|
|
4195
|
+
state.modelCatalog = await readModelCatalog()
|
|
4195
4196
|
} catch (error) {
|
|
4196
|
-
|
|
4197
|
-
showToast(`无法读取当前 DSH 的模型目录:${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
4197
|
+
showToast(`无法读取当前 DSH 的模型目录:${error.message},重新打开可重试。`, 'error')
|
|
4198
4198
|
}
|
|
4199
|
-
return state.modelCatalog
|
|
4199
|
+
return state.modelCatalog || []
|
|
4200
4200
|
}
|
|
4201
4201
|
|
|
4202
4202
|
function modelRouteFields(currentProvider, currentModel) {
|
|
@@ -4416,86 +4416,6 @@ function confirmDeleteToken(token) {
|
|
|
4416
4416
|
} })
|
|
4417
4417
|
}
|
|
4418
4418
|
|
|
4419
|
-
function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
|
|
4420
|
-
return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
|
|
4421
|
-
}
|
|
4422
|
-
|
|
4423
|
-
function openSheet(options) {
|
|
4424
|
-
return openModal({ ...options, presentation: 'sheet' })
|
|
4425
|
-
}
|
|
4426
|
-
|
|
4427
|
-
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal', className = '', onClose }) {
|
|
4428
|
-
const previouslyFocused = document.activeElement
|
|
4429
|
-
const isSheet = presentation === 'sheet'
|
|
4430
|
-
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
4431
|
-
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'} ${className}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dialog-title' })
|
|
4432
|
-
let busy = false
|
|
4433
|
-
let formDirty = false
|
|
4434
|
-
let closed = false
|
|
4435
|
-
const close = (explicit = false) => {
|
|
4436
|
-
if (busy || closed) return
|
|
4437
|
-
if (!explicit && formDirty) {
|
|
4438
|
-
showToast('表单有未保存的修改,请先保存,或使用“取消”放弃修改。', 'error')
|
|
4439
|
-
return
|
|
4440
|
-
}
|
|
4441
|
-
document.removeEventListener('keydown', onKeyDown)
|
|
4442
|
-
closed = true
|
|
4443
|
-
backdrop.remove()
|
|
4444
|
-
onClose?.()
|
|
4445
|
-
if (previouslyFocused instanceof HTMLElement) previouslyFocused.focus()
|
|
4446
|
-
}
|
|
4447
|
-
const closeButton = actionButton('×', () => close(), 'ghost', { 'aria-label': '关闭对话框' })
|
|
4448
|
-
const cancel = actionButton(cancelLabel, () => close(true))
|
|
4449
|
-
const primary = primaryLabel ? actionButton(primaryLabel, async () => {
|
|
4450
|
-
if (busy) return
|
|
4451
|
-
busy = true
|
|
4452
|
-
primary.disabled = true
|
|
4453
|
-
cancel.disabled = true
|
|
4454
|
-
const original = primary.textContent
|
|
4455
|
-
primary.textContent = '正在处理…'
|
|
4456
|
-
try {
|
|
4457
|
-
const shouldClose = await onPrimary()
|
|
4458
|
-
busy = false
|
|
4459
|
-
if (shouldClose !== false) close(true)
|
|
4460
|
-
else { primary.disabled = false; cancel.disabled = false; primary.textContent = original }
|
|
4461
|
-
} catch (error) {
|
|
4462
|
-
busy = false
|
|
4463
|
-
primary.disabled = false
|
|
4464
|
-
cancel.disabled = false
|
|
4465
|
-
primary.textContent = original
|
|
4466
|
-
showToast(friendlyError(error), 'error')
|
|
4467
|
-
}
|
|
4468
|
-
}, primaryVariant) : null
|
|
4469
|
-
const onKeyDown = (event) => {
|
|
4470
|
-
if (event.key === 'Escape') { event.preventDefault(); close() }
|
|
4471
|
-
if (event.key === 'Tab') trapFocus(event, dialog)
|
|
4472
|
-
}
|
|
4473
|
-
dialog.append(
|
|
4474
|
-
element('header', { class: 'dialog-header' }, element('div', {}, element('h2', { id: 'dialog-title' }, title), description ? element('p', {}, description) : null), closeButton),
|
|
4475
|
-
element('div', { class: 'dialog-body' }, body),
|
|
4476
|
-
element('footer', { class: 'dialog-footer' }, cancel, primary),
|
|
4477
|
-
)
|
|
4478
|
-
backdrop.append(dialog)
|
|
4479
|
-
backdrop.addEventListener('mousedown', (event) => { if (event.target === backdrop) close() })
|
|
4480
|
-
if (body.matches?.('form') || body.querySelector?.('form')) {
|
|
4481
|
-
body.addEventListener('input', () => { formDirty = true })
|
|
4482
|
-
body.addEventListener('change', () => { formDirty = true })
|
|
4483
|
-
}
|
|
4484
|
-
document.body.append(backdrop)
|
|
4485
|
-
document.addEventListener('keydown', onKeyDown)
|
|
4486
|
-
window.setTimeout(() => (dialog.querySelector('input, textarea, select, button') || dialog).focus(), 0)
|
|
4487
|
-
return { close, dialog }
|
|
4488
|
-
}
|
|
4489
|
-
|
|
4490
|
-
function trapFocus(event, container) {
|
|
4491
|
-
const focusable = [...container.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])')]
|
|
4492
|
-
if (!focusable.length) return
|
|
4493
|
-
const first = focusable[0]
|
|
4494
|
-
const last = focusable.at(-1)
|
|
4495
|
-
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
|
|
4496
|
-
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
|
|
4497
|
-
}
|
|
4498
|
-
|
|
4499
4419
|
function formatDate(value) {
|
|
4500
4420
|
try { return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value)) } catch { return value }
|
|
4501
4421
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--font-ui:ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei UI", Arial, sans-serif;--font-reading:var(--font-ui);--font-mono:"SFMono-Regular", "Cascadia Code", Menlo, Consolas, monospace;--bg:#ebebeb;--surface:#f4f4f4;--surface-raised:#fafafa;--surface-soft:#eeeeef;--surface-hover:rgb(118 118 128 / 9%);--dialog-surface:#f4f4f4;--menu-surface:rgb(232 234 236 / 98%);--menu-surface-solid:#e8eaec;--document-surface:rgb(218 222 226 / 32%);--selection-background:#b8c8d6;--selection-text:#17212b;--activity-surface:rgba(255, 255, 255, 0.14);--text:#1d1d1f;--text-secondary:#515154;--text-tertiary:#6e6e73;--border:rgb(60 60 67 / 14%);--border-strong:rgb(60 60 67 / 24%);--accent:#3a3a3c;--accent-hover:#1d1d1f;--accent-soft:#e2e2e5;--on-accent:#ffffff;--success:#248a3d;--success-soft:#e8f5eb;--warning:#c93400;--warning-soft:#fff1e8;--danger:#d70015;--danger-soft:#ffebed;--shadow:0 24px 64px rgb(0 0 0 / 20%);--knowledge-embedded-surface:rgba(255, 255, 255, 0.08);--knowledge-embedded-filter:saturate(.65) contrast(1.015) blur(24px);--knowledge-embedded-control:rgba(255, 255, 255, 0.27);}@media(prefers-color-scheme:dark){:root:not([data-color-scheme]){--bg:#101719;--surface:#182022;--surface-raised:#20292b;--surface-soft:rgb(184 204 205 / 6%);--surface-hover:rgb(105 182 186 / 9%);--dialog-surface:#20292b;--menu-surface:rgb(29 38 40 / 98%);--menu-surface-solid:#1d2628;--document-surface:rgb(130 138 146 / 6%);--selection-background:#455b68;--selection-text:#ffffff;--activity-surface:rgb(29 36 38 / 24%);--text:#e3eaeb;--text-secondary:#bbc9cc;--text-tertiary:#95a7ab;--border:rgb(184 204 205 / 10%);--border-strong:rgb(184 204 205 / 18%);--accent:#69b6ba;--accent-hover:#8aced0;--accent-soft:rgb(105 182 186 / 13%);--on-accent:#101819;--success:#30d158;--success-soft:rgb(48 209 88 / 13%);--warning:#ff9f0a;--warning-soft:rgb(255 159 10 / 13%);--danger:#ff453a;--danger-soft:rgb(255 69 58 / 14%);--shadow:0 28px 72px rgb(0 0 0 / 42%);--knowledge-embedded-surface:rgb(16 23 25 / 18%);--knowledge-embedded-filter:saturate(.55) contrast(1.02) blur(24px);--knowledge-embedded-control:rgb(39 50 53 / 72%);}}:root[data-color-scheme="dark"]{--bg:#101719;--surface:#182022;--surface-raised:#20292b;--surface-soft:rgb(184 204 205 / 6%);--surface-hover:rgb(105 182 186 / 9%);--dialog-surface:#20292b;--menu-surface:rgb(29 38 40 / 98%);--menu-surface-solid:#1d2628;--document-surface:rgb(130 138 146 / 6%);--selection-background:#455b68;--selection-text:#ffffff;--activity-surface:rgb(29 36 38 / 24%);--text:#e3eaeb;--text-secondary:#bbc9cc;--text-tertiary:#95a7ab;--border:rgb(184 204 205 / 10%);--border-strong:rgb(184 204 205 / 18%);--accent:#69b6ba;--accent-hover:#8aced0;--accent-soft:rgb(105 182 186 / 13%);--on-accent:#101819;--success:#30d158;--success-soft:rgb(48 209 88 / 13%);--warning:#ff9f0a;--warning-soft:rgb(255 159 10 / 13%);--danger:#ff453a;--danger-soft:rgb(255 69 58 / 14%);--shadow:0 28px 72px rgb(0 0 0 / 42%);--knowledge-embedded-surface:rgb(16 23 25 / 18%);--knowledge-embedded-filter:saturate(.55) contrast(1.02) blur(24px);--knowledge-embedded-control:rgb(39 50 53 / 72%);}
|
package/web/dialogs.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Shared modal lifecycle, keyboard handling and dirty-form protection. */
|
|
2
|
+
export function createDialogPresenter({ element, actionButton, interfaceIcon, showToast, friendlyError }) {
|
|
3
|
+
let sequence = 0
|
|
4
|
+
const stack = []
|
|
5
|
+
function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
|
|
6
|
+
return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function openSheet(options) {
|
|
10
|
+
return openModal({ ...options, presentation: 'sheet' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal', className = '', onClose }) {
|
|
14
|
+
const previouslyFocused = document.activeElement
|
|
15
|
+
const isSheet = presentation === 'sheet'
|
|
16
|
+
const titleId = `knowledge-dialog-title-${++sequence}`
|
|
17
|
+
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
18
|
+
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'} ${className}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': titleId, tabindex: '-1' })
|
|
19
|
+
let busy = false
|
|
20
|
+
let formDirty = false
|
|
21
|
+
let closed = false
|
|
22
|
+
const close = (explicit = false) => {
|
|
23
|
+
if (busy || closed) return
|
|
24
|
+
if (!explicit && formDirty) {
|
|
25
|
+
showToast('表单有未保存的修改,请先保存,或使用“取消”放弃修改。', 'error')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
29
|
+
closed = true
|
|
30
|
+
stack.splice(stack.indexOf(dialog), 1)
|
|
31
|
+
backdrop.remove()
|
|
32
|
+
onClose?.()
|
|
33
|
+
if (previouslyFocused instanceof HTMLElement) previouslyFocused.focus()
|
|
34
|
+
}
|
|
35
|
+
const closeButton = actionButton(interfaceIcon('close'), () => close(), 'ghost', { 'aria-label': '关闭对话框' })
|
|
36
|
+
const cancel = actionButton(cancelLabel, () => close(true))
|
|
37
|
+
const primary = primaryLabel ? actionButton(primaryLabel, async () => {
|
|
38
|
+
if (busy) return
|
|
39
|
+
busy = true
|
|
40
|
+
primary.disabled = true
|
|
41
|
+
cancel.disabled = true
|
|
42
|
+
const original = primary.textContent
|
|
43
|
+
primary.textContent = '正在处理…'
|
|
44
|
+
try {
|
|
45
|
+
const shouldClose = await onPrimary()
|
|
46
|
+
busy = false
|
|
47
|
+
if (shouldClose !== false) close(true)
|
|
48
|
+
else { primary.disabled = false; cancel.disabled = false; primary.textContent = original }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
busy = false
|
|
51
|
+
primary.disabled = false
|
|
52
|
+
cancel.disabled = false
|
|
53
|
+
primary.textContent = original
|
|
54
|
+
showToast(friendlyError(error), 'error')
|
|
55
|
+
}
|
|
56
|
+
}, primaryVariant) : null
|
|
57
|
+
const onKeyDown = (event) => {
|
|
58
|
+
if (stack.at(-1) !== dialog || event.defaultPrevented) return
|
|
59
|
+
if (event.key === 'Escape') { event.preventDefault(); close() }
|
|
60
|
+
if (event.key === 'Tab') trapFocus(event, dialog)
|
|
61
|
+
}
|
|
62
|
+
dialog.append(
|
|
63
|
+
element('header', { class: 'dialog-header' }, element('div', {}, element('h2', { id: titleId }, title), description ? element('p', {}, description) : null), closeButton),
|
|
64
|
+
element('div', { class: 'dialog-body' }, body),
|
|
65
|
+
element('footer', { class: 'dialog-footer' }, cancel, primary),
|
|
66
|
+
)
|
|
67
|
+
backdrop.append(dialog)
|
|
68
|
+
backdrop.addEventListener('mousedown', (event) => { if (event.target === backdrop) close() })
|
|
69
|
+
if (body.matches?.('form') || body.querySelector?.('form')) {
|
|
70
|
+
body.addEventListener('input', () => { formDirty = true })
|
|
71
|
+
body.addEventListener('change', () => { formDirty = true })
|
|
72
|
+
}
|
|
73
|
+
document.body.append(backdrop)
|
|
74
|
+
stack.push(dialog)
|
|
75
|
+
document.addEventListener('keydown', onKeyDown)
|
|
76
|
+
window.setTimeout(() => { if (!closed && stack.at(-1) === dialog) (focusableElements(dialog)[0] || dialog).focus() }, 0)
|
|
77
|
+
return { close, dialog }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function trapFocus(event, container) {
|
|
81
|
+
const focusable = focusableElements(container)
|
|
82
|
+
if (!focusable.length) return
|
|
83
|
+
const first = focusable[0]
|
|
84
|
+
const last = focusable.at(-1)
|
|
85
|
+
if (event.shiftKey && (document.activeElement === first || !container.contains(document.activeElement))) { event.preventDefault(); last.focus() }
|
|
86
|
+
else if (!event.shiftKey && (document.activeElement === last || !container.contains(document.activeElement))) { event.preventDefault(); first.focus() }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function focusableElements(container) {
|
|
90
|
+
return [...container.querySelectorAll('button, input, select, textarea, a[href], [tabindex]')]
|
|
91
|
+
.filter(node => !node.disabled && node.tabIndex >= 0 && !node.closest('[hidden], [inert], [aria-hidden="true"]') && node.getClientRects().length > 0)
|
|
92
|
+
}
|
|
93
|
+
return { openConfirm, openSheet, openModal }
|
|
94
|
+
}
|