@jseeio/jsee 0.3.7 → 0.3.9
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/.claude/settings.local.json +12 -0
- package/.eslintrc.js +38 -0
- package/AGENTS.md +38 -0
- package/CHANGELOG.md +86 -0
- package/CLAUDE.md +5 -0
- package/README.md +60 -42
- package/bin/jsee +1 -1
- package/dist/jsee.js +1 -1
- package/dist/jsee.runtime.js +1 -1
- package/jest-puppeteer.config.js +7 -5
- package/jest.unit.config.js +8 -0
- package/load/index.html +16 -4
- package/package.json +17 -13
- package/src/app.js +35 -11
- package/src/cli.js +591 -330
- package/src/constants.js +12 -0
- package/src/main.js +356 -183
- package/src/utils.js +748 -3
- package/src/worker.js +42 -18
- package/templates/bulma-app.vue +3 -2
- package/templates/bulma-input.vue +23 -18
- package/templates/bulma-output.vue +72 -7
- package/templates/common-inputs.js +2 -13
- package/templates/common-outputs.js +57 -2
- package/templates/file-picker-base.vue +169 -0
- package/templates/file-picker.vue +350 -0
- package/test/fixtures/lodash-like.js +15 -0
- package/test/fixtures/upload-sample.csv +3 -0
- package/test/test-basic.test.js +383 -17
- package/test/test-python.test.js +2 -5
- package/test/unit/cli-fetch.test.js +126 -0
- package/test/unit/utils.test.js +806 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :id="id" class="vfp">
|
|
3
|
+
<!-- Based on https://github.com/rowanwins/vue-file-picker/ -->
|
|
4
|
+
<div
|
|
5
|
+
class="vfp-bgArea"
|
|
6
|
+
:class="{ 'vfp-active': isActive }"
|
|
7
|
+
@dragover="setActive"
|
|
8
|
+
@dragleave="cancelActive"
|
|
9
|
+
@drop="fileAdded"
|
|
10
|
+
>
|
|
11
|
+
<!-- icon -->
|
|
12
|
+
<div class="vfp-iconHolder vfp-gridItem"
|
|
13
|
+
v-if="!showUrlInputAuto"
|
|
14
|
+
>
|
|
15
|
+
<slot name="icon">
|
|
16
|
+
<svg height="40" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
|
17
|
+
<path
|
|
18
|
+
d="M18 32h12v-12h8l-14-14-14 14h8zm-8 4h28v4h-28z"
|
|
19
|
+
fill="#CACFD2"
|
|
20
|
+
/>
|
|
21
|
+
</svg>
|
|
22
|
+
</slot>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- invisible native file input -->
|
|
26
|
+
<input
|
|
27
|
+
id="vfp-filePicker"
|
|
28
|
+
class="vfp-inputfile vfp-gridItem"
|
|
29
|
+
type="file"
|
|
30
|
+
name="vfp-filePicker"
|
|
31
|
+
:accept="accept"
|
|
32
|
+
:multiple="allowMultiple"
|
|
33
|
+
@change="fileAdded"
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<!-- main label OR url input (switches) -->
|
|
37
|
+
<label
|
|
38
|
+
v-if="!showUrlInputAuto"
|
|
39
|
+
class="vfp-label vfp-gridItem vfp-clickable"
|
|
40
|
+
for="vfp-filePicker"
|
|
41
|
+
>
|
|
42
|
+
<slot name="label">
|
|
43
|
+
<strong>{{ label }}</strong>
|
|
44
|
+
</slot>
|
|
45
|
+
<span class="vfp-info">{{ info }}</span>
|
|
46
|
+
</label>
|
|
47
|
+
|
|
48
|
+
<!-- secondary toggle -->
|
|
49
|
+
<div
|
|
50
|
+
v-if="!showUrlInputAuto"
|
|
51
|
+
>
|
|
52
|
+
<!-- Trigger File input -->
|
|
53
|
+
<button
|
|
54
|
+
class="vfp-button vfp-gridItem"
|
|
55
|
+
@click="activateFileDialog"
|
|
56
|
+
style="margin-right:5px"
|
|
57
|
+
>
|
|
58
|
+
From Disk
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<button
|
|
62
|
+
v-if="!showUrlInputAuto"
|
|
63
|
+
class="vfp-button vfp-gridItem"
|
|
64
|
+
@click="activateUrl"
|
|
65
|
+
>
|
|
66
|
+
From URL
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- v-model.trim="urlInput" -->
|
|
71
|
+
<div v-if="showUrlInputAuto">
|
|
72
|
+
<input
|
|
73
|
+
class="vfp-urlInput vfp-gridItem"
|
|
74
|
+
type="text"
|
|
75
|
+
v-model.trim="urlModel"
|
|
76
|
+
placeholder="Paste file URL here"
|
|
77
|
+
:style="{
|
|
78
|
+
borderColor: urlSuccess ? 'green' : urlError ? 'red' : ''
|
|
79
|
+
}"
|
|
80
|
+
/>
|
|
81
|
+
<button
|
|
82
|
+
class="vfp-urlInput vfp-gridItem vfp-button"
|
|
83
|
+
@click="loadUrl"
|
|
84
|
+
>
|
|
85
|
+
Load
|
|
86
|
+
</button>
|
|
87
|
+
<button
|
|
88
|
+
class="vfp-urlInput vfp-gridItem vfp-button"
|
|
89
|
+
@click="clearUrl"
|
|
90
|
+
>
|
|
91
|
+
Cancel
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
100
|
+
<script>
|
|
101
|
+
export default {
|
|
102
|
+
name: 'FilePicker',
|
|
103
|
+
props: {
|
|
104
|
+
id: { type: String, default: 'filePicker' },
|
|
105
|
+
accept: { type: String, default: '*/*' },
|
|
106
|
+
allowMultiple: { type: Boolean, default: false },
|
|
107
|
+
modelValue: { type: [String, Object], default: '' },
|
|
108
|
+
url: { type: String, default: '' },
|
|
109
|
+
raw: { type: Boolean, default: false },
|
|
110
|
+
autoload: { type: Boolean, default: false },
|
|
111
|
+
labelValue: { type: String, default: 'Choose File' }
|
|
112
|
+
},
|
|
113
|
+
emits : ['update:modelValue', 'update:url', 'change'],
|
|
114
|
+
data () {
|
|
115
|
+
return {
|
|
116
|
+
isActive: false,
|
|
117
|
+
urlInput: '',
|
|
118
|
+
urlSuccess: false,
|
|
119
|
+
urlError: false,
|
|
120
|
+
label: this.labelValue,
|
|
121
|
+
info: 'or drag and drop it here',
|
|
122
|
+
showUrlInput: true,
|
|
123
|
+
urlAutoLoadHandled: false
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
computed: {
|
|
127
|
+
showUrlInputAuto () {
|
|
128
|
+
return this.url.length > 0 && this.showUrlInput
|
|
129
|
+
},
|
|
130
|
+
requiresTypeCheck () { return this.accept !== '*/*' },
|
|
131
|
+
acceptedTypes () { return this.accept.split(',') },
|
|
132
|
+
urlModel: {
|
|
133
|
+
get () { return this.url },
|
|
134
|
+
set (v) { this.$emit('update:url', v) }
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
watch: {
|
|
138
|
+
autoload (value) {
|
|
139
|
+
if (!value) {
|
|
140
|
+
this.urlAutoLoadHandled = false
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
this.tryAutoLoadUrl()
|
|
144
|
+
},
|
|
145
|
+
url (value) {
|
|
146
|
+
if (!value) {
|
|
147
|
+
this.urlAutoLoadHandled = false
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
this.tryAutoLoadUrl()
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
mounted () {
|
|
154
|
+
this.tryAutoLoadUrl()
|
|
155
|
+
},
|
|
156
|
+
methods: {
|
|
157
|
+
/* drag-drop handling */
|
|
158
|
+
cancelHandlers (e) { e.preventDefault(); e.stopPropagation() },
|
|
159
|
+
setActive (e) { this.isActive = true; this.cancelHandlers(e) },
|
|
160
|
+
cancelActive (e) { this.isActive = false; this.cancelHandlers(e) },
|
|
161
|
+
|
|
162
|
+
fileAdded (e) {
|
|
163
|
+
this.isActive = false;
|
|
164
|
+
this.cancelHandlers(e);
|
|
165
|
+
|
|
166
|
+
const wasDropped = !!e.dataTransfer;
|
|
167
|
+
if (wasDropped && this.urlModel && this.urlModel.length > 0) {
|
|
168
|
+
console.log('[File picker] URL mode active, ignoring dropped files');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const files = wasDropped ? e.dataTransfer.files : e.target.files;
|
|
172
|
+
this.label = Array.from(files).map(f => f.name).join(', ');
|
|
173
|
+
const totalSizeinKB = Array.from(files).reduce((acc, f) => acc + f.size / 1024, 0).toFixed(2)
|
|
174
|
+
this.info = `Selected ${files.length} file(s) of size ${totalSizeinKB} KB`;
|
|
175
|
+
|
|
176
|
+
if (wasDropped && !this.allowMultiple && files.length > 1)
|
|
177
|
+
throw new Error('vue-file-picker: Multiple files are not allowed');
|
|
178
|
+
if (wasDropped && this.requiresTypeCheck)
|
|
179
|
+
for (const f of files)
|
|
180
|
+
if (!this.acceptedTypes.includes(f.type))
|
|
181
|
+
throw new Error('vue-file-picker: File type not allowed');
|
|
182
|
+
|
|
183
|
+
console.log('[File picker] Files added:', files);
|
|
184
|
+
this.loadFile(files);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
loadFile (e) {
|
|
188
|
+
const files = e.target ? e.target.files : e
|
|
189
|
+
if (this.raw) {
|
|
190
|
+
const fileValue = this.allowMultiple ? Array.from(files) : files[0]
|
|
191
|
+
this.$emit('update:modelValue', fileValue)
|
|
192
|
+
this.$emit('change')
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
const reader = new FileReader()
|
|
196
|
+
reader.readAsText(files[0])
|
|
197
|
+
reader.onload = () => {
|
|
198
|
+
// No need to check for reactivity here, as the parent component will handle it
|
|
199
|
+
// Just trigger basic change event
|
|
200
|
+
this.$emit('update:modelValue', reader.result)
|
|
201
|
+
this.$emit('change')
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
loadUrl () {
|
|
206
|
+
if (this.urlModel && this.urlModel.length > 0) {
|
|
207
|
+
if (this.raw) {
|
|
208
|
+
this.urlSuccess = true
|
|
209
|
+
this.urlError = false
|
|
210
|
+
this.label = this.urlModel.split('/').pop().split('?')[0] || 'File from URL'
|
|
211
|
+
this.rawUrl = this.urlModel.split('/').slice(0, -1).join('/') // Extract URL path
|
|
212
|
+
this.showUrlInput = false // Hide URL input after successful load
|
|
213
|
+
this.info = `Loaded URL handle: ${this.rawUrl || this.urlModel}`
|
|
214
|
+
this.$emit('update:modelValue', { kind: 'url', url: this.urlModel })
|
|
215
|
+
this.$emit('change')
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fetch(this.urlModel)
|
|
220
|
+
.then(response => response.text())
|
|
221
|
+
.then(text => {
|
|
222
|
+
this.urlSuccess = true
|
|
223
|
+
this.urlError = false
|
|
224
|
+
this.label = this.urlModel.split('/').pop().split('?')[0] || 'File from URL'
|
|
225
|
+
this.rawUrl = this.urlModel.split('/').slice(0, -1).join('/') // Extract URL path
|
|
226
|
+
this.showUrlInput = false // Hide URL input after successful load
|
|
227
|
+
// Show file size
|
|
228
|
+
this.info = `Loaded from URL: ${this.rawUrl} (${(text.length / 1024).toFixed(2)} KB)`
|
|
229
|
+
this.$emit('update:modelValue', text)
|
|
230
|
+
this.$emit('change')
|
|
231
|
+
})
|
|
232
|
+
.catch(error => {
|
|
233
|
+
this.urlSuccess = false
|
|
234
|
+
this.urlError = true
|
|
235
|
+
console.error('Error fetching URL:', error)
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
tryAutoLoadUrl () {
|
|
241
|
+
if (!this.autoload || this.urlAutoLoadHandled) {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
if (!(this.urlModel && this.urlModel.length > 0)) {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
this.urlAutoLoadHandled = true
|
|
248
|
+
this.loadUrl()
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/* switch to URL mode */
|
|
252
|
+
activateUrl () {
|
|
253
|
+
this.urlModel = this.urlModel || 'http://'
|
|
254
|
+
this.showUrlInput = true
|
|
255
|
+
this.$nextTick(() => this.$el.querySelector('.vfp-urlInput').focus())
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
activateFileDialog () {
|
|
259
|
+
this.$el.querySelector('#vfp-filePicker').click()
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
clearUrl () {
|
|
263
|
+
this.showUrlInput = false
|
|
264
|
+
// this.urlModel = ''
|
|
265
|
+
// this.urlSuccess = false
|
|
266
|
+
// this.urlError = false
|
|
267
|
+
// this.$emit('update:url', '')
|
|
268
|
+
// this.$emit('update:modelValue', '')
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
</script>
|
|
273
|
+
|
|
274
|
+
<style lang="scss">
|
|
275
|
+
.vfp {
|
|
276
|
+
display: flex;
|
|
277
|
+
min-height: 130px;
|
|
278
|
+
width: 100%;
|
|
279
|
+
|
|
280
|
+
.vfp-bgArea {
|
|
281
|
+
transition: 0.3s;
|
|
282
|
+
background: #F2F3F4;
|
|
283
|
+
display: grid;
|
|
284
|
+
grid-template-rows: 40% 32% 28%;
|
|
285
|
+
padding: 20px 10px;
|
|
286
|
+
width: 100%;
|
|
287
|
+
outline: 1px dashed #CACFD2;
|
|
288
|
+
outline-offset: -10px;
|
|
289
|
+
color: #3b3e40;
|
|
290
|
+
text-align: center;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.vfp-inputfile {
|
|
294
|
+
width: 0.1px;
|
|
295
|
+
height: 0.1px;
|
|
296
|
+
opacity: 0;
|
|
297
|
+
position: absolute;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.vfp-gridItem { align-self: center; justify-self: center; }
|
|
301
|
+
|
|
302
|
+
.vfp-label { cursor: pointer; font-size: 0.9rem; font-family: monospace; display:block; }
|
|
303
|
+
.vfp-info {
|
|
304
|
+
font-size: 9px;
|
|
305
|
+
color: #7f8c8d;
|
|
306
|
+
margin-top: 1px;
|
|
307
|
+
margin-bottom: 5px;
|
|
308
|
+
display: block;
|
|
309
|
+
}
|
|
310
|
+
.vfp-urlToggle { cursor: pointer; font-size: 0.8rem; }
|
|
311
|
+
.vfp-urlInput {
|
|
312
|
+
margin-top: 10px;
|
|
313
|
+
width: calc(100% - 20px);
|
|
314
|
+
padding: 6px 6px !important;
|
|
315
|
+
border: 1px solid #CACFD2;
|
|
316
|
+
border-radius: 4px;
|
|
317
|
+
font-size: 0.9rem;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.vfp-button {
|
|
321
|
+
cursor: pointer;
|
|
322
|
+
border: none;
|
|
323
|
+
padding: 4px 10px;
|
|
324
|
+
border-radius: 4px;
|
|
325
|
+
|
|
326
|
+
// hover effect
|
|
327
|
+
background-color: #E4E7EA;
|
|
328
|
+
&:hover {
|
|
329
|
+
background-color: #D7DBDD;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
button.vfp-urlInput {
|
|
334
|
+
margin-top: 2px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.vfp-active {
|
|
338
|
+
background-color: #D7DBDD;
|
|
339
|
+
outline-color: #F2F3F4;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@media only screen and (max-width: 440px) {
|
|
343
|
+
.vfp-bgArea {
|
|
344
|
+
padding: 18px 10px;
|
|
345
|
+
grid-template-rows: 45% 25% 30%;
|
|
346
|
+
grid-row-gap: 5px;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
(function (root) {
|
|
2
|
+
function kebabCase (input) {
|
|
3
|
+
if (typeof input !== 'string') {
|
|
4
|
+
return ''
|
|
5
|
+
}
|
|
6
|
+
return input
|
|
7
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
8
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
9
|
+
.replace(/^-+|-+$/g, '')
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
root._ = root._ || {}
|
|
14
|
+
root._.kebabCase = kebabCase
|
|
15
|
+
})(typeof self !== 'undefined' ? self : window)
|