neo.mjs 5.15.5 → 5.16.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.
Files changed (33) hide show
  1. package/apps/ServiceWorker.mjs +2 -2
  2. package/buildScripts/docs/jsdocx.mjs +8 -6
  3. package/docs/app/view/MainContainer.mjs +10 -0
  4. package/docs/app/view/classdetails/HeaderComponent.mjs +3 -3
  5. package/docs/app/view/classdetails/MainContainer.mjs +14 -4
  6. package/docs/app/view/classdetails/MembersList.mjs +5 -5
  7. package/examples/ServiceWorker.mjs +2 -2
  8. package/examples/form/field/fileupload/MainContainer.mjs +68 -10
  9. package/examples/form/field/fileupload/README.md +9 -0
  10. package/examples/form/field/fileupload/server.mjs +49 -0
  11. package/examples/form/field/switch/MainContainer.mjs +124 -0
  12. package/examples/form/field/switch/app.mjs +6 -0
  13. package/examples/form/field/switch/index.html +11 -0
  14. package/examples/form/field/switch/neo-config.json +6 -0
  15. package/package.json +2 -1
  16. package/resources/scss/src/apps/docs/HeaderContainer.scss +1 -1
  17. package/resources/scss/src/apps/docs/MainContainer.scss +5 -1
  18. package/resources/scss/src/apps/docs/classdetails/HeaderComponent.scss +2 -5
  19. package/resources/scss/src/apps/docs/classdetails/MainContainer.scss +1 -1
  20. package/resources/scss/src/component/Splitter.scss +3 -1
  21. package/resources/scss/src/form/field/FileUpload.scss +248 -2
  22. package/resources/scss/src/form/field/Switch.scss +85 -115
  23. package/resources/scss/src/tree/List.scss +2 -1
  24. package/resources/scss/theme-dark/form/field/FileUpload.scss +20 -4
  25. package/resources/scss/theme-dark/form/field/Switch.scss +10 -10
  26. package/resources/scss/theme-light/form/field/FileUpload.scss +20 -4
  27. package/resources/scss/theme-light/form/field/Switch.scss +10 -10
  28. package/src/DefaultConfig.mjs +2 -2
  29. package/src/component/Splitter.mjs +27 -22
  30. package/src/form/field/FileUpload.mjs +512 -4
  31. package/src/form/field/Switch.mjs +11 -11
  32. package/src/main/addon/Markdown.mjs +2 -2
  33. package/src/main/addon/ResizeObserver.mjs +79 -0
@@ -77,7 +77,7 @@ class Splitter extends Component {
77
77
  me.addDomListeners([
78
78
  {'drag:end' : me.onDragEnd, scope: me},
79
79
  {'drag:start': me.onDragStart, scope: me}
80
- ]);
80
+ ])
81
81
  }
82
82
 
83
83
  /**
@@ -87,20 +87,24 @@ class Splitter extends Component {
87
87
  * @protected
88
88
  */
89
89
  afterSetDirection(value, oldValue) {
90
- let me = this,
91
- cls = me.cls;
90
+ let me = this,
91
+ cls = me.cls,
92
+ height = value === 'vertical' ? null : me.size,
93
+ width = value !== 'vertical' ? null : me.size;
92
94
 
93
95
  NeoArray.add(cls, `neo-${value}`);
94
96
 
95
97
  if (oldValue) {
96
- NeoArray.remove(cls, `neo-${oldValue}`);
98
+ NeoArray.remove(cls, `neo-${oldValue}`)
97
99
  }
98
100
 
99
101
  me.set({
100
102
  cls,
101
- height: value === 'vertical' ? null : me.size,
102
- width : value !== 'vertical' ? null : me.size
103
- });
103
+ height,
104
+ minHeight: height,
105
+ minWidth : width,
106
+ width
107
+ })
104
108
  }
105
109
 
106
110
  /**
@@ -110,7 +114,7 @@ class Splitter extends Component {
110
114
  * @protected
111
115
  */
112
116
  afterSetSize(value, oldValue) {
113
- this[this.direction === 'vertical' ? 'width' : 'height'] = value;
117
+ this[this.direction === 'vertical' ? 'width' : 'height'] = value
114
118
  }
115
119
 
116
120
  /**
@@ -121,7 +125,7 @@ class Splitter extends Component {
121
125
  * @returns {String}
122
126
  */
