clickgo 6.8.0 → 6.8.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/dist/app/demo.cga +0 -0
- package/dist/app/task.cga +0 -0
- package/dist/clickgo.d.ts +21 -0
- package/dist/control/common.cgc +0 -0
- package/dist/control/iconview.cgc +0 -0
- package/dist/control/jodit.cgc +0 -0
- package/dist/control/task.cgc +0 -0
- package/dist/control/tuieditor.cgc +0 -0
- package/dist/index.js +1 -1
- package/dist/lib/core.d.ts +37 -1
- package/dist/lib/form.d.ts +39 -0
- package/dist/lib/task.d.ts +59 -2
- package/dist/lib/tool.d.ts +14 -1
- package/dist/theme/modern-light.cgt +0 -0
- package/dist/theme/modern.cgt +0 -0
- package/dist/theme/software-light.cgt +0 -0
- package/doc/clickgo-rag.md +1675 -790
- package/package.json +9 -4
- package/test/core-module-loading.mjs +169 -0
- package/test/form-notify.mjs +90 -0
- package/test/iconview.mjs +70 -0
- package/test/jodit-lifecycle.mjs +212 -0
- package/test/scroll.mjs +145 -0
- package/test/task-package-loading.mjs +94 -0
- package/test/task-tray.mjs +331 -0
- package/test/tool-assets.mjs +64 -0
- package/test/tuieditor-locale.mjs +175 -0
- package/test/vflow.mjs +122 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import vm from 'node:vm';
|
|
4
|
+
import { setImmediate } from 'node:timers/promises';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { JSDOM } from 'jsdom';
|
|
7
|
+
|
|
8
|
+
const source = await readFile(new URL('../dist/sources/control/tuieditor/code.ts', import.meta.url), 'utf8');
|
|
9
|
+
const compiled = ts.transpileModule(source, {
|
|
10
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: false },
|
|
11
|
+
}).outputText;
|
|
12
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
13
|
+
const { document } = dom.window;
|
|
14
|
+
const requests = [];
|
|
15
|
+
const editors = [];
|
|
16
|
+
function deferred() {
|
|
17
|
+
let resolve;
|
|
18
|
+
let reject;
|
|
19
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
20
|
+
return { promise, resolve, reject };
|
|
21
|
+
}
|
|
22
|
+
class AbstractControl {
|
|
23
|
+
get locale() { return this.testLocale; }
|
|
24
|
+
propBoolean(name) { return this.props[name] === true; }
|
|
25
|
+
watch(name, callback, options) {
|
|
26
|
+
this.watches.set(name, callback);
|
|
27
|
+
if (options?.immediate) callback();
|
|
28
|
+
}
|
|
29
|
+
emit() {}
|
|
30
|
+
}
|
|
31
|
+
class Editor {
|
|
32
|
+
constructor(options) {
|
|
33
|
+
this.options = options;
|
|
34
|
+
this.i18n = { get: key => key };
|
|
35
|
+
options.el.appendChild(document.createElement('div'));
|
|
36
|
+
editors.push(this);
|
|
37
|
+
}
|
|
38
|
+
insertToolbarItem() {}
|
|
39
|
+
}
|
|
40
|
+
let main;
|
|
41
|
+
const clickgo = {
|
|
42
|
+
modules: {},
|
|
43
|
+
getCdn: () => 'https://cdn.example',
|
|
44
|
+
getDirname: () => 'https://app.example/clickgo',
|
|
45
|
+
core: { getModule: () => main },
|
|
46
|
+
control: { AbstractControl },
|
|
47
|
+
tool: {
|
|
48
|
+
loadScript(url) {
|
|
49
|
+
const pending = deferred();
|
|
50
|
+
requests.push({ url, ...pending });
|
|
51
|
+
return pending.promise;
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
dom: { createElement: name => document.createElement(name), watchStyle() {} },
|
|
55
|
+
};
|
|
56
|
+
// Build the actual shared module once, then evaluate controls independently as different tasks do.
|
|
57
|
+
const coreSource = await readFile(new URL('../dist/lib/core.ts', import.meta.url), 'utf8');
|
|
58
|
+
const tree = ts.createSourceFile('core.ts', coreSource, ts.ScriptTarget.ES2022, true);
|
|
59
|
+
const names = new Set(['getModule', 'loadModule']);
|
|
60
|
+
const selected = tree.statements.filter(node =>
|
|
61
|
+
(ts.isVariableStatement(node) && node.declarationList.declarations.some(item => item.name.getText(tree) === 'modules')) ||
|
|
62
|
+
(ts.isFunctionDeclaration(node) && node.body && names.has(node.name?.text)));
|
|
63
|
+
const core = {};
|
|
64
|
+
vm.runInNewContext(ts.transpileModule(selected.map(node => node.getText(tree)).join('\n'), {
|
|
65
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: false },
|
|
66
|
+
}).outputText, {
|
|
67
|
+
exports: core, window: { toastui: { Editor } }, clickgo,
|
|
68
|
+
lTool: { ...clickgo.tool, loadAssets: async () => true, loadStyle() {}, urlResolve: (base, path) => new URL(path, base).href },
|
|
69
|
+
});
|
|
70
|
+
const shared = await core.getModule('@toast-ui/editor');
|
|
71
|
+
main = Promise.resolve(shared);
|
|
72
|
+
function loadControl() {
|
|
73
|
+
const exports = {};
|
|
74
|
+
vm.runInNewContext(compiled, { exports, require: () => clickgo });
|
|
75
|
+
return exports.default;
|
|
76
|
+
}
|
|
77
|
+
let Control = loadControl();
|
|
78
|
+
function create(locale) {
|
|
79
|
+
const control = new Control();
|
|
80
|
+
control.testLocale = locale;
|
|
81
|
+
control.watches = new Map();
|
|
82
|
+
control.element = document.createElement('div');
|
|
83
|
+
const content = document.createElement('div');
|
|
84
|
+
const pop = document.createElement('div');
|
|
85
|
+
control.element.append(content, pop);
|
|
86
|
+
control.refs = { content, pop };
|
|
87
|
+
document.body.appendChild(control.element);
|
|
88
|
+
return control;
|
|
89
|
+
}
|
|
90
|
+
await create('en').onMounted();
|
|
91
|
+
await create('ar').onMounted();
|
|
92
|
+
assert.equal(requests.length, 0, 'English and unsupported languages need no locale scripts');
|
|
93
|
+
assert.equal(editors.at(-1).options.language, 'en');
|
|
94
|
+
assert.equal(editors.at(-1).options.el.dir, 'rtl');
|
|
95
|
+
|
|
96
|
+
const base = deferred();
|
|
97
|
+
main = base.promise;
|
|
98
|
+
const first = create('sc');
|
|
99
|
+
Control = loadControl();
|
|
100
|
+
const second = create('sc');
|
|
101
|
+
const a = first.onMounted();
|
|
102
|
+
const b = second.onMounted();
|
|
103
|
+
assert.equal(requests.length, 0, 'locale registration waits for the main library');
|
|
104
|
+
base.resolve(shared);
|
|
105
|
+
await setImmediate();
|
|
106
|
+
assert.equal(requests.length, 1, 'independent task control modules share one pending language request');
|
|
107
|
+
assert.ok(requests[0].url.endsWith('/i18n/zh-cn.min.js'));
|
|
108
|
+
assert.equal(editors.length, 2, 'instances wait until their locale is registered');
|
|
109
|
+
requests[0].resolve(true);
|
|
110
|
+
await Promise.all([a, b]);
|
|
111
|
+
assert.equal(first.access.tuieditor.options.language, 'zh-CN');
|
|
112
|
+
assert.equal(second.access.tuieditor.options.language, 'zh-CN');
|
|
113
|
+
assert.notEqual(first.access.tuieditor, second.access.tuieditor);
|
|
114
|
+
await create('sc').onMounted();
|
|
115
|
+
assert.equal(requests.length, 1, 'ready locale scripts are reused across task control modules');
|
|
116
|
+
|
|
117
|
+
Control = loadControl();
|
|
118
|
+
await create('sc').onMounted();
|
|
119
|
+
assert.equal(requests.length, 1, 'a later task reuses the same completed language request');
|
|
120
|
+
|
|
121
|
+
first.testLocale = 'ja';
|
|
122
|
+
first.watches.get('locale')();
|
|
123
|
+
assert.equal(requests.length, 1, 'changing task locale does not reload an existing editor');
|
|
124
|
+
assert.equal(first.access.tuieditor.options.language, 'zh-CN');
|
|
125
|
+
main = Promise.resolve(shared);
|
|
126
|
+
const japanese = create('ja');
|
|
127
|
+
const c = japanese.onMounted();
|
|
128
|
+
await setImmediate();
|
|
129
|
+
assert.ok(requests.at(-1).url.endsWith('/i18n/ja-jp.min.js'));
|
|
130
|
+
requests.at(-1).resolve(true);
|
|
131
|
+
await c;
|
|
132
|
+
assert.equal(japanese.access.tuieditor.options.language, 'ja-JP', 'new editors use the current task language');
|
|
133
|
+
|
|
134
|
+
const french = create('fr');
|
|
135
|
+
const d = french.onMounted();
|
|
136
|
+
await setImmediate();
|
|
137
|
+
requests.at(-1).resolve(false);
|
|
138
|
+
await d;
|
|
139
|
+
assert.equal(french.access.tuieditor.options.language, 'en', 'locale failure falls back without disabling editing');
|
|
140
|
+
const beforeRetry = requests.length;
|
|
141
|
+
const retry = create('fr').onMounted();
|
|
142
|
+
await setImmediate();
|
|
143
|
+
assert.equal(requests.length, beforeRetry + 1, 'failed locale requests can be retried');
|
|
144
|
+
requests.at(-1).resolve(true);
|
|
145
|
+
await retry;
|
|
146
|
+
assert.equal(editors.at(-1).options.language, 'fr-FR');
|
|
147
|
+
|
|
148
|
+
const rejected = shared.loadLanguage('de-DE');
|
|
149
|
+
const rejectedWaiter = shared.loadLanguage('de-de');
|
|
150
|
+
requests.at(-1).reject(new Error('external loader rejected'));
|
|
151
|
+
assert.equal(await rejected, false);
|
|
152
|
+
assert.equal(await rejectedWaiter, false, 'external loader errors release all waiters');
|
|
153
|
+
const rejectedRetry = shared.loadLanguage('de-DE');
|
|
154
|
+
requests.at(-1).resolve(true);
|
|
155
|
+
assert.equal(await rejectedRetry, true, 'external errors do not poison the shared cache');
|
|
156
|
+
|
|
157
|
+
const removed = create('tc');
|
|
158
|
+
const e = removed.onMounted();
|
|
159
|
+
await setImmediate();
|
|
160
|
+
assert.ok(requests.at(-1).url.endsWith('/i18n/zh-tw.min.js'));
|
|
161
|
+
removed.element.remove();
|
|
162
|
+
const count = editors.length;
|
|
163
|
+
requests.at(-1).resolve(true);
|
|
164
|
+
await e;
|
|
165
|
+
assert.equal(editors.length, count, 'removed hosts do not receive late editor instances');
|
|
166
|
+
|
|
167
|
+
Control = loadControl();
|
|
168
|
+
main = Promise.resolve(null);
|
|
169
|
+
const missing = create('sc');
|
|
170
|
+
const beforeMissing = requests.length;
|
|
171
|
+
await missing.onMounted();
|
|
172
|
+
assert.equal(missing.notInit, true);
|
|
173
|
+
assert.equal(missing.isLoading, false);
|
|
174
|
+
assert.equal(requests.length, beforeMissing, 'a missing main library triggers no locale request');
|
|
175
|
+
console.log('Toast UI cross-task locale requests, reuse, fallback, retries and initialization language passed.');
|
package/test/vflow.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import vm from 'node:vm';
|
|
4
|
+
import { JSDOM } from 'jsdom';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { getClassPrototype, lang } from '../dist/lib/tool.js';
|
|
7
|
+
|
|
8
|
+
const dom = new JSDOM('<!doctype html><html><body></body></html>');
|
|
9
|
+
for (const name of ['window', 'document', 'Element', 'HTMLElement', 'SVGElement', 'Node']) {
|
|
10
|
+
Object.defineProperty(globalThis, name, { configurable: true, value: dom.window[name] });
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperties(HTMLElement.prototype, {
|
|
13
|
+
offsetWidth: { configurable: true, get: () => 80 },
|
|
14
|
+
offsetHeight: { configurable: true, get: () => 20 },
|
|
15
|
+
});
|
|
16
|
+
const { createApp, reactive, nextTick, h } = await import('vue');
|
|
17
|
+
const state = reactive({ locale: 'en', direction: 'h', count: 2 });
|
|
18
|
+
const errors = [];
|
|
19
|
+
const watched = [];
|
|
20
|
+
process.on('unhandledRejection', error => { errors.push(error); });
|
|
21
|
+
|
|
22
|
+
// Keep the framework's computed $el access and component-root mounting order.
|
|
23
|
+
class AbstractControl {
|
|
24
|
+
get element() { return this.$el; }
|
|
25
|
+
get locale() { return state.locale; }
|
|
26
|
+
get localeDirection() { return lang.getDirection(this.locale); }
|
|
27
|
+
nextTick() { return this.$nextTick(); }
|
|
28
|
+
watch(...args) { return this.$watch(...args); }
|
|
29
|
+
emit(...args) { this.$emit(...args); }
|
|
30
|
+
propInt(name) { return parseInt(this.props[name]); }
|
|
31
|
+
}
|
|
32
|
+
const clickgo = {
|
|
33
|
+
control: { AbstractControl },
|
|
34
|
+
dom: {
|
|
35
|
+
isRtl: el => el instanceof Element && dom.window.getComputedStyle(el).direction === 'rtl',
|
|
36
|
+
watchStyle: (el, names, callback, immediate) => {
|
|
37
|
+
assert.ok(el instanceof HTMLElement && el.isConnected, 'style watcher needs a mounted element');
|
|
38
|
+
watched.push(el);
|
|
39
|
+
if (immediate) {
|
|
40
|
+
for (const name of names) callback(name, name === 'font' ? '13px sans-serif' : '0px');
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
watchSize() {},
|
|
44
|
+
unwatchSize() {},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const source = await readFile(new URL('../dist/sources/control/vflow/code.ts', import.meta.url), 'utf8');
|
|
48
|
+
const exports = {};
|
|
49
|
+
vm.runInNewContext(ts.transpileModule(source, {
|
|
50
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
51
|
+
}).outputText, { exports, require: () => clickgo });
|
|
52
|
+
const instance = new exports.default();
|
|
53
|
+
const prototype = getClassPrototype(instance);
|
|
54
|
+
const layout = await readFile(new URL('../dist/sources/control/vflow/layout.html', import.meta.url), 'utf8');
|
|
55
|
+
const Flow = {
|
|
56
|
+
template: '<div><slot /></div>',
|
|
57
|
+
mounted() { this.$emit('clientwidth', 200); this.$emit('clientheight', 100); },
|
|
58
|
+
};
|
|
59
|
+
const Vflow = {
|
|
60
|
+
template: layout,
|
|
61
|
+
components: { 'cg-flow': Flow },
|
|
62
|
+
props: Object.fromEntries(Object.entries(instance.props).map(([name, value]) => [name, {
|
|
63
|
+
default: () => structuredClone(value),
|
|
64
|
+
}])),
|
|
65
|
+
emits: instance.emits,
|
|
66
|
+
data: () => Object.fromEntries(Object.entries(instance).filter(([name]) => !['props', 'emits'].includes(name))),
|
|
67
|
+
computed: prototype.access,
|
|
68
|
+
methods: prototype.method,
|
|
69
|
+
created() { this.props = this.$props; },
|
|
70
|
+
async mounted() { await this.$nextTick(); this.onMounted(); },
|
|
71
|
+
};
|
|
72
|
+
const host = document.createElement('div');
|
|
73
|
+
document.body.append(host);
|
|
74
|
+
const app = createApp({
|
|
75
|
+
render() {
|
|
76
|
+
return h('div', { dir: lang.getDirection(state.locale) }, [
|
|
77
|
+
h(Vflow, { ref: 'flow', data: state.count, direction: state.direction }, {
|
|
78
|
+
default: item => h('span', `Line ${item.row}`),
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
app.config.errorHandler = error => { errors.push(error); };
|
|
84
|
+
const root = app.mount(host);
|
|
85
|
+
|
|
86
|
+
/** --- Drain Vue render, mount and deferred item-size work. --- */
|
|
87
|
+
async function flush() {
|
|
88
|
+
for (let i = 0; i < 10; ++i) await nextTick();
|
|
89
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
90
|
+
assert.deepEqual(errors, [], 'Vflow must mount/update without runtime errors');
|
|
91
|
+
}
|
|
92
|
+
await flush();
|
|
93
|
+
assert.equal(watched.length, 1);
|
|
94
|
+
assert.equal(root.$refs.flow.element, host.querySelector('[direction]'));
|
|
95
|
+
assert.equal(root.$refs.flow.length, 160);
|
|
96
|
+
assert.equal(root.$refs.flow.isRtl, false);
|
|
97
|
+
assert.equal(host.querySelector('.item').style.left, '0px');
|
|
98
|
+
state.count = 102;
|
|
99
|
+
await flush();
|
|
100
|
+
assert.equal(root.$refs.flow.pos.length, 102);
|
|
101
|
+
assert.ok(host.querySelectorAll('.item').length < 102, 'only visible items are rendered');
|
|
102
|
+
state.locale = 'ar';
|
|
103
|
+
await flush();
|
|
104
|
+
assert.equal(root.$refs.flow.isRtl, true);
|
|
105
|
+
assert.equal(host.querySelector('.item').style.left, '');
|
|
106
|
+
assert.equal(host.querySelector('.item').style.right, '0px');
|
|
107
|
+
state.direction = 'v';
|
|
108
|
+
await flush();
|
|
109
|
+
assert.equal(root.$refs.flow.isRtl, false);
|
|
110
|
+
assert.equal(root.$refs.flow.length, 2040);
|
|
111
|
+
state.count = 0;
|
|
112
|
+
await flush();
|
|
113
|
+
assert.equal(root.$refs.flow.pos.length, 0);
|
|
114
|
+
state.count = 2;
|
|
115
|
+
state.direction = 'h';
|
|
116
|
+
state.locale = 'en';
|
|
117
|
+
await flush();
|
|
118
|
+
assert.equal(root.$refs.flow.length, 160);
|
|
119
|
+
assert.equal(host.querySelector('.item').style.left, '0px');
|
|
120
|
+
app.unmount();
|
|
121
|
+
await flush();
|
|
122
|
+
console.log('Vflow real Vue mount, virtualization, data/direction updates and live RTL switching passed.');
|