base-paint 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/blocks.d.ts +169 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +897 -0
- package/dist/blocks.js.map +1 -0
- package/dist/controls.d.ts +26 -1
- package/dist/controls.d.ts.map +1 -1
- package/dist/controls.js +467 -9
- package/dist/controls.js.map +1 -1
- package/dist/font.d.ts +12 -0
- package/dist/font.d.ts.map +1 -0
- package/dist/font.js +12 -0
- package/dist/font.js.map +1 -0
- package/dist/helpShell.d.ts +57 -0
- package/dist/helpShell.d.ts.map +1 -0
- package/dist/helpShell.js +65 -0
- package/dist/helpShell.js.map +1 -0
- package/dist/output/saveHtml.d.ts +29 -0
- package/dist/output/saveHtml.d.ts.map +1 -0
- package/dist/output/saveHtml.js +229 -0
- package/dist/output/saveHtml.js.map +1 -0
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +183 -5
- package/dist/style.js.map +1 -1
- package/package.json +9 -3
package/dist/controls.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* (`base.js:96`)本就是页面侧代码。两侧分工见契约 §3.3「toast 移动端收窄的分工(FX-76-2)」。
|
|
45
45
|
*/
|
|
46
46
|
import { STYLE_PREFIX } from './style.js';
|
|
47
|
-
import { ACTION_BAR_DEFAULTS, ACTION_BAR_KINDS, ACTION_ID_ATTR, COPY_ACTION_IDS, COPY_TEXT_DEFAULTS, DEFAULT_DATA_ATTR, ESCAPE_HTML_CHARS, ESCAPE_HTML_ENTITIES, STATUS_DEFAULT_TEXT, STATUS_KINDS, TOAST_DEFAULTS, } from './spec/index.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
48
|
/* ── 错误形态(§3.3:与既有 RenderError 并列、互不继承、一律抛出、不返空) ── */
|
|
49
49
|
/** 控件层错误。**不**从 `src/index.ts` 导出:`SPEC_FROZEN_SURFACE` 44 条内无该运行时条目,
|
|
50
50
|
* 调用方按 `name === 'ControlsError'` + `code` 判定(与 `fillTemplate` 的 `TemplateError` 同口径)。 */
|
|
@@ -406,6 +406,57 @@ function helpersDataAttr(input) {
|
|
|
406
406
|
const attr = input === undefined || input === null ? undefined : input.dataAttr;
|
|
407
407
|
return typeof attr === 'string' && attr !== '' ? attr : DEFAULT_DATA_ATTR;
|
|
408
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 = '没有找到相关场景,换个词试试~';
|
|
409
460
|
/** 冻结签名:`buildSharedHelpersJs(input?: SharedHelpersInput): string`。
|
|
410
461
|
*
|
|
411
462
|
* 产出恒为**非空**、**经典 script** 作用域可跑的 IIFE(无 `import`/`export`/顶层 `await`),
|
|
@@ -420,12 +471,43 @@ function helpersDataAttr(input) {
|
|
|
420
471
|
* `maxStack`/`timeoutMs`/移动端收窄取冻结常量;④ 关闭按钮。
|
|
421
472
|
* 产出文本里的文案/数值**全部**取自冻结常量(不产第二份真相)。
|
|
422
473
|
* 注:`execCommand` 兜底用临时 textarea 的两个内联定位属性(`position`/`left`),属临时节点
|
|
423
|
-
* 定位而非控件样式,不构成第二份样式常量。
|
|
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` 字符串口径)。 */
|
|
424
500
|
export const buildSharedHelpersJs = (input) => {
|
|
425
501
|
const prefix = helpersPrefix(input);
|
|
426
502
|
const dataAttr = helpersDataAttr(input);
|
|
427
503
|
const markerAttr = 'data-' + prefix.replace(/-+$/, '') + '-helpers';
|
|
428
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;
|
|
429
511
|
const lines = [
|
|
430
512
|
'(function () {',
|
|
431
513
|
" 'use strict';",
|
|
@@ -450,6 +532,57 @@ export const buildSharedHelpersJs = (input) => {
|
|
|
450
532
|
' var MAX_STACK = ' + TOAST_DEFAULTS.maxStack + ';',
|
|
451
533
|
' var MOBILE_MAX_STACK = ' + TOAST_DEFAULTS.mobileMaxStack + ';',
|
|
452
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') + ';',
|
|
453
586
|
'',
|
|
454
587
|
' function boot() {',
|
|
455
588
|
' if (!document.body) return;',
|
|
@@ -458,10 +591,15 @@ export const buildSharedHelpersJs = (input) => {
|
|
|
458
591
|
' marker.setAttribute(MARKER_ATTR, "1");',
|
|
459
592
|
' marker.hidden = true;',
|
|
460
593
|
' document.body.appendChild(marker);',
|
|
461
|
-
|
|
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();',
|
|
462
598
|
' }',
|
|
463
599
|
'',
|
|
464
600
|
' function onClick(ev) {',
|
|
601
|
+
' if (ev[HANDLED_PROP]) return;',
|
|
602
|
+
' ev[HANDLED_PROP] = 1;',
|
|
465
603
|
' var node = ev.target;',
|
|
466
604
|
' if (!node || typeof node.closest !== "function") return;',
|
|
467
605
|
' var close = node.closest("." + CLOSE_CLASS);',
|
|
@@ -474,29 +612,30 @@ export const buildSharedHelpersJs = (input) => {
|
|
|
474
612
|
' if (!btn) return;',
|
|
475
613
|
' var text = btn.getAttribute(TEXT_ATTR);',
|
|
476
614
|
' if (text === null) return;',
|
|
477
|
-
' copy(text);',
|
|
615
|
+
' copy(text, btn);',
|
|
478
616
|
' }',
|
|
479
617
|
'',
|
|
480
|
-
' function copy(text) {',
|
|
618
|
+
' function copy(text, btn) {',
|
|
481
619
|
' var clip = navigator.clipboard;',
|
|
482
620
|
' if (clip && typeof clip.writeText === "function") {',
|
|
483
621
|
' try {',
|
|
484
622
|
' var done = clip.writeText(text);',
|
|
485
623
|
' if (done && typeof done.then === "function") {',
|
|
486
|
-
' done.then(function () { feedback(OK_MSG, false); }, function () { fallback(text); });',
|
|
624
|
+
' done.then(function () { markCopied(btn); feedback(OK_MSG, false); }, function () { fallback(text, btn); });',
|
|
487
625
|
' return;',
|
|
488
626
|
' }',
|
|
627
|
+
' markCopied(btn);',
|
|
489
628
|
' feedback(OK_MSG, false);',
|
|
490
629
|
' return;',
|
|
491
630
|
' } catch (err) {',
|
|
492
|
-
' fallback(text);',
|
|
631
|
+
' fallback(text, btn);',
|
|
493
632
|
' return;',
|
|
494
633
|
' }',
|
|
495
634
|
' }',
|
|
496
|
-
' fallback(text);',
|
|
635
|
+
' fallback(text, btn);',
|
|
497
636
|
' }',
|
|
498
637
|
'',
|
|
499
|
-
' function fallback(text) {',
|
|
638
|
+
' function fallback(text, btn) {',
|
|
500
639
|
' var sink = document.createElement("textarea");',
|
|
501
640
|
' sink.value = text;',
|
|
502
641
|
' sink.setAttribute("readonly", "readonly");',
|
|
@@ -508,9 +647,19 @@ export const buildSharedHelpersJs = (input) => {
|
|
|
508
647
|
' var done = false;',
|
|
509
648
|
' try { done = document.execCommand("copy"); } catch (err) { done = false; }',
|
|
510
649
|
' if (sink.parentNode) sink.parentNode.removeChild(sink);',
|
|
650
|
+
' if (done) markCopied(btn);',
|
|
511
651
|
' feedback(done ? OK_MSG : FAIL_MSG, !done);',
|
|
512
652
|
' }',
|
|
513
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
|
+
'',
|
|
514
663
|
' function feedback(msg, bad) {',
|
|
515
664
|
' var cap = MAX_STACK;',
|
|
516
665
|
' if (window.matchMedia && window.matchMedia("(max-width: " + MOBILE_MAX_PX + "px)").matches) cap = MOBILE_MAX_STACK;',
|
|
@@ -567,6 +716,315 @@ export const buildSharedHelpersJs = (input) => {
|
|
|
567
716
|
' }, TIMEOUT_MS);',
|
|
568
717
|
' }',
|
|
569
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
|
+
'',
|
|
570
1028
|
' if (document.body) boot();',
|
|
571
1029
|
' else document.addEventListener("DOMContentLoaded", boot);',
|
|
572
1030
|
'}());',
|