clickgo 6.5.2 → 6.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clickgo",
3
- "version": "6.5.2",
3
+ "version": "6.7.0",
4
4
  "description": "Background interface, software interface, mobile phone APP interface operation library.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -8,7 +8,8 @@ const sizeUnwatches = [];
8
8
 
9
9
  globalThis.document = {
10
10
  'documentElement': {
11
- 'clientWidth': 360
11
+ 'clientWidth': 360,
12
+ 'clientHeight': 800
12
13
  }
13
14
  };
14
15
 
@@ -32,6 +33,14 @@ class AbstractControl {
32
33
  return Boolean(this.props[name]);
33
34
  }
34
35
 
36
+ propNumber(name) {
37
+ return Number(this.props[name]);
38
+ }
39
+
40
+ parentByName(name) {
41
+ return this.parents?.[name] ?? null;
42
+ }
43
+
35
44
  emit() {}
36
45
  }
37
46
 
@@ -56,6 +65,9 @@ const clickgo = new SyntheticModule(['control', 'dom'], function() {
56
65
  },
57
66
  isRtl(element) {
58
67
  return element.rtl;
68
+ },
69
+ index(element) {
70
+ return element.order;
59
71
  }
60
72
  });
61
73
  });
@@ -146,6 +158,10 @@ group.dock = {
146
158
  group.updateFloatLayout();
147
159
  assert.equal(group.floatMaxWidth, 280, 'LTR left Dock must use only the viewport space on its right');
148
160
  assert.equal(group.floatStyle['max-width'], '280px');
161
+ assert.equal(group.floatStyle.height, undefined, 'short floating panels must size to their content');
162
+ group.props.grow = true;
163
+ assert.equal(group.floatStyle.height, '300px', 'grow floating panels fill the available height');
164
+ group.props.grow = false;
149
165
 
150
166
  group.element.rtl = true;
151
167
  areaRect.left = 320;
@@ -159,14 +175,106 @@ areaRect.right = 40;
159
175
  group.updateFloatLayout();
160
176
  assert.equal(group.floatMaxWidth, 320, 'RTL right Dock must open toward the viewport space on its right');
161
177
 
178
+ group.element.rtl = false;
179
+ group.dock.getFloatBounds = () => ({ 'left': 30, 'right': 330, 'top': 60, 'bottom': 220 });
180
+ areaRect.left = 300;
181
+ areaRect.right = 340;
182
+ group.updateFloatLayout();
183
+ assert.equal(group.floatMaxWidth, 270, 'floating panels must stay inside their Form, not only the viewport');
184
+ assert.equal(group.floatMaxHeight, '160px');
185
+ assert.equal(group.floatTop, 60);
186
+ group.dock.positionData = 'left';
187
+ group.updateFloatLayout();
188
+ assert.equal(group.floatMaxWidth, 0, 'zero available width must remain zero rather than use a fallback');
189
+ assert.equal(group.floatStyle['max-width'], '0px');
190
+
191
+ const columnMod = new SourceTextModule(await readFile(new URL('../dist/sources/control/dock-column/code.js', import.meta.url), 'utf8'));
192
+ await columnMod.link(() => clickgo);
193
+ await columnMod.evaluate();
194
+ const DockColumn = columnMod.namespace.default;
195
+
196
+ document.documentElement.clientWidth = 1200;
197
+ document.documentElement.clientHeight = 900;
198
+ const multiForm = {
199
+ 'element': {
200
+ 'isConnected': true,
201
+ 'offsetWidth': 1100,
202
+ 'getBoundingClientRect': () => ({ 'left': 60, 'right': 1160, 'top': 30, 'bottom': 930 })
203
+ }
204
+ };
205
+ const multiDock = new Dock(multiForm, 600);
206
+ await multiDock.onMounted();
207
+ assert.deepEqual(multiDock.getFloatBounds(), { 'left': 60, 'right': 1160, 'top': 30, 'bottom': 900 });
208
+
209
+ const projectColumn = new DockColumn(multiForm, 600);
210
+ const mainColumn = new DockColumn(multiForm, 600);
211
+ projectColumn.parents = mainColumn.parents = { 'dock': multiDock };
212
+ projectColumn.element = { 'order': 0 };
213
+ mainColumn.element = { 'order': 1 };
214
+ projectColumn.props.width = 220;
215
+ mainColumn.props.width = 340;
216
+ // Child groups mount before their column: both columns must allocate through the same root.
217
+ const projectIndex = projectColumn.registerGroup();
218
+ const mainIndex = mainColumn.registerGroup();
219
+ assert.notEqual(projectIndex, mainIndex);
220
+ await projectColumn.onMounted();
221
+ await mainColumn.onMounted();
222
+ assert.equal(projectColumn.expandedData, true);
223
+ assert.equal(mainColumn.expandedData, true);
224
+
225
+ function resizeMultiForm(width) {
226
+ multiForm.element.offsetWidth = width;
227
+ multiDock.access.formSizeWatch.handler();
228
+ }
229
+ resizeMultiForm(850);
230
+ assert.equal(projectColumn.autoCollapsed, true, 'the auxiliary column collapses first');
231
+ assert.equal(mainColumn.autoCollapsed, false, 'keep the main column open when it still fits');
232
+ assert.equal(projectColumn.expandedPreference, true, 'automatic collapse must preserve user preference');
233
+ assert.equal(projectColumn.props.expanded, true);
234
+ assert.match(multiDock.widthComp, /var\(--dock-collapsed-width\).*340px/);
235
+
236
+ resizeMultiForm(650);
237
+ assert.equal(projectColumn.expandedData, false);
238
+ assert.equal(mainColumn.expandedData, false, 'collapse the main column too when workspace space is insufficient');
239
+ resizeMultiForm(1100);
240
+ assert.equal(projectColumn.expandedData, true);
241
+ assert.equal(mainColumn.expandedData, true);
242
+
243
+ document.documentElement.clientWidth = 600;
244
+ multiDock.access.viewportSizeWatch.handler();
245
+ assert.equal(projectColumn.expandedData, false);
246
+ assert.equal(mainColumn.expandedData, false, 'viewport clipping must also limit multiple expanded columns');
247
+ document.documentElement.clientWidth = 1200;
248
+ multiDock.access.viewportSizeWatch.handler();
249
+ assert.equal(mainColumn.expandedData, true);
250
+ projectColumn.toggle();
251
+ assert.equal(projectColumn.expandedPreference, false);
252
+ resizeMultiForm(650);
253
+ resizeMultiForm(1100);
254
+ assert.equal(projectColumn.expandedData, false, 'a column the user closed must remain closed after resizing');
255
+ assert.equal(mainColumn.expandedData, true);
256
+
257
+ multiDock.toggleFloat(projectIndex);
258
+ multiDock.toggleFloat(mainIndex);
259
+ assert.equal(multiDock.floatGroup, mainIndex, 'only one floating group may be open across columns');
260
+ mainColumn.unregisterGroup(mainIndex);
261
+ assert.equal(multiDock.floatGroup, -1);
262
+ multiDock.toggleFloat(projectIndex);
263
+ projectColumn.onUnmounted();
264
+ assert.equal(multiDock.columns.length, 1);
265
+ assert.equal(multiDock.floatGroup, -1, 'column removal closes its floating panel');
266
+ mainColumn.onUnmounted();
267
+ multiDock.onUnmounted();
268
+
162
269
  const dockStyle = await readFile(new URL('../dist/sources/control/dock/style.scss', import.meta.url), 'utf8');
163
270
  assert.match(dockStyle, /border-inline-end/, 'left Dock border must follow the inline end edge');
164
271
  assert.match(dockStyle, /border-inline-start/, 'right Dock border must follow the inline start edge');
165
- assert.match(dockStyle, /:dir\(rtl\)[\s\S]*scaleX\(-1\)/, 'Dock chevron must mirror in RTL');
272
+ const toggleStyle = await readFile(new URL('../dist/sources/control/sidebar-toggle/style.scss', import.meta.url), 'utf8');
273
+ assert.match(toggleStyle, /:dir\(rtl\)[\s\S]*scaleX\(-1\)/, 'shared sidebar chevron must mirror in RTL');
166
274
 
167
275
  const groupStyle = await readFile(new URL('../dist/sources/control/dock-group/style.scss', import.meta.url), 'utf8');
168
276
  assert.match(groupStyle, /inset-inline-start:\s*100%/, 'left Dock floating panel must open toward inline end');
169
277
  assert.match(groupStyle, /inset-inline-end:\s*100%/, 'right Dock floating panel must open toward inline start');
170
278
  assert.match(groupStyle, /max-width:\s*calc\(100vw - 40px\)/, 'floating panel must reserve the collapsed Dock width');
171
279
 
172
- console.log('Dock lifecycle, RTL layout and viewport-bound floating-panel checks passed.');
280
+ console.log('Dock lifecycle, multiple columns, preference restoration, RTL and Form/viewport bounds passed.');
@@ -0,0 +1,75 @@
1
+ // Run after Sass compilation: node test/form-modal-layer.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { JSDOM } from 'jsdom';
5
+ import * as tool from '../dist/lib/tool.js';
6
+
7
+ const dom = new JSDOM('<!doctype html><html><head></head><body></body></html>');
8
+ for (const name of ['window', 'document', 'Element', 'SVGElement', 'Node', 'HTMLElement']) {
9
+ globalThis[name] = dom.window[name];
10
+ }
11
+ const { createApp, compile, nextTick } = await import('vue');
12
+ const template = await readFile(new URL('../dist/sources/control/form/layout.html', import.meta.url), 'utf8');
13
+ const formStyle = document.createElement('style');
14
+ formStyle.textContent = await readFile(new URL('../dist/sources/control/form/style.css', import.meta.url), 'utf8');
15
+ document.head.append(formStyle);
16
+ const dockStyle = document.createElement('style');
17
+ dockStyle.textContent = tool.stylePrepend(await readFile(
18
+ new URL('../dist/sources/control/dock-group/style.css', import.meta.url), 'utf8',
19
+ ), 'dock-group_').style;
20
+ document.head.append(dockStyle);
21
+
22
+ for (const border of ['normal', 'thin', 'plain', 'none']) {
23
+ const host = document.createElement('div');
24
+ document.body.append(host);
25
+ const app = createApp({
26
+ render: compile(template),
27
+ data: () => ({
28
+ border, isNative: false, isNativeNoFrameFirst: false, flashTimer: undefined,
29
+ stateMinData: false, stateMaxData: false, taskPosition: 'bottom',
30
+ isShow: true, isInside: false, formFocus: true, widthData: 500,
31
+ heightData: 400, leftData: 0, topData: 0, zIndex: 1,
32
+ isMask: false, isResize: false, iconDataUrl: '', title: 'Dock',
33
+ isMin: false, isMax: false, isClose: false, direction: 'v',
34
+ isLoading: false, loading: false, loadingComp: false,
35
+ background: '', padding: '', stepShowData: false, stepData: [],
36
+ stepDown: () => {}, stepValue: '', stepValues: [],
37
+ }),
38
+ });
39
+ app.component('cg-loading', { render: () => null });
40
+ app.component('cg-step', { render: () => null });
41
+ const form = app.mount(host);
42
+ const wrap = host.querySelector('.wrap');
43
+ const inner = wrap.querySelector(':scope > .inner');
44
+ const content = inner.querySelector(':scope > .content');
45
+ const dock = document.createElement('div');
46
+ dock.className = 'dock-group_wrap dock-group_collapsed dock-group_floating';
47
+ dock.innerHTML = '<div class="dock-group_content"><button>Images</button></div>';
48
+ content.append(dock);
49
+ const floating = dock.firstElementChild;
50
+ const button = floating.firstElementChild;
51
+ assert.equal(dom.window.getComputedStyle(floating).zIndex, '10');
52
+
53
+ // --- 模态打开、嵌套模态仍遮罩、关闭后的恢复均不得重建 Dock 内容 ---
54
+ for (const masked of [true, true, false, true, false]) {
55
+ form.isMask = masked;
56
+ await nextTick();
57
+ const mask = wrap.querySelector(':scope > .mask');
58
+ assert.equal(Boolean(mask), masked);
59
+ assert.equal(dock.firstElementChild, floating);
60
+ assert.equal(floating.firstElementChild, button);
61
+ assert.equal(dom.window.getComputedStyle(floating).display, 'flex');
62
+ if (mask) {
63
+ assert.equal(mask.parentElement, inner.parentElement);
64
+ assert.equal(dom.window.getComputedStyle(mask).zIndex, '10');
65
+ // --- 子浮层不能参与遮罩所在层的排序,即使两者 z-index 相同且浮层 DOM 在后 ---
66
+ assert.equal(dom.window.getComputedStyle(inner).isolation, 'isolate');
67
+ assert.equal(inner.contains(floating), true);
68
+ assert.equal(inner.contains(mask), false);
69
+ }
70
+ }
71
+ app.unmount();
72
+ host.remove();
73
+ }
74
+ dom.window.close();
75
+ console.log('Form modal layer checks passed: Dock floats stay inside the content stacking context for all borders; modal toggles preserve panel content.');
@@ -140,7 +140,7 @@ for (const path of localeSources) {
140
140
  tables.push({ ...table, path });
141
141
  }
142
142
  }
