neo.mjs 5.15.4 → 5.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/ServiceWorker.mjs +2 -2
- package/buildScripts/docs/jsdocx.mjs +8 -6
- package/docs/app/view/classdetails/HeaderComponent.mjs +3 -3
- package/docs/app/view/classdetails/MembersList.mjs +5 -5
- package/examples/ServiceWorker.mjs +2 -2
- package/examples/form/field/fileupload/MainContainer.mjs +68 -10
- package/examples/form/field/fileupload/README.md +9 -0
- package/examples/form/field/fileupload/server.mjs +49 -0
- package/package.json +2 -1
- package/resources/scss/src/apps/docs/classdetails/HeaderComponent.scss +3 -1
- package/resources/scss/src/form/field/FileUpload.scss +248 -2
- package/resources/scss/theme-dark/form/field/FileUpload.scss +20 -4
- package/resources/scss/theme-light/form/field/FileUpload.scss +20 -4
- package/src/DefaultConfig.mjs +2 -2
- package/src/form/field/FileUpload.mjs +512 -4
- package/src/main/addon/Markdown.mjs +2 -2
- package/src/main/addon/ResizeObserver.mjs +79 -0
- package/src/manager/DomEvent.mjs +1 -0
package/apps/ServiceWorker.mjs
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
import fs
|
2
|
-
import helper
|
3
|
-
import jsdocx
|
4
|
-
import path
|
1
|
+
import fs from 'fs-extra';
|
2
|
+
import helper from 'neo-jsdoc-x/src/lib/helper.js';
|
3
|
+
import jsdocx from 'neo-jsdoc-x';
|
4
|
+
import path from 'path';
|
5
|
+
import showdown from 'showdown';
|
5
6
|
|
6
7
|
const __dirname = path.resolve(),
|
8
|
+
markdown = new showdown.Converter(),
|
7
9
|
cwd = process.cwd(),
|
8
10
|
requireJson = path => JSON.parse(fs.readFileSync((path))),
|
9
11
|
packageJson = requireJson(path.resolve(cwd, 'package.json')),
|
@@ -324,12 +326,12 @@ jsdocx.parse(options)
|
|
324
326
|
namespace.classData.push(item);
|
325
327
|
|
326
328
|
if (item.description) {
|
327
|
-
item.description = item.description
|
329
|
+
item.description = markdown.makeHtml(item.description)
|
328
330
|
}
|
329
331
|
|
330
332
|
item.params?.forEach(param => {
|
331
333
|
if (param.description) {
|
332
|
-
param.description = param.description
|
334
|
+
param.description = markdown.makeHtml(param.description)
|
333
335
|
}
|
334
336
|
});
|
335
337
|
|
@@ -50,7 +50,7 @@ class MembersList extends Base {
|
|
50
50
|
* @member {Object} _vdom={cn: []}
|
51
51
|
*/
|
52
52
|
_vdom:
|
53
|
-
|
53
|
+
{cn: []}
|
54
54
|
}
|
55
55
|
|
56
56
|
/**
|
@@ -329,9 +329,9 @@ class MembersList extends Base {
|
|
329
329
|
|
330
330
|
me.update();
|
331
331
|
|
332
|
-
|
332
|
+
setTimeout(() => {
|
333
333
|
Neo.main.addon.HighlightJS.syntaxHighlightInit();
|
334
|
-
}, 100)
|
334
|
+
}, 100)
|
335
335
|
}
|
336
336
|
|
337
337
|
/**
|
@@ -412,7 +412,7 @@ class MembersList extends Base {
|
|
412
412
|
cn : [{
|
413
413
|
innerHTML: description.innerHTML
|
414
414
|
},
|
415
|
-
|
415
|
+
MembersList.createParametersTable(nestedParams)]
|
416
416
|
}
|
417
417
|
}
|
418
418
|
|
@@ -425,7 +425,7 @@ class MembersList extends Base {
|
|
425
425
|
tag : 'td',
|
426
426
|
innerHTML: param.type ? MembersList.escapeHtml(param.type.names.join(' | ')) : ''
|
427
427
|
},
|
428
|
-
|
428
|
+
description]
|
429
429
|
});
|
430
430
|
|
431
431
|
if (hasDefaultValues) {
|
@@ -1,9 +1,7 @@
|
|
1
|
-
import CheckBox from '../../../../src/form/field/CheckBox.mjs';
|
2
1
|
import ConfigurationViewport from '../../../ConfigurationViewport.mjs';
|
3
2
|
import FileUploadField from '../../../../src/form/field/FileUpload.mjs';
|
4
3
|
import NumberField from '../../../../src/form/field/Number.mjs';
|
5
|
-
import
|
6
|
-
import TextField from '../../../../src/form/field/Text.mjs';
|
4
|
+
import Panel from '../../../../src/container/Panel.mjs';
|
7
5
|
|
8
6
|
/**
|
9
7
|
* @class Neo.examples.form.field.text.MainContainer
|
@@ -24,19 +22,79 @@ class MainContainer extends ConfigurationViewport {
|
|
24
22
|
module : NumberField,
|
25
23
|
labelText: 'width',
|
26
24
|
listeners: {change: me.onConfigChange.bind(me, 'width')},
|
27
|
-
maxValue :
|
28
|
-
minValue :
|
25
|
+
maxValue : 350,
|
26
|
+
minValue : 200,
|
29
27
|
stepSize : 5,
|
30
28
|
style : {marginTop: '10px'},
|
31
29
|
value : me.exampleComponent.width
|
32
|
-
}]
|
30
|
+
}];
|
33
31
|
}
|
34
32
|
|
35
33
|
createExampleComponent() {
|
36
|
-
return Neo.create(
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
return Neo.create(Panel, {
|
35
|
+
style : 'padding:1em',
|
36
|
+
items : [{
|
37
|
+
module : FileUploadField,
|
38
|
+
id : 'my-downloadable-test',
|
39
|
+
uploadUrl : 'http://127.0.0.1:3000/file-upload-test',
|
40
|
+
documentStatusUrl : 'http://127.0.0.1:3000/document-status-downloadable',
|
41
|
+
documentDeleteUrl : 'http://127.0.0.1:3000/document-delete',
|
42
|
+
downloadUrl : 'http://127.0.0.1:3000/getDocument',
|
43
|
+
width : 350,
|
44
|
+
maxSize : '10mb',
|
45
|
+
types : {
|
46
|
+
png : 1,
|
47
|
+
jpg : 1,
|
48
|
+
xls : 1,
|
49
|
+
pdf : 1
|
50
|
+
}
|
51
|
+
}, {
|
52
|
+
module : FileUploadField,
|
53
|
+
id : 'my-not-downloadable-test',
|
54
|
+
uploadUrl : 'http://127.0.0.1:3000/file-upload-test',
|
55
|
+
documentStatusUrl : 'http://127.0.0.1:3000/document-status-not-downloadable',
|
56
|
+
documentDeleteUrl : 'http://127.0.0.1:3000/document-delete',
|
57
|
+
downloadUrl : 'http://127.0.0.1:3000/getDocument',
|
58
|
+
width : 350,
|
59
|
+
maxSize : '10mb',
|
60
|
+
types : {
|
61
|
+
png : 1,
|
62
|
+
jpg : 1,
|
63
|
+
xls : 1,
|
64
|
+
pdf : 1
|
65
|
+
}
|
66
|
+
}, {
|
67
|
+
module : FileUploadField,
|
68
|
+
id : 'my-upload-fail-test',
|
69
|
+
uploadUrl : 'http://127.0.0.1:3000/file-upload-test-fail',
|
70
|
+
documentStatusUrl : 'http://127.0.0.1:3000/document-status',
|
71
|
+
documentDeleteUrl : 'http://127.0.0.1:3000/document-delete',
|
72
|
+
downloadUrl : 'http://127.0.0.1:3000/getDocument',
|
73
|
+
width : 350,
|
74
|
+
maxSize : '10mb',
|
75
|
+
types : {
|
76
|
+
png : 1,
|
77
|
+
jpg : 1,
|
78
|
+
xls : 1,
|
79
|
+
pdf : 1
|
80
|
+
}
|
81
|
+
}, {
|
82
|
+
module : FileUploadField,
|
83
|
+
id : 'my-scan-fail-test',
|
84
|
+
uploadUrl : 'http://127.0.0.1:3000/file-upload-test',
|
85
|
+
documentStatusUrl : 'http://127.0.0.1:3000/document-status-fail',
|
86
|
+
documentDeleteUrl : 'http://127.0.0.1:3000/document-delete',
|
87
|
+
downloadUrl : 'http://127.0.0.1:3000/getDocument',
|
88
|
+
width : 350,
|
89
|
+
maxSize : '10mb',
|
90
|
+
types : {
|
91
|
+
png : 1,
|
92
|
+
jpg : 1,
|
93
|
+
xls : 1,
|
94
|
+
pdf : 1
|
95
|
+
}
|
96
|
+
}]
|
97
|
+
});
|
40
98
|
}
|
41
99
|
}
|
42
100
|
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import express from "express";
|
2
|
+
import cors from "cors"
|
3
|
+
|
4
|
+
const app = express();
|
5
|
+
const port = 3000;
|
6
|
+
|
7
|
+
app.use(cors());
|
8
|
+
|
9
|
+
app.post('/file-upload-test', async (req, res) => {
|
10
|
+
|
11
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
12
|
+
|
13
|
+
res.set('Content-Type', 'application/json');
|
14
|
+
res.send('{"success":true,"documentId":"1"}');
|
15
|
+
})
|
16
|
+
|
17
|
+
app.post('/file-upload-test-fail', async (req, res) => {
|
18
|
+
res.set('Content-Type', 'application/json');
|
19
|
+
res.send('{"success":false,"message":"Something went wrong"}');
|
20
|
+
});
|
21
|
+
|
22
|
+
app.get('/document-status', async(req, res) => {
|
23
|
+
res.set('Content-Type', 'application/json');
|
24
|
+
res.send('{"status":"scanning"}');
|
25
|
+
});
|
26
|
+
|
27
|
+
app.get('/document-delete', async(req, res) => {
|
28
|
+
res.set('Content-Type', 'application/json');
|
29
|
+
res.send('');
|
30
|
+
});
|
31
|
+
|
32
|
+
app.get('/document-status-fail', async(req, res) => {
|
33
|
+
res.set('Content-Type', 'application/json');
|
34
|
+
res.send('{"status":"scan-failed"}');
|
35
|
+
});
|
36
|
+
|
37
|
+
app.get('/document-status-downloadable', async(req, res) => {
|
38
|
+
res.set('Content-Type', 'application/json');
|
39
|
+
res.send('{"status":"downloadable"}');
|
40
|
+
});
|
41
|
+
|
42
|
+
app.get('/document-status-not-downloadable', async(req, res) => {
|
43
|
+
res.set('Content-Type', 'application/json');
|
44
|
+
res.send('{"status":"not-downloadable"}');
|
45
|
+
});
|
46
|
+
|
47
|
+
app.listen(port, () => {
|
48
|
+
console.log(`Example app listening on port ${port}`)
|
49
|
+
});
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "neo.mjs",
|
3
|
-
"version": "5.
|
3
|
+
"version": "5.16.0",
|
4
4
|
"description": "The webworkers driven UI framework",
|
5
5
|
"type": "module",
|
6
6
|
"repository": {
|
@@ -57,6 +57,7 @@
|
|
57
57
|
"neo-jsdoc-x": "1.0.5",
|
58
58
|
"postcss": "^8.4.27",
|
59
59
|
"sass": "^1.65.1",
|
60
|
+
"showdown": "^2.1.0",
|
60
61
|
"webpack": "^5.88.2",
|
61
62
|
"webpack-cli": "^5.1.4",
|
62
63
|
"webpack-dev-server": "4.15.1",
|
@@ -9,6 +9,8 @@
|
|
9
9
|
|
10
10
|
.neo-docs-classdetails-headercomponent {
|
11
11
|
background-color: v(docs-classdetails-headercomponent-background-color);
|
12
|
+
overflow-y : auto;
|
13
|
+
max-height : 250px;
|
12
14
|
padding : 15px;
|
13
15
|
|
14
16
|
.neo-docs-header-description {
|
@@ -23,4 +25,4 @@
|
|
23
25
|
font-weight : 800;
|
24
26
|
text-shadow : 2px 2px 3px rgba(200,200,200,0.1);
|
25
27
|
}
|
26
|
-
}
|
28
|
+
}
|
@@ -1,4 +1,250 @@
|
|
1
1
|
.neo-file-upload-field {
|
2
|
-
background-color: v(fileuploadfield-background-color);
|
3
|
-
color
|
2
|
+
background-color : v(fileuploadfield-background-color);
|
3
|
+
border-color : v(fileuploadfield-border-color);
|
4
|
+
color : v(fileuploadfield-color);
|
5
|
+
}
|
6
|
+
|
7
|
+
.neo-file-upload-field {
|
8
|
+
--upload-icon-codepoint : "\f062";
|
9
|
+
--cancel-icon-codepoint : "\f00d";
|
10
|
+
--delete-icon-codepoint : "\f014";
|
11
|
+
--success-icon-codepoint : "\f00c";
|
12
|
+
--download-icon-codepoint : "\f019";
|
13
|
+
|
14
|
+
min-height : 3.5rem;
|
15
|
+
position : relative;
|
16
|
+
display : flex;
|
17
|
+
align-items : center;
|
18
|
+
padding : 0.5rem;
|
19
|
+
gap : 0.5rem;
|
20
|
+
border : 1px solid var(--fileuploadfield-border-color);
|
21
|
+
border-radius : 2px;
|
22
|
+
|
23
|
+
// Space for any error message
|
24
|
+
margin-bottom : 2.5rem;
|
25
|
+
|
26
|
+
&:focus-within {
|
27
|
+
&::after {
|
28
|
+
content : "";
|
29
|
+
pointer-events : none;
|
30
|
+
position : absolute;
|
31
|
+
top : -2px;
|
32
|
+
right : -2px;
|
33
|
+
bottom : -2px;
|
34
|
+
left : -2px;
|
35
|
+
border : 1px solid var(--fileuploadfield-focus-color);
|
36
|
+
border-radius : 3px;
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
// Icons
|
41
|
+
i, button {
|
42
|
+
font-style : normal;
|
43
|
+
font-size : 18px;
|
44
|
+
flex : 0 0 2rem;
|
45
|
+
height : 2rem;
|
46
|
+
border-radius : 50%;
|
47
|
+
font-family : var(--fa-style-family,"Font Awesome 6 Free");
|
48
|
+
display : flex;
|
49
|
+
align-items : center;
|
50
|
+
justify-content : center;
|
51
|
+
border : 0 none;
|
52
|
+
background : transparent;
|
53
|
+
color : inherit;
|
54
|
+
|
55
|
+
&::after {
|
56
|
+
font-weight : var(--fa-style, 900);
|
57
|
+
display : flex;
|
58
|
+
align-items : center;
|
59
|
+
justify-content : center;
|
60
|
+
aspect-ratio : 1;
|
61
|
+
border-radius : 50%;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
// Cursor appearance is invitation to click
|
66
|
+
.neo-file-upload-action-button {
|
67
|
+
cursor : pointer;
|
68
|
+
&:hover {
|
69
|
+
background-color: var(--fileuploadfield-hover-color);
|
70
|
+
}
|
71
|
+
&:active {
|
72
|
+
background-color: var(--fileuploadfield-pressed-color);
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
.neo-file-upload-filename {
|
77
|
+
font-weight : bold;
|
78
|
+
}
|
79
|
+
|
80
|
+
.neo-file-upload-filename, .neo-file-upload-state {
|
81
|
+
white-space : nowrap;
|
82
|
+
overflow : hidden;
|
83
|
+
text-overflow : ellipsis;
|
84
|
+
color : inherit; // For when it becomes a link
|
85
|
+
}
|
86
|
+
|
87
|
+
// The file input is only visible in the ready state
|
88
|
+
&:not(.neo-file-upload-state-ready) {
|
89
|
+
input[type="file"] {
|
90
|
+
display : none;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
&.neo-invalid {
|
95
|
+
.neo-file-upload-error-message {
|
96
|
+
display : initial;
|
97
|
+
}
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
// During upload, it's a progress circle.
|
102
|
+
// Of the upload fails, the process circle stops but remains visible
|
103
|
+
.neo-file-upload-state-uploading, .neo-file-upload-state-upload-failed {
|
104
|
+
.neo-file-upload-state-icon {
|
105
|
+
background-image : conic-gradient(
|
106
|
+
var(--fileuploadfield-progress-color) 0 var(--upload-progress),
|
107
|
+
transparent var(--upload-progress) 1turn
|
108
|
+
);
|
109
|
+
|
110
|
+
&::after {
|
111
|
+
background-color : var(--fileuploadfield-background-color);
|
112
|
+
content : var(--upload-icon-codepoint);
|
113
|
+
flex : 0 0 calc(100% - 6px);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
.neo-file-upload-state-processing {
|
119
|
+
.neo-file-upload-state-icon {
|
120
|
+
background-image : conic-gradient(
|
121
|
+
from 0deg, transparent 0deg,
|
122
|
+
var(--fileuploadfield-progress-color) 180deg,
|
123
|
+
transparent 180deg
|
124
|
+
);
|
125
|
+
animation: spinner-rotation 3s linear infinite;
|
126
|
+
|
127
|
+
&::after {
|
128
|
+
content : "";
|
129
|
+
flex : 0 0 calc(100% - 6px);
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
// While uploading and scanning, we can abort the whole upload/scan process
|
135
|
+
.neo-file-upload-state-uploading, .neo-file-upload-state-processing {
|
136
|
+
.neo-file-upload-action-button {
|
137
|
+
&::after {
|
138
|
+
content : var(--cancel-icon-codepoint);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
// If the upload or scan failed, we show an error UI and the action button cancels
|
144
|
+
.neo-file-upload-state-upload-failed, .neo-file-upload-state-scan-failed {
|
145
|
+
--fileuploadfield-progress-color : var(--fileuploadfield-error-color);
|
146
|
+
border-color : var(--fileuploadfield-error-color);
|
147
|
+
|
148
|
+
.neo-file-upload-state-icon, .neo-file-upload-action-button {
|
149
|
+
&::after {
|
150
|
+
content : var(--cancel-icon-codepoint );
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
.neo-file-upload-state-icon, .neo-file-upload-state {
|
155
|
+
color : var(--fileuploadfield-error-color);
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
.neo-file-upload-state-scan-failed {
|
160
|
+
.neo-file-upload-state-icon {
|
161
|
+
border : 3px solid var(--fileuploadfield-error-color);
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
.neo-file-upload-state-not-downloadable {
|
166
|
+
.neo-file-upload-state-icon {
|
167
|
+
color : var(--fileuploadfield-success-color);
|
168
|
+
border : 3px solid var(--fileuploadfield-success-color);
|
169
|
+
|
170
|
+
&::after {
|
171
|
+
content : var(--success-icon-codepoint);
|
172
|
+
}
|
173
|
+
}
|
174
|
+
.neo-file-upload-action-button {
|
175
|
+
&::after {
|
176
|
+
content : var(--delete-icon-codepoint);
|
177
|
+
}
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
.neo-file-upload-state-downloadable {
|
182
|
+
&:has(.neo-file-upload-filename:hover) {
|
183
|
+
background-color: var(--fileuploadfield-hover-color);
|
184
|
+
}
|
185
|
+
&:has(.neo-file-upload-filename:active) {
|
186
|
+
background-color: var(--fileuploadfield-pressed-color);
|
187
|
+
}
|
188
|
+
.neo-file-upload-state-icon {
|
189
|
+
background-color : var(--fileuploadfield-downloadable-state-color);
|
190
|
+
border : 0 none;
|
191
|
+
|
192
|
+
&::after {
|
193
|
+
content : var(--download-icon-codepoint);
|
194
|
+
background-color: transparent;
|
195
|
+
}
|
196
|
+
}
|
197
|
+
.neo-file-upload-action-button {
|
198
|
+
&::after {
|
199
|
+
content : var(--delete-icon-codepoint);
|
200
|
+
}
|
201
|
+
}
|
202
|
+
}
|
203
|
+
|
204
|
+
.neo-file-upload-state-ready {
|
205
|
+
// Only the input field is visible when in ready state
|
206
|
+
// It takes up the whole component, and is the only interactive item
|
207
|
+
:not(input[type="file"]) {
|
208
|
+
display : none;
|
209
|
+
}
|
210
|
+
input::file-selector-button {
|
211
|
+
position : absolute;
|
212
|
+
border : 0 none;
|
213
|
+
margin : 0;
|
214
|
+
top : 0;
|
215
|
+
left : 0;
|
216
|
+
right : 0;
|
217
|
+
bottom : 0;
|
218
|
+
background-color : var(--fileuploadfield-background-color);
|
219
|
+
color : var(--fileuploadfield-color);
|
220
|
+
cursor : pointer;
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
.neo-file-upload-body {
|
225
|
+
flex : 1 1 0%;
|
226
|
+
display : flex;
|
227
|
+
flex-flow : column nowrap;
|
228
|
+
line-height : 1;
|
229
|
+
gap : 0.2rem;
|
230
|
+
overflow : hidden;
|
231
|
+
align-items : flex-start;}
|
232
|
+
|
233
|
+
.neo-file-upload-error-message {
|
234
|
+
display : none;
|
235
|
+
position : absolute;
|
236
|
+
inset-inline-start : 0;
|
237
|
+
white-space : nowrap;
|
238
|
+
top : 100%;
|
239
|
+
padding : 0.5rem;
|
240
|
+
color : var(--fileuploadfield-error-color);
|
241
|
+
}
|
242
|
+
|
243
|
+
@keyframes spinner-rotation {
|
244
|
+
0% {
|
245
|
+
transform: rotate(0deg);
|
246
|
+
}
|
247
|
+
100% {
|
248
|
+
transform: rotate(360deg);
|
249
|
+
}
|
4
250
|
}
|
@@ -1,11 +1,27 @@
|
|
1
1
|
$neoMap: map-merge($neoMap, (
|
2
|
-
'fileuploadfield-background-color':
|
3
|
-
'fileuploadfield-color'
|
2
|
+
'fileuploadfield-background-color' : rgb(62, 51, 51),
|
3
|
+
'fileuploadfield-color' : #fff,
|
4
|
+
'fileuploadfield-border-color' : #d5d5d5,
|
5
|
+
'fileuploadfield-progress-color' : #fff,
|
6
|
+
'fileuploadfield-error-color' : #f00,
|
7
|
+
'fileuploadfield-success-color' : rgb(71, 226, 71),
|
8
|
+
'fileuploadfield-hover-color' : #0f0f0f,
|
9
|
+
'fileuploadfield-pressed-color' : #0e0e0e,
|
10
|
+
'fileuploadfield-downloadable-state-color' : #181818,
|
11
|
+
'fileuploadfield-focus-color' : #80d3f4
|
4
12
|
));
|
5
13
|
|
6
14
|
@if $useCssVars == true {
|
7
15
|
:root .neo-theme-dark { // .neo-fileuploadfield
|
8
|
-
--fileuploadfield-background-color: #{neo(fileuploadfield-background-color)};
|
9
|
-
--fileuploadfield-color
|
16
|
+
--fileuploadfield-background-color : #{neo(fileuploadfield-background-color)};
|
17
|
+
--fileuploadfield-color : #{neo(fileuploadfield-color)};
|
18
|
+
--fileuploadfield-border-color : #{neo(fileuploadfield-border-color)};
|
19
|
+
--fileuploadfield-progress-color : #{neo(fileuploadfield-progress-color)};
|
20
|
+
--fileuploadfield-error-color : #{neo(fileuploadfield-error-color)};
|
21
|
+
--fileuploadfield-success-color : #{neo(fileuploadfield-success-color)};
|
22
|
+
--fileuploadfield-hover-color : #{neo(fileuploadfield-hover-color)};
|
23
|
+
--fileuploadfield-pressed-color : #{neo(fileuploadfield-pressed-color)};
|
24
|
+
--fileuploadfield-downloadable-state-color : #{neo(fileuploadfield-pressed-color)};
|
25
|
+
--fileuploadfield-focus-color : #{neo(fileuploadfield-focus-color)};
|
10
26
|
}
|
11
27
|
}
|
@@ -1,11 +1,27 @@
|
|
1
1
|
$neoMap: map-merge($neoMap, (
|
2
|
-
'fileuploadfield-background-color':
|
3
|
-
'fileuploadfield-color'
|
2
|
+
'fileuploadfield-background-color' : #fff,
|
3
|
+
'fileuploadfield-color' : #000,
|
4
|
+
'fileuploadfield-border-color' : #d5d5d5,
|
5
|
+
'fileuploadfield-progress-color' : #5d94f3,
|
6
|
+
'fileuploadfield-error-color' : #f00,
|
7
|
+
'fileuploadfield-success-color' : rgb(112, 169, 112),
|
8
|
+
'fileuploadfield-hover-color' : #f0f0f0,
|
9
|
+
'fileuploadfield-pressed-color' : #e0e0e0,
|
10
|
+
'fileuploadfield-downloadable-state-color' : #d9d9d9,
|
11
|
+
'fileuploadfield-focus-color' : #80d3f4
|
4
12
|
));
|
5
13
|
|
6
14
|
@if $useCssVars == true {
|
7
15
|
:root .neo-theme-light { // .neo-fileuploadfield
|
8
|
-
--fileuploadfield-background-color: #{neo(fileuploadfield-background-color)};
|
9
|
-
--fileuploadfield-color
|
16
|
+
--fileuploadfield-background-color : #{neo(fileuploadfield-background-color)};
|
17
|
+
--fileuploadfield-color : #{neo(fileuploadfield-color)};
|
18
|
+
--fileuploadfield-border-color : #{neo(fileuploadfield-border-color)};
|
19
|
+
--fileuploadfield-progress-color : #{neo(fileuploadfield-progress-color)};
|
20
|
+
--fileuploadfield-error-color : #{neo(fileuploadfield-error-color)};
|
21
|
+
--fileuploadfield-success-color : #{neo(fileuploadfield-success-color)};
|
22
|
+
--fileuploadfield-hover-color : #{neo(fileuploadfield-hover-color)};
|
23
|
+
--fileuploadfield-pressed-color : #{neo(fileuploadfield-pressed-color)};
|
24
|
+
--fileuploadfield-downloadable-state-color : #{neo(fileuploadfield-pressed-color)};
|
25
|
+
--fileuploadfield-focus-color : #{neo(fileuploadfield-focus-color)};
|
10
26
|
}
|
11
27
|
}
|
package/src/DefaultConfig.mjs
CHANGED
@@ -245,12 +245,12 @@ const DefaultConfig = {
|
|
245
245
|
useVdomWorker: true,
|
246
246
|
/**
|
247
247
|
* buildScripts/injectPackageVersion.mjs will update this value
|
248
|
-
* @default '5.
|
248
|
+
* @default '5.16.0'
|
249
249
|
* @memberOf! module:Neo
|
250
250
|
* @name config.version
|
251
251
|
* @type String
|
252
252
|
*/
|
253
|
-
version: '5.
|
253
|
+
version: '5.16.0'
|
254
254
|
};
|
255
255
|
|
256
256
|
Object.assign(DefaultConfig, {
|
@@ -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[]}
|
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}
|
100
|
+
* @member {Object} _vdom
|
26
101
|
*/
|
27
|
-
_vdom:
|
28
|
-
|
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
|
|
@@ -44,7 +44,7 @@ class Markdown extends Base {
|
|
44
44
|
*/
|
45
45
|
construct(config) {
|
46
46
|
super.construct(config);
|
47
|
-
DomAccess.addScript({src: this.showdownPath})
|
47
|
+
DomAccess.addScript({src: this.showdownPath})
|
48
48
|
}
|
49
49
|
|
50
50
|
/**
|
@@ -55,7 +55,7 @@ class Markdown extends Base {
|
|
55
55
|
markdownToHtml(markdown) {
|
56
56
|
let converter = new showdown.Converter();
|
57
57
|
|
58
|
-
return converter.makeHtml(markdown)
|
58
|
+
return converter.makeHtml(markdown)
|
59
59
|
}
|
60
60
|
}
|
61
61
|
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import Base from '../../core/Base.mjs';
|
2
|
+
import DomAccess from '../DomAccess.mjs'
|
3
|
+
|
4
|
+
/**
|
5
|
+
* @class Neo.main.addon.ResizeObserver
|
6
|
+
* @extends Neo.core.Base
|
7
|
+
* @singleton
|
8
|
+
*/
|
9
|
+
class ResizeObserver extends Base {
|
10
|
+
static config = {
|
11
|
+
/**
|
12
|
+
* @member {String} className='Neo.main.addon.ResizeObserver'
|
13
|
+
* @protected
|
14
|
+
*/
|
15
|
+
className: 'Neo.main.addon.ResizeObserver',
|
16
|
+
/**
|
17
|
+
* @member {#ResizeObserver|null} instance=null
|
18
|
+
* @protected
|
19
|
+
*/
|
20
|
+
instance: null,
|
21
|
+
/**
|
22
|
+
* Remote method access for other workers
|
23
|
+
* @member {Object} remote
|
24
|
+
* @protected
|
25
|
+
*/
|
26
|
+
remote: {
|
27
|
+
app: [
|
28
|
+
'register',
|
29
|
+
'unregister'
|
30
|
+
]
|
31
|
+
},
|
32
|
+
/**
|
33
|
+
* @member {Boolean} singleton=true
|
34
|
+
* @protected
|
35
|
+
*/
|
36
|
+
singleton: true
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* @param {Object} config
|
41
|
+
*/
|
42
|
+
construct(config) {
|
43
|
+
let me = this;
|
44
|
+
|
45
|
+
me.resizeObserver = new ResizeObserver(me.onResize.bind(me))
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Internal callback for the ResizeObserver instance
|
50
|
+
* @param {HTMLElement[]} entries
|
51
|
+
* @param {ResizeObserver} observer
|
52
|
+
* @protected
|
53
|
+
*/
|
54
|
+
onResize(entries, observer) {
|
55
|
+
console.log('onResize', entries)
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* @param {Object} data
|
60
|
+
* @param {String} data.id
|
61
|
+
*/
|
62
|
+
register(data) {
|
63
|
+
this.instance.observe(DomAccess.getElement(data.id))
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* @param {Object} data
|
68
|
+
* @param {String} data.id
|
69
|
+
*/
|
70
|
+
unregister(data) {
|
71
|
+
this.instance.unobserve(DomAccess.getElement(data.id))
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
Neo.applyClassConfig(ResizeObserver);
|
76
|
+
|
77
|
+
let instance = Neo.applyClassConfig(ResizeObserver);
|
78
|
+
|
79
|
+
export default instance;
|