123
127
  beforeSetDirection(value, oldValue) {
124
- return this.beforeSetEnumValue(value, oldValue, 'direction');
128
+ return this.beforeSetEnumValue(value, oldValue, 'direction')
125
129
  }
126
130
 
127
131
  /**
@@ -132,7 +136,7 @@ class Splitter extends Component {
132
136
  * @returns {String}
133
137
  */
134
138
  beforeSetResizeTarget(value, oldValue) {
135
- return this.beforeSetEnumValue(value, oldValue, 'resizeTarget');
139
+ return this.beforeSetEnumValue(value, oldValue, 'resizeTarget')
136
140
  }
137
141
 
138
142
  /**
@@ -163,24 +167,25 @@ class Splitter extends Component {
163
167
  style.flex = 'none';
164
168
 
165
169
  if (me.direction === 'vertical') {
166
- newSize = data.clientX - data.offsetX - size;
170
+ newSize = data.clientX - data.offsetX - size - parentRect.left;
167
171
 
168
172
  if (resizeNext) {
169
- newSize = parentRect.width - newSize;
173
+ console.log(parentRect);
174
+ newSize = parentRect.width - newSize - 2 * size
170
175
  } else {
171
- newSize += size;
176
+ newSize += size
172
177
  }
173
178
 
174
179
  newSize = Math.min(Math.max(newSize, 0), parentRect.width - size);
175
180
 
176
- style.width = `${newSize}px`;
181
+ style.width = `${newSize}px`
177
182
  } else {
178
- newSize = data.clientY - data.offsetY - size;
183
+ newSize = data.clientY - data.offsetY - size - parentRect.top;
179
184
 
180
185
  if (resizeNext) {
181
- newSize = parentRect.height - newSize;
186
+ newSize = parentRect.height - newSize - 2 * size
182
187
  } else {
183
- newSize += size;
188
+ newSize += size
184
189
  }
185
190
 
186
191
  newSize = Math.min(Math.max(newSize, 0), parentRect.height - size);
@@ -188,8 +193,8 @@ class Splitter extends Component {
188
193
  style.height = `${newSize}px`;
189
194
  }
190
195
 
191
- sibling.style = style;
192
- });
196
+ sibling.style = style
197
+ })
193
198
  }
194
199
 
195
200
  /**
@@ -214,20 +219,20 @@ class Splitter extends Component {
214
219
  owner : me,
215
220
  useProxyWrapper : false,
216
221
  ...me.dragZoneConfig
217
- });
222
+ })
218
223
  } else {
219
224
  me.dragZone.set({
220
225
  bodyCursorStyle: vertical ? 'ew-resize !important' : 'ns-resize !important',
221
226
  moveHorizontal : vertical,
222
227
  moveVertical : !vertical
223
- });
228
+ })
224
229
  }
225
230
 
226
231
  me.dragZone.dragStart(data);
227
232
 
228
233
  style.opacity = 0.5;
229
234
 
230
- me.style = style;
235
+ me.style = style
231
236
  }
232
237
  }
233
238
 
@@ -1,6 +1,81 @@
1
1
  import Base from '../../form/field/Base.mjs';
2
+ import NeoArray from '../../util/Array.mjs';
3
+
4
+ const
5
+ sizeRE = /^(\d+)(kb|mb|gb)?$/i,
6
+ sizeMultiplier = {
7
+ unit : 1,
8
+ kb : 1000,
9
+ mb : 1000000,
10
+ gb : 1000000000
11
+ };
2
12
 
3
13
  /**
14
+ * An accessible file uploading widget which automatically commences an upload as soon as
15
+ * a file is selected using the UI.
16
+ *
17
+ * The URL to which the file must be uploaded is specified in the {@link #member-uploadUrl} property.
18
+ * This service must return a JSON status response in the following form for successful uploads:
19
+ *
20
+ * ```json
21
+ * {
22
+ * "success" : true,
23
+ * "documentId" : 1
24
+ * }
25
+ * ```
26
+ *
27
+ * And the following form for unsuccessful uploads:
28
+ *
29
+ * ```json
30
+ * {
31
+ * "success" : false,
32
+ * "message" : "Why the upload was rejected"
33
+ * }
34
+ * ```
35
+ *
36
+ * The name of the `documentId` property is configured in {@link #member-documentIdParameter}.
37
+ * It defaults to `'documentId'`.
38
+ *
39
+ * The `documentId` is used when requesting the document malware scan status, and when requesting
40
+ * that the document be deleted, or downloaded.
41
+ *
42
+ * If the upload is successful, then the {@link #member-documentStatusUrl} is polled until the
43
+ * malware scan. The document id returned from the upload is passed in the parameter named
44
+ * by the {@link #member-documentIdParameter}. It defaults to `'documentId'`.
45
+ *
46
+ * This service must return a JSON status response in the following if the scan is still progressing:
47
+ *
48
+ * ```json
49
+ * {
50
+ * "status" : "scanning"
51
+ * }
52
+ * ```
53
+ *
54
+ * And the following form is malware was detected:
55
+ *
56
+ * ```json
57
+ * {
58
+ * "status" : "scan-failed"
59
+ * }
60
+ * ```
61
+ *
62
+ * After a successful scan, a document may or may not be downloadable.
63
+ *
64
+ * For a downloadable document, the response must be:
65
+ *
66
+ * ```json
67
+ * {
68
+ * "status" : "downloadable"
69
+ * }
70
+ * ```
71
+ *
72
+ * For a non-downloadable document, the response must be:
73
+ *
74
+ * ```json
75
+ * {
76
+ * "status" : "not-downloadable"
77
+ * }
78
+ * ```
4
79
  * @class Neo.form.field.FileUpload
5
80
  * @extends Neo.form.field.Base
6
81
  */