143
- assert.equal(tables.length, 24, 'all 24 built-in TypeScript locale tables must be present');
143
+ assert.equal(tables.length, 26, 'all 26 built-in TypeScript locale tables must be present');
144
144
  for (const { node, path, line } of tables) {
145
145
  const tableCodes = node.properties.map(getPropertyName);
146
146
  assert.deepEqual([...tableCodes].sort(), [...codes].sort(), `${path}:${line} must contain exactly 16 locale entries`);
@@ -186,4 +186,4 @@ const demoLocaleCodes = [...demoLocaleData.matchAll(/'([^']+)'/g)].map((match) =
186
186
  assert.deepEqual(demoLocaleCodes, codes, 'demo global locale selector must expose all 16 locales in registry order');
187
187
 
188
188
  dom.window.close();
189
- console.log('Locale registry, 24 TypeScript tables, 16 task app locale JSON files and the Demo selector are complete.');
189
+ console.log(`Locale registry, ${tables.length} TypeScript tables, 16 task app locale JSON files and the Demo selector are complete.`);
@@ -0,0 +1,66 @@
1
+ // Run: node --experimental-vm-modules test/pop-max-size.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { SourceTextModule, SyntheticModule } from 'node:vm';
5
+ import { JSDOM } from 'jsdom';
6
+ import ts from 'typescript';
7
+
8
+ const dom = new JSDOM('<!doctype html><style>.pop { max-width: 480px; max-height: 300px; }</style><div id="trigger"></div><div class="pop"></div>');
9
+ for (const name of ['window', 'document', 'Element', 'HTMLElement', 'PointerEvent']) {
10
+ Object.defineProperty(globalThis, name, {
11
+ 'configurable': true,
12
+ 'value': dom.window[name],
13
+ });
14
+ }
15
+ globalThis.getComputedStyle = dom.window.getComputedStyle.bind(dom.window);
16
+ Object.defineProperty(window, 'innerWidth', { 'configurable': true, 'value': 1024 });
17
+ Object.defineProperty(window, 'innerHeight', { 'configurable': true, 'writable': true, 'value': 768 });
18
+
19
+ let refreshSize;
20
+ const domModule = new SyntheticModule(['findParentByData', 'watchSize'], function() {
21
+ this.setExport('findParentByData', () => null);
22
+ this.setExport('watchSize', (_id, _pop, callback) => { refreshSize = callback; });
23
+ });
24
+ const toolModule = new SyntheticModule(['sleep'], function() {
25
+ this.setExport('sleep', () => Promise.resolve());
26
+ });
27
+ const empty = new SyntheticModule([], function() {});
28
+ const source = await readFile(new URL('../dist/lib/form.ts', import.meta.url), 'utf8');
29
+ const formModule = new SourceTextModule(ts.transpileModule(source, {
30
+ 'compilerOptions': {
31
+ 'target': ts.ScriptTarget.ES2022,
32
+ 'module': ts.ModuleKind.ESNext,
33
+ },
34
+ }).outputText);
35
+ await formModule.link((specifier) => {
36
+ if (specifier === './dom') {
37
+ return domModule;
38
+ }
39
+ if (specifier === './tool') {
40
+ return toolModule;
41
+ }
42
+ return empty;
43
+ });
44
+ await formModule.evaluate();
45
+
46
+ const trigger = document.querySelector('#trigger');
47
+ const pop = document.querySelector('.pop');
48
+ formModule.namespace.showPop(trigger, pop, 'v', { 'autoPosition': true });
49
+ await Promise.resolve();
50
+ assert.equal(pop.style.maxHeight, 'min(300px, 760px)', 'CSS max-height must survive the viewport limit');
51
+ assert.equal(pop.style.maxWidth, 'min(480px, 1016px)', 'CSS max-width must survive the viewport limit');
52
+
53
+ window.innerHeight = 240;
54
+ refreshSize();
55
+ assert.equal(pop.style.maxHeight, 'min(300px, 232px)', 'a smaller viewport must still constrain the popup');
56
+
57
+ pop.style.maxHeight = '180px';
58
+ refreshSize();
59
+ assert.equal(pop.style.maxHeight, 'min(180px, 232px)', 'an explicit inline limit must take effect');
60
+
61
+ pop.style.maxHeight = '';
62
+ refreshSize();
63
+ assert.equal(pop.style.maxHeight, 'min(300px, 232px)', 'removing an inline limit must restore the CSS limit');
64
+
65
+ dom.window.close();
66
+ console.log('Popup CSS limits, viewport limits, and inline overrides passed.');
@@ -14,15 +14,15 @@ const codes = [
14
14
  /** --- 包含 localeData 的全部内置控件 --- */
15
15
  const localeControls = [
16
16
  'arteditor', 'calendar', 'captcha', 'date', 'datepanel', 'daterange', 'empty', 'jodit', 'label',
17
- 'levelselect', 'map', 'monaco', 'number', 'page', 'palette', 'property', 'select', 'text', 'tuieditor',
17
+ 'levelselect', 'map', 'monaco', 'number', 'page', 'palette', 'property', 'select', 'sidebar-toggle', 'text', 'toolbox', 'tuieditor',
18
18
  ];
19
19
  /** --- 合并到 common.cgc 的多语言控件 --- */
20
20
  const commonControls = new Set([
21
21
  'calendar', 'date', 'datepanel', 'daterange', 'empty', 'label', 'levelselect', 'number', 'palette',
22
- 'select', 'text',
22
+ 'select', 'sidebar-toggle', 'text', 'toolbox',
23
23
  ]);
24
- /** --- 六套内置主题 --- */
25
- const themes = ['admin', 'cyber', 'dark', 'light', 'modern', 'modern-light'];
24
+ /** --- 全部内置主题 --- */
25
+ const themes = ['admin', 'cyber', 'dark', 'light', 'modern', 'modern-light', 'compact-dark', 'compact-light'];
26
26
  /** --- CGA 格式常量,与编译器的公开包格式保持一致 --- */
27
27
  const cgaHeaderLength = 106;
28
28
  const cgaKeyContext = Buffer.from('ClickGo/Application/Package');
@@ -335,7 +335,7 @@ for (const code of codes) {
335
335
  }
336
336
  }
337
337
 
338
- // --- 19 个控件的归档语言表必须与 TypeScript 源码一致 ---
338
+ // --- 所有多语言控件的归档语言表必须与 TypeScript 源码一致 ---
339
339
  const archivedControlCode = new Map();
340
340
  for (const control of localeControls) {
341
341
  const packageName = commonControls.has(control) ? 'common' : control;
@@ -365,6 +365,10 @@ const rtlCodeChecks = [
365
365
  ['dist/control/common.cgc', 'date', ['autoPosition', 'autoScroll']],
366
366
  ['dist/control/common.cgc', 'daterange', ['autoPosition', 'autoScroll']],
367
367
  ['dist/control/common.cgc', 'dock-group', ['isRtl', 'floatMaxWidth']],
368
+ ['dist/control/common.cgc', 'dock', ['updateColumns', 'getFloatBounds']],
369
+ ['dist/control/common.cgc', 'dock-column', ['registerGroup', 'expandedPreference']],
370
+ ['dist/control/common.cgc', 'toolbox', ['getBoundingClientRect', 'toggleColumns']],
371
+ ['dist/control/common.cgc', 'sidebar-toggle', ['chevronRight', 'tipLabelComp', 'update:expanded']],
368
372
  ['dist/control/table.cgc', 'table', ['localeDirection', 'getScrollLeft', 'setScrollInlineOffset']],
369
373
  ['dist/control/desc.cgc', 'desc', ['getScrollInlineOffset', 'setScrollInlineOffset']],
370
374
  ['dist/control/iconview.cgc', 'iconview', ['isRtl']],
@@ -382,12 +386,16 @@ for (const [archive, control, tokens] of rtlCodeChecks) {
382
386
 
383
387
  // --- 控件 RTL 样式必须进入 cgc,并与 Sass 生成 CSS 一致 ---
384
388
  const rtlStyleChecks = [
389
+ ['dist/control/form.cgc', 'form/style.css', 'dist/sources/control/form/style.css', ['isolation:isolate']],
385
390
  ['dist/control/common.cgc', 'button/style.css', 'dist/sources/control/button/style.css', [':dir(rtl)', 'border-inline-end']],
386
391
  ['dist/control/common.cgc', 'list/style.css', 'dist/sources/control/list/style.css', [':dir(rtl)', 'margin-inline-end']],
387
392
  ['dist/control/common.cgc', 'tab/style.css', 'dist/sources/control/tab/style.css', ['border-inline-start-width', 'border-inline-end-width']],
388
393
  ['dist/control/common.cgc', 'timeline/style.css', 'dist/sources/control/timeline/style.css', ['border-inline-start']],
389
- ['dist/control/common.cgc', 'dock/style.css', 'dist/sources/control/dock/style.css', [':dir(rtl)', 'border-inline-end']],
394
+ ['dist/control/common.cgc', 'dock/style.css', 'dist/sources/control/dock/style.css', ['border-inline-end']],
390
395
  ['dist/control/common.cgc', 'dock-group/style.css', 'dist/sources/control/dock-group/style.css', ['inset-inline-start', 'max-width:calc(100vw - 40px)']],
396
+ ['dist/control/common.cgc', 'dock-column/style.css', 'dist/sources/control/dock-column/style.css', ['border-inline-start']],
397
+ ['dist/control/common.cgc', 'toolbox/style.css', 'dist/sources/control/toolbox/style.css', ['border-inline-end']],
398
+ ['dist/control/common.cgc', 'sidebar-toggle/style.css', 'dist/sources/control/sidebar-toggle/style.css', [':dir(rtl)', 'border-bottom', '--sidebar-toggle-background']],
391
399
  ['dist/control/table.cgc', 'table/style.css', 'dist/sources/control/table/style.css', ['border-inline-end', 'inset-inline-end']],
392
400
  ['dist/control/xterm.cgc', 'xterm/style.css', 'dist/sources/control/xterm/style.css', ['direction:ltr']],
393
401
  ];
@@ -398,7 +406,7 @@ for (const [archive, entry, sourcePath, tokens] of rtlStyleChecks) {
398
406
  }
399
407
  }
400
408
 
401
- // --- 六套主题必须全部包含最新的逻辑方向样式 ---
409
+ // --- 全部主题必须全部包含最新的逻辑方向样式 ---
402
410
  for (const theme of themes) {
403
411
  const archive = `dist/theme/${theme}.cgt`;
404
412
  const archivedConfig = JSON.parse(await readZipText(archive, 'config.json'));
@@ -416,4 +424,4 @@ for (const theme of themes) {
416
424
  );
417
425
  }
418
426
 
419
- console.log('Archive check passed: task.cga has 16 locales, 19 control locale tables are current, and RTL output is present in all 6 themes.');
427
+ console.log(`Archive check passed: task.cga has 16 locales, ${localeControls.length} control locale tables are current, and RTL output is present in all ${themes.length} themes.`);
@@ -0,0 +1,75 @@
1
+ // Run after TypeScript compilation: node --experimental-vm-modules test/sidebar-toggle.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { SourceTextModule, SyntheticModule } from 'node:vm';
5
+
6
+ class AbstractControl {
7
+ constructor() {
8
+ this.emitted = [];
9
+ this.locale = 'sc';
10
+ }
11
+ propBoolean(name) {
12
+ return [true, 'true', ''].includes(this.props[name]);
13
+ }
14
+ l(key) {
15
+ return this.localeData[this.locale][key];
16
+ }
17
+ emit(name, value) {
18
+ this.emitted.push([name, value]);
19
+ }
20
+ }
21
+
22
+ const clickgo = new SyntheticModule(['control'], function() {
23
+ this.setExport('control', { AbstractControl });
24
+ });
25
+ const mod = new SourceTextModule(await readFile(new URL('../dist/sources/control/sidebar-toggle/code.js', import.meta.url), 'utf8'));
26
+ await mod.link(() => clickgo);
27
+ await mod.evaluate();
28
+ const SidebarToggle = mod.namespace.default;
29
+ const toggle = new SidebarToggle();
30
+
31
+ for (const [position, expanded, pointsRight] of [
32
+ ['left', false, true], ['left', true, false], ['right', false, false], ['right', true, true],
33
+ ]) {
34
+ toggle.props.position = position;
35
+ toggle.props.expanded = expanded;
36
+ assert.equal(toggle.chevronRight, pointsRight, `${position}, expanded=${expanded}`);
37
+ }
38
+
39
+ toggle.props.expanded = 'false';
40
+ toggle.props.label = 'Project';
41
+ assert.equal(toggle.tipLabelComp, '展开侧栏 · Project');
42
+ toggle.props.expanded = 'true';
43
+ assert.equal(toggle.tipLabelComp, '收起侧栏 · Project');
44
+ toggle.locale = 'ar';
45
+ assert.equal(toggle.tipLabelComp, 'طي الشريط الجانبي · Project');
46
+ toggle.props.tipLabel = 'Use one tool column';
47
+ assert.equal(toggle.tipLabelComp, 'Use one tool column', 'Toolbox keeps its column-specific operation');
48
+
49
+ for (const key of ['Enter', ' ']) {
50
+ toggle.props.expanded = false;
51
+ const count = toggle.emitted.length;
52
+ let prevented = false;
53
+ let stopped = false;
54
+ const event = {
55
+ key, 'repeat': false,
56
+ 'preventDefault': () => { prevented = true; },
57
+ 'stopPropagation': () => { stopped = true; },
58
+ };
59
+ toggle.keydown(event);
60
+ assert.equal(prevented && stopped, true);
61
+ assert.deepEqual(toggle.emitted[count], ['update:expanded', true]);
62
+ assert.equal(toggle.props.expanded, false, 'the owner applies the requested state');
63
+ toggle.keydown({ ...event, 'repeat': true });
64
+ assert.equal(toggle.emitted.length, count + 1, 'holding a key must not repeatedly flip the sidebar');
65
+ }
66
+ toggle.keydown({
67
+ 'key': 'Tab',
68
+ 'preventDefault': () => assert.fail('Tab keeps native focus navigation'),
69
+ 'stopPropagation': () => assert.fail('Other keys are not consumed'),
70
+ });
71
+ toggle.props.expanded = true;
72
+ toggle.toggle();
73
+ assert.deepEqual(toggle.emitted.at(-1), ['update:expanded', false]);
74
+
75
+ console.log('Shared sidebar toggle direction, localized tips, controlled state and keyboard activation passed.');
@@ -0,0 +1,88 @@
1
+ // Run after TypeScript compilation: node --experimental-vm-modules test/toolbox.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { SourceTextModule, SyntheticModule } from 'node:vm';
5
+ import { JSDOM } from 'jsdom';
6
+
7
+ const dom = new JSDOM('<div id="tools"></div>');
8
+ globalThis.HTMLElement = dom.window.HTMLElement;
9
+ const tools = dom.window.document.getElementById('tools');
10
+ class AbstractControl {
11
+ constructor() {
12
+ this.refs = { tools };
13
+ this.emitted = [];
14
+ }
15
+ emit(name, value) {
16
+ this.emitted.push([name, value]);
17
+ }
18
+ propNumber(name) {
19
+ return Number(this.props[name]);
20
+ }
21
+ watch(name, callback, options) {
22
+ if (options?.immediate) {
23
+ callback();
24
+ }
25
+ }
26
+ }
27
+ const clickgo = new SyntheticModule(['control'], function() {
28
+ this.setExport('control', { AbstractControl });
29
+ });
30
+ const mod = new SourceTextModule(await readFile(new URL('../dist/sources/control/toolbox/code.js', import.meta.url), 'utf8'));
31
+ await mod.link(() => clickgo);
32
+ await mod.evaluate();
33
+ const Toolbox = mod.namespace.default;
34
+ const toolbox = new Toolbox();
35
+ toolbox.props.columns = '2';
36
+ toolbox.onMounted();
37
+ assert.equal(toolbox.columnsData, 2);
38
+ toolbox.toggleColumns();
39
+ assert.equal(toolbox.columnsData, 1);
40
+ assert.deepEqual(toolbox.emitted, [['update:columns', 1]]);
41
+
42
+ function button(x, y, { disabled = false, hidden = false } = {}) {
43
+ const item = dom.window.document.createElement('div');
44
+ item.setAttribute('data-cg-control', 'button');
45
+ item.setAttribute('role', 'button');
46
+ item.setAttribute('tabindex', '0');
47
+ if (disabled) {
48
+ item.setAttribute('data-cg-disabled', '');
49
+ }
50
+ Object.defineProperty(item, 'offsetHeight', { 'value': hidden ? 0 : 24 });
51
+ item.getBoundingClientRect = () => ({ 'left': x, 'top': y, 'width': 24, 'height': 24 });
52
+ tools.append(item);
53
+ return item;
54
+ }
55
+ // A one-tool group precedes a two-column group; DOM index arithmetic would pick the wrong row.
56
+ const browse = button(0, 0);
57
+ const paint = button(0, 40);
58
+ const erase = button(30, 40);
59
+ button(0, 80, { 'disabled': true });
60
+ button(30, 80, { 'hidden': true });
61
+ const select = button(0, 120);
62
+
63
+ function move(from, key, expected) {
64
+ from.focus();
65
+ let prevented = false;
66
+ toolbox.keydown({ 'target': from, key, 'preventDefault': () => { prevented = true; } });
67
+ assert.equal(prevented, true);
68
+ assert.equal(dom.window.document.activeElement, expected, key);
69
+ }
70
+ move(browse, 'ArrowDown', paint);
71
+ move(paint, 'ArrowRight', erase);
72
+ move(erase, 'ArrowLeft', paint);
73
+ move(paint, 'ArrowDown', select);
74
+ move(select, 'Home', browse);
75
+ move(browse, 'End', select);
76
+ move(browse, 'ArrowUp', browse);
77
+
78
+ // RTL changes the rendered physical positions; navigation must follow those positions.
79
+ paint.getBoundingClientRect = () => ({ 'left': 30, 'top': 40, 'width': 24, 'height': 24 });
80
+ erase.getBoundingClientRect = () => ({ 'left': 0, 'top': 40, 'width': 24, 'height': 24 });
81
+ move(paint, 'ArrowLeft', erase);
82
+ move(erase, 'ArrowRight', paint);
83
+
84
+ const outside = dom.window.document.createElement('div');
85
+ outside.setAttribute('data-cg-control', 'button');
86
+ outside.setAttribute('role', 'button');
87
+ toolbox.keydown({ 'target': outside, 'key': 'ArrowDown', 'preventDefault': () => assert.fail('header/outside buttons are not tools') });
88
+ console.log('Toolbox column binding and keyboard navigation across groups, hidden/disabled tools and RTL passed.');