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/dist/app/demo.cga +0 -0
- package/dist/app/task.cga +0 -0
- package/dist/control/common.cgc +0 -0
- package/dist/control/form.cgc +0 -0
- package/dist/global.css +1 -1
- package/dist/index.js +1 -1
- package/dist/theme/compact-dark.cgt +0 -0
- package/dist/theme/compact-light.cgt +0 -0
- package/doc/clickgo-rag.md +552 -335
- package/package.json +1 -1
- package/test/dock-lifecycle.mjs +111 -3
- package/test/form-modal-layer.mjs +75 -0
- package/test/locale-integrity.mjs +2 -2
- package/test/pop-max-size.mjs +66 -0
- package/test/rtl-archive.mjs +16 -8
- package/test/sidebar-toggle.mjs +75 -0
- package/test/toolbox.mjs +88 -0
package/package.json
CHANGED
package/test/dock-lifecycle.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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.');
|
package/test/rtl-archive.mjs
CHANGED
|
@@ -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
|
-
// ---
|
|
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', ['
|
|
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(
|
|
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.');
|
package/test/toolbox.mjs
ADDED
|
@@ -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.');
|