@@ -17,15 +92,448 @@ class FileUpload extends Base {
17
92
  */
18
93
  ntype: 'file-upload-field',
19
94
  /**
20
- * @member {String[]}} baseCls=['neo-file-upload-field']
95
+ * @member {String[]} baseCls=['neo-file-upload-field']
21
96
  * @protected
22
97
  */
23
98
  baseCls: ['neo-file-upload-field'],
24
99
  /**
25
- * @member {Object}} _vdom
100
+ * @member {Object} _vdom
26
101
  */
27
- _vdom:
28
- {tag: 'input', type: 'file'}
102
+ _vdom: {
103
+ cn : [
104
+ {
105
+ tag : 'i',
106
+ cls : 'neo-file-upload-state-icon'
107
+ },
108
+ {
109
+ cls : 'neo-file-upload-body',
110
+ cn : [{
111
+ cls : 'neo-file-upload-filename'
112
+ }, {
113
+ cls : 'neo-file-upload-state'
114
+ }]
115
+ },
116
+ {
117
+ cls : 'neo-file-upload-action-button',
118
+ tag : 'button'
119
+ },
120
+ {
121
+ cls : 'neo-file-upload-input',
122
+ tag : 'input',
123
+ type : 'file'
124
+ },
125
+ {
126
+ cls : 'neo-file-upload-error-message'
127
+ }
128
+ ]
129
+ },
130
+
131
+ cls : [],
132
+
133
+ /**
134
+ * The URL of the file upload service to which the selected file is sent.
135
+ *
136
+ * This service must return a JSON response of the form:
137
+ *
138
+ * ```json
139
+ * {
140
+ * "success" : true,
141
+ * "message" : "Only needed if the success property is false",
142
+ * "documentId" : 1
143
+ * }
144
+ * ```
145
+ *
146
+ * The document id is needed so that this widget can follow up and request the results of the
147
+ * scan operation to see if the file was accepted, and whether it is to be subsequently downloadable.
148
+ *
149
+ * The document status request URL must be configured in {@link #member-documentStatusUrl}
150
+ * @member {String} uploadUrl
151
+ */
152
+ uploadUrl_ : null,
153
+
154
+ /**
155
+ * The name of the JSON property in which the document id is returned in the upload response
156
+ * JSON packet and the HTTP parameter which is used when requesting a malware scan and a document
157
+ * deletion.
158
+ *
159
+ * @member {String} downloadUrl
160
+ */
161
+ documentIdParameter : 'documentId',
162
+
163
+ /**
164
+ * The URL from which the file may be downloaded after it has finished its scan.
165
+ *
166
+ * The document id returned from the {@link #member-uploadUrl upload} is passed in the parameter named
167
+ * by the {@link #member-documentIdParameter}. It defaults to `'documentId'`.
168
+ *
169
+ * @member {String} downloadUrl
170
+ */
171
+ downloadUrl_ : null,
172
+
173
+ /**
174
+ * The URL of the file status reporting service.
175
+ *
176
+ * This widget will use this service after a successful upload to determine its next
177
+ * state.
178
+ *
179
+ * This service must return a JSON response of the form:
180
+ *
181
+ * ```json
182
+ * {
183
+ * "status" : "scanning" or "scan-failed" or "downloadable or "not-downloadable"
184
+ * }
185
+ * ```
186
+ *
187
+ * @member {String} documentStatusUrl
188
+ */
189
+ documentStatusUrl : null,
190
+
191
+ /**
192
+ * The URL of the file deletion service.
193
+ *
194
+ * This widget will use this service after a successful upload to determine its next
195
+ * state.
196
+ *
197
+ * If this service yields an HTTP 200 status, the deletion is taken to have been successful.
198
+ *
199
+ * @member {String} documentDeleteUrl
200
+ */
201
+ documentDeleteUrl : null,
202
+
203
+ headers_ : {},
204
+
205
+ /**
206
+ * @member {String} state_=null
207
+ */
208
+ state_: 'ready',
209
+
210
+ /**
211
+ * @member {Object} types=null
212
+ */
213
+ types_ : null,
214
+
215
+ /**
216
+ * @member {String|Number} maxSize
217
+ */
218
+ maxSize_: null
219
+ }
220
+
221
+ /**
222
+ * @param {Object} config
223
+ */
224
+ construct(config) {
225
+ super.construct(config);
226
+
227
+ const me = this;
228
+
229
+ me.addDomListeners([
230
+ { input : me.onInputValueChange, scope: me},
231
+ { click : me.onActionButtonClick, delegate : '.neo-file-upload-action-button', scope : me}
232
+ ]);
233
+ }
234
+
235
+ async clear() {
236
+ const me = this;
237
+
238
+ me.vdom.cn[3] = {
239
+ cls : 'neo-file-upload-input',
240
+ tag : 'input',
241
+ type : 'file',
242
+ value : ''
243
+ };
244
+ me.state = 'ready';
245
+ me.error = '';
246
+
247
+ // We have to wait for the DOM to have changed, and the input field to be visible
248
+ await new Promise(resolve => setTimeout(resolve, 100));
249
+ me.focus(me.vdom.cn[3].id);
250
+ }
251
+
252
+ /**
253
+ * @param {Object} data
254
+ * @protected
255
+ */
256
+ onInputValueChange({ files }) {
257
+ const
258
+ me = this,
259
+ { types } = me;
260
+
261
+ if (files.length) {
262
+ const
263
+ file = files.item(0),
264
+ pointPos = file.name.lastIndexOf('.'),
265
+ type = pointPos > -1 ? file.name.slice(pointPos + 1) : '';
266
+
267
+ if (me.types && !types[type]) {
268
+ me.error = `Please use these file types: .${Object.keys(types).join(' .')}`;
269
+ }
270
+ else if (file.size > me.maxSize) {
271
+ me.error = `File size exceeds ${String(me._maxSize).toUpperCase()}`;
272
+ }
273
+ // If it passes the type and maxSize check, upload it
274
+ else {
275
+ me.fileSize = me.formatSize(file.size);
276
+ me.error = '';
277
+ me.upload(file);
278
+ }
279
+ }
280
+ // If cleared, we go back to ready state
281
+ else {
282
+ me.state = 'ready';
283
+ }
284
+ }
285
+
286
+ async upload(file) {
287
+ const
288
+ me = this,
289
+ xhr = me.xhr = new XMLHttpRequest(),
290
+ { upload } = xhr,
291
+ fileData = new FormData();
292
+
293
+ // Show the action button
294
+ me.state = 'starting';
295
+
296
+ // We have to wait for the DOM to have changed, and the action button to be visible
297
+ await new Promise(resolve => setTimeout(resolve, 100));
298
+ me.focus(me.vdom.cn[2].id);
299
+
300
+ me.vdom.cn[1].cn[0].innerHTML = file.name;
301
+ me.update();
302
+ me.state = 'uploading';
303
+
304
+ fileData.append("file", file);
305
+
306
+ // React to upload state changes
307
+ upload.addEventListener('progress', me.onUploadProgress.bind(me));
308
+ upload.addEventListener('error', me.onUploadError.bind(me));
309
+ upload.addEventListener('abort', me.onUploadAbort.bind(me));
310
+ xhr.addEventListener('loadend', me.onUploadDone.bind(me));
311
+
312
+ xhr.open("POST", me.uploadUrl, true);
313
+
314
+ xhr.send(fileData);
315
+ }
316
+
317
+ onUploadProgress({ loaded, total }) {
318
+ const
319
+ progress = this.progress = loaded / total,
320
+ { vdom } = this;
321
+
322
+ (vdom.style || (vdom.style = {}))['--upload-progress'] = `${progress}turn`;
323
+
324
+ vdom.cn[1].cn[1].innerHTML = `Uploading... (${Math.round(progress * 100)}%)`;
325
+
326
+ this.uploadSize = loaded;
327
+ this.update();
328
+ }
329
+
330
+ onUploadAbort(e) {
331
+ this.xhr = null;
332
+ this.clear();
333
+ }
334
+
335
+ onUploadError(e) {
336
+ this.xhr = null;
337
+ this.state = 'upload-failed';
338
+ this.error = e.type;
339
+ }
340
+
341
+ onUploadDone({ loaded, target : xhr }) {
342
+ const me = this;
343
+
344
+ me.xhr = null;
345
+
346
+ if (loaded !== 0) {
347
+ const response = JSON.parse(xhr.response);
348
+
349
+ if (response.success) {
350
+ me.documentId = response[me.documentIdParameter];
351
+
352
+ // The status check phase is optional.
353
+ // If no URL specified, the file is taken to be downloadable.
354
+ if (me.documentStatusUrl) {
355
+ me.state = 'processing';
356
+
357
+ // Start polling the server to see when the scan has a result;
358
+ me.checkDocumentStatus();
359
+ }
360
+ else {
361
+ me.state = 'downloadable';
362
+ }
363
+ }
364
+ else {
365
+ me.error = response.message;
366
+ me.state = 'upload-failed';
367
+ }
368
+ }
369
+ }
370
+
371
+ onActionButtonClick() {
372
+ const
373
+ me = this,
374
+ { state } = me;
375
+
376
+ // When they click the action button, depending on which state we are in, we go to
377
+ // different states.
378
+ switch (state) {
379
+ // During upload, its an abort
380
+ case 'uploading':
381
+ me.abortUpload();
382
+ break;
383
+
384
+ // If the upload or the scan failed, the document will not have been
385
+ // saved, so we just go back to ready state
386
+ case 'upload-failed':
387
+ case 'scan-failed':
388
+ me.clear();
389
+ me.state = 'ready';
390
+ break;
391
+
392
+ // During scanning and for stored documents, we need to tell the server the document
393
+ // is not required.
394
+ case 'processing':
395
+ case 'downloadable':
396
+ case 'not-downloadable':
397
+ me.deleteDocument();
398
+ break;
399
+ }
400
+ }
401
+
402
+ abortUpload() {
403
+ this.xhr?.abort();
404
+ }
405
+
406
+ async deleteDocument() {
407
+ // We ask the server to delete using our this.documentId
408
+ const statusResponse = await fetch(`${this.documentDeleteUrl}?${this.documentIdParameter}=${this.documentId}`);
409
+
410
+ // Success
411
+ if (String(statusResponse.status).slice(0, 1) === '2') {
412
+ this.clear();
413
+ this.state = 'ready';
414
+ }
415
+ else {
416
+ this.error = `Document delete service error: ${statusResponse.statusText}`;
417
+ }
418
+ }
419
+
420
+ async checkDocumentStatus() {
421
+ const me = this;
422
+
423
+ if (this.state === 'processing') {
424
+ const statusResponse = await fetch(`${this.documentStatusUrl}?${me.documentIdParameter}=${this.documentId}`);
425
+
426
+ // Success
427
+ if (String(statusResponse.status).slice(0, 1) === '2') {
428
+ const status = (await statusResponse.json()).status;
429
+
430
+ switch (status) {
431
+ case 'scanning':
432
+ setTimeout(() => me.checkDocumentStatus(), 2000);
433
+ break;
434
+ default:
435
+ me.state = status;
436
+ }
437
+ }
438
+ else {
439
+ this.error = `Document status service error: ${statusResponse.statusText}`;
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Triggered after the state config got changed
446
+ * @param {String} value
447
+ * @param {String} oldValue
448
+ * @protected
449
+ */
450
+ afterSetState(value, oldValue) {
451
+ const
452
+ me = this,
453
+ {
454
+ vdom
455
+ } = me,
456
+ anchor = vdom.cn[1].cn[0],
457
+ status = vdom.cn[1].cn[1];
458
+
459
+ switch (value) {
460
+ case 'ready':
461
+ anchor.tag = 'div';
462
+ anchor.href = '';
463
+ break;
464
+ case 'upload-failed':
465
+ status.innerHTML = `Upload failed... (${Math.round(me.progress * 100)}%)`;
466
+ break;
467
+ case 'processing':
468
+ status.innerHTML = `Scanning... (${me.formatSize(me.uploadSize)})`;
469
+ break;
470
+ case 'scan-failed':
471
+ status.innerHTML = `Malware found in file. \u2022 ${me.fileSize}`;
472
+ me.error = 'Please check the file and try again';
473
+ break;
474
+ case 'downloadable':
475
+ anchor.tag = 'a';
476
+ anchor.href = `${me.downloadUrl}?${me.documentIdParameter}=${me.documentId}`;
477
+ status.innerHTML = me.fileSize;
478
+ break;
479
+ case 'not-downloadable':
480
+ status.innerHTML = `Successfully uploaded \u2022 ${me.fileSize}`;
481
+ }
482
+
483
+ me.update();
484
+
485
+ // Processing above may mutate cls
486
+ const { cls } = me;
487
+
488
+ NeoArray.remove(cls, 'neo-file-upload-state-' + oldValue);
489
+ NeoArray.add(cls, 'neo-file-upload-state-' + value);
490
+ me.cls = cls;
491
+ }
492
+
493
+ beforeGetMaxSize(maxSize) {
494
+ // Not configured means no limit
495
+ if (maxSize == null) {
496
+ return Number.MAX_SAFE_INTEGER;
497
+ }
498
+
499
+ // Split eg "100mb" into the numeric and units parts
500
+ const sizeParts = sizeRE.exec(maxSize);
501
+
502
+ if (sizeParts) {
503
+ // Convert mb to 1000000 etc
504
+ const multiplier = sizeMultiplier[(sizeParts[2]||'unit').toLowerCase()];
505
+
506
+ return parseInt(sizeParts[1]) * multiplier;
507
+ }
508
+ }
509
+
510
+ set error(text) {
511
+ const { cls } = this;
512
+
513
+ if (text) {
514
+ this.vdom.cn[4].cn = [{
515
+ vtype : 'text',
516
+ html : text
517
+ }];
518
+ NeoArray.add(cls, 'neo-invalid');
519
+ }
520
+ else {
521
+ NeoArray.remove(cls, 'neo-invalid');
522
+ }
523
+
524
+ this.cls = cls;
525
+ this.update();
526
+ }
527
+
528
+ formatSize(bytes, separator = '', postFix = '') {
529
+ if (bytes) {
530
+ const
531
+ sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'],
532
+ i = Math.min(parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString(), 10), sizes.length - 1);
533
+
534
+ return `${(bytes / (1024 ** i)).toFixed(i ? 1 : 0)}${separator}${sizes[i]}${postFix}`;
535
+ }
536
+ return 'n/a';
29
537
  }
30
538
  }
31
539
 
@@ -17,24 +17,24 @@ class Switch extends CheckBox {
17
17
  */
18
18
  ntype: 'switchfield',
19
19
  /**
20
- * @member {String[]} baseCls=['neo-checkboxfield']
20
+ * @member {String[]} baseCls=['neo-switchfield']
21
21
  */
22
22
  baseCls: ['neo-switchfield'],
23
23
  /**
24
24
  * @member {Object} _vdom
25
25
  */
26
26
  _vdom:
27
- {cn: [
28
- {tag: 'label', cls: ['neo-checkbox-label'], cn: [
29
- {tag: 'span', cls: []},
30
- {tag: 'input', cls: ['neo-checkbox-input']},
31
- {tag: 'i', cls: ['neo-checkbox-icon'], removeDom: true},
32
- {tag: 'span', cls: ['neo-checkbox-value-label']}
33
- ]},
34
- {cls: ['neo-error-wrapper'], removeDom: true, cn: [
35
- {cls: ['neo-error']}
36
- ]}
27
+ {cn: [
28
+ {tag: 'label', cls: ['neo-checkbox-label'], cn: [
29
+ {tag: 'span', cls: []},
30
+ {tag: 'input', cls: ['neo-checkbox-input']},
31
+ {tag: 'i', cls: ['neo-checkbox-icon'], removeDom: true},
32
+ {tag: 'span', cls: ['neo-checkbox-value-label']}
33
+ ]},
34
+ {cls: ['neo-error-wrapper'], removeDom: true, cn: [
35
+ {cls: ['neo-error']}
37
36
  ]}
37
+ ]}
38
38
  }
39
39
  }
40
40