base-paint 0.1.0 → 0.3.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/dist/blocks.d.ts +169 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +862 -0
- package/dist/blocks.js.map +1 -0
- package/dist/charts.d.ts +62 -0
- package/dist/charts.d.ts.map +1 -0
- package/dist/charts.js +1677 -0
- package/dist/charts.js.map +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +7 -1
- package/dist/contract.js.map +1 -1
- package/dist/controls.d.ts +139 -0
- package/dist/controls.d.ts.map +1 -0
- package/dist/controls.js +1182 -0
- package/dist/controls.js.map +1 -0
- package/dist/help.d.ts +24 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +604 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/spec/charts.d.ts +222 -0
- package/dist/spec/charts.d.ts.map +1 -0
- package/dist/spec/charts.js +35 -0
- package/dist/spec/charts.js.map +1 -0
- package/dist/spec/controls.d.ts +329 -0
- package/dist/spec/controls.d.ts.map +1 -0
- package/dist/spec/controls.js +119 -0
- package/dist/spec/controls.js.map +1 -0
- package/dist/spec/help.d.ts +353 -0
- package/dist/spec/help.d.ts.map +1 -0
- package/dist/spec/help.js +167 -0
- package/dist/spec/help.js.map +1 -0
- package/dist/spec/index.d.ts +40 -0
- package/dist/spec/index.d.ts.map +1 -0
- package/dist/spec/index.js +164 -0
- package/dist/spec/index.js.map +1 -0
- package/dist/spec/style.d.ts +46 -0
- package/dist/spec/style.d.ts.map +1 -0
- package/dist/spec/style.js +39 -0
- package/dist/spec/style.js.map +1 -0
- package/dist/spec/template.d.ts +363 -0
- package/dist/spec/template.d.ts.map +1 -0
- package/dist/spec/template.js +252 -0
- package/dist/spec/template.js.map +1 -0
- package/dist/spec/text.d.ts +157 -0
- package/dist/spec/text.d.ts.map +1 -0
- package/dist/spec/text.js +83 -0
- package/dist/spec/text.js.map +1 -0
- package/dist/style.d.ts +19 -0
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +1091 -0
- package/dist/style.js.map +1 -1
- package/dist/template.d.ts +27 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +362 -0
- package/dist/template.js.map +1 -0
- package/dist/text.d.ts +19 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +448 -0
- package/dist/text.js.map +1 -0
- package/package.json +5 -3
package/dist/controls.js
ADDED
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
/** base-paint/controls:#76 控件层运行时(契约 §3.3/§6.3)。
|
|
2
|
+
*
|
|
3
|
+
* 落地冻结面 44 条里 #76 的 13 条 `pending`(10 个运行时出口 + 3 个类型出口):
|
|
4
|
+
* `copyText`/`createCopyRuntime`/`bindCopyAction`/`buildSharedHelpersJs`/
|
|
5
|
+
* `renderToast`/`createToastController`/`renderActionBar`/`renderStatusBadge`/
|
|
6
|
+
* `renderEmptyState`/`renderErrorReceipt`。签名逐字取 `src/spec/controls.ts`,本文件不改任何冻结值。
|
|
7
|
+
*
|
|
8
|
+
* 三条硬边界(逐条对齐 §3.3/§6.3,违反即红):
|
|
9
|
+
* 1. **无宿主 ≠ 无浏览器**(C-1):本层不依赖 DSH 宿主注入(无宿主 DOM 容器、无宿主全局),
|
|
10
|
+
* 浏览器能力一律经**显式端口**(`CopyPorts`/`ToastHostPort`/`CopyActionHostPort`)传入;
|
|
11
|
+
* 2. **模块代码不得触碰 DOM**(C-8/AC-7):本文件(编译进 `dist/controls.js`)的**代码**里不出现
|
|
12
|
+
* `document.`/`window.`/`navigator.`——DOM 只允许出现在 `buildSharedHelpersJs` **产出的 JS 文本**里
|
|
13
|
+
* (`SHARED_HELPERS_JS_RULE.domAllowed = true`),由 §6.3 追加验收的 dist 扫描断言钉死;
|
|
14
|
+
* 3. **零内联脚本**(C-2):渲染出的按钮只带 `ACTION_ID_ATTR` 与 `DEFAULT_DATA_ATTR`,
|
|
15
|
+
* 激活一律走 `bindCopyAction`(事件委派),产出 JS 必须是**经典 script** 作用域可跑的 IIFE。
|
|
16
|
+
*
|
|
17
|
+
* 文档未规定处的取值(本文显式记账,不留暗猜;契约 §3.3「文档无规定」项):
|
|
18
|
+
* - 各控件 HTML 结构/类名细节 → 只冻结「`ilife-` 前缀 + `CONTROL_STYLE_SECTIONS` 命名空间」;
|
|
19
|
+
* 逐区尺寸/色值 owner #75,本文件**不产**任何样式常量(数值一律读 `ACTION_BAR_DEFAULTS` 等冻结常量);
|
|
20
|
+
* - `copyText` 非空串失败的 `reason` → 通道 2 返回假值 = `'fallback-failed'`/通道 2 **同步抛错** =
|
|
21
|
+
* `'fallback-threw'`(只冻结了空串短路的 `'empty'`;两通道皆失败一律**不抛错**,§3.3);
|
|
22
|
+
* - `copyText` 的 toast 挂载与回调**先后** → 先挂反馈(失败徽章恒在,不被回调异常吞掉),再回调一次;
|
|
23
|
+
* - `copyText('', ports)` 的判定**次序** → 先校验 `ports`(非法即 `bad-input`)再判空串短路,即
|
|
24
|
+
* `copyText('', null)` 抛 `bad-input` 而非返回 `reason:'empty'`(次序文档无规定,本文定死);
|
|
25
|
+
* - `renderToast` 的 `actions` 数量上限 → 不截断(旧层 `slice(0, 2)` 未进冻结面);
|
|
26
|
+
* - `renderToast` 非法 `badge.type` → 回落 `'ok'`(旧层口径);
|
|
27
|
+
* - `renderActionBar` 的 `CopyButtonInput.text` 缺席 → 仍渲染按钮、不写 `DEFAULT_DATA_ATTR`(binder 靠
|
|
28
|
+
* `readDataText → undefined` 跳过);`text === ''` → 仍写 `data-t=""`(binder 送进空串短路,无反馈);
|
|
29
|
+
* 场景按钮 `kind` 取冻结 `ACTION_BAR_KINDS` **全量**(含 `ghost`),不以 §3.3 表格的「primary/red」为限;
|
|
30
|
+
* `format` 不参与渲染(序列化归 #77);
|
|
31
|
+
* - `renderStatusBadge` 的 `text === ''` → 视为缺省(旧层口径);非法 `status` 降级 `'empty'`(冻结语义);
|
|
32
|
+
* - `renderEmptyState` 的 `actionHtml` → **受信 HTML 透传**(契约同口径,调用方负责其内容安全),可含
|
|
33
|
+
* 内联 `onclick`;「零注入面」只约束 base-paint **自产**标记,不约束调用方透传的受信片段;
|
|
34
|
+
* - helpers JS 的反馈栈容量 → 恒取冻结 `TOAST_DEFAULTS.maxStack`/`mobileMaxStack`(按视口),**不读**
|
|
35
|
+
* 静态 `renderToast` 产出的 `data-max`(后者供页面自建 `createToastController` 读取,两栈互不相干);
|
|
36
|
+
* - `renderErrorReceipt` 的 `retryPrompt` → 修正重试按钮的**文案**(缺省 `修正重试`);该按钮**不带**
|
|
37
|
+
* `ACTION_ID_ATTR`——冻结输入面没有 retry actionId,不得自造 id(§6.3 红线 8),绑定归调用方;
|
|
38
|
+
* - `copyText` 判定通道 2 成功用**真值**(旧层 `_fbCopy(s) ? ok() : fail()` 同口径);对布尔返回值
|
|
39
|
+
* 与 `=== true` 等价,只影响调用方返回非布尔真值的情形;
|
|
40
|
+
* - **「≤820px 收窄为 3」是页面运行时行为,不是模块行为**(FX-76-2 总架构师裁定):模块侧
|
|
41
|
+
* `createToastController` 保持**宿主无关**(AC-7 不变,不读任何浏览器全局);收窄由**页面侧**
|
|
42
|
+
* `buildSharedHelpersJs` 的**产出文本**读 `matchMedia('(max-width: mobileMaxPx)')` 承担
|
|
43
|
+
* (`domAllowed`),或由调用方经 `ToastInput.maxStack` 显式施加。旧层 `window.matchMedia`
|
|
44
|
+
* (`base.js:96`)本就是页面侧代码。两侧分工见契约 §3.3「toast 移动端收窄的分工(FX-76-2)」。
|
|
45
|
+
*/
|
|
46
|
+
import { STYLE_PREFIX } from './style.js';
|
|
47
|
+
import { ACTION_BAR_DEFAULTS, ACTION_BAR_KINDS, ACTION_ID_ATTR, CONTROL_STYLE_SECTIONS, COPY_ACTION_IDS, COPY_TEXT_DEFAULTS, DEFAULT_DATA_ATTR, ESCAPE_HTML_CHARS, ESCAPE_HTML_ENTITIES, HELP_COPY_ACTIONS, HELP_SHELL_ID, STATUS_DEFAULT_TEXT, STATUS_KINDS, TOAST_DEFAULTS, } from './spec/index.js';
|
|
48
|
+
/* ── 错误形态(§3.3:与既有 RenderError 并列、互不继承、一律抛出、不返空) ── */
|
|
49
|
+
/** 控件层错误。**不**从 `src/index.ts` 导出:`SPEC_FROZEN_SURFACE` 44 条内无该运行时条目,
|
|
50
|
+
* 调用方按 `name === 'ControlsError'` + `code` 判定(与 `fillTemplate` 的 `TemplateError` 同口径)。 */
|
|
51
|
+
export class ControlsError extends Error {
|
|
52
|
+
code;
|
|
53
|
+
constructor(code, message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = 'ControlsError';
|
|
56
|
+
this.code = code;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function badInput(message) {
|
|
60
|
+
throw new ControlsError('bad-input', message);
|
|
61
|
+
}
|
|
62
|
+
/* ── 转义(AC-14:唯一口径 = 冻结的 `ESCAPE_HTML_CHARS`/`ESCAPE_HTML_ENTITIES`) ── */
|
|
63
|
+
const ESCAPE_RE = new RegExp('[' + ESCAPE_HTML_CHARS.join('') + ']', 'g');
|
|
64
|
+
function esc(value) {
|
|
65
|
+
return value.replace(ESCAPE_RE, (ch) => ESCAPE_HTML_ENTITIES[ch] ?? ch);
|
|
66
|
+
}
|
|
67
|
+
/* ── 入参校验小件(失败行为恒为 `ControlsError` code `bad-input`) ── */
|
|
68
|
+
function assertPlainObject(value, field) {
|
|
69
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
70
|
+
badInput(field + ' 必须是对象');
|
|
71
|
+
}
|
|
72
|
+
function assertActionId(value, field) {
|
|
73
|
+
if (typeof value !== 'string' || value === '')
|
|
74
|
+
badInput(field + ' 必须是非空字符串');
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
/** 零注入面:含内联事件处理器字段(`onclick`/`onClick`/任何 `on*`)的入参一律拒。 */
|
|
78
|
+
function assertNoInlineHandler(value, field) {
|
|
79
|
+
for (const key of Object.keys(value)) {
|
|
80
|
+
if (/^on/i.test(key))
|
|
81
|
+
badInput(field + ' 不得含内联事件处理器字段:' + key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isStatusKind(value) {
|
|
85
|
+
return STATUS_KINDS.includes(value);
|
|
86
|
+
}
|
|
87
|
+
/* ── toast(`renderToast` + `createToastController`) ─────────────────── */
|
|
88
|
+
/** 图标字形:`TOAST_ICONS` 是**冻结的枚举名**(不是字形),字形属文档未规定项(旧层 `base.js:74` 同值)。 */
|
|
89
|
+
const TOAST_ICON_GLYPHS = Object.freeze({
|
|
90
|
+
copy: '📋',
|
|
91
|
+
ok: '✅',
|
|
92
|
+
warn: '⚠️',
|
|
93
|
+
danger: '❌',
|
|
94
|
+
info: '💡',
|
|
95
|
+
});
|
|
96
|
+
/** 徽章类型白名单:取冻结类型 `ToastBadge['type']` 的成员(非法值回落 `'ok'`,旧层口径)。 */
|
|
97
|
+
const TOAST_BADGE_TYPES = ['ok', 'warn', 'danger'];
|
|
98
|
+
/** 关闭按钮文案(文档无规定;沿用旧基线 `✓ 知道了`)。 */
|
|
99
|
+
const TOAST_CLOSE_LABEL = '✓ 知道了';
|
|
100
|
+
/** 关闭按钮的命名空间类(helpers JS 按 `prefix` 派生同名选择器,见 `buildSharedHelpersJs`)。 */
|
|
101
|
+
const TOAST_CLOSE_CLASS = 'toast-close';
|
|
102
|
+
/** toast 结构类名(**静态产出器与 helpers 运行时共用同一份**,故提为常量):
|
|
103
|
+
* `body` 包裹 `title-row`(+ 可选 detail),close 在 body 之外 —— 与旧层
|
|
104
|
+
* `.hm-toast-icon + .hm-toast-body(> .hm-toast-title-row + .hm-toast-detail) + .hm-toast-close`
|
|
105
|
+
* **同构**(旧层取证 `.scratch/t76/old-controls.md` §1.1;返修 W1 根因修)。
|
|
106
|
+
* 运行时详情类名沿用 `toast-title-detail`(既有 helpers 产出,不新增类名)。 */
|
|
107
|
+
const TOAST_BODY_CLASS = 'toast-body';
|
|
108
|
+
const TOAST_TITLE_ROW_CLASS = 'toast-title-row';
|
|
109
|
+
const TOAST_DETAIL_CLASS = 'toast-title-detail';
|
|
110
|
+
function positiveInt(value, fallback) {
|
|
111
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
112
|
+
}
|
|
113
|
+
/** 冻结签名:`renderToast(input: ToastInput): string`(`input.msg` 非字符串 → `bad-input`)。 */
|
|
114
|
+
export const renderToast = (input) => {
|
|
115
|
+
assertPlainObject(input, 'renderToast: input');
|
|
116
|
+
const msg = input.msg;
|
|
117
|
+
if (typeof msg !== 'string')
|
|
118
|
+
badInput('renderToast: input.msg 必须是字符串');
|
|
119
|
+
const toast = input;
|
|
120
|
+
const icon = typeof toast.icon === 'string' && toast.icon in TOAST_ICON_GLYPHS ? toast.icon : TOAST_DEFAULTS.defaultIcon;
|
|
121
|
+
const maxStack = positiveInt(toast.maxStack, TOAST_DEFAULTS.maxStack);
|
|
122
|
+
const head = ['<span class="' + STYLE_PREFIX + 'toast-icon" aria-hidden="true">' + TOAST_ICON_GLYPHS[icon] + '</span>'];
|
|
123
|
+
const titleRow = ['<span class="' + STYLE_PREFIX + 'toast-title">' + esc(msg) + '</span>'];
|
|
124
|
+
const badge = toast.badge;
|
|
125
|
+
if (badge !== undefined && badge !== null) {
|
|
126
|
+
assertPlainObject(badge, 'renderToast: input.badge');
|
|
127
|
+
if (typeof badge.text !== 'string')
|
|
128
|
+
badInput('renderToast: input.badge.text 必须是字符串');
|
|
129
|
+
const badgeType = TOAST_BADGE_TYPES.includes(badge.type) ? badge.type : 'ok';
|
|
130
|
+
titleRow.push('<span class="' + STYLE_PREFIX + 'toast-chip ' + STYLE_PREFIX + 'toast-chip-' + badgeType + '">' + esc(badge.text) + '</span>');
|
|
131
|
+
}
|
|
132
|
+
if (typeof toast.count === 'string' && toast.count !== '') {
|
|
133
|
+
titleRow.push('<span class="' + STYLE_PREFIX + 'toast-count">' + esc(toast.count) + '</span>');
|
|
134
|
+
}
|
|
135
|
+
const actions = toast.actions;
|
|
136
|
+
if (actions !== undefined && actions !== null) {
|
|
137
|
+
if (!Array.isArray(actions))
|
|
138
|
+
badInput('renderToast: input.actions 必须是数组');
|
|
139
|
+
actions.forEach((action, i) => {
|
|
140
|
+
const field = 'renderToast: input.actions[' + i + ']';
|
|
141
|
+
assertPlainObject(action, field);
|
|
142
|
+
assertNoInlineHandler(action, field);
|
|
143
|
+
const actionId = assertActionId(action.actionId, field + '.actionId');
|
|
144
|
+
if (typeof action.label !== 'string')
|
|
145
|
+
badInput(field + '.label 必须是字符串');
|
|
146
|
+
titleRow.push('<button type="button" class="' + STYLE_PREFIX + 'toast-act" ' + ACTION_ID_ATTR + '="' + esc(actionId) + '">' + esc(action.label) + '</button>');
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const body = ['<div class="' + STYLE_PREFIX + TOAST_TITLE_ROW_CLASS + '">' + titleRow.join('') + '</div>'];
|
|
150
|
+
if (typeof toast.detail === 'string' && toast.detail !== '') {
|
|
151
|
+
body.push('<div class="' + STYLE_PREFIX + 'toast-detail">' + esc(toast.detail) + '</div>');
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(toast.lines) && toast.lines.length > 0) {
|
|
154
|
+
body.push('<div class="' + STYLE_PREFIX + 'toast-lines">' + toast.lines.map((line) => esc(String(line))).join('<br>') + '</div>');
|
|
155
|
+
}
|
|
156
|
+
if (typeof toast.code === 'string' && toast.code !== '') {
|
|
157
|
+
body.push('<pre class="' + STYLE_PREFIX + 'toast-code">' + esc(toast.code) + '</pre>');
|
|
158
|
+
}
|
|
159
|
+
return ('<div class="' + STYLE_PREFIX + 'toast" role="' + TOAST_DEFAULTS.role + '" aria-live="' + TOAST_DEFAULTS.ariaLive + '" data-max="' + maxStack + '">'
|
|
160
|
+
+ head.join('')
|
|
161
|
+
+ '<div class="' + STYLE_PREFIX + TOAST_BODY_CLASS + '">' + body.join('') + '</div>'
|
|
162
|
+
+ '<button type="button" class="' + STYLE_PREFIX + TOAST_CLOSE_CLASS + '">' + TOAST_CLOSE_LABEL + '</button>'
|
|
163
|
+
+ '</div>');
|
|
164
|
+
};
|
|
165
|
+
/** 冻结签名:`createToastController(port: ToastHostPort): ToastController`。
|
|
166
|
+
*
|
|
167
|
+
* 堆叠口径(旧层 `stackCap()` 同语义):容量 = 栈内各条 `maxStack` 的**最大值**,空栈回落
|
|
168
|
+
* `TOAST_DEFAULTS.maxStack`;超容量 FIFO 挤出最旧;单条独立计时(`timeoutMs` 缺省 4500)。
|
|
169
|
+
* `flush()` 清栈;`dispose()` = `flush()` + 之后 `show()` 变 no-op(幂等)。 */
|
|
170
|
+
export const createToastController = (port) => {
|
|
171
|
+
assertPlainObject(port, 'createToastController: port');
|
|
172
|
+
if (typeof port.mount !== 'function')
|
|
173
|
+
badInput('createToastController: port.mount 必须是函数');
|
|
174
|
+
const entries = [];
|
|
175
|
+
let disposed = false;
|
|
176
|
+
const drop = (entry) => {
|
|
177
|
+
if (entry.timer !== null) {
|
|
178
|
+
clearTimeout(entry.timer);
|
|
179
|
+
entry.timer = null;
|
|
180
|
+
}
|
|
181
|
+
const index = entries.indexOf(entry);
|
|
182
|
+
if (index >= 0)
|
|
183
|
+
entries.splice(index, 1);
|
|
184
|
+
entry.handle.remove();
|
|
185
|
+
};
|
|
186
|
+
const capacity = () => {
|
|
187
|
+
let cap = 0;
|
|
188
|
+
for (const entry of entries)
|
|
189
|
+
if (entry.cap > cap)
|
|
190
|
+
cap = entry.cap;
|
|
191
|
+
return cap > 0 ? cap : TOAST_DEFAULTS.maxStack;
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
show(input) {
|
|
195
|
+
if (disposed)
|
|
196
|
+
return;
|
|
197
|
+
const html = renderToast(input);
|
|
198
|
+
const cap = positiveInt(input === null || input === undefined ? undefined : input.maxStack, TOAST_DEFAULTS.maxStack);
|
|
199
|
+
const timeoutMs = positiveInt(input === null || input === undefined ? undefined : input.timeoutMs, TOAST_DEFAULTS.timeoutMs);
|
|
200
|
+
const entry = { handle: port.mount(html), cap, timer: null };
|
|
201
|
+
entries.push(entry);
|
|
202
|
+
entry.timer = setTimeout(() => drop(entry), timeoutMs);
|
|
203
|
+
while (entries.length > capacity()) {
|
|
204
|
+
const oldest = entries[0];
|
|
205
|
+
if (oldest === undefined)
|
|
206
|
+
break;
|
|
207
|
+
drop(oldest);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
flush() {
|
|
211
|
+
for (const entry of [...entries])
|
|
212
|
+
drop(entry);
|
|
213
|
+
},
|
|
214
|
+
dispose() {
|
|
215
|
+
if (disposed)
|
|
216
|
+
return;
|
|
217
|
+
disposed = true;
|
|
218
|
+
for (const entry of [...entries])
|
|
219
|
+
drop(entry);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
/* ── copyText 双通道(Q13/AC-16①) ──────────────────────────────────── */
|
|
224
|
+
function assertCopyPorts(ports, field) {
|
|
225
|
+
if (ports === null || typeof ports !== 'object')
|
|
226
|
+
badInput(field + ': ports 必填');
|
|
227
|
+
if (typeof ports.fallback !== 'function')
|
|
228
|
+
badInput(field + ': ports.fallback 必须是函数');
|
|
229
|
+
const clipboard = ports.clipboard;
|
|
230
|
+
if (clipboard !== null && clipboard !== undefined && typeof clipboard.writeText !== 'function') {
|
|
231
|
+
badInput(field + ': ports.clipboard.writeText 必须是函数');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function pickCopyText(value, fallback) {
|
|
235
|
+
return typeof value === 'string' && value !== '' ? value : fallback;
|
|
236
|
+
}
|
|
237
|
+
/** 成功反馈:`opts.toast.ok` 逐字段覆盖(`silent` 只静默它,不静默失败徽章)。 */
|
|
238
|
+
function okToastInput(opts) {
|
|
239
|
+
const override = opts.toast === undefined || opts.toast === null ? undefined : opts.toast.ok;
|
|
240
|
+
const input = {
|
|
241
|
+
msg: pickCopyText(override?.msg, COPY_TEXT_DEFAULTS.okMessage),
|
|
242
|
+
detail: typeof override?.detail === 'string' ? override.detail : COPY_TEXT_DEFAULTS.okDetail,
|
|
243
|
+
};
|
|
244
|
+
return typeof override?.icon === 'string' ? { ...input, icon: override.icon } : input;
|
|
245
|
+
}
|
|
246
|
+
/** 失败反馈:徽章恒在(`badge.type` 恒 `danger`,文案取冻结的 `STATUS_DEFAULT_TEXT.danger`)。 */
|
|
247
|
+
function failToastInput(opts) {
|
|
248
|
+
const override = opts.toast === undefined || opts.toast === null ? undefined : opts.toast.fail;
|
|
249
|
+
return {
|
|
250
|
+
msg: pickCopyText(override?.msg, COPY_TEXT_DEFAULTS.failMessage),
|
|
251
|
+
detail: typeof override?.detail === 'string' ? override.detail : COPY_TEXT_DEFAULTS.failDetail,
|
|
252
|
+
icon: typeof override?.icon === 'string' ? override.icon : 'danger',
|
|
253
|
+
badge: { type: 'danger', text: STATUS_DEFAULT_TEXT.danger },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** 冻结签名:`copyText(text: string, ports: CopyPorts, opts?: CopyTextOptions): Promise<CopyTextOutcome>`。 */
|
|
257
|
+
export const copyText = async (text, ports, opts) => {
|
|
258
|
+
assertCopyPorts(ports, 'copyText');
|
|
259
|
+
if (typeof text !== 'string')
|
|
260
|
+
badInput('copyText: text 必须是字符串');
|
|
261
|
+
const options = opts === null || opts === undefined || typeof opts !== 'object' ? {} : opts;
|
|
262
|
+
if (COPY_TEXT_DEFAULTS.emptyTextShortCircuit && text === '') {
|
|
263
|
+
return { ok: false, channel: null, reason: 'empty' };
|
|
264
|
+
}
|
|
265
|
+
const mountFeedback = (input) => {
|
|
266
|
+
const host = ports.toast;
|
|
267
|
+
if (host === null || host === undefined)
|
|
268
|
+
return;
|
|
269
|
+
if (typeof host.mount !== 'function')
|
|
270
|
+
badInput('copyText: ports.toast.mount 必须是函数');
|
|
271
|
+
host.mount(renderToast(input));
|
|
272
|
+
};
|
|
273
|
+
const settle = (ok, channel, reason) => {
|
|
274
|
+
if (ok) {
|
|
275
|
+
if (options.silent !== true)
|
|
276
|
+
mountFeedback(okToastInput(options));
|
|
277
|
+
if (typeof options.onOk === 'function')
|
|
278
|
+
options.onOk(channel);
|
|
279
|
+
return { ok: true, channel };
|
|
280
|
+
}
|
|
281
|
+
if (COPY_TEXT_DEFAULTS.failBadgeAlwaysOn)
|
|
282
|
+
mountFeedback(failToastInput(options));
|
|
283
|
+
if (typeof options.onFail === 'function')
|
|
284
|
+
options.onFail(reason ?? 'unknown');
|
|
285
|
+
return { ok: false, channel: null, reason };
|
|
286
|
+
};
|
|
287
|
+
const clipboard = ports.clipboard ?? null;
|
|
288
|
+
if (clipboard !== null) {
|
|
289
|
+
try {
|
|
290
|
+
await clipboard.writeText(text);
|
|
291
|
+
return settle(true, 'clipboard');
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// 通道 1 失败 → 通道 2(不得只留 execCommand 的单通道,§3.3)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 通道 2:**同步抛错同样不得逃出**(§3.3「两通道皆失败 → `{ok:false,…}`(不抛错)」+「失败徽章恒在」;
|
|
298
|
+
// 旧层 `_fbCopy` 整段 try/catch、失败返回 false 同口径)。只包住 `ports.fallback` 调用本身,
|
|
299
|
+
// 不包 `settle()`——否则反馈通道抛错会被误判成「通道 2 失败」而重复出徽章。
|
|
300
|
+
let fallbackOk = false;
|
|
301
|
+
try {
|
|
302
|
+
fallbackOk = Boolean(ports.fallback(text));
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return settle(false, null, 'fallback-threw');
|
|
306
|
+
}
|
|
307
|
+
return fallbackOk ? settle(true, 'fallback') : settle(false, null, 'fallback-failed');
|
|
308
|
+
};
|
|
309
|
+
/** 冻结签名:`createCopyRuntime(ports: CopyPorts): CopyRuntime`。
|
|
310
|
+
*
|
|
311
|
+
* 除转发 `copyText` 外,本 runtime 记录自己挂载的反馈节点,`dispose()` 一并移除(幂等)。
|
|
312
|
+
* 「dispose 后仍可复制」是文档未规定处的取值:`dispose` 只回收反馈节点,不改端口语义。 */
|
|
313
|
+
export const createCopyRuntime = (ports) => {
|
|
314
|
+
assertCopyPorts(ports, 'createCopyRuntime');
|
|
315
|
+
const handles = new Set();
|
|
316
|
+
const host = ports.toast;
|
|
317
|
+
const wrapped = host === null || host === undefined
|
|
318
|
+
? ports
|
|
319
|
+
: {
|
|
320
|
+
clipboard: ports.clipboard,
|
|
321
|
+
fallback: ports.fallback,
|
|
322
|
+
toast: {
|
|
323
|
+
mount: (html) => {
|
|
324
|
+
const handle = host.mount(html);
|
|
325
|
+
handles.add(handle);
|
|
326
|
+
return {
|
|
327
|
+
remove: () => {
|
|
328
|
+
handles.delete(handle);
|
|
329
|
+
handle.remove();
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
copyText: (text, opts) => copyText(text, wrapped, opts),
|
|
337
|
+
dispose: () => {
|
|
338
|
+
for (const handle of [...handles])
|
|
339
|
+
handle.remove();
|
|
340
|
+
handles.clear();
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
/* ── 复制接线(FX-3③/FX-17;#90 仅凭契约即可接线) ──────────────────── */
|
|
345
|
+
/** 冻结签名:`bindCopyAction(port, ports, opts?): { dispose(): void }`。
|
|
346
|
+
*
|
|
347
|
+
* - id 来源**只有** `port.listActionIds()`:不猜 id、不通配前缀、不订阅未列出的 id;
|
|
348
|
+
* - 重复 id 由 binder 自行去重(同一 id 只订阅一次);
|
|
349
|
+
* - 激活时读 `port.readDataText(actionId)`:非字符串(含 `undefined`)→ 跳过(不抛错、不产 toast),
|
|
350
|
+
* 非复制按钮(场景按钮)即由此跳过,不另设白名单;
|
|
351
|
+
* - 反馈一律走 `ports.toast`(`copyText` 内部),binder 不另出反馈;
|
|
352
|
+
* - **激活回调恒不产生未处理拒绝**(FX-76-6):`copyText` 正常路径的失败已在内部 `settle()` 处理,
|
|
353
|
+
* 这里只兜「逃出 `copyText` 的异常」(端口实现抛错/`ports.toast.mount` 抛错/回调抛错)——
|
|
354
|
+
* 契约 §3.3 明写 binder 不另出反馈,故不产第二份 UI 反馈,也不重入 `opts.onFail`(防重复回调);
|
|
355
|
+
* - `dispose()` 解绑全部已订阅 id 且幂等;解绑后激活不再触发复制。 */
|
|
356
|
+
export const bindCopyAction = (port, ports, opts) => {
|
|
357
|
+
assertPlainObject(port, 'bindCopyAction: port');
|
|
358
|
+
if (typeof port.listActionIds !== 'function')
|
|
359
|
+
badInput('bindCopyAction: port.listActionIds 必须是函数');
|
|
360
|
+
if (typeof port.readDataText !== 'function')
|
|
361
|
+
badInput('bindCopyAction: port.readDataText 必须是函数');
|
|
362
|
+
if (typeof port.onActivate !== 'function')
|
|
363
|
+
badInput('bindCopyAction: port.onActivate 必须是函数');
|
|
364
|
+
assertCopyPorts(ports, 'bindCopyAction');
|
|
365
|
+
const unbinds = [];
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
let disposed = false;
|
|
368
|
+
const list = port.listActionIds();
|
|
369
|
+
for (const actionId of Array.isArray(list) ? list : []) {
|
|
370
|
+
if (typeof actionId !== 'string' || actionId === '' || seen.has(actionId))
|
|
371
|
+
continue;
|
|
372
|
+
seen.add(actionId);
|
|
373
|
+
const off = port.onActivate(actionId, () => {
|
|
374
|
+
if (disposed)
|
|
375
|
+
return;
|
|
376
|
+
const text = port.readDataText(actionId);
|
|
377
|
+
if (typeof text !== 'string')
|
|
378
|
+
return;
|
|
379
|
+
// FX-76-6:`void` 丢弃结果,但**必须**接住 rejection(否则端口抛错 → unhandledRejection)。
|
|
380
|
+
// 反馈只经 `ports.toast`(§3.3),故此处不产反馈、不重入 `opts.onFail`。
|
|
381
|
+
void copyText(text, ports, opts).catch(() => { });
|
|
382
|
+
});
|
|
383
|
+
unbinds.push(typeof off === 'function' ? off : () => { });
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
dispose: () => {
|
|
387
|
+
if (disposed)
|
|
388
|
+
return;
|
|
389
|
+
disposed = true;
|
|
390
|
+
for (const off of unbinds)
|
|
391
|
+
off();
|
|
392
|
+
unbinds.length = 0;
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
};
|
|
396
|
+
/* ── 共享 JS 文本的唯一产出者(FX-2③/FX-18;#74 只消费) ─────────────── */
|
|
397
|
+
/** JS 字符串字面量(产出文本里嵌值唯一出口)。 */
|
|
398
|
+
function jsStr(value) {
|
|
399
|
+
return JSON.stringify(value);
|
|
400
|
+
}
|
|
401
|
+
function helpersPrefix(input) {
|
|
402
|
+
const prefix = input === undefined || input === null ? undefined : input.prefix;
|
|
403
|
+
return typeof prefix === 'string' && prefix !== '' ? prefix : STYLE_PREFIX;
|
|
404
|
+
}
|
|
405
|
+
function helpersDataAttr(input) {
|
|
406
|
+
const attr = input === undefined || input === null ? undefined : input.dataAttr;
|
|
407
|
+
return typeof attr === 'string' && attr !== '' ? attr : DEFAULT_DATA_ATTR;
|
|
408
|
+
}
|
|
409
|
+
/* ── #88 S4:HELP 速查台运行时增强的类名来源(**不引 `src/help.ts`**) ────────────
|
|
410
|
+
* helpers 运行时注入的元素必须与 `src/help.ts` 的 `cls()` 同一命名空间,否则样式落空。
|
|
411
|
+
* 这里**不** `import` help.ts:help.ts 已 `import { STYLE_PREFIX } from './style.js'`,
|
|
412
|
+
* controls.ts 再引 help.ts 会形成 `controls → help → template` 的潜在模块环;改为
|
|
413
|
+
* 「`CONTROL_STYLE_SECTIONS` 闭集里 kebab 后与 `HELP_SHELL_ID` 同值者」这条**与 help.ts:87-93
|
|
414
|
+
* 逐条同构**的派生式,闭集漂移即在此 fail-fast(不静默换命名空间)。两侧一致性由
|
|
415
|
+
* `test/help-center-js-88.test.mjs` 机读钉死(对照 `renderHelpShell` 的真实产出类名)。 */
|
|
416
|
+
/** 样式区名 kebab(`helpShell` → `help-shell`):与 `src/help.ts:81-83` 同一映射。 */
|
|
417
|
+
function styleSectionSlug(section) {
|
|
418
|
+
return section.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase());
|
|
419
|
+
}
|
|
420
|
+
/** HELP 壳类名命名空间 kebab 后缀(`help-shell`;缺省前缀下根 = `ilife-help-shell`)。
|
|
421
|
+
* **延迟求值**(不在模块顶层):`style.ts → charts.ts → controls.ts → style.ts` 存在模块环,
|
|
422
|
+
* 顶层读 `STYLE_PREFIX` 会在 `style.js` 初始化完成前触发 TDZ(实测 `ReferenceError`),
|
|
423
|
+
* 故派生式只在 `buildSharedHelpersJs()` **调用时**执行(此时全部模块已初始化)。 */
|
|
424
|
+
function helpShellSlug() {
|
|
425
|
+
const section = CONTROL_STYLE_SECTIONS.find((name) => STYLE_PREFIX + styleSectionSlug(name) === HELP_SHELL_ID);
|
|
426
|
+
if (section === undefined) {
|
|
427
|
+
throw new Error('base-paint/controls:CONTROL_STYLE_SECTIONS 闭集缺与 HELP_SHELL_ID 同 kebab 的区名('
|
|
428
|
+
+ HELP_SHELL_ID + ')');
|
|
429
|
+
}
|
|
430
|
+
return styleSectionSlug(section);
|
|
431
|
+
}
|
|
432
|
+
/** 回到顶部按钮的 `id`(视觉尺 H-19 逐字点名 `#backTop`;`backTop` 是**冻结规格值**)。 */
|
|
433
|
+
const HELP_BACKTOP_ID = 'backTop';
|
|
434
|
+
/** H-19:`scrollY > 400` 才出现(阈值逐字取规格)。 */
|
|
435
|
+
const HELP_BACKTOP_MIN_Y = 400;
|
|
436
|
+
/** H-19:按钮字形 `↑`(18px/600 由 CSS 承担)。 */
|
|
437
|
+
const HELP_BACKTOP_LABEL = '↑';
|
|
438
|
+
/** 无障碍名(文档未规定 → 本票取值,与 `TOAST_CLOSE_LABEL` 同口径记账)。 */
|
|
439
|
+
const HELP_BACKTOP_ARIA = '回到顶部';
|
|
440
|
+
/** 搜索框占位文案(F3 `卡路里.html` 的 `#sB` 逐字:`搜索全部场景`)。 */
|
|
441
|
+
const HELP_SEARCH_PLACEHOLDER = '搜索全部场景';
|
|
442
|
+
/** 搜索框无障碍名(文档未规定 → 本票取值)。 */
|
|
443
|
+
const HELP_SEARCH_ARIA = '搜索全部场景';
|
|
444
|
+
/** 清空按钮字形(F3 `#sClear` 逐字:`✕`)与无障碍名。 */
|
|
445
|
+
const HELP_CLEAR_LABEL = '✕';
|
|
446
|
+
const HELP_CLEAR_ARIA = '清空搜索';
|
|
447
|
+
/** H-16 双反馈的**按钮通道**类名(#121;规格逐字 `copied`,**非** `ilife-` 前缀 → 不占样式区命名空间)。
|
|
448
|
+
* CSS 侧两处已就位:`style.ts` 的 `copyButton` 区(`.ilife-copy-btn.copied`,#75)与本区追加的
|
|
449
|
+
* helpShell 复制按钮 `.copied`(#121);本常量只供运行时**加类**,不产第二份样式真相。 */
|
|
450
|
+
const HELP_COPY_COPIED_CLASS = 'copied';
|
|
451
|
+
/** 复制成功态停留时长 = 弹簧动画时长 **450ms**(`docs/visual-spec-help.md:195,197`「按钮变绿进入
|
|
452
|
+
* `copied` 态并跑 450ms 弹簧动画」)。与 `style.ts` 的 `transition: transform .45s …` 同源数值,
|
|
453
|
+
* 由 `test/copy-copied-121.test.mjs` 跨文件交叉钉死(改一侧不改另一侧即红)。 */
|
|
454
|
+
const HELP_COPY_COPIED_MS = 450;
|
|
455
|
+
/** 命中计数文案(F3 `#hitC` 逐字:`匹配 N 个场景`)。 */
|
|
456
|
+
const HELP_HIT_PREFIX = '匹配 ';
|
|
457
|
+
const HELP_HIT_SUFFIX = ' 个场景';
|
|
458
|
+
/** 零命中文案(F3 `#emptyC` 逐字)。 */
|
|
459
|
+
const HELP_HIT_EMPTY = '没有找到相关场景,换个词试试~';
|
|
460
|
+
/** 冻结签名:`buildSharedHelpersJs(input?: SharedHelpersInput): string`。
|
|
461
|
+
*
|
|
462
|
+
* 产出恒为**非空**、**经典 script** 作用域可跑的 IIFE(无 `import`/`export`/顶层 `await`),
|
|
463
|
+
* 逐项满足 `SHARED_HELPERS_JS_RULE`:自包含(不依赖其它脚本或既有全局)/幂等(判据**只落 DOM**:
|
|
464
|
+
* 标记属性 + `querySelector` 早退,**不**用全局哨兵)/允许页面侧 DOM(`document.*` 读取与事件绑定)/
|
|
465
|
+
* 不向 `window.<id>`/`globalThis.<id>` 赋值/不引 `node:`。
|
|
466
|
+
*
|
|
467
|
+
* 功能面(文档无规定,本文记账):① 幂等挂载点标记;② 事件委派 `[ACTION_ID_ATTR]` 点击 →
|
|
468
|
+
* 读 `dataAttr` 文本 → 复制(`navigator.clipboard` → `execCommand` 兜底);③ 反馈块
|
|
469
|
+
* **与 `renderToast` 同构**(`.toast` > `.toast-icon` + `.toast-body` > `.toast-title-row` + 可选
|
|
470
|
+
* `.toast-title-detail`,`.toast-close` 在 body 之外;W1 根因修 + F-c 图标),`prefix` 命名空间类,
|
|
471
|
+
* `maxStack`/`timeoutMs`/移动端收窄取冻结常量;④ 关闭按钮。
|
|
472
|
+
* 产出文本里的文案/数值**全部**取自冻结常量(不产第二份真相)。
|
|
473
|
+
* 注:`execCommand` 兜底用临时 textarea 的两个内联定位属性(`position`/`left`),属临时节点
|
|
474
|
+
* 定位而非控件样式,不构成第二份样式常量。
|
|
475
|
+
*
|
|
476
|
+
* **#88 S4 追加(HELP 速查台运行时增强;文档无规定 → 本文记账)**:全部挂进**既有 `boot()`**、
|
|
477
|
+
* 共用**既有幂等 marker**(`MARKER_SEL` + `querySelector` 早退),**不新增第二个标记**;每一项都
|
|
478
|
+
* 先判 HELP 壳是否在页面(helpers 被所有技能页面共享,非 HELP 页逐项早退、零副作用):
|
|
479
|
+
* - **卡级复制按钮**(`card-copy`):每张场景卡的**卡头**注入 1 个按钮,`actionId`/文案恒读
|
|
480
|
+
* `HELP_COPY_ACTIONS.prompt`,`dataAttr` 取同卡 `<pre class="prompt">` 的原文(=该卡 prompt 逐字;
|
|
481
|
+
* #88 S5 起改以 `<pre>` 为**主源**,读不到才回落 Sheet 内 prompt 按钮的 data-t),
|
|
482
|
+
* 复用上面的 `[ACTION_ID_ATTR]` 委派(零新增监听、零契约变更);卡内已有该类即跳过(幂等)。
|
|
483
|
+
* - **搜索**(`tab-search` + `tab-search-input` + `tab-search-clear` + `page-hitcount`):跨分组过滤卡片、
|
|
484
|
+
* `<mark class="card-mark">` 高亮命中、自动展开命中卡片的 Sheet 与子功能组、命中计数、
|
|
485
|
+
* 清空复原、`Enter` 在命中分组页间跳页(自动跳第一个命中页)。
|
|
486
|
+
* - **Sheet 参数实时预览**:`editable_fields` 的静态值换成输入框,输入即重组
|
|
487
|
+
* 「prompt + 空行 + `label: value` 行」(F3 `buildPrompt` 语义),并同步 prompt/params 复制按钮的
|
|
488
|
+
* `dataAttr`(否则复制到的是编辑前的旧文本)。
|
|
489
|
+
* - **回到顶部**(`btn-backtop`,`id="backTop"`,H-19):`scrollTop > 400` 加 `-show`,点击平滑回顶。
|
|
490
|
+
* 纯度:只用 `document.*`(含只读 `document.scrollingElement`)+ 既有只读 `window.matchMedia`;
|
|
491
|
+
* 不向 `window.<id>`/`globalThis.<id>` 赋值、不引 `node:`、不用 `classList`(类名走 `className` 字符串
|
|
492
|
+
* 增删,兼容 `style.test.mjs` T28 的「动效纯 CSS」断言)、不产内联 `on*`。
|
|
493
|
+
*
|
|
494
|
+
* **#121 追加(H-16 双反馈的按钮通道;文档 `docs/visual-spec-help.md:195,197`)**:复制**成功**时给
|
|
495
|
+
* **被点击的那个按钮**加 `copied` 类(`HELP_COPY_COPIED_CLASS`,规格逐字、无 `ilife-` 前缀),
|
|
496
|
+
* `HELP_COPY_COPIED_MS`(450ms)后移除;失败路径**不**加类(不静默变绿)。落点仍在**既有
|
|
497
|
+
* `boot()`/既有委派**内:`onClick` 把命中的按钮一并传给 `copy()` → 两条成功通道
|
|
498
|
+
* (`navigator.clipboard`/`execCommand` 兜底)各自在成功分支调 `markCopied(btn)`,零新增监听、
|
|
499
|
+
* 零新增 marker、零新签名;类名增删复用既有 `addClass`/`removeClass`(`className` 字符串口径)。 */
|
|
500
|
+
export const buildSharedHelpersJs = (input) => {
|
|
501
|
+
const prefix = helpersPrefix(input);
|
|
502
|
+
const dataAttr = helpersDataAttr(input);
|
|
503
|
+
const markerAttr = 'data-' + prefix.replace(/-+$/, '') + '-helpers';
|
|
504
|
+
const markerSelector = '[' + markerAttr + '="1"]';
|
|
505
|
+
// #88 S4:HELP 壳命名空间(缺省 `ilife-help-shell`;`prefix` 覆盖时随动,不残留缺省前缀)。
|
|
506
|
+
const shellClass = prefix + helpShellSlug();
|
|
507
|
+
const shellPart = (suffix) => shellClass + '-' + suffix;
|
|
508
|
+
const promptAttr = 'data-' + prefix.replace(/-+$/, '') + '-help-prompt';
|
|
509
|
+
const copyAction = HELP_COPY_ACTIONS.prompt;
|
|
510
|
+
const paramsAction = HELP_COPY_ACTIONS.params;
|
|
511
|
+
const lines = [
|
|
512
|
+
'(function () {',
|
|
513
|
+
" 'use strict';",
|
|
514
|
+
' var MARKER_ATTR = ' + jsStr(markerAttr) + ';',
|
|
515
|
+
' var MARKER_SEL = ' + jsStr(markerSelector) + ';',
|
|
516
|
+
' var ACTION_ATTR = ' + jsStr(ACTION_ID_ATTR) + ';',
|
|
517
|
+
' var TEXT_ATTR = ' + jsStr(dataAttr) + ';',
|
|
518
|
+
' var STACK_CLASS = ' + jsStr(prefix + 'toast-stack') + ';',
|
|
519
|
+
' var TOAST_CLASS = ' + jsStr(prefix + 'toast') + ';',
|
|
520
|
+
' var ICON_CLASS = ' + jsStr(prefix + 'toast-icon') + ';',
|
|
521
|
+
' var ICON_GLYPH = ' + jsStr(TOAST_ICON_GLYPHS[TOAST_DEFAULTS.defaultIcon]) + ';',
|
|
522
|
+
' var TITLE_CLASS = ' + jsStr(prefix + 'toast-title') + ';',
|
|
523
|
+
' var BODY_CLASS = ' + jsStr(prefix + TOAST_BODY_CLASS) + ';',
|
|
524
|
+
' var TITLE_ROW_CLASS = ' + jsStr(prefix + TOAST_TITLE_ROW_CLASS) + ';',
|
|
525
|
+
' var DETAIL_CLASS = ' + jsStr(prefix + TOAST_DETAIL_CLASS) + ';',
|
|
526
|
+
' var CLOSE_CLASS = ' + jsStr(prefix + TOAST_CLOSE_CLASS) + ';',
|
|
527
|
+
' var OK_MSG = ' + jsStr(COPY_TEXT_DEFAULTS.okMessage) + ';',
|
|
528
|
+
' var FAIL_MSG = ' + jsStr(COPY_TEXT_DEFAULTS.failMessage) + ';',
|
|
529
|
+
' var FAIL_DETAIL = ' + jsStr(COPY_TEXT_DEFAULTS.failDetail) + ';',
|
|
530
|
+
' var CLOSE_LABEL = ' + jsStr(TOAST_CLOSE_LABEL) + ';',
|
|
531
|
+
' var TIMEOUT_MS = ' + TOAST_DEFAULTS.timeoutMs + ';',
|
|
532
|
+
' var MAX_STACK = ' + TOAST_DEFAULTS.maxStack + ';',
|
|
533
|
+
' var MOBILE_MAX_STACK = ' + TOAST_DEFAULTS.mobileMaxStack + ';',
|
|
534
|
+
' var MOBILE_MAX_PX = ' + TOAST_DEFAULTS.mobileMaxPx + ';',
|
|
535
|
+
' var LF = String.fromCharCode(10);',
|
|
536
|
+
' var LF2 = LF + LF;',
|
|
537
|
+
// #88 S4:HELP 速查台类名/选择器(同一命名空间,逐条由 `shellClass` 派生)
|
|
538
|
+
' var SHELL_SEL = ' + jsStr('.' + shellClass) + ';',
|
|
539
|
+
' var CARD_CLASS = ' + jsStr(shellPart('card')) + ';',
|
|
540
|
+
' var CARD_TOP_CLASS = ' + jsStr(shellPart('card-top')) + ';',
|
|
541
|
+
' var CARD_COPY_CLASS = ' + jsStr(shellPart('card-copy')) + ';',
|
|
542
|
+
' var CARD_MARK_CLASS = ' + jsStr(shellPart('card-mark')) + ';',
|
|
543
|
+
' var CARD_HIDDEN_CLASS = ' + jsStr(shellPart('card-hidden')) + ';',
|
|
544
|
+
' var SHEET_CLASS = ' + jsStr(shellPart('sheet')) + ';',
|
|
545
|
+
' var PROMPT_CLASS = ' + jsStr(shellPart('prompt')) + ';',
|
|
546
|
+
' var PROMPT_ATTR = ' + jsStr(promptAttr) + ';',
|
|
547
|
+
' var FIELD_CLASS = ' + jsStr(shellPart('field')) + ';',
|
|
548
|
+
' var FIELD_LABEL_CLASS = ' + jsStr(shellPart('field-label')) + ';',
|
|
549
|
+
' var FIELD_VALUE_CLASS = ' + jsStr(shellPart('field-value')) + ';',
|
|
550
|
+
' var FIELD_HINT_CLASS = ' + jsStr(shellPart('field-hint')) + ';',
|
|
551
|
+
' var FIELD_INPUT_CLASS = ' + jsStr(shellPart('field-input')) + ';',
|
|
552
|
+
' var TAB_BAR_CLASS = ' + jsStr(shellPart('tab-bar')) + ';',
|
|
553
|
+
' var TAB_INPUT_CLASS = ' + jsStr(shellPart('tab-input')) + ';',
|
|
554
|
+
' var SEARCH_BOX_CLASS = ' + jsStr(shellPart('tab-search')) + ';',
|
|
555
|
+
' var SEARCH_INPUT_CLASS = ' + jsStr(shellPart('tab-search-input')) + ';',
|
|
556
|
+
' var SEARCH_CLEAR_CLASS = ' + jsStr(shellPart('tab-search-clear')) + ';',
|
|
557
|
+
' var HITCOUNT_CLASS = ' + jsStr(shellPart('page-hitcount')) + ';',
|
|
558
|
+
' var PAGE_CLASS = ' + jsStr(shellPart('page')) + ';',
|
|
559
|
+
' var SUBGROUP_CLASS = ' + jsStr(shellPart('subgroup')) + ';',
|
|
560
|
+
' var SUBGROUP_HIDDEN_CLASS = ' + jsStr(shellPart('subgroup-hidden')) + ';',
|
|
561
|
+
' var CLI_CLASS = ' + jsStr(shellPart('cli')) + ';',
|
|
562
|
+
' var BACKTOP_CLASS = ' + jsStr(shellPart('btn-backtop')) + ';',
|
|
563
|
+
' var BACKTOP_SHOW_CLASS = ' + jsStr(shellPart('btn-backtop-show')) + ';',
|
|
564
|
+
' var BACKTOP_ID = ' + jsStr(HELP_BACKTOP_ID) + ';',
|
|
565
|
+
' var BACKTOP_MIN_Y = ' + HELP_BACKTOP_MIN_Y + ';',
|
|
566
|
+
' var BACKTOP_LABEL = ' + jsStr(HELP_BACKTOP_LABEL) + ';',
|
|
567
|
+
' var BACKTOP_ARIA = ' + jsStr(HELP_BACKTOP_ARIA) + ';',
|
|
568
|
+
' var SEARCH_PLACEHOLDER = ' + jsStr(HELP_SEARCH_PLACEHOLDER) + ';',
|
|
569
|
+
' var SEARCH_ARIA = ' + jsStr(HELP_SEARCH_ARIA) + ';',
|
|
570
|
+
' var CLEAR_LABEL = ' + jsStr(HELP_CLEAR_LABEL) + ';',
|
|
571
|
+
' var CLEAR_ARIA = ' + jsStr(HELP_CLEAR_ARIA) + ';',
|
|
572
|
+
' var HIT_PREFIX = ' + jsStr(HELP_HIT_PREFIX) + ';',
|
|
573
|
+
' var HIT_SUFFIX = ' + jsStr(HELP_HIT_SUFFIX) + ';',
|
|
574
|
+
' var HIT_EMPTY = ' + jsStr(HELP_HIT_EMPTY) + ';',
|
|
575
|
+
' var COPY_ACTION = ' + jsStr(copyAction.actionId) + ';',
|
|
576
|
+
' var COPY_LABEL = ' + jsStr(copyAction.label) + ';',
|
|
577
|
+
' var COPIED_CLASS = ' + jsStr(HELP_COPY_COPIED_CLASS) + ';',
|
|
578
|
+
' var COPIED_MS = ' + HELP_COPY_COPIED_MS + ';',
|
|
579
|
+
' var PARAMS_ACTION = ' + jsStr(paramsAction.actionId) + ';',
|
|
580
|
+
' var searchTerm = "";',
|
|
581
|
+
' var clickBound = false;',
|
|
582
|
+
// #88 S5(B 段红队 S3-a):委派去重的**跨实例**判据——同一页面里 helpers 被多次注入时,
|
|
583
|
+
// 每个实例各有自己的闭包(`clickBound` 管不住别人),故在**事件对象**上打一次性标记:
|
|
584
|
+
// 同一事件只会被第一个处理器消费,一次点击恒只复制 1 次/出 1 个 toast。
|
|
585
|
+
' var HANDLED_PROP = ' + jsStr(prefix + 'help-click-handled') + ';',
|
|
586
|
+
'',
|
|
587
|
+
' function boot() {',
|
|
588
|
+
' if (!document.body) return;',
|
|
589
|
+
' if (document.querySelector(MARKER_SEL)) return;',
|
|
590
|
+
' var marker = document.createElement("span");',
|
|
591
|
+
' marker.setAttribute(MARKER_ATTR, "1");',
|
|
592
|
+
' marker.hidden = true;',
|
|
593
|
+
' document.body.appendChild(marker);',
|
|
594
|
+
// #88 S5(B 段红队 S3-a):委派**只挂一次**——marker 被删后重入 boot() 不得重复 addEventListener,
|
|
595
|
+
// 否则一次点击触发 N 次复制/N 个 toast。按钮/搜索框/#backTop 各自的幂等由自身早退保证。
|
|
596
|
+
' if (!clickBound) { clickBound = true; document.addEventListener("click", onClick); }',
|
|
597
|
+
' initHelpShell();',
|
|
598
|
+
' }',
|
|
599
|
+
'',
|
|
600
|
+
' function onClick(ev) {',
|
|
601
|
+
' if (ev[HANDLED_PROP]) return;',
|
|
602
|
+
' ev[HANDLED_PROP] = 1;',
|
|
603
|
+
' var node = ev.target;',
|
|
604
|
+
' if (!node || typeof node.closest !== "function") return;',
|
|
605
|
+
' var close = node.closest("." + CLOSE_CLASS);',
|
|
606
|
+
' if (close) {',
|
|
607
|
+
' var box = close.parentNode;',
|
|
608
|
+
' if (box && box.parentNode) box.parentNode.removeChild(box);',
|
|
609
|
+
' return;',
|
|
610
|
+
' }',
|
|
611
|
+
' var btn = node.closest("[" + ACTION_ATTR + "]");',
|
|
612
|
+
' if (!btn) return;',
|
|
613
|
+
' var text = btn.getAttribute(TEXT_ATTR);',
|
|
614
|
+
' if (text === null) return;',
|
|
615
|
+
' copy(text, btn);',
|
|
616
|
+
' }',
|
|
617
|
+
'',
|
|
618
|
+
' function copy(text, btn) {',
|
|
619
|
+
' var clip = navigator.clipboard;',
|
|
620
|
+
' if (clip && typeof clip.writeText === "function") {',
|
|
621
|
+
' try {',
|
|
622
|
+
' var done = clip.writeText(text);',
|
|
623
|
+
' if (done && typeof done.then === "function") {',
|
|
624
|
+
' done.then(function () { markCopied(btn); feedback(OK_MSG, false); }, function () { fallback(text, btn); });',
|
|
625
|
+
' return;',
|
|
626
|
+
' }',
|
|
627
|
+
' markCopied(btn);',
|
|
628
|
+
' feedback(OK_MSG, false);',
|
|
629
|
+
' return;',
|
|
630
|
+
' } catch (err) {',
|
|
631
|
+
' fallback(text, btn);',
|
|
632
|
+
' return;',
|
|
633
|
+
' }',
|
|
634
|
+
' }',
|
|
635
|
+
' fallback(text, btn);',
|
|
636
|
+
' }',
|
|
637
|
+
'',
|
|
638
|
+
' function fallback(text, btn) {',
|
|
639
|
+
' var sink = document.createElement("textarea");',
|
|
640
|
+
' sink.value = text;',
|
|
641
|
+
' sink.setAttribute("readonly", "readonly");',
|
|
642
|
+
' sink.setAttribute("aria-hidden", "true");',
|
|
643
|
+
' sink.style.position = "fixed";',
|
|
644
|
+
' sink.style.left = "-9999px";',
|
|
645
|
+
' document.body.appendChild(sink);',
|
|
646
|
+
' sink.select();',
|
|
647
|
+
' var done = false;',
|
|
648
|
+
' try { done = document.execCommand("copy"); } catch (err) { done = false; }',
|
|
649
|
+
' if (sink.parentNode) sink.parentNode.removeChild(sink);',
|
|
650
|
+
' if (done) markCopied(btn);',
|
|
651
|
+
' feedback(done ? OK_MSG : FAIL_MSG, !done);',
|
|
652
|
+
' }',
|
|
653
|
+
'',
|
|
654
|
+
' /* #121(H-16 双反馈的按钮通道):成功态加 `copied` 类,450ms 后移除。',
|
|
655
|
+
' 只在**成功**分支调用 → 失败路径不静默变绿;类名走 className 字符串(addClass/removeClass),',
|
|
656
|
+
' 不用 DOM 的类名列表 API(纯度口径);按钮缺失/已离页时 removeClass 仍安全(操作的是节点自身)。 */',
|
|
657
|
+
' function markCopied(btn) {',
|
|
658
|
+
' if (!btn) return;',
|
|
659
|
+
' addClass(btn, COPIED_CLASS);',
|
|
660
|
+
' setTimeout(function () { removeClass(btn, COPIED_CLASS); }, COPIED_MS);',
|
|
661
|
+
' }',
|
|
662
|
+
'',
|
|
663
|
+
' function feedback(msg, bad) {',
|
|
664
|
+
' var cap = MAX_STACK;',
|
|
665
|
+
' if (window.matchMedia && window.matchMedia("(max-width: " + MOBILE_MAX_PX + "px)").matches) cap = MOBILE_MAX_STACK;',
|
|
666
|
+
' var host = document.querySelector("." + STACK_CLASS);',
|
|
667
|
+
' if (!host) {',
|
|
668
|
+
' host = document.createElement("div");',
|
|
669
|
+
' host.className = STACK_CLASS;',
|
|
670
|
+
' document.body.appendChild(host);',
|
|
671
|
+
' }',
|
|
672
|
+
' var box = document.createElement("div");',
|
|
673
|
+
' box.className = bad ? TOAST_CLASS + " " + TOAST_CLASS + "-danger" : TOAST_CLASS;',
|
|
674
|
+
' box.setAttribute("role", ' + jsStr(TOAST_DEFAULTS.role) + ');',
|
|
675
|
+
' box.setAttribute("aria-live", ' + jsStr(TOAST_DEFAULTS.ariaLive) + ');',
|
|
676
|
+
// W1 根因修(结构对齐)+ F-c 图标:运行时 DOM 与静态产出器/旧层同构——
|
|
677
|
+
// box > .toast-icon + .toast-body(> .toast-title-row(> .toast-title) + 可选 .toast-title-detail) + .toast-close
|
|
678
|
+
// 旧层为 `.hm-toast-icon + .hm-toast-body(> .hm-toast-title-row + .hm-toast-detail) + .hm-toast-close`
|
|
679
|
+
// (`.scratch/t76/old-controls.md` §1.1)。图标字形取 `TOAST_ICON_GLYPHS[TOAST_DEFAULTS.defaultIcon]`
|
|
680
|
+
// (📋,旧层 `base.js:74`/契约 `:141` 缺省;与静态产出器 `renderToast` 同源,不产第二份真相)。
|
|
681
|
+
// 缺 body 包裹时标题/详情/关闭被排进同一 flex 行,
|
|
682
|
+
// 靠 `flex-wrap` + `flex:1 1 100%` 补丁才勉强分层,且把关闭按钮挤到第三行(R1 实测 113px 高)。
|
|
683
|
+
// F-c 自证:新增仅用 `document.createElement/className/setAttribute/textContent/appendChild`
|
|
684
|
+
// (`SHARED_HELPERS_JS_RULE.domAllowed=true` 允许 DOM 读取;未向 `window/globalThis` 赋值、
|
|
685
|
+
// 未引 `node:`、未用 `classList/show`),故不破坏 #76 冻结产出内容契约与 T28 纯 CSS 动效断言。
|
|
686
|
+
' var icon = document.createElement("span");',
|
|
687
|
+
' icon.className = ICON_CLASS;',
|
|
688
|
+
' icon.setAttribute("aria-hidden", "true");',
|
|
689
|
+
' icon.textContent = ICON_GLYPH;',
|
|
690
|
+
' var body = document.createElement("div");',
|
|
691
|
+
' body.className = BODY_CLASS;',
|
|
692
|
+
' var titleRow = document.createElement("div");',
|
|
693
|
+
' titleRow.className = TITLE_ROW_CLASS;',
|
|
694
|
+
' var title = document.createElement("span");',
|
|
695
|
+
' title.className = TITLE_CLASS;',
|
|
696
|
+
' title.textContent = msg;',
|
|
697
|
+
' titleRow.appendChild(title);',
|
|
698
|
+
' body.appendChild(titleRow);',
|
|
699
|
+
' if (bad) {',
|
|
700
|
+
' var detail = document.createElement("div");',
|
|
701
|
+
' detail.className = DETAIL_CLASS;',
|
|
702
|
+
' detail.textContent = FAIL_DETAIL;',
|
|
703
|
+
' body.appendChild(detail);',
|
|
704
|
+
' }',
|
|
705
|
+
' box.appendChild(icon);',
|
|
706
|
+
' box.appendChild(body);',
|
|
707
|
+
' var close = document.createElement("button");',
|
|
708
|
+
' close.type = "button";',
|
|
709
|
+
' close.className = CLOSE_CLASS;',
|
|
710
|
+
' close.textContent = CLOSE_LABEL;',
|
|
711
|
+
' box.appendChild(close);',
|
|
712
|
+
' host.appendChild(box);',
|
|
713
|
+
' while (host.children.length > cap) host.removeChild(host.children[0]);',
|
|
714
|
+
' setTimeout(function () {',
|
|
715
|
+
' if (box.parentNode) box.parentNode.removeChild(box);',
|
|
716
|
+
' }, TIMEOUT_MS);',
|
|
717
|
+
' }',
|
|
718
|
+
'',
|
|
719
|
+
// ── #88 S4:HELP 速查台运行时增强(见上方产出内容契约的记账;全部由既有 boot() 调用) ──
|
|
720
|
+
' function initHelpShell() {',
|
|
721
|
+
' var shell = document.querySelector(SHELL_SEL);',
|
|
722
|
+
' if (!shell) return;',
|
|
723
|
+
' var cards = allOf(shell, "." + CARD_CLASS);',
|
|
724
|
+
' if (cards.length === 0) return;',
|
|
725
|
+
' injectCardCopy(cards);',
|
|
726
|
+
' initSheetPreview(cards);',
|
|
727
|
+
' initSearch(shell, cards);',
|
|
728
|
+
' initBackTop();',
|
|
729
|
+
' }',
|
|
730
|
+
'',
|
|
731
|
+
' function allOf(root, sel) {',
|
|
732
|
+
' return Array.prototype.slice.call(root.querySelectorAll(sel));',
|
|
733
|
+
' }',
|
|
734
|
+
'',
|
|
735
|
+
' function attrSel(name, value) {',
|
|
736
|
+
' return "[" + name + "=\\"" + value + "\\"]";',
|
|
737
|
+
' }',
|
|
738
|
+
'',
|
|
739
|
+
' function hasClass(el, name) {',
|
|
740
|
+
' return (" " + el.className + " ").indexOf(" " + name + " ") > -1;',
|
|
741
|
+
' }',
|
|
742
|
+
'',
|
|
743
|
+
' function addClass(el, name) {',
|
|
744
|
+
' if (hasClass(el, name)) return;',
|
|
745
|
+
' el.className = el.className ? el.className + " " + name : name;',
|
|
746
|
+
' }',
|
|
747
|
+
'',
|
|
748
|
+
' function removeClass(el, name) {',
|
|
749
|
+
' if (!el.className) return;',
|
|
750
|
+
' var parts = el.className.split(" ");',
|
|
751
|
+
' var kept = [];',
|
|
752
|
+
' for (var i = 0; i < parts.length; i++) { if (parts[i] && parts[i] !== name) kept.push(parts[i]); }',
|
|
753
|
+
' el.className = kept.join(" ");',
|
|
754
|
+
' }',
|
|
755
|
+
'',
|
|
756
|
+
' /* 卡级复制按钮(#88 R1-1):卡头注入 1 个按钮——actionId/文案恒读冻结常量,',
|
|
757
|
+
' 文本取同卡 `<pre class="prompt">` 的**原文**(=该卡 prompt 逐字,与 F3 卡面同源);',
|
|
758
|
+
' 读不到 <pre> 才回落同卡 Sheet 内 prompt 按钮的 data-t,两者都无才跳过',
|
|
759
|
+
' (#88 S5/B 段红队 S3-a:真实壳恒有 <pre>,故 436/436 恒有按钮,不静默丢卡)→ 复用既有委派。 */',
|
|
760
|
+
' function injectCardCopy(cards) {',
|
|
761
|
+
' for (var i = 0; i < cards.length; i++) {',
|
|
762
|
+
' var card = cards[i];',
|
|
763
|
+
' if (card.querySelector("." + CARD_COPY_CLASS)) continue;',
|
|
764
|
+
' var pre = card.querySelector("." + PROMPT_CLASS);',
|
|
765
|
+
' var src = pre !== null ? pre : card.querySelector(attrSel(ACTION_ATTR, COPY_ACTION));',
|
|
766
|
+
' if (src === null) continue;',
|
|
767
|
+
' var text = pre !== null ? pre.textContent : src.getAttribute(TEXT_ATTR);',
|
|
768
|
+
' if (text === null) continue;',
|
|
769
|
+
' var btn = document.createElement("button");',
|
|
770
|
+
' btn.type = "button";',
|
|
771
|
+
' btn.className = CARD_COPY_CLASS;',
|
|
772
|
+
' btn.textContent = COPY_LABEL;',
|
|
773
|
+
' btn.setAttribute(ACTION_ATTR, COPY_ACTION);',
|
|
774
|
+
' btn.setAttribute(TEXT_ATTR, text);',
|
|
775
|
+
' var head = card.querySelector("." + CARD_TOP_CLASS);',
|
|
776
|
+
' if (head) head.appendChild(btn); else card.insertBefore(btn, card.firstChild);',
|
|
777
|
+
' }',
|
|
778
|
+
' }',
|
|
779
|
+
'',
|
|
780
|
+
' /* Sheet 参数实时预览:静态值换输入框,输入即重组「prompt + 空行 + label: value 行」。 */',
|
|
781
|
+
' function initSheetPreview(cards) {',
|
|
782
|
+
' for (var i = 0; i < cards.length; i++) {',
|
|
783
|
+
' var card = cards[i];',
|
|
784
|
+
' var pre = card.querySelector("." + PROMPT_CLASS);',
|
|
785
|
+
' if (!pre) continue;',
|
|
786
|
+
' if (pre.getAttribute(PROMPT_ATTR) === null) pre.setAttribute(PROMPT_ATTR, pre.textContent);',
|
|
787
|
+
' var fields = allOf(card, "." + FIELD_CLASS);',
|
|
788
|
+
' for (var j = 0; j < fields.length; j++) bindField(card, fields[j]);',
|
|
789
|
+
' }',
|
|
790
|
+
' }',
|
|
791
|
+
'',
|
|
792
|
+
' function bindField(card, field) {',
|
|
793
|
+
' if (field.querySelector("." + FIELD_INPUT_CLASS)) return;',
|
|
794
|
+
' var value = field.querySelector("." + FIELD_VALUE_CLASS);',
|
|
795
|
+
' var input = document.createElement("input");',
|
|
796
|
+
' input.type = "text";',
|
|
797
|
+
' input.className = FIELD_INPUT_CLASS;',
|
|
798
|
+
' input.value = value ? value.textContent : "";',
|
|
799
|
+
' input.setAttribute("value", input.value);',
|
|
800
|
+
' var hint = field.querySelector("." + FIELD_HINT_CLASS);',
|
|
801
|
+
' if (hint) input.setAttribute("placeholder", hint.textContent);',
|
|
802
|
+
' input.addEventListener("input", function () { refreshPreview(card); });',
|
|
803
|
+
' if (value && value.parentNode === field) field.replaceChild(input, value);',
|
|
804
|
+
' else field.appendChild(input);',
|
|
805
|
+
' }',
|
|
806
|
+
'',
|
|
807
|
+
' function refreshPreview(card) {',
|
|
808
|
+
' var pre = card.querySelector("." + PROMPT_CLASS);',
|
|
809
|
+
' if (!pre) return;',
|
|
810
|
+
' var base = pre.getAttribute(PROMPT_ATTR);',
|
|
811
|
+
' if (base === null) return;',
|
|
812
|
+
' var lines = fieldLines(card);',
|
|
813
|
+
' var text = lines.length > 0 ? base + LF2 + lines.join(LF) : base;',
|
|
814
|
+
' pre.textContent = text;',
|
|
815
|
+
' syncCopyText(card, text, lines);',
|
|
816
|
+
' if (searchTerm !== "") markCard(card, searchTerm);',
|
|
817
|
+
' }',
|
|
818
|
+
'',
|
|
819
|
+
' function fieldLines(card) {',
|
|
820
|
+
' var fields = allOf(card, "." + FIELD_CLASS);',
|
|
821
|
+
' var lines = [];',
|
|
822
|
+
' for (var i = 0; i < fields.length; i++) {',
|
|
823
|
+
' var input = fields[i].querySelector("." + FIELD_INPUT_CLASS);',
|
|
824
|
+
' var labelEl = fields[i].querySelector("." + FIELD_LABEL_CLASS);',
|
|
825
|
+
' var label = labelEl ? labelEl.textContent : "";',
|
|
826
|
+
' var raw = input ? input.value : "";',
|
|
827
|
+
' var val = raw.replace(/^\\s+|\\s+$/g, "");',
|
|
828
|
+
' if (val !== "") lines.push(label + ": " + val);',
|
|
829
|
+
' }',
|
|
830
|
+
' return lines;',
|
|
831
|
+
' }',
|
|
832
|
+
'',
|
|
833
|
+
' /* 复制按钮文本同步:prompt 目标(卡头 + Sheet 内)= 预览文本;params 目标=label: value 行,',
|
|
834
|
+
' 无字段时回落该卡 CLI 文本(与冻结壳 paramsText 同口径)。 */',
|
|
835
|
+
' function syncCopyText(card, text, lines) {',
|
|
836
|
+
' var buttons = allOf(card, "[" + ACTION_ATTR + "]");',
|
|
837
|
+
' for (var i = 0; i < buttons.length; i++) {',
|
|
838
|
+
' var id = buttons[i].getAttribute(ACTION_ATTR);',
|
|
839
|
+
' if (id === COPY_ACTION) buttons[i].setAttribute(TEXT_ATTR, text);',
|
|
840
|
+
' else if (id === PARAMS_ACTION) buttons[i].setAttribute(TEXT_ATTR, lines.length > 0 ? lines.join(LF) : cliTextOf(card));',
|
|
841
|
+
' }',
|
|
842
|
+
' }',
|
|
843
|
+
'',
|
|
844
|
+
' function cliTextOf(card) {',
|
|
845
|
+
' var cli = card.querySelector("." + CLI_CLASS);',
|
|
846
|
+
' return cli ? cli.textContent : "";',
|
|
847
|
+
' }',
|
|
848
|
+
'',
|
|
849
|
+
' /* 搜索:过滤 + mark 标签高亮 + 自动展开 + 命中计数 + 清空 + 跳页。 */',
|
|
850
|
+
' function initSearch(shell, cards) {',
|
|
851
|
+
' var tabBar = shell.querySelector("." + TAB_BAR_CLASS);',
|
|
852
|
+
' if (!tabBar || !tabBar.parentNode) return;',
|
|
853
|
+
' if (shell.querySelector("." + SEARCH_BOX_CLASS)) return;',
|
|
854
|
+
' var box = document.createElement("div");',
|
|
855
|
+
' box.className = SEARCH_BOX_CLASS;',
|
|
856
|
+
' var input = document.createElement("input");',
|
|
857
|
+
' input.type = "search";',
|
|
858
|
+
' input.className = SEARCH_INPUT_CLASS;',
|
|
859
|
+
' input.setAttribute("placeholder", SEARCH_PLACEHOLDER);',
|
|
860
|
+
' input.setAttribute("aria-label", SEARCH_ARIA);',
|
|
861
|
+
' input.setAttribute("autocomplete", "off");',
|
|
862
|
+
' var clear = document.createElement("button");',
|
|
863
|
+
' clear.type = "button";',
|
|
864
|
+
' clear.className = SEARCH_CLEAR_CLASS;',
|
|
865
|
+
' clear.textContent = CLEAR_LABEL;',
|
|
866
|
+
' clear.setAttribute("aria-label", CLEAR_ARIA);',
|
|
867
|
+
' var count = document.createElement("span");',
|
|
868
|
+
' count.className = HITCOUNT_CLASS;',
|
|
869
|
+
' count.setAttribute("role", "status");',
|
|
870
|
+
' box.appendChild(input);',
|
|
871
|
+
' box.appendChild(clear);',
|
|
872
|
+
' box.appendChild(count);',
|
|
873
|
+
' tabBar.parentNode.insertBefore(box, tabBar.nextSibling);',
|
|
874
|
+
' var state = { sheets: [], subgroups: [], pages: [], index: 0 };',
|
|
875
|
+
' input.addEventListener("input", function () { runSearch(shell, cards, input, count, state); });',
|
|
876
|
+
' input.addEventListener("keydown", function (ev) {',
|
|
877
|
+
' if (ev.key !== "Enter") return;',
|
|
878
|
+
' ev.preventDefault();',
|
|
879
|
+
' if (state.pages.length === 0) return;',
|
|
880
|
+
' state.index = (state.index + 1) % state.pages.length;',
|
|
881
|
+
' jumpTo(state.pages[state.index]);',
|
|
882
|
+
' });',
|
|
883
|
+
' clear.addEventListener("click", function () {',
|
|
884
|
+
' input.value = "";',
|
|
885
|
+
' runSearch(shell, cards, input, count, state);',
|
|
886
|
+
' input.focus();',
|
|
887
|
+
' });',
|
|
888
|
+
' }',
|
|
889
|
+
'',
|
|
890
|
+
' function runSearch(shell, cards, input, count, state) {',
|
|
891
|
+
' var term = input.value.replace(/^\\s+|\\s+$/g, "").toLowerCase();',
|
|
892
|
+
' resetSearch(shell, cards, state);',
|
|
893
|
+
' searchTerm = term;',
|
|
894
|
+
' if (term === "") { count.textContent = ""; state.pages = []; state.index = 0; return; }',
|
|
895
|
+
' var hits = [];',
|
|
896
|
+
' var i;',
|
|
897
|
+
' for (i = 0; i < cards.length; i++) {',
|
|
898
|
+
' var card = cards[i];',
|
|
899
|
+
' if (card.textContent.toLowerCase().indexOf(term) < 0) { addClass(card, CARD_HIDDEN_CLASS); continue; }',
|
|
900
|
+
' hits.push(card);',
|
|
901
|
+
' markCard(card, term);',
|
|
902
|
+
' var sheet = card.querySelector("." + SHEET_CLASS);',
|
|
903
|
+
' if (sheet && !sheet.open) { sheet.open = true; state.sheets.push(sheet); }',
|
|
904
|
+
' }',
|
|
905
|
+
' var subgroups = allOf(shell, "." + SUBGROUP_CLASS);',
|
|
906
|
+
' for (i = 0; i < subgroups.length; i++) {',
|
|
907
|
+
' var inner = allOf(subgroups[i], "." + CARD_CLASS);',
|
|
908
|
+
' var visible = false;',
|
|
909
|
+
' for (var j = 0; j < inner.length; j++) { if (hits.indexOf(inner[j]) > -1) { visible = true; break; } }',
|
|
910
|
+
' if (visible) { if (!subgroups[i].open) { subgroups[i].open = true; state.subgroups.push(subgroups[i]); } }',
|
|
911
|
+
' else addClass(subgroups[i], SUBGROUP_HIDDEN_CLASS);',
|
|
912
|
+
' }',
|
|
913
|
+
' var pages = allOf(shell, "." + PAGE_CLASS);',
|
|
914
|
+
' var hitPages = [];',
|
|
915
|
+
' for (i = 0; i < pages.length; i++) {',
|
|
916
|
+
' var pageCards = allOf(pages[i], "." + CARD_CLASS);',
|
|
917
|
+
' for (var k = 0; k < pageCards.length; k++) { if (hits.indexOf(pageCards[k]) > -1) { hitPages.push(pages[i]); break; } }',
|
|
918
|
+
' }',
|
|
919
|
+
' state.pages = hitPages;',
|
|
920
|
+
' state.index = 0;',
|
|
921
|
+
' count.textContent = hits.length > 0 ? HIT_PREFIX + hits.length + HIT_SUFFIX : HIT_EMPTY;',
|
|
922
|
+
' jumpTo(hitPages[0]);',
|
|
923
|
+
' }',
|
|
924
|
+
'',
|
|
925
|
+
' function resetSearch(shell, cards, state) {',
|
|
926
|
+
' var i;',
|
|
927
|
+
' for (i = 0; i < cards.length; i++) {',
|
|
928
|
+
' removeClass(cards[i], CARD_HIDDEN_CLASS);',
|
|
929
|
+
' unmarkCard(cards[i]);',
|
|
930
|
+
' }',
|
|
931
|
+
' for (i = 0; i < state.sheets.length; i++) state.sheets[i].open = false;',
|
|
932
|
+
' state.sheets = [];',
|
|
933
|
+
' for (i = 0; i < state.subgroups.length; i++) state.subgroups[i].open = false;',
|
|
934
|
+
' state.subgroups = [];',
|
|
935
|
+
' var subgroups = allOf(shell, "." + SUBGROUP_CLASS);',
|
|
936
|
+
' for (i = 0; i < subgroups.length; i++) removeClass(subgroups[i], SUBGROUP_HIDDEN_CLASS);',
|
|
937
|
+
' }',
|
|
938
|
+
'',
|
|
939
|
+
' function jumpTo(page) {',
|
|
940
|
+
' if (!page) return;',
|
|
941
|
+
' var radio = page.querySelector("." + TAB_INPUT_CLASS);',
|
|
942
|
+
' if (radio && radio.checked !== true) radio.checked = true;',
|
|
943
|
+
' }',
|
|
944
|
+
'',
|
|
945
|
+
' function markCard(card, term) {',
|
|
946
|
+
' unmarkCard(card);',
|
|
947
|
+
' var nodes = [];',
|
|
948
|
+
' collectText(card, nodes);',
|
|
949
|
+
' for (var i = 0; i < nodes.length; i++) wrapTerm(nodes[i], term);',
|
|
950
|
+
' }',
|
|
951
|
+
'',
|
|
952
|
+
' function unmarkCard(card) {',
|
|
953
|
+
' var marks = allOf(card, "." + CARD_MARK_CLASS);',
|
|
954
|
+
' for (var i = 0; i < marks.length; i++) {',
|
|
955
|
+
' var parent = marks[i].parentNode;',
|
|
956
|
+
' if (!parent) continue;',
|
|
957
|
+
' parent.replaceChild(document.createTextNode(marks[i].textContent), marks[i]);',
|
|
958
|
+
' if (typeof parent.normalize === "function") parent.normalize();',
|
|
959
|
+
' }',
|
|
960
|
+
' }',
|
|
961
|
+
'',
|
|
962
|
+
' function collectText(root, out) {',
|
|
963
|
+
' var kids = root.childNodes;',
|
|
964
|
+
' for (var i = 0; i < kids.length; i++) {',
|
|
965
|
+
' var node = kids[i];',
|
|
966
|
+
' if (node.nodeType === 3) { if (node.nodeValue) out.push(node); continue; }',
|
|
967
|
+
' if (node.nodeType !== 1) continue;',
|
|
968
|
+
' if (hasClass(node, CARD_MARK_CLASS)) continue;',
|
|
969
|
+
' var tag = node.tagName;',
|
|
970
|
+
' if (tag === "SCRIPT" || tag === "STYLE" || tag === "TEXTAREA" || tag === "INPUT") continue;',
|
|
971
|
+
' collectText(node, out);',
|
|
972
|
+
' }',
|
|
973
|
+
' }',
|
|
974
|
+
'',
|
|
975
|
+
' function wrapTerm(node, term) {',
|
|
976
|
+
' var rest = node.nodeValue;',
|
|
977
|
+
' var at = rest.toLowerCase().indexOf(term);',
|
|
978
|
+
' if (at < 0) return;',
|
|
979
|
+
' var frag = document.createDocumentFragment();',
|
|
980
|
+
' while (at > -1) {',
|
|
981
|
+
' if (at > 0) frag.appendChild(document.createTextNode(rest.slice(0, at)));',
|
|
982
|
+
' var mark = document.createElement("mark");',
|
|
983
|
+
' mark.className = CARD_MARK_CLASS;',
|
|
984
|
+
' mark.textContent = rest.slice(at, at + term.length);',
|
|
985
|
+
' frag.appendChild(mark);',
|
|
986
|
+
' rest = rest.slice(at + term.length);',
|
|
987
|
+
' at = rest.toLowerCase().indexOf(term);',
|
|
988
|
+
' }',
|
|
989
|
+
' if (rest !== "") frag.appendChild(document.createTextNode(rest));',
|
|
990
|
+
' if (node.parentNode) node.parentNode.replaceChild(frag, node);',
|
|
991
|
+
' }',
|
|
992
|
+
'',
|
|
993
|
+
' /* 回到顶部(H-19):scrollTop > BACKTOP_MIN_Y 才加 -show;点击平滑回顶。 */',
|
|
994
|
+
' function initBackTop() {',
|
|
995
|
+
' if (document.querySelector("." + BACKTOP_CLASS)) return;',
|
|
996
|
+
' var btn = document.createElement("button");',
|
|
997
|
+
' btn.type = "button";',
|
|
998
|
+
' btn.id = BACKTOP_ID;',
|
|
999
|
+
' btn.className = BACKTOP_CLASS;',
|
|
1000
|
+
' btn.textContent = BACKTOP_LABEL;',
|
|
1001
|
+
' btn.setAttribute("aria-label", BACKTOP_ARIA);',
|
|
1002
|
+
' btn.setAttribute("title", BACKTOP_ARIA);',
|
|
1003
|
+
' btn.addEventListener("click", function () {',
|
|
1004
|
+
' var root = scrollRoot();',
|
|
1005
|
+
' if (!root) return;',
|
|
1006
|
+
' if (typeof root.scrollTo === "function") {',
|
|
1007
|
+
' try { root.scrollTo({ top: 0, behavior: "smooth" }); return; } catch (err) { root.scrollTop = 0; return; }',
|
|
1008
|
+
' }',
|
|
1009
|
+
' root.scrollTop = 0;',
|
|
1010
|
+
' });',
|
|
1011
|
+
' document.body.appendChild(btn);',
|
|
1012
|
+
' document.addEventListener("scroll", syncBackTop, true);',
|
|
1013
|
+
' syncBackTop();',
|
|
1014
|
+
' }',
|
|
1015
|
+
'',
|
|
1016
|
+
' function scrollRoot() {',
|
|
1017
|
+
' return document.scrollingElement || document.documentElement || document.body;',
|
|
1018
|
+
' }',
|
|
1019
|
+
'',
|
|
1020
|
+
' function syncBackTop() {',
|
|
1021
|
+
' var btn = document.querySelector("." + BACKTOP_CLASS);',
|
|
1022
|
+
' if (!btn) return;',
|
|
1023
|
+
' var root = scrollRoot();',
|
|
1024
|
+
' var y = root ? root.scrollTop : 0;',
|
|
1025
|
+
' if (y > BACKTOP_MIN_Y) addClass(btn, BACKTOP_SHOW_CLASS); else removeClass(btn, BACKTOP_SHOW_CLASS);',
|
|
1026
|
+
' }',
|
|
1027
|
+
'',
|
|
1028
|
+
' if (document.body) boot();',
|
|
1029
|
+
' else document.addEventListener("DOMContentLoaded", boot);',
|
|
1030
|
+
'}());',
|
|
1031
|
+
];
|
|
1032
|
+
return lines.join(String.fromCharCode(10));
|
|
1033
|
+
};
|
|
1034
|
+
function normalizeButtons(buttons) {
|
|
1035
|
+
if (buttons === undefined || buttons === null)
|
|
1036
|
+
return [];
|
|
1037
|
+
if (!Array.isArray(buttons))
|
|
1038
|
+
badInput('renderActionBar: input.buttons 必须是数组');
|
|
1039
|
+
return buttons.map((button, i) => {
|
|
1040
|
+
const field = 'renderActionBar: input.buttons[' + i + ']';
|
|
1041
|
+
assertPlainObject(button, field);
|
|
1042
|
+
assertNoInlineHandler(button, field);
|
|
1043
|
+
if (typeof button.label !== 'string' || button.label === '')
|
|
1044
|
+
badInput(field + '.label 必须是非空字符串');
|
|
1045
|
+
if (!ACTION_BAR_KINDS.includes(button.kind)) {
|
|
1046
|
+
badInput(field + '.kind 必须是 ' + ACTION_BAR_KINDS.join('/'));
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
label: button.label,
|
|
1050
|
+
kind: button.kind,
|
|
1051
|
+
actionId: assertActionId(button.actionId, field + '.actionId'),
|
|
1052
|
+
};
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
function normalizeCopyButton(input, fallbackLabel, field) {
|
|
1056
|
+
if (input === undefined || input === null)
|
|
1057
|
+
return null;
|
|
1058
|
+
assertPlainObject(input, 'renderActionBar: input.' + field);
|
|
1059
|
+
assertNoInlineHandler(input, 'renderActionBar: input.' + field);
|
|
1060
|
+
return {
|
|
1061
|
+
label: typeof input.label === 'string' && input.label !== '' ? input.label : fallbackLabel,
|
|
1062
|
+
actionId: assertActionId(input.actionId, 'renderActionBar: input.' + field + '.actionId'),
|
|
1063
|
+
text: typeof input.text === 'string' ? input.text : undefined,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
function sceneButtonHtml(button) {
|
|
1067
|
+
return '<button type="button" class="' + STYLE_PREFIX + 'action-btn ' + STYLE_PREFIX + 'action-btn-' + button.kind + '" '
|
|
1068
|
+
+ ACTION_ID_ATTR + '="' + esc(button.actionId) + '">' + esc(button.label) + '</button>';
|
|
1069
|
+
}
|
|
1070
|
+
function copyButtonHtml(button) {
|
|
1071
|
+
const textAttr = button.text === undefined ? '' : ' ' + DEFAULT_DATA_ATTR + '="' + esc(button.text) + '"';
|
|
1072
|
+
return '<button type="button" class="' + STYLE_PREFIX + 'copy-btn ' + STYLE_PREFIX + 'copy-btn-ghost" '
|
|
1073
|
+
+ ACTION_ID_ATTR + '="' + esc(button.actionId) + '"' + textAttr + '>' + esc(button.label) + '</button>';
|
|
1074
|
+
}
|
|
1075
|
+
/** 冻结签名:`renderActionBar(input: ActionBarInput): string`。
|
|
1076
|
+
*
|
|
1077
|
+
* 场景按钮(`primary`/`red`/`ghost`)一行;复制数据/复制日志 ghost 按钮**独立一行**
|
|
1078
|
+
* (`ACTION_BAR_DEFAULTS.ghostOwnRow`);复制文本渲染期写入 `DEFAULT_DATA_ATTR`、id 写入 `ACTION_ID_ATTR`,
|
|
1079
|
+
* 零内联脚本。缺 `actionId`/空串/同次渲染内重复 → `ControlsError` code `bad-input`。
|
|
1080
|
+
* 逐区尺寸(`minHeightPx`/`fontSizePx`/`fontWeight`/`ghostBorderAlpha`/`evenRowPairs`)由 #75 的
|
|
1081
|
+
* 共享样式区消费(本文件不产样式常量)。 */
|
|
1082
|
+
export const renderActionBar = (input) => {
|
|
1083
|
+
assertPlainObject(input, 'renderActionBar: input');
|
|
1084
|
+
const bar = input;
|
|
1085
|
+
const buttons = normalizeButtons(bar.buttons);
|
|
1086
|
+
const copyData = normalizeCopyButton(bar.copyData, ACTION_BAR_DEFAULTS.copyDataLabel, 'copyData');
|
|
1087
|
+
const copyLog = normalizeCopyButton(bar.copyLog, ACTION_BAR_DEFAULTS.copyLogLabel, 'copyLog');
|
|
1088
|
+
const ids = new Set();
|
|
1089
|
+
for (const button of buttons) {
|
|
1090
|
+
if (ids.has(button.actionId))
|
|
1091
|
+
badInput('renderActionBar: actionId 同次渲染内重复:' + button.actionId);
|
|
1092
|
+
ids.add(button.actionId);
|
|
1093
|
+
}
|
|
1094
|
+
for (const copy of [copyData, copyLog]) {
|
|
1095
|
+
if (copy === null)
|
|
1096
|
+
continue;
|
|
1097
|
+
if (ids.has(copy.actionId))
|
|
1098
|
+
badInput('renderActionBar: actionId 同次渲染内重复:' + copy.actionId);
|
|
1099
|
+
ids.add(copy.actionId);
|
|
1100
|
+
}
|
|
1101
|
+
const sceneHtml = buttons.map(sceneButtonHtml).join('');
|
|
1102
|
+
const ghostHtml = [copyData, copyLog].filter((copy) => copy !== null).map(copyButtonHtml).join('');
|
|
1103
|
+
const rowClass = STYLE_PREFIX + 'action-row';
|
|
1104
|
+
const ghostRowClass = rowClass + ' ' + STYLE_PREFIX + 'action-row-ghost';
|
|
1105
|
+
const rows = [];
|
|
1106
|
+
if (sceneHtml !== '' && ghostHtml !== '' && !ACTION_BAR_DEFAULTS.ghostOwnRow) {
|
|
1107
|
+
rows.push('<div class="' + rowClass + '">' + sceneHtml + ghostHtml + '</div>');
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
if (sceneHtml !== '')
|
|
1111
|
+
rows.push('<div class="' + rowClass + '">' + sceneHtml + '</div>');
|
|
1112
|
+
if (ghostHtml !== '')
|
|
1113
|
+
rows.push('<div class="' + ghostRowClass + '">' + ghostHtml + '</div>');
|
|
1114
|
+
}
|
|
1115
|
+
return '<div class="' + STYLE_PREFIX + 'action-bar">' + rows.join('') + '</div>';
|
|
1116
|
+
};
|
|
1117
|
+
/* ── 状态三控件 ───────────────────────────────────────────────────────── */
|
|
1118
|
+
/** 冻结签名:`renderStatusBadge(input: StatusBadgeInput): string`。
|
|
1119
|
+
* 非法 `status` 降级 `'empty'`(不抛错,防无样式徽章);`text` 缺省/空串取 `STATUS_DEFAULT_TEXT`。 */
|
|
1120
|
+
export const renderStatusBadge = (input) => {
|
|
1121
|
+
const badge = input === null || input === undefined || typeof input !== 'object' ? undefined : input;
|
|
1122
|
+
const status = badge !== undefined && isStatusKind(badge.status) ? badge.status : 'empty';
|
|
1123
|
+
const text = badge === undefined ? undefined : badge.text;
|
|
1124
|
+
const label = typeof text === 'string' && text !== '' ? text : STATUS_DEFAULT_TEXT[status];
|
|
1125
|
+
return '<span class="' + STYLE_PREFIX + 'status-badge ' + STYLE_PREFIX + 'status-badge-' + status + '">' + esc(label) + '</span>';
|
|
1126
|
+
};
|
|
1127
|
+
/** 冻结签名:`renderEmptyState(input: EmptyStateInput): string`。
|
|
1128
|
+
* `icon`/`text`/`hint` 一律转义;`actionHtml` 受信 HTML 透传(不转义);`text` 缺失/非字符串 → `bad-input`。 */
|
|
1129
|
+
export const renderEmptyState = (input) => {
|
|
1130
|
+
assertPlainObject(input, 'renderEmptyState: input');
|
|
1131
|
+
const state = input;
|
|
1132
|
+
if (typeof state.text !== 'string')
|
|
1133
|
+
badInput('renderEmptyState: input.text 必须是字符串');
|
|
1134
|
+
const parts = [];
|
|
1135
|
+
if (typeof state.icon === 'string' && state.icon !== '') {
|
|
1136
|
+
parts.push('<div class="' + STYLE_PREFIX + 'empty-icon">' + esc(state.icon) + '</div>');
|
|
1137
|
+
}
|
|
1138
|
+
parts.push('<div class="' + STYLE_PREFIX + 'empty-text">' + esc(state.text) + '</div>');
|
|
1139
|
+
if (typeof state.hint === 'string' && state.hint !== '') {
|
|
1140
|
+
parts.push('<div class="' + STYLE_PREFIX + 'empty-hint">' + esc(state.hint) + '</div>');
|
|
1141
|
+
}
|
|
1142
|
+
if (typeof state.actionHtml === 'string' && state.actionHtml !== '') {
|
|
1143
|
+
parts.push('<div class="' + STYLE_PREFIX + 'empty-action">' + state.actionHtml + '</div>');
|
|
1144
|
+
}
|
|
1145
|
+
return '<div class="' + STYLE_PREFIX + 'empty">' + parts.join('') + '</div>';
|
|
1146
|
+
};
|
|
1147
|
+
/** 修正重试按钮缺省文案(文档无规定;沿用旧基线)。 */
|
|
1148
|
+
const ERROR_RETRY_LABEL = '修正重试';
|
|
1149
|
+
/** 错误标题前缀(文档无规定;沿用旧基线 `❌ `)。 */
|
|
1150
|
+
const ERROR_TITLE_PREFIX = '❌ ';
|
|
1151
|
+
/** 冻结签名:`renderErrorReceipt(input: ErrorReceiptInput): string`。
|
|
1152
|
+
* 缺 `dataText`/`logText` → **不渲染**对应复制按钮(容错,不抛错);id 缺省取
|
|
1153
|
+
* `COPY_ACTION_IDS.errorReceipt.*`;**不读** `window.__hmPayload` 之类旧全局(AC-7)。 */
|
|
1154
|
+
export const renderErrorReceipt = (input) => {
|
|
1155
|
+
assertPlainObject(input, 'renderErrorReceipt: input');
|
|
1156
|
+
const receipt = input;
|
|
1157
|
+
if (typeof receipt.message !== 'string')
|
|
1158
|
+
badInput('renderErrorReceipt: input.message 必须是字符串');
|
|
1159
|
+
const retryLabel = typeof receipt.retryPrompt === 'string' && receipt.retryPrompt !== '' ? receipt.retryPrompt : ERROR_RETRY_LABEL;
|
|
1160
|
+
const actions = [
|
|
1161
|
+
'<button type="button" class="' + STYLE_PREFIX + 'copy-btn ' + STYLE_PREFIX + 'copy-btn-primary ' + STYLE_PREFIX + 'copy-btn-wide">'
|
|
1162
|
+
+ esc(retryLabel) + '</button>',
|
|
1163
|
+
];
|
|
1164
|
+
const ids = new Set();
|
|
1165
|
+
const pushCopy = (text, rawId, fallbackId, label, field) => {
|
|
1166
|
+
if (typeof text !== 'string')
|
|
1167
|
+
return;
|
|
1168
|
+
const actionId = rawId === undefined ? fallbackId : assertActionId(rawId, 'renderErrorReceipt: input.' + field);
|
|
1169
|
+
if (ids.has(actionId))
|
|
1170
|
+
badInput('renderErrorReceipt: actionId 同次渲染内重复:' + actionId);
|
|
1171
|
+
ids.add(actionId);
|
|
1172
|
+
actions.push('<button type="button" class="' + STYLE_PREFIX + 'copy-btn ' + STYLE_PREFIX + 'copy-btn-ghost" '
|
|
1173
|
+
+ ACTION_ID_ATTR + '="' + esc(actionId) + '" ' + DEFAULT_DATA_ATTR + '="' + esc(text) + '">' + esc(label) + '</button>');
|
|
1174
|
+
};
|
|
1175
|
+
pushCopy(receipt.dataText, receipt.dataActionId, COPY_ACTION_IDS.errorReceipt.copyData, ACTION_BAR_DEFAULTS.copyDataLabel, 'dataActionId');
|
|
1176
|
+
pushCopy(receipt.logText, receipt.logActionId, COPY_ACTION_IDS.errorReceipt.copyLog, ACTION_BAR_DEFAULTS.copyLogLabel, 'logActionId');
|
|
1177
|
+
return '<div class="' + STYLE_PREFIX + 'error">'
|
|
1178
|
+
+ '<div class="' + STYLE_PREFIX + 'error-title">' + esc(ERROR_TITLE_PREFIX + receipt.message) + '</div>'
|
|
1179
|
+
+ '<div class="' + STYLE_PREFIX + 'error-actions">' + actions.join('') + '</div>'
|
|
1180
|
+
+ '</div>';
|
|
1181
|
+
};
|
|
1182
|
+
//# sourceMappingURL=controls.js.map
|