ru.coon 3.0.80 → 3.0.81
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/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/src/IndentTemplate.js +324 -0
- package/src/Template.js +420 -0
- package/src/app/viewPort/Permalink.d2 +27 -0
- package/src/app/viewPort/Router.d2 +23 -0
- package/src/nav/MenuFavorite.js +84 -0
- package/src/uielement/component/UiCustomController.js +3 -0
- package/src/uielement/plugin/OpenPanelPlugin.js +52 -28
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# Version 3.0.81, [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/bd688d381953e5cc5d8011296b21dcd891bf1d7b)
|
|
2
|
+
* ## Fixes
|
|
3
|
+
* <span style='color:red'>fix</span> ([d82071], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/d82071dc2c72b15caf116564cd84ec5b5900bfc6))
|
|
4
|
+
|
|
5
|
+
* refactoring ([9429b5], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/9429b536e6d3b30cfbf0afe349b6638dd62933bf))
|
|
6
|
+
* update: CHANGELOG.md ([ef0a27], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/ef0a27156e4b211f6d5fbd1b37dedc6b978b03c9))
|
|
7
|
+
|
|
1
8
|
# Version 3.0.80, [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/3b43f1d4de9199df4725890955ab76c6e4491cdc)
|
|
2
9
|
* refactoring OpenPanelPlugin ([67cd0c], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/67cd0c0b004f89f7fb3607497a43f960ae4f0735))
|
|
3
10
|
* update: CHANGELOG.md ([6e78bf], [link](http://gitlab-dbr.sigma-it.local/dbr/ru.coon/-/commit/6e78bf0dd1e0394557f2aba6b2fa56c1f022fc86))
|
package/package.json
CHANGED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/* eslint-disable require-jsdoc */
|
|
2
|
+
/**
|
|
3
|
+
* A template engine that uses indentation-based syntax to define structure,
|
|
4
|
+
* supporting mixins, loops, and variable interpolation.
|
|
5
|
+
*/
|
|
6
|
+
class IndentTemplate {
|
|
7
|
+
/**
|
|
8
|
+
* Creates a new IndentTemplate instance.
|
|
9
|
+
* @param {string} [template=''] - The initial template string
|
|
10
|
+
*/
|
|
11
|
+
constructor(template = '') {
|
|
12
|
+
this._template = template;
|
|
13
|
+
this._ast = null;
|
|
14
|
+
this._mixins = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sets or changes the template string.
|
|
19
|
+
* @param {string} template - The template string to set
|
|
20
|
+
* @returns {IndentTemplate} This instance for chaining
|
|
21
|
+
*/
|
|
22
|
+
setTemplate(template) {
|
|
23
|
+
this._template = template;
|
|
24
|
+
this._ast = null;
|
|
25
|
+
this._mixins = {};
|
|
26
|
+
return this; // chainable
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Compiles the template by parsing it into an AST and collecting mixins.
|
|
31
|
+
* @returns {IndentTemplate} This instance for chaining
|
|
32
|
+
* @throws {Error} If the template is empty or has invalid syntax
|
|
33
|
+
*/
|
|
34
|
+
compile() {
|
|
35
|
+
if (!this._template) {
|
|
36
|
+
throw new Error('Шаблон пуст. Используй setTemplate(template).');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rawLines = this._template
|
|
40
|
+
.split('\n')
|
|
41
|
+
.map((l) => l.replace(/\r$/, ''))
|
|
42
|
+
.filter((l) => l.trim().length > 0);
|
|
43
|
+
|
|
44
|
+
const lines = rawLines.map((line) => {
|
|
45
|
+
const m = line.match(/^(\s*)/);
|
|
46
|
+
const indent = m ? m[1].length : 0;
|
|
47
|
+
const level = Math.floor(indent / 2); // 2 пробела = 1 уровень
|
|
48
|
+
return {
|
|
49
|
+
level,
|
|
50
|
+
content: line.trim(),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
this._mixins = {};
|
|
55
|
+
let index = 0;
|
|
56
|
+
|
|
57
|
+
const parseBlock = (expectedLevel) => {
|
|
58
|
+
const nodes = [];
|
|
59
|
+
|
|
60
|
+
while (index < lines.length) {
|
|
61
|
+
const {level, content} = lines[index];
|
|
62
|
+
|
|
63
|
+
if (level < expectedLevel) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (level > expectedLevel) {
|
|
67
|
+
// некорректный отступ — считаем конец этого блока
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- миксин определение ------------------------------------
|
|
72
|
+
if (content.startsWith('@mixin ')) {
|
|
73
|
+
if (expectedLevel !== 0) {
|
|
74
|
+
throw new Error('@mixin должен быть на верхнем уровне без отступов');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const def = content.slice(7).trim(); // после "@mixin "
|
|
78
|
+
const parts = def.split(/\s+/);
|
|
79
|
+
const name = parts[0];
|
|
80
|
+
const params = parts.slice(1); // список имён параметров
|
|
81
|
+
|
|
82
|
+
index++;
|
|
83
|
+
// тело миксина — блок на уровне 1
|
|
84
|
+
const body = parseBlock(expectedLevel + 1);
|
|
85
|
+
|
|
86
|
+
this._mixins[name] = {params, body};
|
|
87
|
+
continue; // миксин не попадает в основной AST
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- цикл @for ---------------------------------------------
|
|
91
|
+
if (content.startsWith('@for ')) {
|
|
92
|
+
const forLine = content.slice(5).trim(); // убираем "@for "
|
|
93
|
+
const match = forLine.match(/^(\w+)\s+in\s+(.+)$/);
|
|
94
|
+
if (!match) {
|
|
95
|
+
throw new Error('Неверный синтаксис @for: ' + content);
|
|
96
|
+
}
|
|
97
|
+
const varName = match[1];
|
|
98
|
+
const listExpr = match[2].trim();
|
|
99
|
+
|
|
100
|
+
index++;
|
|
101
|
+
const children = parseBlock(expectedLevel + 1);
|
|
102
|
+
|
|
103
|
+
nodes.push({
|
|
104
|
+
type: 'for',
|
|
105
|
+
varName,
|
|
106
|
+
listExpr,
|
|
107
|
+
children,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- использование миксина: @use name(arg1, arg2) ----------
|
|
113
|
+
if (content.startsWith('@use ')) {
|
|
114
|
+
const call = content.slice(5).trim();
|
|
115
|
+
const m = call.match(/^(\w+)\s*\((.*)\)$/);
|
|
116
|
+
if (!m) {
|
|
117
|
+
throw new Error('Неверный синтаксис @use: ' + content);
|
|
118
|
+
}
|
|
119
|
+
const mixinName = m[1];
|
|
120
|
+
const argsRaw = m[2].trim();
|
|
121
|
+
const argExprs = argsRaw ?
|
|
122
|
+
argsRaw.split(',').map((s) => s.trim()) :
|
|
123
|
+
[];
|
|
124
|
+
|
|
125
|
+
index++;
|
|
126
|
+
|
|
127
|
+
nodes.push({
|
|
128
|
+
type: 'use',
|
|
129
|
+
mixinName,
|
|
130
|
+
argExprs,
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- обычный тег -------------------------------------------
|
|
136
|
+
const [tag, ...restParts] = content.split(/\s+/);
|
|
137
|
+
const text = restParts.join(' ');
|
|
138
|
+
const selfClosing = new Set(['br', 'hr', 'img', 'input', 'meta', 'link']);
|
|
139
|
+
const lowerTag = tag.toLowerCase();
|
|
140
|
+
const isSelfClosing = selfClosing.has(lowerTag);
|
|
141
|
+
|
|
142
|
+
index++;
|
|
143
|
+
|
|
144
|
+
let children = [];
|
|
145
|
+
if (!text && !isSelfClosing && index < lines.length) {
|
|
146
|
+
const next = lines[index];
|
|
147
|
+
if (next.level > expectedLevel) {
|
|
148
|
+
children = parseBlock(expectedLevel + 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
nodes.push({
|
|
153
|
+
type: 'tag',
|
|
154
|
+
tag,
|
|
155
|
+
text,
|
|
156
|
+
isSelfClosing,
|
|
157
|
+
children,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return nodes;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
this._ast = parseBlock(0);
|
|
165
|
+
return this; // chainable
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Builds the final HTML output by rendering the compiled template with provided data.
|
|
170
|
+
* @param {Object} [data={}] - The data object to render the template with
|
|
171
|
+
* @returns {string} The rendered HTML string
|
|
172
|
+
*/
|
|
173
|
+
build(data = {}) {
|
|
174
|
+
if (!this._ast) {
|
|
175
|
+
this.compile();
|
|
176
|
+
}
|
|
177
|
+
return this._renderNodes(this._ast, data);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ===================== RENDER =====================
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Renders an array of nodes with the given scope.
|
|
184
|
+
* @param {Array} nodes - Array of AST nodes to render
|
|
185
|
+
* @param {Object} scope - The current scope/data context
|
|
186
|
+
* @returns {string} Concatenated rendered nodes
|
|
187
|
+
* @private
|
|
188
|
+
*/
|
|
189
|
+
_renderNodes(nodes, scope) {
|
|
190
|
+
return nodes.map((node) => this._renderNode(node, scope)).join('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Renders a single node based on its type.
|
|
195
|
+
* @param {Object} node - The AST node to render
|
|
196
|
+
* @param {Object} scope - The current scope/data context
|
|
197
|
+
* @returns {string} The rendered HTML for this node
|
|
198
|
+
* @private
|
|
199
|
+
*/
|
|
200
|
+
_renderNode(node, scope) {
|
|
201
|
+
// for
|
|
202
|
+
if (node.type === 'for') {
|
|
203
|
+
const list = this._resolvePath(scope, node.listExpr);
|
|
204
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
let out = '';
|
|
208
|
+
for (const item of list) {
|
|
209
|
+
const childScope = Object.create(scope);
|
|
210
|
+
childScope[node.varName] = item;
|
|
211
|
+
out += this._renderNodes(node.children, childScope);
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// use mixin
|
|
217
|
+
if (node.type === 'use') {
|
|
218
|
+
const def = this._mixins[node.mixinName];
|
|
219
|
+
if (!def) {
|
|
220
|
+
throw new Error(`Миксин "${node.mixinName}" не найден`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const {params, body} = def;
|
|
224
|
+
const argValues = node.argExprs.map((expr) => this._resolvePath(scope, expr));
|
|
225
|
+
|
|
226
|
+
const childScope = Object.create(scope);
|
|
227
|
+
params.forEach((name, i) => {
|
|
228
|
+
childScope[name] = argValues[i];
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return this._renderNodes(body, childScope);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// обычный тег
|
|
235
|
+
if (node.type === 'tag') {
|
|
236
|
+
const {tag, text, isSelfClosing, children} = node;
|
|
237
|
+
|
|
238
|
+
if (isSelfClosing) {
|
|
239
|
+
return `<${tag}>`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const innerText = text ?
|
|
243
|
+
this._renderTextWithTemplates(text, scope) :
|
|
244
|
+
'';
|
|
245
|
+
|
|
246
|
+
const childrenHtml = children && children.length ?
|
|
247
|
+
this._renderNodes(children, scope) :
|
|
248
|
+
'';
|
|
249
|
+
|
|
250
|
+
return `<${tag}>${innerText}${childrenHtml}</${tag}>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Renders text with template expressions replaced by values from scope.
|
|
258
|
+
* @param {string} text - Text potentially containing template expressions
|
|
259
|
+
* @param {Object} scope - The current scope/data context
|
|
260
|
+
* @returns {string} Text with expressions replaced by values
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
_renderTextWithTemplates(text, scope) {
|
|
264
|
+
let result = '';
|
|
265
|
+
let lastIndex = 0;
|
|
266
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
267
|
+
|
|
268
|
+
text.replace(regex, (match, expr, offset) => {
|
|
269
|
+
// статичный кусок до {{ ... }}
|
|
270
|
+
const staticPart = text.slice(lastIndex, offset);
|
|
271
|
+
result += this._escape(staticPart);
|
|
272
|
+
|
|
273
|
+
const value = this._resolvePath(scope, expr.trim());
|
|
274
|
+
const strVal = value == null ? '' : String(value);
|
|
275
|
+
result += this._escape(strVal);
|
|
276
|
+
|
|
277
|
+
lastIndex = offset + match.length;
|
|
278
|
+
return '';
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// хвост после последнего {{ ... }}
|
|
282
|
+
const tail = text.slice(lastIndex);
|
|
283
|
+
result += this._escape(tail);
|
|
284
|
+
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolves a dot-notation path in the scope object.
|
|
290
|
+
* @param {Object} scope - The scope object to look in
|
|
291
|
+
* @param {string} path - Dot-notation path like "user.name"
|
|
292
|
+
* @returns {*} The resolved value or undefined
|
|
293
|
+
* @private
|
|
294
|
+
*/
|
|
295
|
+
_resolvePath(scope, path) {
|
|
296
|
+
if (!path) {
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
const parts = path.split('.');
|
|
300
|
+
let value = scope;
|
|
301
|
+
for (const part of parts) {
|
|
302
|
+
if (value == null) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
value = value[part];
|
|
306
|
+
}
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Escapes HTML special characters in text.
|
|
312
|
+
* @param {string} text - Text to escape
|
|
313
|
+
* @returns {string} Escaped text
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
_escape(text) {
|
|
317
|
+
return String(text)
|
|
318
|
+
.replace(/&/g, '&')
|
|
319
|
+
.replace(/</g, '<')
|
|
320
|
+
.replace(/>/g, '>');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export default IndentTemplate;
|
package/src/Template.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A template engine that parses and compiles templates with custom syntax.
|
|
3
|
+
* Supports elements, mixins, loops, conditions, and variable interpolation.
|
|
4
|
+
*/
|
|
5
|
+
class Template {
|
|
6
|
+
/**
|
|
7
|
+
* Creates a new Template instance.
|
|
8
|
+
* @param {string} source - The template source string
|
|
9
|
+
*/
|
|
10
|
+
constructor(source) {
|
|
11
|
+
this.source = source;
|
|
12
|
+
this.ast = this.parse(source);
|
|
13
|
+
this.renderFn = this.compile(this.ast);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds the final output by rendering the compiled template with provided context.
|
|
18
|
+
* @param {Object} context - The context object to render the template with
|
|
19
|
+
* @returns {string} The rendered output string
|
|
20
|
+
*/
|
|
21
|
+
build(context = {}) {
|
|
22
|
+
return this.renderFn(context);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ========== ПАРСЕР ==========
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses the template source into an Abstract Syntax Tree (AST).
|
|
29
|
+
* @param {string} source - The template source string
|
|
30
|
+
* @returns {Object} The parsed AST representation
|
|
31
|
+
*/
|
|
32
|
+
parse(source) {
|
|
33
|
+
const lines = source.split('\n').filter((line) => line.trim() !== '');
|
|
34
|
+
const mixins = new Map();
|
|
35
|
+
const {nodes} = this.parseBlock(lines, 0, 0, mixins);
|
|
36
|
+
return {type: 'Root', children: nodes, mixins};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parses a block of lines at a specific indentation level.
|
|
41
|
+
* @param {string[]} lines - Array of template lines
|
|
42
|
+
* @param {number} start - Starting index in the lines array
|
|
43
|
+
* @param {number} indentLevel - Current indentation level
|
|
44
|
+
* @param {Map} mixins - Map of defined mixins
|
|
45
|
+
* @returns {Object} Object containing parsed nodes and the next index
|
|
46
|
+
*/
|
|
47
|
+
parseBlock(lines, start, indentLevel, mixins) {
|
|
48
|
+
const nodes = [];
|
|
49
|
+
let i = start;
|
|
50
|
+
|
|
51
|
+
while (i < lines.length) {
|
|
52
|
+
const line = lines[i];
|
|
53
|
+
const match = line.match(/^ */);
|
|
54
|
+
const currentIndent = match ? match[0].length : 0;
|
|
55
|
+
|
|
56
|
+
if (currentIndent < indentLevel) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (currentIndent !== indentLevel) {
|
|
60
|
+
throw new Error(`Invalid indentation at line ${i + 1}. Expected ${indentLevel}, got ${currentIndent}.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const content = line.trim();
|
|
64
|
+
let node;
|
|
65
|
+
|
|
66
|
+
if (content.startsWith('@mixin ')) {
|
|
67
|
+
const mixin = this.parseMixinDecl(content);
|
|
68
|
+
const {nodes: body, nextIndex} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
69
|
+
mixin.body = body;
|
|
70
|
+
mixins.set(mixin.name, mixin);
|
|
71
|
+
node = mixin;
|
|
72
|
+
i = nextIndex;
|
|
73
|
+
} else if (content.startsWith('@use ')) {
|
|
74
|
+
const call = this.parseMixinCall(content);
|
|
75
|
+
const def = mixins.get(call.name);
|
|
76
|
+
if (!def) {
|
|
77
|
+
throw new Error(`Undefined mixin: ${call.name}`);
|
|
78
|
+
}
|
|
79
|
+
node = {type: 'MixinCall', definition: def, args: call.args};
|
|
80
|
+
i++;
|
|
81
|
+
} else if (content.startsWith('@each ')) {
|
|
82
|
+
const each = this.parseEach(content);
|
|
83
|
+
const {nodes: body, nextIndex} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
84
|
+
each.children = body;
|
|
85
|
+
node = each;
|
|
86
|
+
i = nextIndex;
|
|
87
|
+
} else if (content.startsWith('@if ')) {
|
|
88
|
+
const condition = content.slice(4).trim();
|
|
89
|
+
const ifNode = {type: 'If', condition, thenBranch: [], elseBranch: []};
|
|
90
|
+
const {nodes: thenBody, nextIndex: afterThen} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
91
|
+
ifNode.thenBranch = thenBody;
|
|
92
|
+
i = afterThen;
|
|
93
|
+
|
|
94
|
+
// Проверяем, есть ли @else сразу после
|
|
95
|
+
if (i < lines.length) {
|
|
96
|
+
const elseLine = lines[i].trim();
|
|
97
|
+
const elseIndent = lines[i].match(/^ */)[0].length;
|
|
98
|
+
if (elseIndent === indentLevel && elseLine === '@else') {
|
|
99
|
+
const {nodes: elseBody, nextIndex: afterElse} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
100
|
+
ifNode.elseBranch = elseBody;
|
|
101
|
+
i = afterElse;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
node = ifNode;
|
|
106
|
+
} else {
|
|
107
|
+
node = this.parseElement(content);
|
|
108
|
+
const {nodes: children, nextIndex} = this.parseBlock(lines, i + 1, indentLevel + 2, mixins);
|
|
109
|
+
node.children = children;
|
|
110
|
+
i = nextIndex;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (node.type !== 'MixinDecl') {
|
|
114
|
+
nodes.push(node);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {nodes, nextIndex: i};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parses an element line into a structured object.
|
|
123
|
+
* @param {string} line - A line containing an element definition
|
|
124
|
+
* @returns {Object} Parsed element object
|
|
125
|
+
*/
|
|
126
|
+
parseElement(line) {
|
|
127
|
+
const tagMatch = line.match(/^([a-zA-Z][a-zA-Z0-9]*)\b/);
|
|
128
|
+
if (!tagMatch) {
|
|
129
|
+
throw new Error(`Invalid tag: "${line}"`);
|
|
130
|
+
}
|
|
131
|
+
const tagName = tagMatch[1];
|
|
132
|
+
const rest = line.slice(tagMatch[0].length).trim();
|
|
133
|
+
|
|
134
|
+
const selfClosingTags = new Set([
|
|
135
|
+
'img', 'br', 'hr', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
const attrs = {};
|
|
139
|
+
let text = '';
|
|
140
|
+
|
|
141
|
+
// Простой парсинг: всё, что содержит = → атрибут; остальное — текст
|
|
142
|
+
const tokens = rest.match(/(?:[^"\s=]+|"[^"]*")+/g) || [];
|
|
143
|
+
let inAttrs = true;
|
|
144
|
+
for (const t of tokens) {
|
|
145
|
+
if (t.includes('=')) {
|
|
146
|
+
const [key, val] = t.split('=', 2);
|
|
147
|
+
let finalVal = val;
|
|
148
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
149
|
+
(val.startsWith('\'') && val.endsWith('\''))) {
|
|
150
|
+
finalVal = val.slice(1, -1);
|
|
151
|
+
}
|
|
152
|
+
attrs[key] = finalVal;
|
|
153
|
+
} else if (inAttrs && !t.startsWith('"') && !t.includes('{{')) {
|
|
154
|
+
// Предполагаем, что это булев атрибут
|
|
155
|
+
attrs[t] = true;
|
|
156
|
+
} else {
|
|
157
|
+
inAttrs = false;
|
|
158
|
+
if (text) {
|
|
159
|
+
text += ' ';
|
|
160
|
+
}
|
|
161
|
+
text += t;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
type: 'Element',
|
|
167
|
+
tagName,
|
|
168
|
+
attrs,
|
|
169
|
+
text,
|
|
170
|
+
children: [],
|
|
171
|
+
selfClosing: selfClosingTags.has(tagName),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parses a mixin declaration line.
|
|
177
|
+
* @param {string} line - A line containing a mixin declaration
|
|
178
|
+
* @returns {Object} Parsed mixin declaration object
|
|
179
|
+
*/
|
|
180
|
+
parseMixinDecl(line) {
|
|
181
|
+
const match = line.match(/^@mixin\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/);
|
|
182
|
+
if (!match) {
|
|
183
|
+
throw new Error(`Invalid mixin declaration: ${line}`);
|
|
184
|
+
}
|
|
185
|
+
const name = match[1];
|
|
186
|
+
const params = match[2].split(',').map((p) => p.trim()).filter((p) => p);
|
|
187
|
+
return {type: 'MixinDecl', name, params, body: []};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parses a mixin call line.
|
|
192
|
+
* @param {string} line - A line containing a mixin call
|
|
193
|
+
* @returns {Object} Parsed mixin call object
|
|
194
|
+
*/
|
|
195
|
+
parseMixinCall(line) {
|
|
196
|
+
const match = line.match(/^@use\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)/);
|
|
197
|
+
if (!match) {
|
|
198
|
+
throw new Error(`Invalid mixin call: ${line}`);
|
|
199
|
+
}
|
|
200
|
+
const name = match[1];
|
|
201
|
+
const argsStr = match[2];
|
|
202
|
+
|
|
203
|
+
const args = [];
|
|
204
|
+
let current = '';
|
|
205
|
+
let inQuote = null;
|
|
206
|
+
for (let k = 0; k < argsStr.length; k++) {
|
|
207
|
+
const c = argsStr[k];
|
|
208
|
+
if (c === inQuote) {
|
|
209
|
+
inQuote = null;
|
|
210
|
+
} else if (!inQuote && (c === '"' || c === '\'')) {
|
|
211
|
+
inQuote = c;
|
|
212
|
+
} else if (!inQuote && c === ',') {
|
|
213
|
+
args.push(current.trim());
|
|
214
|
+
current = '';
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
current += c;
|
|
218
|
+
}
|
|
219
|
+
if (current.trim()) {
|
|
220
|
+
args.push(current.trim());
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {type: 'MixinCall', name, args};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Parses an each loop declaration.
|
|
228
|
+
* @param {string} line - A line containing an each loop declaration
|
|
229
|
+
* @returns {Object} Parsed each loop object
|
|
230
|
+
*/
|
|
231
|
+
parseEach(line) {
|
|
232
|
+
const match = line.match(/^@each\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+in\s+([a-zA-Z_][a-zA-Z0-9_.]*)/);
|
|
233
|
+
if (!match) {
|
|
234
|
+
throw new Error(`Invalid @each: ${line}`);
|
|
235
|
+
}
|
|
236
|
+
return {type: 'Each', iterator: match[1], collection: match[2], children: []};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ========== КОМПИЛЯТОР ==========
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Compiles the AST into a render function.
|
|
243
|
+
* @param {Object} ast - The Abstract Syntax Tree to compile
|
|
244
|
+
* @returns {Function} A function that renders the template with a given context
|
|
245
|
+
*/
|
|
246
|
+
compile(ast) {
|
|
247
|
+
const code = this.generateCode(ast);
|
|
248
|
+
return new Function('ctx', `
|
|
249
|
+
const escape = (s) => {
|
|
250
|
+
if (s == null) return '';
|
|
251
|
+
return String(s)
|
|
252
|
+
.replace(/&/g, '&')
|
|
253
|
+
.replace(/</g, '<')
|
|
254
|
+
.replace(/>/g, '>')
|
|
255
|
+
.replace(/"/g, '"')
|
|
256
|
+
.replace(/'/g, ''');
|
|
257
|
+
};
|
|
258
|
+
with(ctx || {}) {
|
|
259
|
+
return ${code};
|
|
260
|
+
}
|
|
261
|
+
`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Generates JavaScript code from the AST nodes.
|
|
266
|
+
* @param {Object} node - The AST node to generate code for
|
|
267
|
+
* @returns {string} Generated JavaScript code as a string
|
|
268
|
+
*/
|
|
269
|
+
generateCode(node) {
|
|
270
|
+
switch (node.type) {
|
|
271
|
+
case 'Root':
|
|
272
|
+
return node.children.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
273
|
+
|
|
274
|
+
case 'Element':
|
|
275
|
+
const {tagName, attrs, text, children: nodeChildren, selfClosing} = node;
|
|
276
|
+
let attrStr = '';
|
|
277
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
278
|
+
if (val === true) {
|
|
279
|
+
attrStr += ` ${key}`;
|
|
280
|
+
} else {
|
|
281
|
+
const expr = this.interpolateToExpr(val, true);
|
|
282
|
+
attrStr += ` ${key}="\${${expr}}}`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const textExpr = text ? this.interpolateToExpr(text, false) : '';
|
|
287
|
+
const childrenExpr = nodeChildren.map((child) => this.generateCode(child)).join(' + ');
|
|
288
|
+
const content = [textExpr, childrenExpr].filter((s) => s).join(' + ');
|
|
289
|
+
|
|
290
|
+
if (selfClosing) {
|
|
291
|
+
return '`<' + tagName + attrStr + '>`';
|
|
292
|
+
} else {
|
|
293
|
+
if (content) {
|
|
294
|
+
return '`<' + tagName + attrStr + '>\${' + content + '}</' + tagName + '>``';
|
|
295
|
+
} else {
|
|
296
|
+
return '`<' + tagName + attrStr + '></' + tagName + '>``';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case 'Each':
|
|
301
|
+
const {iterator, collection, children} = node;
|
|
302
|
+
const body = children.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
303
|
+
return `(Array.isArray(${collection}) ? ${collection}.map(${iterator} => ${body}).join('') : '')`;
|
|
304
|
+
|
|
305
|
+
case 'If':
|
|
306
|
+
const {condition, thenBranch, elseBranch} = node;
|
|
307
|
+
const thenCode = thenBranch.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
308
|
+
const elseCode = elseBranch.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
309
|
+
return `(${condition} ? ${thenCode} : ${elseCode})`;
|
|
310
|
+
|
|
311
|
+
case 'MixinCall':
|
|
312
|
+
const {definition, args} = node;
|
|
313
|
+
const paramMap = {};
|
|
314
|
+
definition.params.forEach((param, idx) => {
|
|
315
|
+
paramMap[param] = args[idx] || '""';
|
|
316
|
+
});
|
|
317
|
+
const substitutedBody = this.substituteParamsInAST(definition.body, paramMap);
|
|
318
|
+
const bodyCode = substitutedBody.map((child) => this.generateCode(child)).join(' + ') || '""';
|
|
319
|
+
return bodyCode;
|
|
320
|
+
|
|
321
|
+
default:
|
|
322
|
+
return '""';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Substitutes parameters in AST nodes with actual values.
|
|
328
|
+
* @param {Array} nodes - Array of AST nodes
|
|
329
|
+
* @param {Object} paramMap - Parameter mapping object
|
|
330
|
+
* @returns {Array} New array of AST nodes with substituted parameters
|
|
331
|
+
*/
|
|
332
|
+
substituteParamsInAST(nodes, paramMap) {
|
|
333
|
+
return nodes.map((node) => {
|
|
334
|
+
const newNode = {...node};
|
|
335
|
+
if (node.type === 'Element') {
|
|
336
|
+
const newAttrs = {};
|
|
337
|
+
for (const [key, val] of Object.entries(node.attrs)) {
|
|
338
|
+
newAttrs[key] = this.substituteInString(val, paramMap);
|
|
339
|
+
}
|
|
340
|
+
newNode.attrs = newAttrs;
|
|
341
|
+
newNode.text = this.substituteInString(node.text, paramMap);
|
|
342
|
+
newNode.children = this.substituteParamsInAST(node.children, paramMap);
|
|
343
|
+
} else if (node.type === 'Each' || node.type === 'If') {
|
|
344
|
+
newNode.children = this.substituteParamsInAST(node.children || node.thenBranch || [], paramMap);
|
|
345
|
+
if (node.elseBranch) {
|
|
346
|
+
newNode.elseBranch = this.substituteParamsInAST(node.elseBranch, paramMap);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return newNode;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Substitutes parameter placeholders in a string with actual values.
|
|
355
|
+
* @param {string} str - String with parameter placeholders
|
|
356
|
+
* @param {Object} paramMap - Parameter mapping object
|
|
357
|
+
* @returns {string} String with substituted parameters
|
|
358
|
+
*/
|
|
359
|
+
substituteInString(str, paramMap) {
|
|
360
|
+
if (!str) {
|
|
361
|
+
return str;
|
|
362
|
+
}
|
|
363
|
+
let result = str;
|
|
364
|
+
for (const [param, replacement] of Object.entries(paramMap)) {
|
|
365
|
+
const regex = new RegExp(`{{\\s*${param}\\s*}}`, 'g');
|
|
366
|
+
result = result.replace(regex, replacement);
|
|
367
|
+
}
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Converts a string with interpolation expressions to executable JavaScript code.
|
|
373
|
+
* @param {string} str - String with interpolation expressions
|
|
374
|
+
* @param {boolean} inAttr - Whether the string is inside an attribute
|
|
375
|
+
* @returns {string} JavaScript code expression
|
|
376
|
+
*/
|
|
377
|
+
interpolateToExpr(str, inAttr = false) {
|
|
378
|
+
if (!str || !/{{.*?}}/.test(str)) {
|
|
379
|
+
if (inAttr) {
|
|
380
|
+
return JSON.stringify(str);
|
|
381
|
+
} else {
|
|
382
|
+
return `escape(${JSON.stringify(str)})`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
str = str.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
387
|
+
const parts = [];
|
|
388
|
+
let lastIndex = 0;
|
|
389
|
+
str.replace(/{{\s*([^}]+?)\s*}}/g, (match, expr, offset) => {
|
|
390
|
+
if (offset > lastIndex) {
|
|
391
|
+
const literal = str.slice(lastIndex, offset);
|
|
392
|
+
if (inAttr) {
|
|
393
|
+
parts.push(JSON.stringify(literal));
|
|
394
|
+
} else {
|
|
395
|
+
parts.push(`escape(${JSON.stringify(literal)})`;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
expr = expr.trim();
|
|
399
|
+
if (inAttr) {
|
|
400
|
+
parts.push(expr);
|
|
401
|
+
} else {
|
|
402
|
+
parts.push(`escape(${expr})`);
|
|
403
|
+
}
|
|
404
|
+
lastIndex = offset + match.length;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
if (lastIndex < str.length) {
|
|
408
|
+
const literal = str.slice(lastIndex);
|
|
409
|
+
if (inAttr) {
|
|
410
|
+
parts.push(JSON.stringify(literal));
|
|
411
|
+
} else {
|
|
412
|
+
parts.push(`escape(${JSON.stringify(literal)})`;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return parts.join(' + ') || '""';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export default Template;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Router{
|
|
2
|
+
assignUrl
|
|
3
|
+
urlAction
|
|
4
|
+
}
|
|
5
|
+
OCP{
|
|
6
|
+
label: openComponentPlugin
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
RegisterComponent{
|
|
10
|
+
RouteUnit{
|
|
11
|
+
shape: class
|
|
12
|
+
action: string
|
|
13
|
+
componentType: string
|
|
14
|
+
componentId: string
|
|
15
|
+
parameters: object
|
|
16
|
+
shortUrl: string
|
|
17
|
+
id: string
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
OCP -> RegisterComponent
|
|
22
|
+
Menu -> RegisterComponent
|
|
23
|
+
|
|
24
|
+
RegisterComponent -> OpenComponent
|
|
25
|
+
RegisterComponent -> Router.assignUrl
|
|
26
|
+
OpenComponent -> CenterView.setActiveComponent
|
|
27
|
+
Router.urlAction -> RegisterComponent
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
changeUrl->Router.routes./\#r/ReportId{label: загрузить репорт в dev-режиме}
|
|
2
|
+
changeUrl->Router.routes./\#p/CustomPanelId :загрузить кастомную панель в dev-режиме
|
|
3
|
+
changeUrl->Router.routes./\#UiElementCd :начитать uiElementCd для загрузки компонента пункта меню
|
|
4
|
+
|
|
5
|
+
Router: {
|
|
6
|
+
routes: {
|
|
7
|
+
/\#r/ReportId
|
|
8
|
+
/\#p/CustomPanelId
|
|
9
|
+
/\#UiElementCd
|
|
10
|
+
}
|
|
11
|
+
routes./\#r/ReportId -> ReportPanel_create
|
|
12
|
+
routes./\#p/CustomPanelId -> loadUlElement(xtype=UiCustomPanel): async load
|
|
13
|
+
loadUlElement(xtype=UiCustomPanel) -> UiCustomPanel_instancing
|
|
14
|
+
routes./\#UiElementCd -> loadUlElement
|
|
15
|
+
loadUlElement -> 'xtype=ReportPanel'
|
|
16
|
+
loadUlElement -> 'xtype=UiCustomPanel' -> loadUlElement(xtype=UiCustomPanel)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Router.loadUlElement.'xtype=ReportPanel' -> ComponentFactory.createRootNavComponent
|
|
20
|
+
|
|
21
|
+
ComponentFactory: {
|
|
22
|
+
createRootNavComponent
|
|
23
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Ext.define('Coon.nav.menu.MenuFavoriteElement', {
|
|
2
|
+
extend: 'Ext.Component',
|
|
3
|
+
|
|
4
|
+
xtype: 'MenuFavoriteElement',
|
|
5
|
+
|
|
6
|
+
config: {
|
|
7
|
+
menuItem: {},
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
renderData: {
|
|
11
|
+
iconCls: '',
|
|
12
|
+
iconBaseClass: 'svg-icon svg-icon-size-18',
|
|
13
|
+
text: '',
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
renderTpl: [
|
|
17
|
+
'<div id="{id}-divEl" data-ref="divEl" class="vb-item">',
|
|
18
|
+
'<i id="{id}-iconEl" data-ref="iconEl" class="vb-icon menu-item-main-icon {iconBaseClass} {iconCls}"></i>',
|
|
19
|
+
'<span class="menu-item-text-elem">',
|
|
20
|
+
'<i id="{id}-textEl" data-ref="textEl" class="vb-text svg-icon-size-18">{text}</i>',
|
|
21
|
+
'</span>',
|
|
22
|
+
'</div>'
|
|
23
|
+
],
|
|
24
|
+
childEls: [
|
|
25
|
+
{
|
|
26
|
+
name: 'iconEl',
|
|
27
|
+
itemId: 'iconEl',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'textEl',
|
|
31
|
+
itemId: 'textEl',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'divEl',
|
|
35
|
+
itemId: 'divEl',
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
initComponent() {
|
|
40
|
+
Ext.applyIf(
|
|
41
|
+
menubar.renderData,
|
|
42
|
+
{
|
|
43
|
+
iconCls: this.iconCls,
|
|
44
|
+
text: this.menuItem?.text,
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
this.callParent();
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
setText(text) {
|
|
51
|
+
text = text || this.menuItem.text;
|
|
52
|
+
if (!text) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.textEl.setHtml(text || this.menuItem.text);
|
|
56
|
+
},
|
|
57
|
+
setSelected(state) {
|
|
58
|
+
if (state) {
|
|
59
|
+
this.divEl.addCls('selected');
|
|
60
|
+
} else {
|
|
61
|
+
this.divEl.removeCls('selected');
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
afterRender() {
|
|
66
|
+
const me = this;
|
|
67
|
+
me.iconCls && me.iconEl.addCls(me.iconCls);
|
|
68
|
+
me.setSelected(me.getMenuItem().selected);
|
|
69
|
+
me.setText();
|
|
70
|
+
me.divEl.on(
|
|
71
|
+
'click',
|
|
72
|
+
function() {
|
|
73
|
+
if (typeof me.onClick === 'function') {
|
|
74
|
+
me.onClick.call(me.scope || me, me.menuItem.MENU_ENTRY_CD);
|
|
75
|
+
}
|
|
76
|
+
me.fireEvent('click', me.menuItem.MENU_ENTRY_CD);
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
onDestroy: function() {
|
|
82
|
+
this.divEl.removeAllListeners();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -16,6 +16,9 @@ Ext.define('Coon.uielement.component.UiCustomController', {
|
|
|
16
16
|
view.on('afterrender', function() {
|
|
17
17
|
this.setCustomPanelTitle();
|
|
18
18
|
this.processSecurePoints(view);
|
|
19
|
+
if (typeof this.onInit === 'function') {
|
|
20
|
+
this.onInit.call(view);
|
|
21
|
+
}
|
|
19
22
|
}, this);
|
|
20
23
|
},
|
|
21
24
|
|
|
@@ -20,7 +20,7 @@ Ext.define('Coon.uielement.plugin.OpenPanelPlugin', {
|
|
|
20
20
|
publishes: 'uiElementCd',
|
|
21
21
|
},
|
|
22
22
|
|
|
23
|
-
init
|
|
23
|
+
init(view) {
|
|
24
24
|
if (!this.handlerName) {
|
|
25
25
|
Coon.log.error('OpenPanelPlugin.init: handlerName is required');
|
|
26
26
|
return;
|
|
@@ -33,6 +33,7 @@ Ext.define('Coon.uielement.plugin.OpenPanelPlugin', {
|
|
|
33
33
|
Coon.log.error('OpenPanelPlugin.init: uiElementCd is required');
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
+
this.initialized = true;
|
|
36
37
|
this.cmpController = view.getController();
|
|
37
38
|
if (this.cmpController) {
|
|
38
39
|
this.getViewModel = () => this.cmpController.getViewModel();
|
|
@@ -44,7 +45,14 @@ Ext.define('Coon.uielement.plugin.OpenPanelPlugin', {
|
|
|
44
45
|
this.initBindable();
|
|
45
46
|
},
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Возвращает значение, подставляя его из ViewModel, если
|
|
50
|
+
* передана строка в формате `{path.to.value}`.
|
|
51
|
+
*
|
|
52
|
+
* @param {String|*} value Значение либо ссылка на значение во ViewModel в фигурных скобках.
|
|
53
|
+
* @returns {*} Разрешённое значение: либо из ViewModel, либо исходный параметр.
|
|
54
|
+
*/
|
|
55
|
+
getResolvedValue(value) {
|
|
48
56
|
if (
|
|
49
57
|
Ext.isString(value) &&
|
|
50
58
|
value.length > 2 &&
|
|
@@ -56,40 +64,48 @@ Ext.define('Coon.uielement.plugin.OpenPanelPlugin', {
|
|
|
56
64
|
return value;
|
|
57
65
|
},
|
|
58
66
|
|
|
59
|
-
handler
|
|
67
|
+
handler() {
|
|
68
|
+
if (!this.initialized) {
|
|
69
|
+
Coon.log.error('OpenPanelPlugin.handler: not initialized');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
60
72
|
if (this.isTraceEnabled()) {
|
|
61
73
|
this.tracePlugin(this.config.uiElementCd);
|
|
62
74
|
}
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
if (this.getUiElementCd()) {
|
|
76
|
+
this.uiElementCd = this.getResolvedValue(this.uiElementCd);
|
|
77
|
+
if (!this.uiElementCd) {
|
|
78
|
+
Coon.log.error('OpenPanelPlugin.handler: uiElementCd is not set');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
66
81
|
const command = Ext.create('command.GetUIElementCommand');
|
|
67
82
|
command.on('failure', function(error) {
|
|
68
83
|
Coon.log.error(`OpenPanelPlugin.handler: failed to get UIElement [${this.uiElementCd}]`, error);
|
|
69
|
-
});
|
|
70
|
-
command.on('complete', function(UIElementBean) {
|
|
71
|
-
const elementNS = Coon.uielement.UIElementBeanFields;
|
|
72
|
-
const exists = Ext.ClassManager.getByAlias('widget.' + UIElementBean[elementNS.$xtype]);
|
|
73
|
-
if (!exists) {
|
|
74
|
-
Coon.log.log(UIElementBean.xtype + ' does not exist');
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
this.panelXType = UIElementBean[elementNS.$xtype];
|
|
78
|
-
try {
|
|
79
|
-
this.openPanel(
|
|
80
|
-
JSON5.parse(UIElementBean['propertyData'])
|
|
81
|
-
);
|
|
82
|
-
} catch (ex) {
|
|
83
|
-
Coon.log.error('OpenPanelPlugin.handler: invalid propertyData JSON5', ex);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
84
|
}, this);
|
|
85
|
+
command.on('complete', this.onUIElementLoaded, this);
|
|
87
86
|
command.execute(this.uiElementCd);
|
|
88
87
|
} else {
|
|
89
88
|
this.openPanel();
|
|
90
89
|
}
|
|
91
90
|
},
|
|
92
91
|
|
|
92
|
+
onUIElementLoaded(UIElementBean) {
|
|
93
|
+
const exists = Ext.ClassManager.getByAlias('widget.' + UIElementBean.xtype);
|
|
94
|
+
if (!exists) {
|
|
95
|
+
Coon.log.log(UIElementBean.xtype + ' does not exist');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.panelXType = UIElementBean.xtype;
|
|
99
|
+
try {
|
|
100
|
+
this.openPanel(
|
|
101
|
+
JSON5.parse(UIElementBean['propertyData'])
|
|
102
|
+
);
|
|
103
|
+
} catch (ex) {
|
|
104
|
+
Coon.log.error('OpenPanelPlugin.handler: invalid propertyData JSON5', ex);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
93
109
|
openInCenterView(panel, properties) {
|
|
94
110
|
this.getCenterView();
|
|
95
111
|
if (!this.centerView) {
|
|
@@ -98,7 +114,7 @@ Ext.define('Coon.uielement.plugin.OpenPanelPlugin', {
|
|
|
98
114
|
Ext.toast(`В режиме редактирования кастомной панели нельзя открыть панель ${this.uiElementCd} во вкладке.`);
|
|
99
115
|
}
|
|
100
116
|
Coon.log.debug('panel cannot be open in centerView from CustomPanelEditor');
|
|
101
|
-
return;
|
|
117
|
+
return true;
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
const componentContextId = Coon.util.ContextManager.generateKey(
|
|
@@ -180,23 +196,31 @@ Ext.define('Coon.uielement.plugin.OpenPanelPlugin', {
|
|
|
180
196
|
setInitArguments() {
|
|
181
197
|
this.initArguments = {};
|
|
182
198
|
for (const name in this.parameters) {
|
|
183
|
-
this.initArguments[name] = this.
|
|
199
|
+
this.initArguments[name] = this.getResolvedValue(this.parameters[name]);
|
|
184
200
|
}
|
|
185
201
|
},
|
|
186
202
|
|
|
187
|
-
openPanel
|
|
203
|
+
openPanel(cmpProperties = {}) {
|
|
188
204
|
const properties = Ext.apply({}, cmpProperties, this.properties);
|
|
189
|
-
this.panelXType = this.
|
|
205
|
+
this.panelXType = this.getResolvedValue(this.panelXType);
|
|
190
206
|
if (!Ext.ClassManager.getByAlias(`widget.${this.panelXType}`)) {
|
|
191
207
|
Coon.log.error(`OpenPanelPlugin.openPanel: panelXType [${this.panelXType}] does not exist`);
|
|
192
208
|
return;
|
|
193
209
|
}
|
|
210
|
+
|
|
211
|
+
if (!this.getCenterView() && this.openInTab) {
|
|
212
|
+
return Coon.log.error('panel cannot be open in centerView from CustomPanelEditor');
|
|
213
|
+
}
|
|
194
214
|
const panel = Ext.widget(this.panelXType, properties);
|
|
195
215
|
|
|
196
216
|
this.setInitArguments();
|
|
197
217
|
|
|
198
218
|
if (this.openInTab) {
|
|
199
|
-
this.openInCenterView(panel, properties);
|
|
219
|
+
const error = this.openInCenterView(panel, properties);
|
|
220
|
+
if (error) {
|
|
221
|
+
panel.destroy();
|
|
222
|
+
return error;
|
|
223
|
+
}
|
|
200
224
|
} else {
|
|
201
225
|
this.openInWindowWrap(panel);
|
|
202
226
|
}
|
package/src/version.js
CHANGED