collab-canvas 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 +16 -5
- package/dist/host.js +957 -161
- package/lib/ccv-logic.js +281 -0
- package/lib/client.js +1058 -1633
- package/lib/doc-model.js +291 -0
- package/lib/selref.js +107 -20
- package/lib/viewer-md.js +4075 -0
- package/package.json +4 -3
- package/lib/docdrop.js +0 -362
- package/lib/doclink.js +0 -436
package/lib/doclink.js
DELETED
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
// ═══ DSH Collab-Canvas · 对话文档名链接(独立功能模块,单独一个文件维护)═══
|
|
2
|
-
// 交互:AI 对话里出现话布文档名(标题或 id)时转为可点链接,点击打开话布
|
|
3
|
-
// 面板并加载该文档
|
|
4
|
-
// 加载方式:与 docdrop 相同——host 端点 /api/canvas/doclink.js 提供源码,
|
|
5
|
-
// client 入口动态执行,改本文件只需刷新页面
|
|
6
|
-
//
|
|
7
|
-
// 识别规则(保守,防误伤):
|
|
8
|
-
// 1) 长标题(≥4 字符):正文出现即转链接,但前后紧邻字符不能是中英文/数字
|
|
9
|
-
// (防“第123条”式的粘连误伤——短标题另有专门规则)
|
|
10
|
-
// 2) 短标题(<4 字符,如 123、222):仅当被「」『』《》【】〔〕包着,或
|
|
11
|
-
// 整个内联 code/strong 元素就是该标题时才转
|
|
12
|
-
// 3) 文档 id(≥6 字符):仅当整个 <code> 元素就是该 id 时转
|
|
13
|
-
// 4) 跳过代码块(pre)、链接(a)、脚本样式区;已处理的链接不重复处理
|
|
14
|
-
//
|
|
15
|
-
// 健壮性约定:与 docdrop 相同——防重复安装、每个回调独立捕获、失败静默降级
|
|
16
|
-
;(function () {
|
|
17
|
-
try {
|
|
18
|
-
if (window.__ccvDoclinkInstalled) return
|
|
19
|
-
|
|
20
|
-
var VERSION = 4 // v2: 上传文档卡片(拖入文档的 @ 引用)可点击 → 在话布中打开
|
|
21
|
-
// v3: 清单请求带 sid + 并取全局清单;捕获阶段接管 deliverables
|
|
22
|
-
// 的 presented 卡片(是话布就直接开话布面板)
|
|
23
|
-
// v4: 删除「正文标题/ID 自动转链」整套 —— 它会扫到右侧栏的
|
|
24
|
-
// 话布编辑器(扫描根兜底是 document.body),把文档正文里的
|
|
25
|
-
// H1 改写并加上 tooltip;且已被官方 present 卡片取代
|
|
26
|
-
var LIST_REFRESH_MS = 30000
|
|
27
|
-
|
|
28
|
-
// 优先用 client.js 挂出来的共用实现(见 client.js 里的 window.__ccvKit):
|
|
29
|
-
// 日志/轻提示条/会话查询串这三样原本在 client/docdrop/doclink 各存了一份。
|
|
30
|
-
// 取不到就退回下面自带的副本。
|
|
31
|
-
function log(msg) {
|
|
32
|
-
var kit = window.__ccvKit
|
|
33
|
-
if (kit && kit.log) { kit.log(msg, "doclink"); return }
|
|
34
|
-
try {
|
|
35
|
-
fetch("/api/canvas/log", {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: { "Content-Type": "application/json" },
|
|
38
|
-
body: JSON.stringify({ line: "[doclink] " + msg })
|
|
39
|
-
}).catch(function () {})
|
|
40
|
-
} catch (_) {}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ─── 画布清单(标题/id ↔ 链接目标)───
|
|
44
|
-
var canvasList = [] // [{id, title}]
|
|
45
|
-
function refreshList() {
|
|
46
|
-
try {
|
|
47
|
-
// 两路并取后合并:
|
|
48
|
-
// 1) 本会话清单 —— 必须带 sid,否则会被 host 归到 default 会话,
|
|
49
|
-
// 本会话新建/打开的文档标题就匹配不到、转不成可点链接。
|
|
50
|
-
// 2) 全局清单(scope=all)—— 让「还没登记进本会话」的文档标题也能点。
|
|
51
|
-
// 不取这一路的话,标题必须先在这个会话里打开过一次才会变成链接。
|
|
52
|
-
// 合并按 id 去重,先到的(会话清单)优先。
|
|
53
|
-
var pSession = fetch("/api/canvas/list?" + sidQ())
|
|
54
|
-
.then(function (r) { return r.json() }).catch(function () { return null })
|
|
55
|
-
var pAll = fetch("/api/canvas/list?scope=all")
|
|
56
|
-
.then(function (r) { return r.json() }).catch(function () { return null })
|
|
57
|
-
Promise.all([pSession, pAll]).then(function (res) {
|
|
58
|
-
var seen = {}
|
|
59
|
-
var merged = []
|
|
60
|
-
res.forEach(function (d) {
|
|
61
|
-
if (!d || !d.ok || !Array.isArray(d.canvases)) return
|
|
62
|
-
d.canvases.forEach(function (c) {
|
|
63
|
-
var id = String(c.id || "")
|
|
64
|
-
var title = String(c.title || "").trim()
|
|
65
|
-
if (!id || !title || seen[id]) return
|
|
66
|
-
seen[id] = 1
|
|
67
|
-
merged.push({ id: id, title: title })
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
canvasList = merged
|
|
71
|
-
// 清单大小必须在 fetch 落地之后才准:安装时那句日志跑在异步之前,
|
|
72
|
-
// 永远显示 0 篇,排障时会误导。这里补一条真实的。
|
|
73
|
-
log("清单已加载 " + canvasList.length + " 篇")
|
|
74
|
-
}).catch(function () {})
|
|
75
|
-
} catch (_) {}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ─── 打开话布(经 client.js 暴露的桥接)───
|
|
79
|
-
function openCanvas(id) {
|
|
80
|
-
if (typeof window.__ccvCanvasToggle !== "function") { log("话布桥接未就绪"); return }
|
|
81
|
-
try {
|
|
82
|
-
if (!window.__ccvPanelOpen) {
|
|
83
|
-
window.__ccvPendingCanvasId = id
|
|
84
|
-
window.__ccvCanvasToggle()
|
|
85
|
-
} else {
|
|
86
|
-
window.dispatchEvent(new CustomEvent("ccv-open-canvas", { detail: { id: id } }))
|
|
87
|
-
}
|
|
88
|
-
} catch (e) { log("打开话布失败: " + (e && e.message)) }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ─── 轻提示条(与 docdrop 同款)───
|
|
92
|
-
function toast(msg) {
|
|
93
|
-
var kit = window.__ccvKit
|
|
94
|
-
if (kit && kit.toast) { kit.toast(msg, "ccv-doclink-toast"); return }
|
|
95
|
-
try {
|
|
96
|
-
var old = document.getElementById("ccv-doclink-toast")
|
|
97
|
-
if (old) old.remove()
|
|
98
|
-
var t = document.createElement("div")
|
|
99
|
-
t.id = "ccv-doclink-toast"
|
|
100
|
-
t.textContent = msg
|
|
101
|
-
t.style.cssText = "position:fixed;left:50%;bottom:96px;transform:translateX(-50%);" +
|
|
102
|
-
"z-index:2147483000;background:var(--dsw-alias-label-primary,#1a1a1a);" +
|
|
103
|
-
"color:var(--dsw-alias-bg-overlay,#fff);font-size:13px;padding:8px 14px;" +
|
|
104
|
-
"border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.25);pointer-events:none;" +
|
|
105
|
-
"opacity:1;transition:opacity .3s"
|
|
106
|
-
document.body.appendChild(t)
|
|
107
|
-
setTimeout(function () { t.style.opacity = "0" }, 2200)
|
|
108
|
-
setTimeout(function () { if (t.parentNode) t.remove() }, 2600)
|
|
109
|
-
} catch (_) {}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─── 上传文档卡片(拖拽上传的 @ 引用卡片)→ 点击在话布中打开 ───
|
|
113
|
-
// 卡片文件名是 docdrop 生成的“时间戳-随机码-原名”格式,精确匹配不误伤
|
|
114
|
-
var UPLOAD_FNAME_RE = /^\d{13}-[a-z0-9]{2,8}-.{1,120}$/i
|
|
115
|
-
function uploadFname(text) {
|
|
116
|
-
var t = String(text || "").trim()
|
|
117
|
-
if (!t || t.length > 150) return null
|
|
118
|
-
return UPLOAD_FNAME_RE.test(t) ? t : null
|
|
119
|
-
}
|
|
120
|
-
// 从点击目标向上找卡片根:最小的“纯文件名”祖先
|
|
121
|
-
function findUploadChip(e) {
|
|
122
|
-
var n = e.target
|
|
123
|
-
for (var i = 0; i < 6 && n && n !== document.body; i++) {
|
|
124
|
-
if (n.nodeType === 1) {
|
|
125
|
-
var t = (n.textContent || "").trim()
|
|
126
|
-
var f = uploadFname(t)
|
|
127
|
-
if (f && t.length <= f.length + 4) return { fname: f, el: n }
|
|
128
|
-
}
|
|
129
|
-
n = n.parentElement
|
|
130
|
-
}
|
|
131
|
-
return null
|
|
132
|
-
}
|
|
133
|
-
function openUploadInCanvas(fname) {
|
|
134
|
-
var shown = fname.replace(/^\d{13}-[a-z0-9]{2,8}-/i, "")
|
|
135
|
-
toast("正在把「" + shown + "」导入话布…")
|
|
136
|
-
fetch("/api/canvas/open-upload?name=" + encodeURIComponent(fname))
|
|
137
|
-
.then(function (r) { return r.json() })
|
|
138
|
-
.then(function (d) {
|
|
139
|
-
if (d && d.ok && d.id) {
|
|
140
|
-
openCanvas(d.id)
|
|
141
|
-
toast((d.created ? "已导入话布并打开" : "已在话布中打开") + "「" + (d.title || shown) + "」")
|
|
142
|
-
} else {
|
|
143
|
-
toast("打开失败: " + ((d && d.error) || "未知错误"))
|
|
144
|
-
}
|
|
145
|
-
}).catch(function (e) { toast("打开失败: " + ((e && e.message) || "网络错误")) })
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 注:原先这里还有 escapeRe / isWordChar / isWrapperChar 三个匹配工具,
|
|
149
|
-
// 只服务于已删除的「标题自动转链」。现在卡片的判定都靠文件名后缀与精确比较,
|
|
150
|
-
// 不需要它们。
|
|
151
|
-
|
|
152
|
-
// ─── 已删除:正文「标题/ID 自动转链」整套(2026-09-12)──────────────
|
|
153
|
-
// 原实现把正文里出现的画布标题改成 <span class="ccv-doc-link">、把文档 id 的
|
|
154
|
-
// <code> 打上同样的类,点击即开话布面板。删除原因有二:
|
|
155
|
-
//
|
|
156
|
-
// 1) 它会扫到不该扫的地方。扫描根是
|
|
157
|
-
// document.querySelector('[data-pane="conversation"]') || document.body
|
|
158
|
-
// 选择器一旦没命中就兜底整个 body —— 右侧栏的话布编辑器也在 body 里,
|
|
159
|
-
// 于是文档正文里的 H1「话布优化」被加上了 .ccv-doc-link 和 tooltip
|
|
160
|
-
// 「点击在话布中打开」,可编辑内容被改写。
|
|
161
|
-
//
|
|
162
|
-
// 2) 它已经被官方卡片取代。现在改完话布文档会调 present 声明该 md 文件,
|
|
163
|
-
// DSH 的 deliverables 行会生成一张可点卡片,由本模块在捕获阶段接管
|
|
164
|
-
// (见 findPresentedCanvas)——入口由工具驱动,不依赖正文里写没写标题。
|
|
165
|
-
//
|
|
166
|
-
// 保留的部分:各种文件卡片的点击接管(路径卡片 / 工作区相对路径 / @短名 /
|
|
167
|
-
// 上传卡片 / deliverables 声明卡片),它们只在点击时判定,且都带面板守卫。
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// ─── 样式:低调虚线下划线 + 原文字色 ───
|
|
171
|
-
function injectStyle() {
|
|
172
|
-
try {
|
|
173
|
-
if (document.getElementById("ccv-doclink-style")) return
|
|
174
|
-
var s = document.createElement("style")
|
|
175
|
-
s.id = "ccv-doclink-style"
|
|
176
|
-
s.textContent =
|
|
177
|
-
// deliverables「已声明文件」卡片:命中话布时由 doclink 打上 data-ccv-canvas,
|
|
178
|
-
// 此时点击会被接管去开话布面板,DSH 那句悬停提示「在侧栏预览」就是错的 ——
|
|
179
|
-
// 隐掉它,并把平时显示的文件描述在悬停时保持可见(否则那一行会变空)。
|
|
180
|
-
'[data-presented-file][data-ccv-canvas="1"] [class*="previewHint"]{display:none !important}' +
|
|
181
|
-
'[data-presented-file][data-ccv-canvas="1"]:hover [class*="secondaryText"]{display:inline !important}'
|
|
182
|
-
;(document.head || document.body).appendChild(s)
|
|
183
|
-
} catch (_) {}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ─── .md/.txt 文件卡片识别(三种形态)─────────────
|
|
187
|
-
// 1) 画布文档路径引用(canvas-docs/….md,含拖入产生的与 DSH 原生绝对路径卡片)
|
|
188
|
-
// 2) 上传副本卡片(时间戳-随机码-原名,走 open-upload)
|
|
189
|
-
// 3) 原生 @ 短名卡片(如 333.md,走 load-doc 注册/打开)
|
|
190
|
-
// 卡片形态要求:元素文本以该文件名/路径结尾,防止长段落中途提及被误触发
|
|
191
|
-
function findCanvasPathChip(e) {
|
|
192
|
-
if (e.target && e.target.closest && e.target.closest("#collab-canvas-panel")) return null
|
|
193
|
-
var n = e.target
|
|
194
|
-
for (var i = 0; i < 6 && n && n !== document.body; i++) {
|
|
195
|
-
if (n.nodeType === 1) {
|
|
196
|
-
var t = (n.textContent || "").trim()
|
|
197
|
-
if (!t || t.length > 300) continue
|
|
198
|
-
var m = /canvas-docs[/\\]([^\s\\/:*?"<>|]+\.(?:md|markdown|txt))/i.exec(t)
|
|
199
|
-
// 元素文本必须以该路径结尾(卡片形态):防止长段落里“中途提到路径”被误触发
|
|
200
|
-
if (m && t.slice(t.length - m[0].length).toLowerCase() === m[0].toLowerCase()) {
|
|
201
|
-
return { rel: "canvas-docs/" + m[1], el: n }
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
n = n.parentElement
|
|
205
|
-
}
|
|
206
|
-
return null
|
|
207
|
-
}
|
|
208
|
-
function findMdNameChip(e) {
|
|
209
|
-
if (e.target && e.target.closest && e.target.closest("#collab-canvas-panel")) return null
|
|
210
|
-
var n = e.target
|
|
211
|
-
for (var i = 0; i < 6 && n && n !== document.body; i++) {
|
|
212
|
-
if (n.nodeType === 1) {
|
|
213
|
-
var t = (n.textContent || "").trim()
|
|
214
|
-
if (!t || t.length > 120 || /\s/.test(t)) continue
|
|
215
|
-
if (!/\.(md|markdown|txt)$/i.test(t)) continue
|
|
216
|
-
var fname = t.replace(/^@/, "")
|
|
217
|
-
if (!/\.(md|markdown|txt)$/i.test(fname)) continue
|
|
218
|
-
return { fname: fname, el: n }
|
|
219
|
-
}
|
|
220
|
-
n = n.parentElement
|
|
221
|
-
}
|
|
222
|
-
return null
|
|
223
|
-
}
|
|
224
|
-
// 纯文件名卡片 → 尝试按 canvas-docs/<文件名> 注册/打开(最常见的工作区位置)
|
|
225
|
-
function openMdNameInCanvas(fname) {
|
|
226
|
-
toast("正在把「" + fname + "」登记为话布…")
|
|
227
|
-
fetch("/api/canvas/load-doc?rel=" + encodeURIComponent("canvas-docs/" + fname) + "&" + sidQ())
|
|
228
|
-
.then(function (r) { return r.json() })
|
|
229
|
-
.then(function (d) {
|
|
230
|
-
if (d && d.ok && d.id) {
|
|
231
|
-
openCanvas(d.id)
|
|
232
|
-
toast((d.created ? "已登记为话布并打开:" : "已在话布中打开:") + (d.title || fname))
|
|
233
|
-
} else {
|
|
234
|
-
toast("工作区里没找到这个文档(仅支持 canvas-docs/ 下的 .md/.txt)")
|
|
235
|
-
}
|
|
236
|
-
}).catch(function (e) { toast("打开失败: " + ((e && e.message) || "网络错误")) })
|
|
237
|
-
}
|
|
238
|
-
// 会话键(与 client.js 同算法):load-doc 建的画布记入当前会话清单
|
|
239
|
-
function sidQ() {
|
|
240
|
-
var kit = window.__ccvKit
|
|
241
|
-
if (kit && kit.sidQ) return kit.sidQ()
|
|
242
|
-
try { const sid = window.__ccvSid; return sid ? "sid=" + encodeURIComponent(sid) : "sid=default"; } catch (_) { return "sid=default"; }
|
|
243
|
-
}
|
|
244
|
-
function openCanvasPathChip(rel) {
|
|
245
|
-
toast("正在把「" + rel.split("/").pop() + "」登记为话布…")
|
|
246
|
-
fetch("/api/canvas/load-doc?rel=" + encodeURIComponent(rel) + "&" + sidQ())
|
|
247
|
-
.then(function (r) { return r.json() })
|
|
248
|
-
.then(function (d) {
|
|
249
|
-
if (d && d.ok && d.id) {
|
|
250
|
-
openCanvas(d.id)
|
|
251
|
-
toast((d.created ? "已登记为话布并打开:" : "已在话布中打开:") + (d.title || rel))
|
|
252
|
-
} else {
|
|
253
|
-
toast("打开失败: " + ((d && d.error) || "未知错误"))
|
|
254
|
-
}
|
|
255
|
-
}).catch(function (e) { toast("打开失败: " + ((e && e.message) || "网络错误")) })
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ─── 本地文档路径卡片(工作区 / 已登记文件夹内的 .md/.txt)──────────
|
|
259
|
-
// 与上面的 canvas-docs 卡片区分:这类走 load-path「就地登记」——
|
|
260
|
-
// 绑定原路径,在话布里保存即写回原文件(不是复制一份)。
|
|
261
|
-
// 判定:元素文本的「最后一个空白分隔词」是带路径分隔符的 .md/.txt。
|
|
262
|
-
// 这样既认绝对路径(D:\x\y.md),也认工作区相对路径(docs/DESIGN.md)。
|
|
263
|
-
function findLocalDocPathChip(e) {
|
|
264
|
-
if (e.target && e.target.closest && e.target.closest("#collab-canvas-panel")) return null
|
|
265
|
-
var n = e.target
|
|
266
|
-
for (var i = 0; i < 6 && n && n !== document.body; i++) {
|
|
267
|
-
if (n.nodeType === 1) {
|
|
268
|
-
var t = (n.textContent || "").trim()
|
|
269
|
-
if (t && t.length <= 300) {
|
|
270
|
-
var words = t.split(/\s+/)
|
|
271
|
-
var last = words[words.length - 1] || ""
|
|
272
|
-
if (/\.(md|markdown|txt)$/i.test(last) && /[\\/]/.test(last)) {
|
|
273
|
-
return { path: last, el: n }
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
n = n.parentElement
|
|
278
|
-
}
|
|
279
|
-
return null
|
|
280
|
-
}
|
|
281
|
-
// 就地打开:host 侧绑定原路径,保存即写回原文件
|
|
282
|
-
function openLocalDocPath(p) {
|
|
283
|
-
toast("正在把「" + p.split(/[\\/]/).pop() + "」就地登记为话布…")
|
|
284
|
-
fetch("/api/canvas/load-path?path=" + encodeURIComponent(p) + "&" + sidQ())
|
|
285
|
-
.then(function (r) { return r.json() })
|
|
286
|
-
.then(function (d) {
|
|
287
|
-
if (d && d.ok && d.id) {
|
|
288
|
-
openCanvas(d.id)
|
|
289
|
-
toast((d.created ? "已登记并打开(就地编辑):" : "已在话布中打开:") + (d.title || p))
|
|
290
|
-
} else {
|
|
291
|
-
toast("打开失败:" + ((d && d.error) || "未知错误"))
|
|
292
|
-
}
|
|
293
|
-
}).catch(function (e) { toast("打开失败:" + ((e && e.message) || "网络错误")) })
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// ─── 接管 deliverables「已声明文件」卡片:是话布就开话布面板 ──────────
|
|
297
|
-
// DSH 的 presented 卡片默认走 openFile()(DSH 文件打开器);右侧菜单才是
|
|
298
|
-
// 系统默认程序 / 文件管理器定位。若被声明的文件本身就是一份话布
|
|
299
|
-
// (canvas-docs/<标题>.md),在话布里打开更合预期。
|
|
300
|
-
// 规则:
|
|
301
|
-
// · 只接管两个预览热区,右侧菜单箭头(aria-haspopup="menu")不碰,
|
|
302
|
-
// 用户仍能用它调系统程序;
|
|
303
|
-
// · 只在该文件名能对应到一个已知话布时才接管,其他文件保持 DSH 原行为;
|
|
304
|
-
// · 必须靠捕获阶段监听才抢得到(见注册处),否则卡片自己的 onClick 先跑。
|
|
305
|
-
// 卡片 → 话布:命中返回 {id,name},否则 null。点击接管与悬停标记共用。
|
|
306
|
-
function canvasForPresentedCard(card) {
|
|
307
|
-
if (!card) return null
|
|
308
|
-
var nameEl = card.querySelector('[class*="fileName"]')
|
|
309
|
-
var name = nameEl ? String(nameEl.textContent || "").trim() : ""
|
|
310
|
-
if (!/\.(md|markdown)$/i.test(name)) return null
|
|
311
|
-
var title = name.replace(/\.(md|markdown)$/i, "")
|
|
312
|
-
for (var i = 0; i < canvasList.length; i++) {
|
|
313
|
-
if (canvasList[i].title === title) return { id: canvasList[i].id, name: name }
|
|
314
|
-
}
|
|
315
|
-
return null
|
|
316
|
-
}
|
|
317
|
-
// 命中话布的卡片打上 data-ccv-canvas:CSS 靠它隐藏 DSH 那句错误的悬停提示
|
|
318
|
-
// (「在侧栏预览」——点击其实被我们接管去开话布面板了)
|
|
319
|
-
function markPresentedCanvasCard(card) {
|
|
320
|
-
if (!card || card.getAttribute("data-ccv-canvas") === "1") return
|
|
321
|
-
if (canvasForPresentedCard(card)) card.setAttribute("data-ccv-canvas", "1")
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function findPresentedCanvas(e) {
|
|
325
|
-
var el = e.target
|
|
326
|
-
if (!el || !el.closest) return null
|
|
327
|
-
var card = el.closest("[data-presented-file]")
|
|
328
|
-
if (!card) return null
|
|
329
|
-
if (el.closest('button[aria-haspopup="menu"]')) return null
|
|
330
|
-
markPresentedCanvasCard(card)
|
|
331
|
-
var hit = canvasForPresentedCard(card)
|
|
332
|
-
if (hit) {
|
|
333
|
-
log("presented 卡片点击: 「" + hit.name + "」命中话布,转开话布面板")
|
|
334
|
-
return hit
|
|
335
|
-
}
|
|
336
|
-
// 诊断:presented 卡片被点了,但没接管时要说清为什么(日志会落盘可复盘)
|
|
337
|
-
var nameEl = card.querySelector('[class*="fileName"]')
|
|
338
|
-
var nm = nameEl ? String(nameEl.textContent || "").trim() : ""
|
|
339
|
-
if (!/\.(md|markdown)$/i.test(nm)) log("presented 卡片点击: 非 md(" + nm + "),不接管")
|
|
340
|
-
else log("presented 卡片点击: 「" + nm + "」未命中话布(清单 " + canvasList.length + " 篇),交给 DSH")
|
|
341
|
-
return null
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ─── 点击委托:文档名链接 + 上传卡片 + 路径卡片 + 原生短名卡片 ───
|
|
345
|
-
function onClick(e) {
|
|
346
|
-
try {
|
|
347
|
-
// 优先级最高:deliverables 声明卡片(DSH 自己的处理在本函数之后才会跑)
|
|
348
|
-
var pc = findPresentedCanvas(e)
|
|
349
|
-
if (pc) {
|
|
350
|
-
e.preventDefault()
|
|
351
|
-
e.stopPropagation()
|
|
352
|
-
openCanvas(pc.id)
|
|
353
|
-
return
|
|
354
|
-
}
|
|
355
|
-
var cchip = findCanvasPathChip(e)
|
|
356
|
-
if (cchip) {
|
|
357
|
-
e.preventDefault()
|
|
358
|
-
e.stopPropagation()
|
|
359
|
-
openCanvasPathChip(cchip.rel)
|
|
360
|
-
return
|
|
361
|
-
}
|
|
362
|
-
var lchip = findLocalDocPathChip(e)
|
|
363
|
-
if (lchip) {
|
|
364
|
-
e.preventDefault()
|
|
365
|
-
e.stopPropagation()
|
|
366
|
-
openLocalDocPath(lchip.path)
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
var chip = findUploadChip(e)
|
|
370
|
-
if (chip) {
|
|
371
|
-
e.preventDefault()
|
|
372
|
-
e.stopPropagation()
|
|
373
|
-
openUploadInCanvas(chip.fname)
|
|
374
|
-
return
|
|
375
|
-
}
|
|
376
|
-
var md = findMdNameChip(e)
|
|
377
|
-
if (md) {
|
|
378
|
-
e.preventDefault()
|
|
379
|
-
e.stopPropagation()
|
|
380
|
-
openMdNameInCanvas(md.fname)
|
|
381
|
-
}
|
|
382
|
-
} catch (err) { log("点击处理失败: " + (err && err.message)) }
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// 悬停时给可点元素加手型和提示(委托,动态出现的卡片也覆盖)
|
|
386
|
-
function onHover(e) {
|
|
387
|
-
try {
|
|
388
|
-
// deliverables 声明卡片:命中话布的当场打标记(CSS 据此隐掉错误的悬停提示)
|
|
389
|
-
var pcard = e.target && e.target.closest ? e.target.closest("[data-presented-file]") : null
|
|
390
|
-
if (pcard) markPresentedCanvasCard(pcard)
|
|
391
|
-
var chip = findUploadChip(e)
|
|
392
|
-
if (chip && !chip.el.__ccvChipMarked) {
|
|
393
|
-
chip.el.__ccvChipMarked = 1
|
|
394
|
-
chip.el.style.cursor = "pointer"
|
|
395
|
-
chip.el.title = "点击在话布中打开这篇文档"
|
|
396
|
-
}
|
|
397
|
-
var cchip = findCanvasPathChip(e)
|
|
398
|
-
if (cchip && !cchip.el.__ccvChipMarked) {
|
|
399
|
-
cchip.el.__ccvChipMarked = 1
|
|
400
|
-
cchip.el.style.cursor = "pointer"
|
|
401
|
-
cchip.el.title = "点击在话布中打开这篇文档"
|
|
402
|
-
}
|
|
403
|
-
var lchip = findLocalDocPathChip(e)
|
|
404
|
-
if (lchip && !lchip.el.__ccvChipMarked) {
|
|
405
|
-
lchip.el.__ccvChipMarked = 1
|
|
406
|
-
lchip.el.style.cursor = "pointer"
|
|
407
|
-
lchip.el.title = "点击在话布中就地打开(编辑会写回原文件)"
|
|
408
|
-
}
|
|
409
|
-
var md = findMdNameChip(e)
|
|
410
|
-
if (md && !md.el.__ccvChipMarked) {
|
|
411
|
-
md.el.__ccvChipMarked = 1
|
|
412
|
-
md.el.style.cursor = "pointer"
|
|
413
|
-
md.el.title = "点击在话布中打开「" + md.fname + "」"
|
|
414
|
-
}
|
|
415
|
-
} catch (_) {}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// ─── 安装(只装一次)───
|
|
419
|
-
window.__ccvDoclinkInstalled = true
|
|
420
|
-
injectStyle()
|
|
421
|
-
refreshList()
|
|
422
|
-
setInterval(refreshList, LIST_REFRESH_MS)
|
|
423
|
-
|
|
424
|
-
var mo = null
|
|
425
|
-
// ⚠️ click 必须用捕获阶段(第三参 true):
|
|
426
|
-
// deliverables 的 presented 卡片自带 React onClick(走 DSH 文件打开器),
|
|
427
|
-
// React 18 把监听挂在根容器上,冒泡时会先于 document 上的监听触发。
|
|
428
|
-
// 只有捕获阶段才抢得到它前面,才能把「打开话布」接上去。
|
|
429
|
-
// mouseover 不需要抢,保持冒泡即可(只做悬停提示)。
|
|
430
|
-
document.addEventListener("click", onClick, true)
|
|
431
|
-
document.addEventListener("mouseover", onHover, false)
|
|
432
|
-
log("文件卡片点击接管已安装 v" + VERSION + "(清单 " + canvasList.length + " 篇)")
|
|
433
|
-
} catch (e) {
|
|
434
|
-
try { console.error("[doclink] 安装失败", e) } catch (_) {}
|
|
435
|
-
}
|
|
436
|
-
})()
|