vgapp 0.7.7 → 0.7.8

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.
@@ -1,55 +1,387 @@
1
- import {execute, mergeDeepObject} from "../functions";
1
+ import {getSVG} from "../../../modules/module-fn";
2
2
 
3
- const TEMPLATES = [
4
- {type: 'pass-open', template: '<span data-vg-toggle="pass" class="[[classes]]" title="Показать / Скрыть" data-bs-toggle="tooltip"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256c13.6 30 40.2 72.5 78.6 108.3C169.2 402.4 222.8 432 288 432s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80zM95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6zM288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80c-.7 0-1.3 0-2 0c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2c0 .7 0 1.3 0 2c0 44.2 35.8 80 80 80zm0-208a128 128 0 1 1 0 256 128 128 0 1 1 0-256z"/></svg></span>'},
5
- {type: 'pass-close', template: '<span data-vg-toggle="pass" class="[[classes]]" title="Показать / Скрыть" data-bs-toggle="tooltip"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L525.6 386.7c39.6-40.6 66.4-86.1 79.9-118.4c3.3-7.9 3.3-16.7 0-24.6c-14.9-35.7-46.2-87.7-93-131.1C465.5 68.8 400.8 32 320 32c-68.2 0-125 26.3-169.3 60.8L38.8 5.1zm151 118.3C226 97.7 269.5 80 320 80c65.2 0 118.8 29.6 159.9 67.7C518.4 183.5 545 226 558.6 256c-12.6 28-36.6 66.8-70.9 100.9l-53.8-42.2c9.1-17.6 14.2-37.5 14.2-58.7c0-70.7-57.3-128-128-128c-32.2 0-61.7 11.9-84.2 31.5l-46.1-36.1zM394.9 284.2l-81.5-63.9c4.2-8.5 6.6-18.2 6.6-28.3c0-5.5-.7-10.9-2-16c.7 0 1.3 0 2 0c44.2 0 80 35.8 80 80c0 9.9-1.8 19.4-5.1 28.2zm9.4 130.3C378.8 425.4 350.7 432 320 432c-65.2 0-118.8-29.6-159.9-67.7C121.6 328.5 95 286 81.4 256c8.3-18.4 21.5-41.5 39.4-64.8L83.1 161.5C60.3 191.2 44 220.8 34.5 243.7c-3.3 7.9-3.3 16.7 0 24.6c14.9 35.7 46.2 87.7 93 131.1C174.5 443.2 239.2 480 320 480c47.8 0 89.9-12.9 126.2-32.5l-41.9-33zM192 256c0 70.7 57.3 128 128 128c13.3 0 26.1-2 38.2-5.8L302 334c-23.5-5.4-43.1-21.2-53.7-42.3l-56.1-44.2c-.2 2.8-.3 5.6-.3 8.5z"/></svg></span>'},
6
- {type: 'loader', template: '<div class="vg-loader"></div>'}
7
- ]
3
+ class LaravelHtmlBuilder {
4
+ constructor(mode = 'string') {
5
+ this.mode = mode; // 'string' или 'dom'
6
+ this.document = typeof document !== 'undefined' ? document : null;
7
+ this.tags = {
8
+ selfClosing: ['img', 'input', 'br', 'hr', 'meta', 'link', 'area', 'base', 'col', 'command', 'embed', 'keygen', 'param', 'source', 'track', 'wbr']
9
+ };
10
+ }
8
11
 
12
+ /**
13
+ * Установка режима работы
14
+ */
15
+ setMode(mode) {
16
+ this.mode = mode;
17
+ return this;
18
+ }
9
19
 
10
- class Templater {
11
- constructor(el, params = {}) {
12
- if (!el) {
13
- throw new Error('Element is required');
20
+ /**
21
+ * Создание HTML элемента
22
+ */
23
+ element(tag, attributes = {}, content = null, option = {}) {
24
+ if (this.mode === 'dom' && this.document) {
25
+ return this._createDomElement(tag, attributes, content, option);
14
26
  }
15
-
16
- this._element = el;
17
- this._params = mergeDeepObject({
18
- insert: 'afterend',
19
- classes: []
20
- }, params);
27
+ return this._createHtmlString(tag, attributes, content, option);
21
28
  }
22
29
 
23
- render(content, callback) {
24
- let tmpl = this.toHTML(content, callback);
30
+ /**
31
+ * Создание DOM элемента
32
+ */
33
+ _createDomElement(tag, attributes, content, option = {}) {
34
+ const element = this.document.createElement(tag);
25
35
 
26
- switch (this._params.insert) {
27
- case 'afterend':
28
- this._element.insertAdjacentHTML('afterend', tmpl);
29
- break;
36
+ // Установка атрибутов
37
+ this._setDomAttributes(element, attributes);
38
+
39
+ // Добавление содержимого
40
+ if (content !== null && !this.tags.selfClosing.includes(tag)) {
41
+ if (Array.isArray(content)) {
42
+ content.forEach(item => {
43
+ if (typeof item === 'string') {
44
+ element.appendChild(this.document.createTextNode(item));
45
+ } else if (item instanceof Node) {
46
+ element.appendChild(item);
47
+ }
48
+ });
49
+ } else if (content instanceof Node) {
50
+ element.appendChild(content);
51
+ } else {
52
+ if ('isHTML' in option && option.isHTML) {
53
+ element.innerHTML = content;
54
+ } else {
55
+ element.textContent = content;
56
+ }
57
+ }
30
58
  }
59
+
60
+ return element;
31
61
  }
32
62
 
33
- toHTML(content = '' | null, callback) {
34
- let tmpl = '';
63
+ /**
64
+ * Установка атрибутов DOM элемента
65
+ */
66
+ _setDomAttributes(element, attributes) {
67
+ if (!attributes) return;
35
68
 
36
- for (const tmplElement of TEMPLATES) {
37
- if (tmplElement.type === this._params.template) {
38
- tmpl = tmplElement.template;
69
+ Object.entries(attributes).forEach(([key, value]) => {
70
+ if (value === null || value === undefined || value === false) {
71
+ return;
39
72
  }
73
+
74
+ if (value === true) {
75
+ element.setAttribute(key, '');
76
+ } else {
77
+ element.setAttribute(key, String(value));
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Создание HTML строки
84
+ */
85
+ _createHtmlString(tag, attributes, content, isHTML = false) {
86
+ const attrString = this._attributesToString(attributes);
87
+ const isSelfClosing = this.tags.selfClosing.includes(tag);
88
+
89
+ if (isSelfClosing) {
90
+ return `<${tag}${attrString}>`;
40
91
  }
41
92
 
42
- if (!tmpl) return;
93
+ const contentStr = content !== null ?
94
+ this._contentToString(content) : '';
43
95
 
44
- tmpl = tmpl.replace('[[classes]]', this._params.classes.join(' '));
45
- execute(callback, [this._element, this._params, tmpl]);
96
+ if (tag === '') {
97
+ return `${contentStr}`;
98
+ } else {
99
+ return `<${tag}${attrString}>${contentStr}</${tag}>`;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Преобразование содержимого в строку
105
+ */
106
+ _contentToString(content) {
107
+ if (Array.isArray(content)) {
108
+ return content.map(item => {
109
+ if (item instanceof Node) {
110
+ return this._nodeToString(item);
111
+ }
112
+ return String(item);
113
+ }).join('');
114
+ }
115
+
116
+ if (content instanceof Node) {
117
+ return this._nodeToString(content);
118
+ }
46
119
 
47
- return tmpl;
120
+ return String(content);
48
121
  }
49
122
 
50
- setContent() {
123
+ /**
124
+ * Преобразование DOM узла в строку
125
+ */
126
+ _nodeToString(node) {
127
+ if (node.outerHTML) {
128
+ return node.outerHTML;
129
+ }
130
+ return String(node);
131
+ }
132
+
133
+ /**
134
+ * Преобразование атрибутов в строку
135
+ */
136
+ _attributesToString(attrs) {
137
+ if (!attrs || Object.keys(attrs).length === 0) {
138
+ return '';
139
+ }
140
+
141
+ const attributes = Object.entries(attrs)
142
+ .map(([key, value]) => {
143
+ if (value === null || value === undefined || value === false) {
144
+ return '';
145
+ }
146
+
147
+ if (value === true) {
148
+ return key;
149
+ }
150
+
151
+ const escapedValue = String(value)
152
+ .replace(/&/g, '&amp;')
153
+ .replace(/"/g, '&quot;')
154
+ .replace(/</g, '&lt;')
155
+ .replace(/>/g, '&gt;');
156
+
157
+ return `${key}="${escapedValue}"`;
158
+ })
159
+ .filter(attr => attr !== '');
160
+
161
+ return attributes.length ? ' ' + attributes.join(' ') : '';
162
+ }
163
+
164
+ /**
165
+ * Методы для конкретных элементов
166
+ */
167
+ div(attributes = {}, content = '', options = {}) {
168
+ return this.element('div', attributes, content, options);
169
+ }
170
+
171
+ span(attributes = {}, content = '', options = {}) {
172
+ return this.element('span', attributes, content, options);
173
+ }
174
+
175
+ i(attributes = {}, content = '', options = {}) {
176
+ return this.element('i', attributes, content, options);
177
+ }
178
+
179
+ p(attributes = {}, content = '', options = {}) {
180
+ return this.element('p', attributes, content, options);
181
+ }
182
+
183
+ a(href, content = '', attributes = {}, options = {}) {
184
+ return this.element('a', { href, ...attributes }, content, options);
185
+ }
186
+
187
+ img(src, alt = '', attributes = {}) {
188
+ return this.element('img', { src, alt, ...attributes });
189
+ }
190
+
191
+ input(name, type = 'text', value = '', attributes = {}) {
192
+ return this.element('input', {
193
+ type,
194
+ name,
195
+ value,
196
+ ...attributes
197
+ });
198
+ }
199
+
200
+ button(content = '', type = 'button', attributes = {}, options = {}) {
201
+ return this.element('button', { type, ...attributes }, content, options);
202
+ }
51
203
 
204
+ form(action = '', method = 'post', attributes = {}, content = '', options = {}) {
205
+ return this.element('form', { action, method, ...attributes }, content, options);
52
206
  }
207
+
208
+ label(forId, content, attributes = {}, options = {}) {
209
+ return this.element('label', { for: forId, ...attributes }, content, options);
210
+ }
211
+
212
+ textarea(name, content = '', attributes = {}) {
213
+ return this.element('textarea', { name, ...attributes }, content);
214
+ }
215
+
216
+ ul(items = [], attributes = {}) {
217
+ const listItems = items.map(item =>
218
+ this.element('li', {}, item)
219
+ );
220
+ return this.element('ul', attributes, listItems);
221
+ }
222
+
223
+ ol(items = [], attributes = {}) {
224
+ const listItems = items.map(item =>
225
+ this.element('li', {}, item)
226
+ );
227
+ return this.element('ol', attributes, listItems);
228
+ }
229
+
230
+ h4(attributes = {}, content = '', options = {}) {
231
+ return this.element('h4', attributes, content, options);
232
+ }
233
+
234
+ /**
235
+ * Создание таблицы
236
+ */
237
+ table(headers = [], rows = [], attributes = {}) {
238
+ const headerCells = headers.map(header =>
239
+ this.element('th', {}, header)
240
+ );
241
+ const headerRow = this.element('tr', {}, headerCells);
242
+ const thead = this.element('thead', {}, headerRow);
243
+
244
+ const bodyRows = rows.map(row => {
245
+ const cells = row.map(cell =>
246
+ this.element('td', {}, cell)
247
+ );
248
+ return this.element('tr', {}, cells);
249
+ });
250
+ const tbody = this.element('tbody', {}, bodyRows);
251
+
252
+ return this.element('table', attributes, [thead, tbody]);
253
+ }
254
+
255
+ /**
256
+ * Методы для форм
257
+ */
258
+ csrfToken(token) {
259
+ return this.element('input', {
260
+ type: 'hidden',
261
+ name: '_token',
262
+ value: token
263
+ });
264
+ }
265
+
266
+ method(method) {
267
+ return this.element('input', {
268
+ type: 'hidden',
269
+ name: '_method',
270
+ value: method
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Создание элемента с обработчиками событий
276
+ */
277
+ withEvents(element, events = {}) {
278
+ if (element instanceof Node && this.mode === 'dom') {
279
+ Object.entries(events).forEach(([event, handler]) => {
280
+ element.addEventListener(event, handler);
281
+ });
282
+ }
283
+ return element;
284
+ }
285
+
286
+ /**
287
+ * Добавление классов
288
+ */
289
+ addClass(element, className) {
290
+ if (element instanceof Node && this.mode === 'dom') {
291
+ element.classList.add(className);
292
+ } else if (typeof element === 'string') {
293
+ // Для строкового режима - модифицируем атрибут class
294
+ const match = element.match(/<(\w+)([^>]*)>/);
295
+ if (match) {
296
+ const [fullMatch, tag, attrs] = match;
297
+ const newAttrs = this._addClassToAttributes(attrs, className);
298
+ return element.replace(fullMatch, `<${tag}${newAttrs}>`);
299
+ }
300
+ }
301
+ return element;
302
+ }
303
+
304
+ _addClassToAttributes(attrs, className) {
305
+ const classMatch = attrs.match(/class="([^"]*)"/);
306
+ if (classMatch) {
307
+ const existingClass = classMatch[1];
308
+ const newClass = existingClass ? `${existingClass} ${className}` : className;
309
+ return attrs.replace(/class="[^"]*"/, `class="${newClass}"`);
310
+ } else {
311
+ return `${attrs} class="${className}"`;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Создание компонента
317
+ */
318
+ component(name, props = {}, slots = {}) {
319
+ // Можно добавить заготовленные компоненты
320
+ const components = {
321
+ 'eye': (props) => {
322
+ const type = props.type || 'open';
323
+ const svg = this.element('', {}, getSVG(`eye-${type}`))
324
+ return this.span(
325
+ {
326
+ 'data-vg-toggle': 'pass',
327
+ 'data-bs-toggle': 'tooltip',
328
+ 'title': 'Скрыть',
329
+ 'class': props.class || ''
330
+ },
331
+ [
332
+ svg
333
+ ]
334
+ );
335
+ },
336
+ 'alert': (props) => {
337
+ const type = props.type || 'info';
338
+ return this.div(
339
+ {
340
+ class: `alert alert-${type}`,
341
+ role: 'alert'
342
+ },
343
+ props.content || ''
344
+ );
345
+ },
346
+ 'card': (props) => {
347
+ const header = props.header ?
348
+ this.div({ class: 'card-header' }, props.header) : '';
349
+ const body = this.div({ class: 'card-body' }, props.body || '');
350
+ const footer = props.footer ?
351
+ this.div({ class: 'card-footer' }, props.footer) : '';
352
+
353
+ return this.div(
354
+ { class: 'card' },
355
+ [header, body, footer]
356
+ );
357
+ }
358
+ };
359
+
360
+ if (components[name]) {
361
+ return components[name](props);
362
+ }
363
+
364
+ return this.div({ class: `component-${name}` }, '');
365
+ }
366
+ }
367
+
368
+ function Html(mode = 'string') {
369
+ const builder = new LaravelHtmlBuilder(mode);
370
+
371
+ const handler = {
372
+ get(target, prop) {
373
+ if (prop in target) {
374
+ return target[prop].bind(target);
375
+ }
376
+
377
+ // Динамическое создание метода для любого тега
378
+ return function(attributes = {}, content = '', options = {}) {
379
+ return target.element(prop, attributes, content, options);
380
+ };
381
+ }
382
+ };
383
+
384
+ return new Proxy(builder, handler);
53
385
  }
54
386
 
55
- export default Templater;
387
+ export default Html;