jvs-ppt-ui 0.0.16 → 0.0.18
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/dist/lib/jvs-ppt-ui.es.js +24043 -21814
- package/dist/lib/jvs-ppt-ui.umd.js +100 -100
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/components/CanvasArea.vue +9739 -9566
- package/src/components/SidebarRight.vue +10 -6
- package/src/components/TemplateGallery.vue +437 -9
- package/src/components/canvas/elements/CanvasGroupElement.vue +214 -213
- package/src/components/canvas/elements/CanvasShapeElement.vue +894 -882
- package/src/components/material/CreateMaterialPanel.vue +2229 -0
- package/src/components/material/MyAssetsPanel.vue +1548 -0
- package/src/components/material/useDamAuth.ts +239 -0
- package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +1 -1
- package/src/composables/canvas/useCanvasText.ts +44 -18
- package/src/composables/sidebar/useSidebarTextFormat.ts +72 -1
- package/src/composables/useSlides.ts +9804 -9627
- package/src/utils/canvas/elementStyles.ts +256 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { useSlides } from '../../composables/useSlides'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 读取指定名称的 Cookie
|
|
6
|
+
*/
|
|
7
|
+
export function getCookieValue(cname: string): string {
|
|
8
|
+
if (typeof document === 'undefined' || !document.cookie) return ''
|
|
9
|
+
const name = cname + '='
|
|
10
|
+
const parts = document.cookie.split(';')
|
|
11
|
+
for (let i = 0; i < parts.length; i++) {
|
|
12
|
+
const item = parts[i]?.trim()
|
|
13
|
+
if (item && item.indexOf(name) === 0) {
|
|
14
|
+
try {
|
|
15
|
+
return decodeURIComponent(item.substring(name.length))
|
|
16
|
+
} catch {
|
|
17
|
+
return item.substring(name.length)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从 Storage 中读取指定 key(兼容 jvs- 前缀包装格式与原始字符串格式)
|
|
26
|
+
*/
|
|
27
|
+
function readStorageKey(storage: Storage | undefined, key: string): string {
|
|
28
|
+
if (!storage) return ''
|
|
29
|
+
try {
|
|
30
|
+
const raw = storage.getItem(key)
|
|
31
|
+
if (!raw) return ''
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw)
|
|
34
|
+
if (parsed && typeof parsed === 'object' && 'content' in parsed) {
|
|
35
|
+
return typeof parsed.content === 'string' ? parsed.content.trim() : ''
|
|
36
|
+
}
|
|
37
|
+
if (typeof parsed === 'string') return parsed.trim()
|
|
38
|
+
} catch {
|
|
39
|
+
return raw.trim()
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return ''
|
|
43
|
+
}
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 提取 jvs_session_v3 登录凭证
|
|
49
|
+
* 判定顺序:
|
|
50
|
+
* 1. document.cookie 中的 jvs_session_v3
|
|
51
|
+
* 2. localStorage / sessionStorage 中的 jvs_session_v3、jvs-jvs_session_v3、jvs-sessionV3、sessionV3
|
|
52
|
+
* 3. materialHeaders 中显式传入的 jvs_session_v3
|
|
53
|
+
*/
|
|
54
|
+
export function resolveJvsSessionV3(extraHeaders?: Record<string, string>): string {
|
|
55
|
+
if (typeof window === 'undefined') return ''
|
|
56
|
+
|
|
57
|
+
// 1. Cookie: jvs_session_v3
|
|
58
|
+
const fromCookie = getCookieValue('jvs_session_v3')
|
|
59
|
+
if (fromCookie && fromCookie.trim()) {
|
|
60
|
+
return fromCookie.trim()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. LocalStorage / SessionStorage
|
|
64
|
+
const candidateKeys = ['jvs_session_v3', 'jvs-jvs_session_v3', 'jvs-sessionV3', 'sessionV3']
|
|
65
|
+
for (const key of candidateKeys) {
|
|
66
|
+
const localVal = readStorageKey(window.localStorage, key)
|
|
67
|
+
if (localVal) return localVal
|
|
68
|
+
const sessionVal = readStorageKey(window.sessionStorage, key)
|
|
69
|
+
if (sessionVal) return sessionVal
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. 外部注入的 Headers
|
|
73
|
+
if (extraHeaders) {
|
|
74
|
+
const headerVal = extraHeaders['jvs_session_v3'] || extraHeaders['JVS_SESSION_V3']
|
|
75
|
+
if (typeof headerVal === 'string' && headerVal.trim()) {
|
|
76
|
+
return headerVal.trim()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return ''
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 提取 DAM 服务基础路径(默认 /jvs-dam)
|
|
85
|
+
*/
|
|
86
|
+
export function resolveDamBaseUrl(configuredUrl?: string): string {
|
|
87
|
+
if (configuredUrl && configuredUrl.trim()) {
|
|
88
|
+
const clean = configuredUrl.trim()
|
|
89
|
+
const damIdx = clean.indexOf('/jvs-dam')
|
|
90
|
+
if (damIdx >= 0) {
|
|
91
|
+
return clean.slice(0, damIdx + '/jvs-dam'.length)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return '/jvs-dam'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type DamMainTab = 'search' | 'mine' | 'create'
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 跨组件共享的素材库顶层 Tab 状态(支持画布占位符「AI生成」直接跳转至「创建素材」)
|
|
101
|
+
*/
|
|
102
|
+
const sharedMaterialMainTab = ref<DamMainTab>('search')
|
|
103
|
+
|
|
104
|
+
export function setSharedMaterialMainTab(tab: DamMainTab) {
|
|
105
|
+
sharedMaterialMainTab.value = tab
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* DAM 登录态与通用请求工具 Composable
|
|
110
|
+
*/
|
|
111
|
+
export function useDamAuth() {
|
|
112
|
+
const { materialApiUrl, materialHeaders } = useSlides()
|
|
113
|
+
const sessionTick = ref(0)
|
|
114
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
115
|
+
|
|
116
|
+
const refreshSessionState = () => {
|
|
117
|
+
sessionTick.value++
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
onMounted(() => {
|
|
121
|
+
window.addEventListener('storage', refreshSessionState)
|
|
122
|
+
window.addEventListener('focus', refreshSessionState)
|
|
123
|
+
// 定期轻量同步 Cookie 中 jvs_session_v3 的变化
|
|
124
|
+
pollTimer = setInterval(refreshSessionState, 2000)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
onUnmounted(() => {
|
|
128
|
+
window.removeEventListener('storage', refreshSessionState)
|
|
129
|
+
window.removeEventListener('focus', refreshSessionState)
|
|
130
|
+
if (pollTimer) {
|
|
131
|
+
clearInterval(pollTimer)
|
|
132
|
+
pollTimer = null
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const sessionV3 = computed(() => {
|
|
137
|
+
// 依赖 sessionTick 以感知 cookie 变化
|
|
138
|
+
void sessionTick.value
|
|
139
|
+
return resolveJvsSessionV3(materialHeaders.value)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
/** 是否已登录(仅当 jvs_session_v3 存在有效内容时为 true) */
|
|
143
|
+
const isDamLoggedIn = computed(() => Boolean(sessionV3.value))
|
|
144
|
+
|
|
145
|
+
const damBaseUrl = computed(() => resolveDamBaseUrl(materialApiUrl.value))
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 构造携带鉴权信息的 Headers
|
|
149
|
+
*/
|
|
150
|
+
function buildDamHeaders(isJson = true): Record<string, string> {
|
|
151
|
+
const headers: Record<string, string> = {
|
|
152
|
+
'jvs-rule-ua': encodeURI('2.4'),
|
|
153
|
+
...(materialHeaders.value || {})
|
|
154
|
+
}
|
|
155
|
+
if (isJson) {
|
|
156
|
+
headers['Content-Type'] = 'application/json;charset=utf-8'
|
|
157
|
+
}
|
|
158
|
+
const token = sessionV3.value || readStorageKey(window?.localStorage, 'jvs-access_token')
|
|
159
|
+
if (token && !headers['Authorization'] && !headers['authorization']) {
|
|
160
|
+
headers['Authorization'] = `Bearer ${token}`
|
|
161
|
+
}
|
|
162
|
+
const tenantId = readStorageKey(window?.localStorage, 'jvs-tenantId')
|
|
163
|
+
if (tenantId && !headers['tenant-id']) {
|
|
164
|
+
headers['tenant-id'] = tenantId
|
|
165
|
+
}
|
|
166
|
+
return headers
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 统一安全的 JSON 请求封装
|
|
171
|
+
*/
|
|
172
|
+
async function damFetchJson<T = any>(
|
|
173
|
+
pathOrUrl: string,
|
|
174
|
+
options: RequestInit & { params?: Record<string, any> } = {}
|
|
175
|
+
): Promise<T> {
|
|
176
|
+
const base = damBaseUrl.value
|
|
177
|
+
let fullUrl = pathOrUrl.startsWith('http') || pathOrUrl.startsWith('/jvs-')
|
|
178
|
+
? pathOrUrl
|
|
179
|
+
: `${base}${pathOrUrl.startsWith('/') ? '' : '/'}${pathOrUrl}`
|
|
180
|
+
|
|
181
|
+
if (options.params) {
|
|
182
|
+
const searchParams = new URLSearchParams()
|
|
183
|
+
Object.entries(options.params).forEach(([k, v]) => {
|
|
184
|
+
if (v !== undefined && v !== null && v !== '') {
|
|
185
|
+
if (Array.isArray(v)) {
|
|
186
|
+
if (v.length > 0) searchParams.append(k, v.join(','))
|
|
187
|
+
} else {
|
|
188
|
+
searchParams.append(k, String(v))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
const qs = searchParams.toString()
|
|
193
|
+
if (qs) {
|
|
194
|
+
fullUrl += (fullUrl.includes('?') ? '&' : '?') + qs
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const isFormData = typeof FormData !== 'undefined' && options.body instanceof FormData
|
|
199
|
+
const mergedHeaders = {
|
|
200
|
+
...buildDamHeaders(!isFormData),
|
|
201
|
+
...((options.headers as Record<string, string>) || {})
|
|
202
|
+
}
|
|
203
|
+
if (isFormData) {
|
|
204
|
+
delete mergedHeaders['Content-Type']
|
|
205
|
+
delete mergedHeaders['content-type']
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const response = await fetch(fullUrl, {
|
|
209
|
+
...options,
|
|
210
|
+
credentials: 'include',
|
|
211
|
+
headers: mergedHeaders
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
const rawText = await response.text()
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
throw new Error(`请求异常 (${response.status})`)
|
|
217
|
+
}
|
|
218
|
+
const trimmed = rawText.trim()
|
|
219
|
+
if (trimmed.startsWith('<') || trimmed.toLowerCase().startsWith('<!doctype')) {
|
|
220
|
+
throw new Error('接口返回了非 JSON 格式响应')
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(rawText) as T
|
|
224
|
+
} catch (err: any) {
|
|
225
|
+
throw new Error(`数据解析失败: ${err.message}`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
sessionV3,
|
|
231
|
+
isDamLoggedIn,
|
|
232
|
+
damBaseUrl,
|
|
233
|
+
sharedMaterialMainTab,
|
|
234
|
+
setSharedMaterialMainTab,
|
|
235
|
+
refreshSessionState,
|
|
236
|
+
buildDamHeaders,
|
|
237
|
+
damFetchJson
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -298,7 +298,7 @@ function onFontSizeEnter(e: KeyboardEvent) {
|
|
|
298
298
|
function onFontSizeBlur(e: FocusEvent) {
|
|
299
299
|
const target = e.target as HTMLInputElement
|
|
300
300
|
const raw = target.value.trim()
|
|
301
|
-
if (raw) {
|
|
301
|
+
if (raw && raw !== fontSizeClean.value) {
|
|
302
302
|
emit('changeFontSize', raw)
|
|
303
303
|
}
|
|
304
304
|
}
|
|
@@ -89,9 +89,8 @@ export function useCanvasText(options: TextOptions) {
|
|
|
89
89
|
|
|
90
90
|
if (textNodes.length === 0) return null
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
let minCost = Infinity
|
|
92
|
+
const charItems: { node: Text; index: number; rect: DOMRect; rowTop: number; rowBottom: number }[] = []
|
|
93
|
+
const rows: { top: number; bottom: number; items: typeof charItems }[] = []
|
|
95
94
|
|
|
96
95
|
for (const node of textNodes) {
|
|
97
96
|
const len = node.nodeValue?.length || 0
|
|
@@ -104,24 +103,51 @@ export function useCanvasText(options: TextOptions) {
|
|
|
104
103
|
const charRect = charRange.getBoundingClientRect()
|
|
105
104
|
if (charRect.width === 0 && charRect.height === 0) continue
|
|
106
105
|
|
|
107
|
-
const
|
|
108
|
-
|
|
106
|
+
const item = { node, index: i, rect: charRect, rowTop: charRect.top, rowBottom: charRect.bottom }
|
|
107
|
+
charItems.push(item)
|
|
109
108
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
// 将同一视觉行(垂直方向重叠)的字符归并,共享整行的上下边界,防止同行小字号字符因字高较矮被误判为不在当前行
|
|
110
|
+
let matchedRow = rows.find(r => Math.min(r.bottom, charRect.bottom) > Math.max(r.top, charRect.top) - 2)
|
|
111
|
+
if (matchedRow) {
|
|
112
|
+
matchedRow.top = Math.min(matchedRow.top, charRect.top)
|
|
113
|
+
matchedRow.bottom = Math.max(matchedRow.bottom, charRect.bottom)
|
|
114
|
+
matchedRow.items.push(item)
|
|
115
|
+
} else {
|
|
116
|
+
rows.push({ top: charRect.top, bottom: charRect.bottom, items: [item] })
|
|
116
117
|
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
for (const r of rows) {
|
|
122
|
+
for (const item of r.items) {
|
|
123
|
+
item.rowTop = r.top
|
|
124
|
+
item.rowBottom = r.bottom
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let bestNode: Text | null = null
|
|
129
|
+
let bestOffset = 0
|
|
130
|
+
let minCost = Infinity
|
|
131
|
+
|
|
132
|
+
for (const item of charItems) {
|
|
133
|
+
const { node, index: i, rect: charRect, rowTop, rowBottom } = item
|
|
134
|
+
const inRow = y >= rowTop && y <= rowBottom
|
|
135
|
+
const dy = inRow ? 0 : Math.min(Math.abs(y - rowTop), Math.abs(y - rowBottom))
|
|
136
|
+
|
|
137
|
+
// 候选点 1:字符左边缘(光标置于 i);在两字符交界处优先保留前一字符右边缘(i + 1),使光标落点归属前一字符
|
|
138
|
+
const costLeft = (dy * 200) + Math.abs(x - charRect.left)
|
|
139
|
+
if (costLeft < minCost - 0.5) {
|
|
140
|
+
minCost = costLeft
|
|
141
|
+
bestNode = node
|
|
142
|
+
bestOffset = i
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 候选点 2:字符右边缘(光标置于 i + 1)
|
|
146
|
+
const costRight = (dy * 200) + Math.abs(x - charRect.right)
|
|
147
|
+
if (costRight <= minCost + 0.5) {
|
|
148
|
+
if (costRight < minCost) minCost = costRight
|
|
149
|
+
bestNode = node
|
|
150
|
+
bestOffset = i + 1
|
|
125
151
|
}
|
|
126
152
|
}
|
|
127
153
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../../utils/listIndent'
|
|
17
17
|
import { parseColor, hexToRgba } from './useSidebarShapeFormat'
|
|
18
18
|
import { getFormatFromElement, getFormatFromSelection } from '../../utils/canvas/formatPainter'
|
|
19
|
+
import { resolveEffectiveElementFontSize } from '../../utils/canvas/elementStyles'
|
|
19
20
|
import { isEllipseShape } from '../../utils/line'
|
|
20
21
|
import { defaultStyleRecipe } from '../presetThemes'
|
|
21
22
|
import {
|
|
@@ -66,8 +67,23 @@ export function useSidebarTextFormat(
|
|
|
66
67
|
}
|
|
67
68
|
})
|
|
68
69
|
|
|
70
|
+
let savedSidebarRange: Range | null = null
|
|
71
|
+
|
|
69
72
|
const styleFontSize = computed({
|
|
70
|
-
get() {
|
|
73
|
+
get() {
|
|
74
|
+
selectionTrigger.value
|
|
75
|
+
const el = selectedElement.value as any
|
|
76
|
+
if (el) {
|
|
77
|
+
const container = typeof document !== 'undefined' ? document.getElementById(`editable-${el.id}`) : null
|
|
78
|
+
const isEditing = Boolean(container && (container.getAttribute('contenteditable') === 'true' || container === document.activeElement || container.contains(document.activeElement)))
|
|
79
|
+
return resolveEffectiveElementFontSize(el, {
|
|
80
|
+
isEditing,
|
|
81
|
+
savedRange: isEditing ? savedSidebarRange : null,
|
|
82
|
+
fallback: '16px'
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
return getStyleVal('fontSize', '16px')
|
|
86
|
+
},
|
|
71
87
|
set(val: string) { setStyleVal('fontSize', val) }
|
|
72
88
|
})
|
|
73
89
|
|
|
@@ -597,7 +613,44 @@ export function useSidebarTextFormat(
|
|
|
597
613
|
return
|
|
598
614
|
}
|
|
599
615
|
}
|
|
616
|
+
|
|
617
|
+
const el = selectedElement.value as any
|
|
618
|
+
const sel = typeof window !== 'undefined' ? window.getSelection() : null
|
|
619
|
+
const container = el ? document.getElementById(`editable-${el.id}`) : null
|
|
620
|
+
if (container && sel) {
|
|
621
|
+
const activeRange = (sel.rangeCount > 0 && container.contains(sel.getRangeAt(0).commonAncestorContainer))
|
|
622
|
+
? sel.getRangeAt(0)
|
|
623
|
+
: (savedSidebarRange && container.contains(savedSidebarRange.commonAncestorContainer) ? savedSidebarRange : null)
|
|
624
|
+
if (activeRange && !activeRange.collapsed) {
|
|
625
|
+
sel.removeAllRanges()
|
|
626
|
+
sel.addRange(activeRange)
|
|
627
|
+
document.execCommand('fontSize', false, '7')
|
|
628
|
+
const fontElements = container.querySelectorAll('font[size="7"]')
|
|
629
|
+
if (fontElements.length > 0) {
|
|
630
|
+
fontElements.forEach((font: any) => {
|
|
631
|
+
const span = document.createElement('span')
|
|
632
|
+
span.style.fontSize = formattedSize
|
|
633
|
+
span.innerHTML = font.innerHTML
|
|
634
|
+
span.querySelectorAll('*').forEach((child: any) => {
|
|
635
|
+
if (child.style && child.style.fontSize) {
|
|
636
|
+
child.style.fontSize = ''
|
|
637
|
+
}
|
|
638
|
+
if (child.tagName === 'FONT') {
|
|
639
|
+
child.removeAttribute('size')
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
font.parentNode.replaceChild(span, font)
|
|
643
|
+
})
|
|
644
|
+
syncContainerContentToElement(el)
|
|
645
|
+
selectionTrigger.value++
|
|
646
|
+
addHistorySnapshot()
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
600
652
|
setStyleVal('fontSize', formattedSize)
|
|
653
|
+
selectionTrigger.value++
|
|
601
654
|
}
|
|
602
655
|
|
|
603
656
|
function syncContainerContentToElement(el: any) {
|
|
@@ -739,9 +792,27 @@ export function useSidebarTextFormat(
|
|
|
739
792
|
// Selection trigger for tracking lists and caret
|
|
740
793
|
const selectionTrigger = ref(0)
|
|
741
794
|
const handleSelectionChange = () => {
|
|
795
|
+
if (typeof window !== 'undefined') {
|
|
796
|
+
const sel = window.getSelection()
|
|
797
|
+
if (sel && sel.rangeCount > 0) {
|
|
798
|
+
const range = sel.getRangeAt(0)
|
|
799
|
+
let node: Node | null = range.commonAncestorContainer
|
|
800
|
+
if (node && node.nodeType !== Node.ELEMENT_NODE) {
|
|
801
|
+
node = node.parentNode
|
|
802
|
+
}
|
|
803
|
+
if (node && (node as HTMLElement).closest && (node as HTMLElement).closest('[id^="editable-"]')) {
|
|
804
|
+
savedSidebarRange = range.cloneRange()
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
742
808
|
selectionTrigger.value++
|
|
743
809
|
}
|
|
744
810
|
|
|
811
|
+
watch(() => selectedElementId.value, () => {
|
|
812
|
+
savedSidebarRange = null
|
|
813
|
+
selectionTrigger.value++
|
|
814
|
+
})
|
|
815
|
+
|
|
745
816
|
onMounted(() => {
|
|
746
817
|
document.addEventListener('selectionchange', handleSelectionChange)
|
|
747
818
|
})
|