agentcss 0.25.5 → 0.26.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/01-tokens/behavior/labels.js +44 -0
- package/02-primitives/combobox/combobox.js +26 -3
- package/02-primitives/date-range/date-range.js +27 -5
- package/02-primitives/popover/popover.js +22 -2
- package/02-primitives/select/select.js +23 -2
- package/02-primitives/upload/upload.js +50 -17
- package/03-patterns/content/content.css +13 -1
- package/03-patterns/content/demo.html +1 -1
- package/03-patterns/drawer/drawer.js +44 -6
- package/03-patterns/dropdown/dropdown.js +21 -1
- package/03-patterns/overlay/overlay.js +39 -2
- package/03-patterns/pagination/pagination.js +37 -10
- package/03-patterns/tooltip/tooltip.js +17 -2
- package/03-patterns/tree/tree.js +21 -1
- package/04-recipes/analysis-report/demo.html +1 -1
- package/04-recipes/data-showcase/demo.html +2 -2
- package/04-recipes/longform/content.html +989 -74
- package/04-recipes/longform/longform.html +3 -3
- package/04-recipes/report/demo.html +1 -1
- package/04-recipes/table/README.md +22 -0
- package/04-recipes/table/demo.html +6 -6
- package/09-assets/bar/bar.js +28 -4
- package/09-assets/model-viewer/model-viewer.js +44 -12
- package/09-assets/scientific-plot/demo.html +6 -6
- package/API.md +32 -0
- package/CHANGELOG.md +129 -0
- package/CONTRIBUTING.md +4 -3
- package/README.md +19 -2
- package/README.zh-CN.md +18 -2
- package/SECURITY.md +16 -4
- package/START-HERE.md +14 -14
- package/SUPPORT.md +2 -2
- package/ai/components.json +9 -4
- package/ai/pdf.js +50 -8
- package/dist/01-tokens/behavior/labels.js +11 -0
- package/dist/02-primitives/combobox/combobox.js +25 -3
- package/dist/02-primitives/date-range/date-range.js +26 -5
- package/dist/02-primitives/popover/popover.js +14 -1
- package/dist/02-primitives/select/select.js +22 -2
- package/dist/02-primitives/upload/upload.js +47 -17
- package/dist/03-patterns/content/content.css +1 -1
- package/dist/03-patterns/drawer/drawer.js +32 -4
- package/dist/03-patterns/dropdown/dropdown.js +20 -1
- package/dist/03-patterns/overlay/overlay.js +25 -1
- package/dist/03-patterns/pagination/pagination.js +34 -10
- package/dist/03-patterns/tooltip/tooltip.js +14 -2
- package/dist/03-patterns/tree/tree.js +20 -1
- package/dist/09-assets/bar/bar.js +23 -4
- package/dist/09-assets/model-viewer/model-viewer.js +38 -12
- package/dist/COSTS.md +14 -14
- package/docs/ACCESSIBILITY.md +2 -2
- package/docs/L10N.md +92 -0
- package/docs/LAYER-BASELINE.md +2 -2
- package/docs/RELEASE-PROCESS.md +7 -1
- package/docs/VERIFY-RELEASE.md +7 -2
- package/index.html +1 -1
- package/llms-full.txt +2 -2
- package/package.json +17 -6
- package/types/index.d.ts +218 -13
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* labels.js — 「文案覆盖合并」工具核
|
|
3
|
+
*
|
|
4
|
+
* ==========================================================================
|
|
5
|
+
* 🔴 为什么要有这个核
|
|
6
|
+
* --------------------------------------------------------------------------
|
|
7
|
+
* 0.26.0 给组件加 `labels`(读屏 / 可见文案覆盖)时,最初的写法是
|
|
8
|
+
* 在 9 个组件里各贴一份合并函数:
|
|
9
|
+
*
|
|
10
|
+
* for (k in defaults) { out[k] = src[k] == null ? defaults[k] : src[k]; }
|
|
11
|
+
*
|
|
12
|
+
* 九份一模一样的循环,只差 `defaults` 的字面量。这与 I-14 是同一条病:
|
|
13
|
+
* **同一份逻辑写 N 份**。后果不是"难看",是**修正只到一处**:
|
|
14
|
+
* · 以后要支持"传 null 表示删掉这条文案"→ 改 1 处、漏 8 处;
|
|
15
|
+
* · 要支持嵌套命名空间 → 同上;
|
|
16
|
+
* · 某处写成 `src[k] || defaults[k]`,空字符串就再也覆盖不掉 ——
|
|
17
|
+
* 而这种偏差在 9 份副本里肉眼几乎看不出来。
|
|
18
|
+
*
|
|
19
|
+
* ⇒ 真值源一份(本文件),生成进使用点,`gen-behavior --check` 证明跟随。
|
|
20
|
+
* 使用点仍然**各自保留自己的 `defaults` 字面量** —— 那是组件的事实,
|
|
21
|
+
* 不是可以共享的代码。
|
|
22
|
+
*
|
|
23
|
+
* ==========================================================================
|
|
24
|
+
* 语义(三句话,必须与实现逐字对应)
|
|
25
|
+
* --------------------------------------------------------------------------
|
|
26
|
+
* ① `undefined` / `null` ⇒ 用默认值("没传"和"显式取消"等价);
|
|
27
|
+
* ② 其他任何值(含 `''`、`0`、`false`)⇒ **原样采用** ——
|
|
28
|
+
* 用 `||` 判断会把空字符串吞掉,那是错误的;
|
|
29
|
+
* ③ 只认 `defaults` 里有的键 —— 拼错的键不会悄悄生效(避免"传了却没反应")。
|
|
30
|
+
* ==========================================================================
|
|
31
|
+
*/
|
|
32
|
+
/* ==== BEHAVIOR CORE BEGIN: labels ==== */
|
|
33
|
+
var flLabels = function (defaults, src) {
|
|
34
|
+
src = src || {};
|
|
35
|
+
var out = {};
|
|
36
|
+
var k;
|
|
37
|
+
for (k in defaults) {
|
|
38
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
39
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
};
|
|
44
|
+
/* ==== BEHAVIOR CORE END: labels ==== */
|
|
@@ -145,6 +145,29 @@
|
|
|
145
145
|
var uid = 0;
|
|
146
146
|
|
|
147
147
|
function create(root, opt) {
|
|
148
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
149
|
+
var DEFAULT_LABELS = {
|
|
150
|
+
suggest: '建议',
|
|
151
|
+
input: '输入',
|
|
152
|
+
noMatch: function (q) { return '没有匹配「' + q + '」的选项'; },
|
|
153
|
+
empty: '暂无可选'
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
157
|
+
var flLabels = function (defaults, src) {
|
|
158
|
+
src = src || {};
|
|
159
|
+
var out = {};
|
|
160
|
+
var k;
|
|
161
|
+
for (k in defaults) {
|
|
162
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
163
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
};
|
|
168
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
169
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
170
|
+
|
|
148
171
|
opt = opt || {};
|
|
149
172
|
var input = root.querySelector('[data-combo-input]');
|
|
150
173
|
var list = root.querySelector('[data-combo-list]');
|
|
@@ -182,14 +205,14 @@
|
|
|
182
205
|
var shown = []; /* 当前过滤后的选项 */
|
|
183
206
|
|
|
184
207
|
list.setAttribute('role', 'listbox');
|
|
185
|
-
list.setAttribute('aria-label', opt.label ||
|
|
208
|
+
list.setAttribute('aria-label', opt.label || L.suggest);
|
|
186
209
|
input.setAttribute('role', 'combobox');
|
|
187
210
|
input.setAttribute('aria-autocomplete', 'list');
|
|
188
211
|
input.setAttribute('aria-expanded', 'false');
|
|
189
212
|
input.setAttribute('aria-controls', list.id);
|
|
190
213
|
if (!input.id) input.id = 'combo-input-' + seq;
|
|
191
214
|
if (!input.getAttribute('aria-label') && !root.querySelector('label[for="' + input.id + '"]')) {
|
|
192
|
-
input.setAttribute('aria-label', opt.label ||
|
|
215
|
+
input.setAttribute('aria-label', opt.label || L.input);
|
|
193
216
|
}
|
|
194
217
|
|
|
195
218
|
/* ---------- 过滤(含高亮命中片段)---------- */
|
|
@@ -226,7 +249,7 @@
|
|
|
226
249
|
if (!shown.length) {
|
|
227
250
|
/* @html-sink escaped — 插值只有 q,且过了 esc() */
|
|
228
251
|
list.innerHTML = '<li class="combo__empty" role="presentation">' +
|
|
229
|
-
(q ?
|
|
252
|
+
(q ? L.noMatch(esc(q)) : L.empty) + '</li>';
|
|
230
253
|
return;
|
|
231
254
|
}
|
|
232
255
|
var ql = q.toLowerCase();
|
|
@@ -119,6 +119,28 @@
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
function create(root, opt) {
|
|
122
|
+
/* 校验文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
123
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
124
|
+
var flLabels = function (defaults, src) {
|
|
125
|
+
src = src || {};
|
|
126
|
+
var out = {};
|
|
127
|
+
var k;
|
|
128
|
+
for (k in defaults) {
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
130
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
136
|
+
var DEFAULT_LABELS = {
|
|
137
|
+
requiredEnd: '请填写结束日期。',
|
|
138
|
+
requiredStart: '请填写开始日期。',
|
|
139
|
+
order: function (f, t) { return '开始日期不能晚于结束日期(当前:' + f + ' → ' + t + ')。'; },
|
|
140
|
+
min: function (min) { return '开始日期不能早于 ' + min + '。'; },
|
|
141
|
+
max: function (max) { return '结束日期不能晚于 ' + max + '。'; }
|
|
142
|
+
};
|
|
143
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
122
144
|
opt = opt || {};
|
|
123
145
|
var from = root.querySelector('[data-dr-from]');
|
|
124
146
|
var to = root.querySelector('[data-dr-to]');
|
|
@@ -158,14 +180,14 @@
|
|
|
158
180
|
function validate() {
|
|
159
181
|
var f = from.value, t = to.value;
|
|
160
182
|
if (!f && !t) { setError(''); paintSummary(''); return true; }
|
|
161
|
-
if (f && !t) { setError(
|
|
162
|
-
if (!f && t) { setError(
|
|
183
|
+
if (f && !t) { setError(L.requiredEnd); return false; }
|
|
184
|
+
if (!f && t) { setError(L.requiredStart); return false; }
|
|
163
185
|
if (f > t) {
|
|
164
|
-
setError(
|
|
186
|
+
setError(L.order(f, t));
|
|
165
187
|
return false;
|
|
166
188
|
}
|
|
167
|
-
if (opt.min && f < opt.min) { setError(
|
|
168
|
-
if (opt.max && t > opt.max) { setError(
|
|
189
|
+
if (opt.min && f < opt.min) { setError(L.min(opt.min)); return false; }
|
|
190
|
+
if (opt.max && t > opt.max) { setError(L.max(opt.max)); return false; }
|
|
169
191
|
setError('');
|
|
170
192
|
paintSummary(f + ' → ' + t + '(' + days(f, t) + ' 天)');
|
|
171
193
|
return true;
|
|
@@ -181,11 +181,12 @@
|
|
|
181
181
|
(加了就是模态,语义就错了)。 */
|
|
182
182
|
box.setAttribute('role', 'dialog');
|
|
183
183
|
box.setAttribute('aria-label', opts.label || '');
|
|
184
|
+
var bodyHtml = sanitizeHtml(opts.html || '', opts.sanitize);
|
|
184
185
|
/* @html-sink raw-html — `opts.html` 是有意的 HTML 入口,调用方负责净化;
|
|
185
|
-
登记在 SECURITY.md「HTML 入口(sink
|
|
186
|
+
登记在 SECURITY.md「HTML 入口(sink)」。`sanitize` / `setSanitizer` 可挂钩。 */
|
|
186
187
|
box.innerHTML =
|
|
187
188
|
'<span class="popover__arrow" aria-hidden="true"></span>' +
|
|
188
|
-
'<div class="popover__body">' +
|
|
189
|
+
'<div class="popover__body">' + bodyHtml + '</div>';
|
|
189
190
|
|
|
190
191
|
/* 箭头要指向触发元素的中心,而不是浮层的中心 */
|
|
191
192
|
positionArrow(box, trigger);
|
|
@@ -294,6 +295,24 @@
|
|
|
294
295
|
return api;
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
/* HTML 净化钩子(F6 / 0.26.0)。
|
|
299
|
+
🔴 默认**不改变任何行为**:没传 `sanitize` 也没有 `setSanitizer()` 时,
|
|
300
|
+
`opts.html` 仍然按"调用方已净化"处理(与 0.25.x 一致)。
|
|
301
|
+
⇒ 想接入 DOMPurify 这类库:
|
|
302
|
+
Popover.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
|
|
303
|
+
或逐次传 `opts.sanitize` 覆盖(逐次优先于模块级)。
|
|
304
|
+
⚠️ 钩子只处理**字符串入口**;传 Node 的入口本来就绕过了 HTML 解析。 */
|
|
305
|
+
var SANITIZER = null;
|
|
306
|
+
|
|
307
|
+
function setSanitizer(fn) {
|
|
308
|
+
SANITIZER = (typeof fn === 'function') ? fn : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sanitizeHtml(html, perCall) {
|
|
312
|
+
var fn = (typeof perCall === 'function') ? perCall : SANITIZER;
|
|
313
|
+
return fn ? String(fn(String(html))) : String(html);
|
|
314
|
+
}
|
|
315
|
+
|
|
297
316
|
/* 箭头对准触发元素中心(浮层可能比触发元素宽) */
|
|
298
317
|
function positionArrow(box, trigger) {
|
|
299
318
|
var arrow = box.querySelector('.popover__arrow');
|
|
@@ -321,5 +340,6 @@
|
|
|
321
340
|
get current() { return current; },
|
|
322
341
|
};
|
|
323
342
|
|
|
343
|
+
Popover.setSanitizer = setSanitizer;
|
|
324
344
|
global.Popover = Popover;
|
|
325
345
|
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -246,6 +246,27 @@
|
|
|
246
246
|
var uid = 0;
|
|
247
247
|
|
|
248
248
|
function create(root, opt) {
|
|
249
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
250
|
+
var DEFAULT_LABELS = {
|
|
251
|
+
option: '选项',
|
|
252
|
+
placeholder: '请选择'
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
256
|
+
var flLabels = function (defaults, src) {
|
|
257
|
+
src = src || {};
|
|
258
|
+
var out = {};
|
|
259
|
+
var k;
|
|
260
|
+
for (k in defaults) {
|
|
261
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
262
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
};
|
|
267
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
268
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
269
|
+
|
|
249
270
|
opt = opt || {};
|
|
250
271
|
var btn = root.querySelector('[data-select-btn]');
|
|
251
272
|
var list = root.querySelector('[data-select-list]');
|
|
@@ -299,7 +320,7 @@
|
|
|
299
320
|
|
|
300
321
|
list.setAttribute('role', 'listbox');
|
|
301
322
|
if (isMulti) list.setAttribute('aria-multiselectable', 'true');
|
|
302
|
-
list.setAttribute('aria-label', opt.label ||
|
|
323
|
+
list.setAttribute('aria-label', opt.label || L.option);
|
|
303
324
|
btn.setAttribute('aria-haspopup', 'listbox');
|
|
304
325
|
btn.setAttribute('aria-expanded', 'false');
|
|
305
326
|
btn.setAttribute('aria-controls', list.id);
|
|
@@ -344,7 +365,7 @@
|
|
|
344
365
|
for (var i = 0; i < options.length; i++) {
|
|
345
366
|
if (hasValue(valueOfOpt(options[i]))) texts.push(optText(options[i]));
|
|
346
367
|
}
|
|
347
|
-
return texts.length ? texts.join('、') : (opt.placeholder ||
|
|
368
|
+
return texts.length ? texts.join('、') : (opt.placeholder || L.placeholder);
|
|
348
369
|
}
|
|
349
370
|
/* ⚠️ 空列表也会被调用(初始化 / 清空后重绘):必须能接住 undefined,
|
|
350
371
|
老实现直接 o.querySelector 会抛 "Cannot read properties of undefined"。 */
|
|
@@ -80,8 +80,42 @@
|
|
|
80
80
|
remove: 'M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6'
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
/* 读屏 / 状态文案默认值(F2 / 0.26.0)。
|
|
84
|
+
值可以是字符串或函数;不传 `labels` 时输出与之前逐字相同。 */
|
|
85
|
+
var DEFAULT_LABELS = {
|
|
86
|
+
pending: '待上传',
|
|
87
|
+
done: '已完成',
|
|
88
|
+
error: '失败',
|
|
89
|
+
remove: function (name) { return '移除 ' + name; },
|
|
90
|
+
uploading: function (pct) { return '上传中 ' + Math.round(pct) + '%'; },
|
|
91
|
+
countMax: function (n) { return '最多 ' + n + ' 个文件'; },
|
|
92
|
+
sizeMax: function (limit) { return '超过 ' + limit; },
|
|
93
|
+
type: '类型不支持',
|
|
94
|
+
folder: '请拖入文件,不支持文件夹',
|
|
95
|
+
none: function (reason) { return reason ? '未添加任何文件:' + reason : '未添加任何文件'; },
|
|
96
|
+
noDrop: '当前浏览器不支持拖放,请点「选择文件」',
|
|
97
|
+
noDropShort: '当前浏览器不支持拖放,请点选择文件',
|
|
98
|
+
summary: function (added, total, rejected) {
|
|
99
|
+
var s = '已添加 ' + added + ' 个文件,共 ' + total + ' 个';
|
|
100
|
+
if (rejected) s += ';' + rejected + ' 个未通过校验';
|
|
101
|
+
return s;
|
|
102
|
+
},
|
|
103
|
+
removed: function (name, rest) { return '已移除 ' + name + ',还剩 ' + rest + ' 个'; }
|
|
104
|
+
};
|
|
84
105
|
|
|
106
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
107
|
+
var flLabels = function (defaults, src) {
|
|
108
|
+
src = src || {};
|
|
109
|
+
var out = {};
|
|
110
|
+
var k;
|
|
111
|
+
for (k in defaults) {
|
|
112
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
113
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
85
119
|
/* ---------------------------------------------------------------- 工具 */
|
|
86
120
|
|
|
87
121
|
/**
|
|
@@ -183,6 +217,7 @@
|
|
|
183
217
|
var opt = opts || {};
|
|
184
218
|
|
|
185
219
|
this.root = root;
|
|
220
|
+
this.labels = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
186
221
|
this.input = root.querySelector('.upload__input');
|
|
187
222
|
this.list = root.querySelector('.upload__list');
|
|
188
223
|
this.status = root.querySelector('.upload__status');
|
|
@@ -299,15 +334,15 @@
|
|
|
299
334
|
for (var i = 0; i < incoming.length; i++) {
|
|
300
335
|
var f = incoming[i];
|
|
301
336
|
if (maxFiles && keep.length >= maxFiles) {
|
|
302
|
-
rejected.push({ file: f, reason: 'count', text:
|
|
337
|
+
rejected.push({ file: f, reason: 'count', text: this.labels.countMax(maxFiles) });
|
|
303
338
|
continue;
|
|
304
339
|
}
|
|
305
340
|
if (this.maxSize && f.size > this.maxSize) {
|
|
306
|
-
rejected.push({ file: f, reason: 'size', text:
|
|
341
|
+
rejected.push({ file: f, reason: 'size', text: this.labels.sizeMax(humanSize(this.maxSize)) });
|
|
307
342
|
continue;
|
|
308
343
|
}
|
|
309
344
|
if (!matchesAccept(f, accept)) {
|
|
310
|
-
rejected.push({ file: f, reason: 'type', text:
|
|
345
|
+
rejected.push({ file: f, reason: 'type', text: this.labels.type });
|
|
311
346
|
continue;
|
|
312
347
|
}
|
|
313
348
|
keep.push(f);
|
|
@@ -336,7 +371,7 @@
|
|
|
336
371
|
if (dirCount) {
|
|
337
372
|
r.rejected.push({
|
|
338
373
|
file: { name: dirCount + ' 个文件夹', size: 0 },
|
|
339
|
-
reason: 'dir', text:
|
|
374
|
+
reason: 'dir', text: this.labels.folder
|
|
340
375
|
});
|
|
341
376
|
}
|
|
342
377
|
|
|
@@ -344,7 +379,7 @@
|
|
|
344
379
|
/* 一个都没收:仍要把拒收项画出来,否则用户"拖了没反应" */
|
|
345
380
|
this.rejected = r.rejected;
|
|
346
381
|
this.render();
|
|
347
|
-
this.announce(r.rejected.length ?
|
|
382
|
+
this.announce(this.labels.none(r.rejected.length ? r.rejected[0].text : ''));
|
|
348
383
|
flEmit(this.root, 'fl-upload', {
|
|
349
384
|
files: currentFiles(this.input), added: 0, rejected: r.rejected
|
|
350
385
|
});
|
|
@@ -354,9 +389,9 @@
|
|
|
354
389
|
if (!writeFiles(this.input, r.keep)) {
|
|
355
390
|
/* 见文件头:宁可明说不可用,也不能装作收下了 */
|
|
356
391
|
this.rejected = [{ file: r.keep[0], reason: 'unsupported',
|
|
357
|
-
text:
|
|
392
|
+
text: this.labels.noDrop }];
|
|
358
393
|
this.render();
|
|
359
|
-
this.announce(
|
|
394
|
+
this.announce(this.labels.noDropShort);
|
|
360
395
|
return false;
|
|
361
396
|
}
|
|
362
397
|
|
|
@@ -441,7 +476,7 @@
|
|
|
441
476
|
/* 状态词给读屏(图标是 aria-hidden)—— 也是色盲用户的第二条线索 */
|
|
442
477
|
var sr = document.createElement('span');
|
|
443
478
|
sr.className = 'upload__status';
|
|
444
|
-
sr.textContent =
|
|
479
|
+
sr.textContent = this.labels[state] || '';
|
|
445
480
|
mark.appendChild(sr);
|
|
446
481
|
|
|
447
482
|
var name = document.createElement('span');
|
|
@@ -477,12 +512,12 @@
|
|
|
477
512
|
var rm = document.createElement('button');
|
|
478
513
|
rm.className = 'upload__remove';
|
|
479
514
|
rm.type = 'button';
|
|
480
|
-
rm.setAttribute('aria-label',
|
|
515
|
+
rm.setAttribute('aria-label', this.labels.remove(file.name || ''));
|
|
481
516
|
rm.appendChild(iconEl('remove'));
|
|
482
517
|
rm.addEventListener('click', function () { self.removeAt(index); });
|
|
483
518
|
li.appendChild(rm);
|
|
484
519
|
|
|
485
|
-
if (pct != null && pct >= 0) meta.textContent =
|
|
520
|
+
if (pct != null && pct >= 0) meta.textContent = self.labels.uploading(pct);
|
|
486
521
|
}
|
|
487
522
|
|
|
488
523
|
return li;
|
|
@@ -536,7 +571,7 @@
|
|
|
536
571
|
mark.appendChild(iconEl('done'));
|
|
537
572
|
var sr = document.createElement('span');
|
|
538
573
|
sr.className = 'upload__status';
|
|
539
|
-
sr.textContent =
|
|
574
|
+
sr.textContent = this.labels.done;
|
|
540
575
|
mark.appendChild(sr);
|
|
541
576
|
}
|
|
542
577
|
if (meta && f) meta.textContent = humanSize(f.size);
|
|
@@ -557,7 +592,7 @@
|
|
|
557
592
|
}
|
|
558
593
|
this.paintProgress(bar, pct);
|
|
559
594
|
/* 进度也写成文本:**信息不能只在图形里**,否则强制色/色盲下丢失 */
|
|
560
|
-
if (meta) meta.textContent =
|
|
595
|
+
if (meta) meta.textContent = this.labels.uploading(pct);
|
|
561
596
|
},
|
|
562
597
|
|
|
563
598
|
/* ================================================================ 移除 */
|
|
@@ -579,7 +614,7 @@
|
|
|
579
614
|
|
|
580
615
|
this.rebind(); /* 索引整体前移,旧闭包里的 i 已过期 */
|
|
581
616
|
this.syncCount();
|
|
582
|
-
this.announce(
|
|
617
|
+
this.announce(this.labels.removed(removed.name || '文件', rest.length));
|
|
583
618
|
flEmit(this.root, 'fl-upload', {
|
|
584
619
|
files: rest, added: 0, removed: removed, rejected: []
|
|
585
620
|
});
|
|
@@ -620,9 +655,7 @@
|
|
|
620
655
|
|
|
621
656
|
summary: function (added, rejected) {
|
|
622
657
|
var total = currentFiles(this.input).length;
|
|
623
|
-
|
|
624
|
-
if (rejected) s += ';' + rejected + ' 个未通过校验';
|
|
625
|
-
return s;
|
|
658
|
+
return this.labels.summary(added, total, rejected);
|
|
626
659
|
},
|
|
627
660
|
|
|
628
661
|
/* ================================================================ 销毁 */
|
|
@@ -180,7 +180,19 @@
|
|
|
180
180
|
height: 0; /* 🔴 hr 默认有 inset 边框,必须清掉 */
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
/* 🔴 必须是**后代**选择器,不是 `> img` —— 和下面 `.prose a` 同一条教训。
|
|
184
|
+
*
|
|
185
|
+
* Markdown / CommonMark 渲染图片出来是 `<p><img></p>`,图片的父节点是 `<p>`,
|
|
186
|
+
* 不是 `.prose` 的直接子代 ⇒ `> img` **一条都匹配不上**,
|
|
187
|
+
* 于是 `max-width:100%` 从未生效。
|
|
188
|
+
* 实测(外部审计 F1):`04-recipes/longform/content.html` 里
|
|
189
|
+
* `<p><img width="600">`,375px 溢出 246px、320px 溢出 301px。
|
|
190
|
+
* 对"给用户和 AI 照抄的配方页"来说,这个缺陷会被原样复制出去。
|
|
191
|
+
*
|
|
192
|
+
* `> a` → `a` 早在 0.9 就踩过一次(段落里的链接落到浏览器默认蓝,
|
|
193
|
+
* 暗色对比度 1.91:1)。同一个坑换了标签又踩一次,所以这次把范围写宽:
|
|
194
|
+
* 正文里任何位置的图片都受同一条约束。 */
|
|
195
|
+
.prose img {
|
|
184
196
|
max-width: 100%;
|
|
185
197
|
height: auto;
|
|
186
198
|
display: block;
|
|
@@ -201,8 +201,44 @@
|
|
|
201
201
|
bottom: 'drawer--bottom'
|
|
202
202
|
};
|
|
203
203
|
|
|
204
|
+
|
|
205
|
+
/* HTML 净化钩子(F6 / 0.26.0)。
|
|
206
|
+
🔴 默认**不改变任何行为**:没传 `sanitize` 也没有 `setSanitizer()` 时,
|
|
207
|
+
`opt.content` 仍然按"调用方已净化"处理(与 0.25.x 一致)。
|
|
208
|
+
⇒ 想接入 DOMPurify 这类库:
|
|
209
|
+
Drawer.setSanitizer(function (html) { return DOMPurify.sanitize(html); });
|
|
210
|
+
或逐次传 `opt.sanitize` 覆盖(逐次优先于模块级)。
|
|
211
|
+
⚠️ 钩子只处理**字符串入口**;传 Node 的入口本来就绕过了 HTML 解析。 */
|
|
212
|
+
var SANITIZER = null;
|
|
213
|
+
|
|
214
|
+
function setSanitizer(fn) {
|
|
215
|
+
SANITIZER = (typeof fn === 'function') ? fn : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sanitizeHtml(html, perCall) {
|
|
219
|
+
var fn = (typeof perCall === 'function') ? perCall : SANITIZER;
|
|
220
|
+
return fn ? String(fn(String(html))) : String(html);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时行为逐字不变。 */
|
|
224
|
+
var DEFAULT_LABELS = { dialog: '抽屉', close: '关闭' };
|
|
225
|
+
|
|
226
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
227
|
+
var flLabels = function (defaults, src) {
|
|
228
|
+
src = src || {};
|
|
229
|
+
var out = {};
|
|
230
|
+
var k;
|
|
231
|
+
for (k in defaults) {
|
|
232
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
233
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
};
|
|
238
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
204
239
|
function open(opt) {
|
|
205
240
|
opt = opt || {};
|
|
241
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
206
242
|
var place = opt.place || 'right';
|
|
207
243
|
var placeCls = PLACE_CLASS[place];
|
|
208
244
|
if (!placeCls) placeCls = PLACE_CLASS.right;
|
|
@@ -226,7 +262,7 @@
|
|
|
226
262
|
d.setAttribute('aria-modal', 'true');
|
|
227
263
|
if (!d.id) d.id = 'drawer-' + (new Date()).getTime();
|
|
228
264
|
if (opt.title) d.setAttribute('aria-labelledby', d.id + '-title');
|
|
229
|
-
else d.setAttribute('aria-label', opt.label ||
|
|
265
|
+
else d.setAttribute('aria-label', opt.label || L.dialog);
|
|
230
266
|
d.tabIndex = -1; /* 焦点陷阱的兜底落点 */
|
|
231
267
|
|
|
232
268
|
var head = document.createElement('div');
|
|
@@ -240,16 +276,18 @@
|
|
|
240
276
|
导致上面所有 close.addEventListener 绑在函数上(静默失败)。 */
|
|
241
277
|
closeBtn.className = 'drawer__close';
|
|
242
278
|
closeBtn.type = 'button';
|
|
243
|
-
closeBtn.setAttribute('aria-label',
|
|
279
|
+
closeBtn.setAttribute('aria-label', L.close);
|
|
244
280
|
closeBtn.textContent = '\u00D7';
|
|
245
281
|
head.appendChild(h);
|
|
246
282
|
head.appendChild(closeBtn);
|
|
247
283
|
|
|
248
284
|
var body = document.createElement('div');
|
|
249
285
|
body.className = 'drawer__body';
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
286
|
+
if (typeof opt.content === 'string') {
|
|
287
|
+
/* @html-sink raw-html — `opt.content` 是有意的 HTML 入口,调用方负责净化;
|
|
288
|
+
登记在 SECURITY.md「HTML 入口(sink)」。`sanitize` / `setSanitizer` 可挂钩。 */
|
|
289
|
+
body.innerHTML = sanitizeHtml(opt.content, opt.sanitize);
|
|
290
|
+
}
|
|
253
291
|
else if (opt.content) body.appendChild(opt.content);
|
|
254
292
|
|
|
255
293
|
d.appendChild(head);
|
|
@@ -374,5 +412,5 @@
|
|
|
374
412
|
return { el: d, close: close };
|
|
375
413
|
}
|
|
376
414
|
|
|
377
|
-
global.Drawer = { open: open };
|
|
415
|
+
global.Drawer = { open: open, setSanitizer: setSanitizer };
|
|
378
416
|
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -330,6 +330,26 @@
|
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
function create(root, opt) {
|
|
333
|
+
/* 读屏文案默认值(F2 / 0.26.0)。不传 `labels` 时输出逐字不变。 */
|
|
334
|
+
var DEFAULT_LABELS = {
|
|
335
|
+
menu: '菜单'
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
339
|
+
var flLabels = function (defaults, src) {
|
|
340
|
+
src = src || {};
|
|
341
|
+
var out = {};
|
|
342
|
+
var k;
|
|
343
|
+
for (k in defaults) {
|
|
344
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
345
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
};
|
|
350
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
351
|
+
var L = flLabels(DEFAULT_LABELS, opt && opt.labels);
|
|
352
|
+
|
|
333
353
|
opt = opt || {};
|
|
334
354
|
var btn = root.querySelector('[data-dd-btn]') || root.querySelector('button');
|
|
335
355
|
var menu = root.querySelector('[data-dd-menu]');
|
|
@@ -344,7 +364,7 @@
|
|
|
344
364
|
/* APG:菜单要么全 aria-hidden,要么全不设 —— 不能半吊子 */
|
|
345
365
|
menu.setAttribute('role', 'menu');
|
|
346
366
|
if (!menu.getAttribute('aria-label') && !menu.getAttribute('aria-labelledby')) {
|
|
347
|
-
menu.setAttribute('aria-label', opt.label ||
|
|
367
|
+
menu.setAttribute('aria-label', opt.label || L.menu);
|
|
348
368
|
}
|
|
349
369
|
Array.prototype.forEach.call(menu.querySelectorAll('li'), function (li) {
|
|
350
370
|
/* 🔴 修一个**静默失效**(实测发现):
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
demo 可以用现代语法(跑在用户浏览器上),库代码不行。
|
|
11
11
|
|
|
12
12
|
API:
|
|
13
|
-
Overlay.toast({ title, desc, variant, duration })
|
|
13
|
+
Overlay.toast({ title, desc, variant, duration, maxVisible, labels })
|
|
14
14
|
Overlay.dialog({ title, desc, actions, danger, initialFocus })
|
|
15
|
+
labels: 读屏文案覆盖(默认中文,不传时行为不变)
|
|
15
16
|
→ 返回 { el, close }
|
|
16
17
|
========================================================================= */
|
|
17
18
|
(function (global) {
|
|
@@ -265,8 +266,36 @@
|
|
|
265
266
|
warning: 'toast--warning'
|
|
266
267
|
};
|
|
267
268
|
|
|
269
|
+
/* 读屏文案的**默认值**与合并规则(F2 / 0.26.0)。
|
|
270
|
+
🔴 只覆盖"会被读屏念出来或用户看得见"的文案;`throw new Error(...)`
|
|
271
|
+
这类是给调用方看的开发期信息,不在这里 —— 它们不经过 UI。
|
|
272
|
+
值可以是字符串,也可以是函数(动态文案,如 `第 N 页`)。
|
|
273
|
+
不传 `labels` 时行为与之前**逐字相同**(默认中文)。 */
|
|
274
|
+
var DEFAULT_LABELS = {
|
|
275
|
+
closeToast: '关闭提示'
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/* ==== BEHAVIOR INJECT BEGIN: labels ==== */
|
|
279
|
+
var flLabels = function (defaults, src) {
|
|
280
|
+
src = src || {};
|
|
281
|
+
var out = {};
|
|
282
|
+
var k;
|
|
283
|
+
for (k in defaults) {
|
|
284
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
285
|
+
out[k] = (src[k] === undefined || src[k] === null) ? defaults[k] : src[k];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
};
|
|
290
|
+
/* ==== BEHAVIOR INJECT END: labels ==== */
|
|
291
|
+
/* 同屏最多几条 toast。超了就把**最旧的**直接摘掉 ——
|
|
292
|
+
否则 20 条并发提示会把屏幕铺满,且最后一条永远读不到。
|
|
293
|
+
0 / 负数表示不限制(调用方自己负责)。 */
|
|
294
|
+
var TOAST_LIMIT = 4;
|
|
295
|
+
|
|
268
296
|
function toast(opts) {
|
|
269
297
|
opts = opts || {};
|
|
298
|
+
var L = flLabels(DEFAULT_LABELS, opts && opts.labels);
|
|
270
299
|
|
|
271
300
|
var region = document.querySelector('.toast-region');
|
|
272
301
|
if (!region) {
|
|
@@ -314,7 +343,7 @@
|
|
|
314
343
|
var closeBtn = document.createElement('button');
|
|
315
344
|
closeBtn.className = 'toast__close';
|
|
316
345
|
closeBtn.type = 'button';
|
|
317
|
-
closeBtn.setAttribute('aria-label',
|
|
346
|
+
closeBtn.setAttribute('aria-label', L.closeToast);
|
|
318
347
|
closeBtn.textContent = '×';
|
|
319
348
|
el.appendChild(closeBtn);
|
|
320
349
|
|
|
@@ -331,6 +360,14 @@
|
|
|
331
360
|
}
|
|
332
361
|
|
|
333
362
|
closeBtn.addEventListener('click', close);
|
|
363
|
+
/* 同屏上限:先摘最旧的,再挂新的。
|
|
364
|
+
🔴 摘的时候**不走 close()** —— 它带 220ms 退场动画,
|
|
365
|
+
动画期间节点仍在 DOM 里,children.length 不减 ⇒ 这里会空转。
|
|
366
|
+
被摘掉的那条已经在屏幕外了,动画没有意义。 */
|
|
367
|
+
var max = (opts.maxVisible === undefined) ? TOAST_LIMIT : opts.maxVisible;
|
|
368
|
+
while (max > 0 && region.children.length >= max) {
|
|
369
|
+
region.removeChild(region.firstChild);
|
|
370
|
+
}
|
|
334
371
|
region.appendChild(el);
|
|
335
372
|
|
|
336
373
|
var timerId = auto > 0 ? setTimeout(close, auto) : null;
|