@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
package/src/worker.js
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
const utils = require('./utils')
|
|
2
2
|
|
|
3
3
|
function log () {
|
|
4
|
-
const args = Array.prototype.slice.call(arguments)
|
|
4
|
+
const args = Array.prototype.slice.call(arguments)
|
|
5
5
|
args.unshift('[Worker]')
|
|
6
|
-
postMessage({_status: 'log', _log: args})
|
|
6
|
+
postMessage({ _status: 'log', _log: args })
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function progress (value) {
|
|
10
|
-
postMessage({_status: 'progress', _progress: value})
|
|
10
|
+
postMessage({ _status: 'progress', _progress: value })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let initialized = false
|
|
14
|
+
let cancelled = false
|
|
15
|
+
let streamInputConfig = {}
|
|
16
|
+
|
|
17
|
+
function isCancelled () {
|
|
18
|
+
return cancelled === true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getStreamOptions () {
|
|
22
|
+
return {
|
|
23
|
+
isCancelled,
|
|
24
|
+
onProgress: progress
|
|
25
|
+
}
|
|
11
26
|
}
|
|
12
27
|
|
|
13
28
|
function initTF (model) {
|
|
@@ -15,7 +30,7 @@ function initTF (model) {
|
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
async function initPython (model) {
|
|
18
|
-
importScripts(
|
|
33
|
+
importScripts('https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js')
|
|
19
34
|
const pyodide = await loadPyodide()
|
|
20
35
|
if (model.imports && Array.isArray(model.imports) && model.imports.length) {
|
|
21
36
|
await pyodide.loadPackage(model.imports.map(i => i.url))
|
|
@@ -26,7 +41,7 @@ async function initPython (model) {
|
|
|
26
41
|
for (let key in data) {
|
|
27
42
|
self[key] = data[key]
|
|
28
43
|
}
|
|
29
|
-
return await pyodide.runPythonAsync(model.code)
|
|
44
|
+
return await pyodide.runPythonAsync(model.code)
|
|
30
45
|
}
|
|
31
46
|
}
|
|
32
47
|
|
|
@@ -34,11 +49,9 @@ async function initJS (model) {
|
|
|
34
49
|
log('Init JS')
|
|
35
50
|
this.container = model.container
|
|
36
51
|
|
|
37
|
-
// Load imports
|
|
38
52
|
if (model.imports && model.imports.length) {
|
|
39
53
|
log('Loading imports...')
|
|
40
54
|
for (let imp of model.imports) {
|
|
41
|
-
// Try creating an url
|
|
42
55
|
if (imp.code) {
|
|
43
56
|
log('Importing from DOM:', imp.url)
|
|
44
57
|
importScripts(URL.createObjectURL(new Blob([imp.code], { type: 'text/javascript' })))
|
|
@@ -51,7 +64,6 @@ async function initJS (model) {
|
|
|
51
64
|
|
|
52
65
|
if (model.code) {
|
|
53
66
|
log('Load code as a string', model)
|
|
54
|
-
// https://github.com/altbdoor/blob-worker/blob/master/blobWorker.js
|
|
55
67
|
importScripts(URL.createObjectURL(new Blob([model.code], { type: 'text/javascript' })))
|
|
56
68
|
} else if (model.url) {
|
|
57
69
|
log('Load script from URL:', model.url)
|
|
@@ -60,14 +72,15 @@ async function initJS (model) {
|
|
|
60
72
|
log('No script provided')
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
// Related:
|
|
64
|
-
// https://stackoverflow.com/questions/37711603/javascript-es6-class-definition-not-accessible-in-window-global
|
|
65
75
|
const target = model.type === 'class'
|
|
66
76
|
? eval(model.name)
|
|
67
77
|
: this[model.name]
|
|
68
78
|
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
let modelFunc = await utils.getModelFuncJS(model, target, {
|
|
80
|
+
log,
|
|
81
|
+
progress,
|
|
82
|
+
isCancelled
|
|
83
|
+
})
|
|
71
84
|
|
|
72
85
|
return modelFunc
|
|
73
86
|
}
|
|
@@ -81,10 +94,19 @@ onmessage = async function (e) {
|
|
|
81
94
|
var data = e.data
|
|
82
95
|
log('Received message of type:', typeof data)
|
|
83
96
|
|
|
84
|
-
if ((typeof data === 'object') && (
|
|
85
|
-
|
|
97
|
+
if ((typeof data === 'object') && (data._cmd === 'cancel')) {
|
|
98
|
+
cancelled = true
|
|
99
|
+
log('Cancel command received')
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (utils.isWorkerInitMessage(data, initialized)) {
|
|
86
104
|
log('Init...')
|
|
87
105
|
let m = data
|
|
106
|
+
streamInputConfig = (m && typeof m._streamInputConfig === 'object' && m._streamInputConfig)
|
|
107
|
+
? m._streamInputConfig
|
|
108
|
+
: {}
|
|
109
|
+
|
|
88
110
|
switch (m.type) {
|
|
89
111
|
case 'tf':
|
|
90
112
|
self.modelFunc = await initTF(m)
|
|
@@ -105,12 +127,14 @@ onmessage = async function (e) {
|
|
|
105
127
|
default:
|
|
106
128
|
throw new Error(`No type information: ${m.type}`)
|
|
107
129
|
}
|
|
108
|
-
|
|
130
|
+
initialized = true
|
|
131
|
+
postMessage({ _status: 'loaded' })
|
|
109
132
|
} else {
|
|
110
|
-
// Execution
|
|
111
133
|
try {
|
|
112
|
-
|
|
113
|
-
const
|
|
134
|
+
cancelled = false
|
|
135
|
+
const runData = utils.wrapStreamInputs(data, streamInputConfig, getStreamOptions())
|
|
136
|
+
log('Run model')
|
|
137
|
+
const results = await self.modelFunc(runData)
|
|
114
138
|
log('Results:', results)
|
|
115
139
|
postMessage(results)
|
|
116
140
|
} catch (error) {
|
package/templates/bulma-app.vue
CHANGED
|
@@ -202,8 +202,9 @@
|
|
|
202
202
|
<button
|
|
203
203
|
v-on:click="$parent.reset(example)"
|
|
204
204
|
class="button is-small example-button"
|
|
205
|
+
style="white-space: normal; width: 100%; text-align: left; font-family: monospace; font-size: 10px;"
|
|
205
206
|
>
|
|
206
|
-
{{ example }}
|
|
207
|
+
{{ JSON.stringify(example, null, 2) }}
|
|
207
208
|
</button>
|
|
208
209
|
</div>
|
|
209
210
|
</div>
|
|
@@ -229,7 +230,7 @@
|
|
|
229
230
|
></vue-output>
|
|
230
231
|
</div>
|
|
231
232
|
</div>
|
|
232
|
-
<pre v-if="$parent.
|
|
233
|
+
<pre v-if="$parent.debug">{{ $parent.outputs }}</pre>
|
|
233
234
|
</div>
|
|
234
235
|
</div>
|
|
235
236
|
</div>
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
<style lang="scss" scoped>
|
|
2
|
+
.control {
|
|
3
|
+
margin-top: -1px;
|
|
4
|
+
}
|
|
5
|
+
</style>
|
|
6
|
+
|
|
1
7
|
<template>
|
|
2
8
|
<div class="field" v-if="input.type == 'int' || input.type == 'float' || input.type == 'number'">
|
|
3
9
|
<label v-bind:for="input.name" class="is-size-7">{{ input.name }}</label>
|
|
@@ -9,6 +15,7 @@
|
|
|
9
15
|
v-bind:placeholder="input.placeholder ? input.placeholder : input.name"
|
|
10
16
|
v-bind:min="input.min"
|
|
11
17
|
v-bind:max="input.max"
|
|
18
|
+
v-bind:disabled="input.disabled"
|
|
12
19
|
v-on:change="changeHandler"
|
|
13
20
|
class="input"
|
|
14
21
|
type="number"
|
|
@@ -76,24 +83,22 @@
|
|
|
76
83
|
<div class="field" v-if="input.type == 'file'">
|
|
77
84
|
<label v-bind:for="input.name" class="is-size-7">{{ input.name }}</label>
|
|
78
85
|
<div class="control">
|
|
79
|
-
<div class="file has-name is-fullwidth" v-bind:class="{ 'is-primary': !input.file }">
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</span>
|
|
96
|
-
</label>
|
|
86
|
+
<div class="file has-name is-fullwidth" v-bind:class="{ 'is-primary': !input.file }" v-if="!input.disabled">
|
|
87
|
+
<file-picker
|
|
88
|
+
v-model="input.value"
|
|
89
|
+
v-model:url="input.url"
|
|
90
|
+
v-bind:raw="input.raw === true || input.stream === true"
|
|
91
|
+
v-bind:autoload="input.urlAutoLoad === true"
|
|
92
|
+
v-on:change="changeHandler"
|
|
93
|
+
></file-picker>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="file has-name is-fullwidth" v-bind:class="{ 'is-primary': !input.file }" v-else>
|
|
96
|
+
<input
|
|
97
|
+
class="input"
|
|
98
|
+
v-bind:id="input.name"
|
|
99
|
+
v-bind:value="input.default"
|
|
100
|
+
disabled
|
|
101
|
+
>
|
|
97
102
|
</div>
|
|
98
103
|
</div>
|
|
99
104
|
</div>
|
|
@@ -1,5 +1,51 @@
|
|
|
1
|
+
<style scoped>
|
|
2
|
+
/* Quickly stretch the card when browser goes native full‑screen
|
|
3
|
+
(actual FS API above) OR when the helper class is present. */
|
|
4
|
+
/*
|
|
5
|
+
.is-fullscreen {
|
|
6
|
+
position: fixed;
|
|
7
|
+
inset: 0;
|
|
8
|
+
z-index: 9999;
|
|
9
|
+
width: 100vw;
|
|
10
|
+
height: 100vh;
|
|
11
|
+
overflow: auto;
|
|
12
|
+
background: #fff;
|
|
13
|
+
}
|
|
14
|
+
.is-fullscreen .card-content, .is-fullscreen .content {
|
|
15
|
+
height: 100% !important;
|
|
16
|
+
}
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/* Full‑screen override */
|
|
20
|
+
.is-fullscreen {
|
|
21
|
+
position: fixed;
|
|
22
|
+
inset: 0;
|
|
23
|
+
z-index: 9999;
|
|
24
|
+
width: 100vw;
|
|
25
|
+
height: 100vh;
|
|
26
|
+
background: #fff;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.is-fullscreen .card-content {
|
|
32
|
+
flex: 1 1 auto;
|
|
33
|
+
overflow: auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Ensure Plotly div or other content can grow */
|
|
37
|
+
.is-fullscreen .content, .is-fullscreen .custom-container {
|
|
38
|
+
height: 100% !important;
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
41
|
+
|
|
1
42
|
<template>
|
|
2
|
-
<div
|
|
43
|
+
<div
|
|
44
|
+
class="card mb-5"
|
|
45
|
+
v-show="!(typeof output.value === 'undefined')"
|
|
46
|
+
:class="{ 'is-fullscreen': isFullScreen }"
|
|
47
|
+
ref="cardRoot"
|
|
48
|
+
>
|
|
3
49
|
<header class="card-header">
|
|
4
50
|
<p class="card-header-title is-size-6" v-if="output.name">
|
|
5
51
|
{{ output.name }}
|
|
@@ -8,22 +54,41 @@
|
|
|
8
54
|
<p class="card-header-icon">
|
|
9
55
|
<button class="button is-small" v-on:click="save()">Save</button>
|
|
10
56
|
<button class="button is-small" v-on:click="copy()">Copy</button>
|
|
57
|
+
<button
|
|
58
|
+
class="button is-small"
|
|
59
|
+
v-if="!isFullScreen"
|
|
60
|
+
@click="toggleFullScreen"
|
|
61
|
+
title="Expand to full screen"
|
|
62
|
+
>
|
|
63
|
+
Fullscreen
|
|
64
|
+
</button>
|
|
65
|
+
<button
|
|
66
|
+
class="button is-small"
|
|
67
|
+
v-else
|
|
68
|
+
@click="toggleFullScreen"
|
|
69
|
+
title="Exit full screen"
|
|
70
|
+
>
|
|
71
|
+
Close
|
|
72
|
+
</button>
|
|
11
73
|
</p>
|
|
12
74
|
</header>
|
|
13
75
|
<div class="card-content">
|
|
14
|
-
<div class="content" v-if="(output.type == 'svg') || (output.type == 'html')">
|
|
76
|
+
<div class="content" :id="outputName" v-if="(output.type == 'svg') || (output.type == 'html')">
|
|
15
77
|
<div v-html="output.value"></div>
|
|
16
78
|
</div>
|
|
17
|
-
<div class="content" v-else-if="output.type == 'object'">
|
|
79
|
+
<div class="content" :id="outputName" v-else-if="output.type == 'object'">
|
|
18
80
|
<json-viewer :value="output.value" copyable sort />
|
|
19
81
|
</div>
|
|
20
|
-
<div class="content" v-else-if="output.type == 'code'">
|
|
82
|
+
<div class="content" :id="outputName" v-else-if="output.type == 'code'">
|
|
21
83
|
<pre>{{ output.value }}</pre>
|
|
22
84
|
</div>
|
|
23
|
-
<div class="content" v-else-if="output.type == 'function'">
|
|
24
|
-
<div ref="customContainer"></div>
|
|
85
|
+
<div class="content" :id="outputName" v-else-if="output.type == 'function'">
|
|
86
|
+
<div class="custom-container" ref="customContainer"></div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="content" :id="outputName" v-else-if="output.type == 'blank'">
|
|
89
|
+
<!-- will be filled by custom render function -->
|
|
25
90
|
</div>
|
|
26
|
-
<div class="content" v-else>
|
|
91
|
+
<div class="content" :id="outputName" v-else>
|
|
27
92
|
<pre>{{ output.value }}</pre>
|
|
28
93
|
</div>
|
|
29
94
|
</div>
|
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
const FileReader = window['FileReader']
|
|
2
|
+
import FilePicker from './file-picker.vue'
|
|
2
3
|
|
|
3
4
|
const component = {
|
|
4
5
|
props: ['input'],
|
|
5
6
|
emits: ['inchange'],
|
|
7
|
+
components: { FilePicker },
|
|
6
8
|
methods: {
|
|
7
9
|
changeHandler () {
|
|
8
10
|
if (this.input.reactive) {
|
|
9
11
|
this.$emit('inchange')
|
|
10
12
|
}
|
|
11
13
|
},
|
|
12
|
-
loadFile (e) {
|
|
13
|
-
const reader = new FileReader()
|
|
14
|
-
this.input.file = e.target.files[0]
|
|
15
|
-
reader.readAsText(this.input.file)
|
|
16
|
-
reader.onload = () => {
|
|
17
|
-
this.input.value = reader.result
|
|
18
|
-
if (typeof this.input.cb !== 'undefined') {
|
|
19
|
-
this.input.cb.run()
|
|
20
|
-
} else if (this.input.reactive) {
|
|
21
|
-
this.$emit('inchange')
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
14
|
call (method) {
|
|
26
15
|
console.log('calling: ', method)
|
|
27
16
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { saveAs } from 'file-saver'
|
|
2
2
|
import domtoimage from 'dom-to-image'
|
|
3
3
|
|
|
4
|
+
const { sanitizeName } = require('../src/utils.js')
|
|
5
|
+
|
|
4
6
|
const Blob = window['Blob']
|
|
5
7
|
|
|
6
8
|
function stringify (v) {
|
|
@@ -12,11 +14,35 @@ function stringify (v) {
|
|
|
12
14
|
const component = {
|
|
13
15
|
props: ['output'],
|
|
14
16
|
emits: ['notification'],
|
|
17
|
+
data () {
|
|
18
|
+
return {
|
|
19
|
+
outputName: 'output',
|
|
20
|
+
isFullScreen: false,
|
|
21
|
+
}
|
|
22
|
+
},
|
|
15
23
|
mounted() {
|
|
24
|
+
this.outputName = this.output.alias
|
|
25
|
+
? this.output.alias
|
|
26
|
+
: this.output.name
|
|
27
|
+
? sanitizeName(this.output.name)
|
|
28
|
+
: 'output_' + Math.floor(Math.random() * 1000000)
|
|
16
29
|
this.executeRenderFunction()
|
|
30
|
+
document.addEventListener('fullscreenchange', this.onFullScreenChange)
|
|
17
31
|
},
|
|
18
|
-
|
|
19
|
-
this.
|
|
32
|
+
beforeUnmount() {
|
|
33
|
+
document.removeEventListener('fullscreenchange', this.onFullScreenChange)
|
|
34
|
+
},
|
|
35
|
+
// updated() {
|
|
36
|
+
// this.executeRenderFunction()
|
|
37
|
+
// },
|
|
38
|
+
watch: {
|
|
39
|
+
'output.value': function (newValue, oldValue) {
|
|
40
|
+
if (newValue !== oldValue) {
|
|
41
|
+
this.$nextTick(() => {
|
|
42
|
+
this.executeRenderFunction()
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
20
46
|
},
|
|
21
47
|
computed: {
|
|
22
48
|
isRenderFunction() {
|
|
@@ -24,6 +50,35 @@ const component = {
|
|
|
24
50
|
}
|
|
25
51
|
},
|
|
26
52
|
methods: {
|
|
53
|
+
toggleFullScreen() {
|
|
54
|
+
const el = this.$refs.cardRoot || this.$el
|
|
55
|
+
if (!this.isFullScreen) {
|
|
56
|
+
if (el.requestFullscreen) {
|
|
57
|
+
el.requestFullscreen()
|
|
58
|
+
} else if (el.webkitRequestFullscreen) {
|
|
59
|
+
el.webkitRequestFullscreen()
|
|
60
|
+
} else if (el.mozRequestFullScreen) {
|
|
61
|
+
el.mozRequestFullScreen()
|
|
62
|
+
} else if (el.msRequestFullscreen) {
|
|
63
|
+
el.msRequestFullscreen()
|
|
64
|
+
}
|
|
65
|
+
// state will flip in onFullScreenChange
|
|
66
|
+
} else {
|
|
67
|
+
if (document.exitFullscreen) {
|
|
68
|
+
document.exitFullscreen()
|
|
69
|
+
} else if (document.webkitExitFullscreen) {
|
|
70
|
+
document.webkitExitFullscreen()
|
|
71
|
+
} else if (document.mozCancelFullScreen) {
|
|
72
|
+
document.mozCancelFullScreen()
|
|
73
|
+
} else if (document.msExitFullscreen) {
|
|
74
|
+
document.msExitFullscreen()
|
|
75
|
+
}
|
|
76
|
+
// state will flip in onFullScreenChange
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
onFullScreenChange() {
|
|
80
|
+
this.isFullScreen = !!document.fullscreenElement
|
|
81
|
+
},
|
|
27
82
|
save () {
|
|
28
83
|
// Prepare filename
|
|
29
84
|
let filename
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Based on https://github.com/rowanwins/vue-file-picker/ -->
|
|
3
|
+
<div
|
|
4
|
+
:id="id"
|
|
5
|
+
class="vfp"
|
|
6
|
+
>
|
|
7
|
+
<div
|
|
8
|
+
class="vfp-bgArea"
|
|
9
|
+
:class="{ 'vfp-active': isActive }"
|
|
10
|
+
@dragover="setActive"
|
|
11
|
+
@dragleave="cancelActive"
|
|
12
|
+
@drop="fileAdded"
|
|
13
|
+
>
|
|
14
|
+
<div class="vfp-iconHolder vfp-gridItem">
|
|
15
|
+
<slot name="icon">
|
|
16
|
+
<svg
|
|
17
|
+
slot="icon"
|
|
18
|
+
height="40"
|
|
19
|
+
viewBox="0 0 48 48"
|
|
20
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
21
|
+
>
|
|
22
|
+
<path
|
|
23
|
+
d="M18 32h12v-12h8l-14-14-14 14h8zm-8 4h28v4h-28z"
|
|
24
|
+
fill="#CACFD2"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</slot>
|
|
28
|
+
</div>
|
|
29
|
+
<input
|
|
30
|
+
id="vfp-filePicker"
|
|
31
|
+
class="vfp-inputfile vfp-gridItem"
|
|
32
|
+
type="file"
|
|
33
|
+
name="vfp-filePicker"
|
|
34
|
+
:accept="accept"
|
|
35
|
+
:multiple="allowMultiple"
|
|
36
|
+
@change="fileAdded"
|
|
37
|
+
>
|
|
38
|
+
<label
|
|
39
|
+
class="vfp-label vfp-gridItem"
|
|
40
|
+
for="vfp-filePicker"
|
|
41
|
+
>
|
|
42
|
+
<slot name="label">
|
|
43
|
+
<!-- <strong>Choose a file</strong> or drop it here -->
|
|
44
|
+
<strong>{{ label }}</strong>
|
|
45
|
+
</slot>
|
|
46
|
+
</label>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
export default {
|
|
53
|
+
name: 'FilePicker',
|
|
54
|
+
props: {
|
|
55
|
+
id: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: true,
|
|
58
|
+
default: 'filePicker'
|
|
59
|
+
},
|
|
60
|
+
accept: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: '*/*'
|
|
63
|
+
},
|
|
64
|
+
allowMultiple: {
|
|
65
|
+
type: Boolean,
|
|
66
|
+
default: false
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
data: function () {
|
|
70
|
+
return {
|
|
71
|
+
isActive: false,
|
|
72
|
+
label: 'Choose a file or drop it here'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
computed: {
|
|
76
|
+
requiresTypeCheck: function () {
|
|
77
|
+
return this.accept !== '*/*'
|
|
78
|
+
},
|
|
79
|
+
acceptedTypes: function () {
|
|
80
|
+
return this.accept.split(',')
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
methods: {
|
|
84
|
+
cancelHandlers (e) {
|
|
85
|
+
e.preventDefault()
|
|
86
|
+
e.stopPropagation()
|
|
87
|
+
},
|
|
88
|
+
setActive (e) {
|
|
89
|
+
this.isActive = true
|
|
90
|
+
this.cancelHandlers(e)
|
|
91
|
+
},
|
|
92
|
+
cancelActive (e) {
|
|
93
|
+
this.isActive = false
|
|
94
|
+
this.cancelHandlers(e)
|
|
95
|
+
},
|
|
96
|
+
fileAdded (e) {
|
|
97
|
+
this.isActive = false
|
|
98
|
+
this.cancelHandlers(e)
|
|
99
|
+
const wasDropped = e.dataTransfer
|
|
100
|
+
const files = wasDropped ? e.dataTransfer.files : e.target.files
|
|
101
|
+
this.label = Array.from(files).map(file => file.name).join(', ')
|
|
102
|
+
|
|
103
|
+
if (wasDropped && !this.allowMultiple && files.length > 1) throw new Error('vue-file-picker: Multiple Files are not allowed')
|
|
104
|
+
|
|
105
|
+
if (wasDropped && this.requiresTypeCheck) {
|
|
106
|
+
for (var i = 0; i < files.length; i++) {
|
|
107
|
+
if (this.acceptedTypes.indexOf(files[i].type) === -1) throw new Error('vue-file-picker: File type not allowed')
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.$emit('vfp-file-added', files)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<style lang="scss">
|
|
117
|
+
|
|
118
|
+
.vfp {
|
|
119
|
+
display: flex;
|
|
120
|
+
height: 100px;
|
|
121
|
+
width: 100%;
|
|
122
|
+
|
|
123
|
+
.vfp-bgArea {
|
|
124
|
+
transition: 0.3s;
|
|
125
|
+
background: #F2F3F4;
|
|
126
|
+
display: grid;
|
|
127
|
+
grid-template-rows: 60% 40%;
|
|
128
|
+
padding: 25px 10px;
|
|
129
|
+
width: 100%;
|
|
130
|
+
outline: 2px dashed #CACFD2;
|
|
131
|
+
outline-offset: -10px;
|
|
132
|
+
color: #3b3e40;
|
|
133
|
+
text-align: center;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.vfp-inputfile {
|
|
137
|
+
width: 0.1px;
|
|
138
|
+
height: 0.1px;
|
|
139
|
+
opacity: 0;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
position: absolute;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.vfp-gridItem {
|
|
145
|
+
align-self: center;
|
|
146
|
+
justify-self: center;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.vfp-label {
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
text-align: center;
|
|
152
|
+
font-size: 0.9rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.vfp-active {
|
|
156
|
+
background-color: #D7DBDD;
|
|
157
|
+
outline-color: #F2F3F4;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@media only screen and (max-width: 440px) {
|
|
161
|
+
.vfp-bgArea {
|
|
162
|
+
padding: 18px 10px;
|
|
163
|
+
grid-template-rows: 50% 50%;
|
|
164
|
+
grid-row-gap: 5px;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
</style>
|