@jseeio/jsee 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,318 @@
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
+ labelValue: { type: String, default: 'Choose File' }
111
+ },
112
+ emits : ['update:modelValue', 'update:url', 'change'],
113
+ data () {
114
+ return {
115
+ isActive: false,
116
+ urlInput: '',
117
+ urlSuccess: false,
118
+ urlError: false,
119
+ label: this.labelValue,
120
+ info: 'or drag and drop it here',
121
+ showUrlInput: true
122
+ }
123
+ },
124
+ computed: {
125
+ showUrlInputAuto () {
126
+ return this.url.length > 0 && this.showUrlInput
127
+ },
128
+ requiresTypeCheck () { return this.accept !== '*/*' },
129
+ acceptedTypes () { return this.accept.split(',') },
130
+ urlModel: {
131
+ get () { return this.url },
132
+ set (v) { this.$emit('update:url', v) }
133
+ }
134
+ },
135
+ methods: {
136
+ /* drag-drop handling */
137
+ cancelHandlers (e) { e.preventDefault(); e.stopPropagation() },
138
+ setActive (e) { this.isActive = true; this.cancelHandlers(e) },
139
+ cancelActive (e) { this.isActive = false; this.cancelHandlers(e) },
140
+
141
+ fileAdded (e) {
142
+ this.isActive = false;
143
+ this.cancelHandlers(e);
144
+
145
+ const wasDropped = !!e.dataTransfer;
146
+ if (wasDropped && this.urlModel && this.urlModel.length > 0) {
147
+ console.log('[File picker] URL mode active, ignoring dropped files');
148
+ return;
149
+ }
150
+ const files = wasDropped ? e.dataTransfer.files : e.target.files;
151
+ this.label = Array.from(files).map(f => f.name).join(', ');
152
+ const totalSizeinKB = Array.from(files).reduce((acc, f) => acc + f.size / 1024, 0).toFixed(2)
153
+ this.info = `Selected ${files.length} file(s) of size ${totalSizeinKB} KB`;
154
+
155
+ if (wasDropped && !this.allowMultiple && files.length > 1)
156
+ throw new Error('vue-file-picker: Multiple files are not allowed');
157
+ if (wasDropped && this.requiresTypeCheck)
158
+ for (const f of files)
159
+ if (!this.acceptedTypes.includes(f.type))
160
+ throw new Error('vue-file-picker: File type not allowed');
161
+
162
+ console.log('[File picker] Files added:', files);
163
+ this.loadFile(files);
164
+ },
165
+
166
+ loadFile (e) {
167
+ const files = e.target ? e.target.files : e
168
+ if (this.raw) {
169
+ const fileValue = this.allowMultiple ? Array.from(files) : files[0]
170
+ this.$emit('update:modelValue', fileValue)
171
+ this.$emit('change')
172
+ return
173
+ }
174
+ const reader = new FileReader()
175
+ reader.readAsText(files[0])
176
+ reader.onload = () => {
177
+ // No need to check for reactivity here, as the parent component will handle it
178
+ // Just trigger basic change event
179
+ this.$emit('update:modelValue', reader.result)
180
+ this.$emit('change')
181
+ }
182
+ },
183
+
184
+ loadUrl () {
185
+ if (this.urlModel && this.urlModel.length > 0) {
186
+ if (this.raw) {
187
+ this.urlSuccess = true
188
+ this.urlError = false
189
+ this.label = this.urlModel.split('/').pop().split('?')[0] || 'File from URL'
190
+ this.rawUrl = this.urlModel.split('/').slice(0, -1).join('/') // Extract URL path
191
+ this.showUrlInput = false // Hide URL input after successful load
192
+ this.info = `Loaded URL handle: ${this.rawUrl || this.urlModel}`
193
+ this.$emit('update:modelValue', { kind: 'url', url: this.urlModel })
194
+ this.$emit('change')
195
+ return
196
+ }
197
+
198
+ fetch(this.urlModel)
199
+ .then(response => response.text())
200
+ .then(text => {
201
+ this.urlSuccess = true
202
+ this.urlError = false
203
+ this.label = this.urlModel.split('/').pop().split('?')[0] || 'File from URL'
204
+ this.rawUrl = this.urlModel.split('/').slice(0, -1).join('/') // Extract URL path
205
+ this.showUrlInput = false // Hide URL input after successful load
206
+ // Show file size
207
+ this.info = `Loaded from URL: ${this.rawUrl} (${(text.length / 1024).toFixed(2)} KB)`
208
+ this.$emit('update:modelValue', text)
209
+ this.$emit('change')
210
+ })
211
+ .catch(error => {
212
+ this.urlSuccess = false
213
+ this.urlError = true
214
+ console.error('Error fetching URL:', error)
215
+ })
216
+ }
217
+ },
218
+
219
+ /* switch to URL mode */
220
+ activateUrl () {
221
+ this.urlModel = this.urlModel || 'http://'
222
+ this.showUrlInput = true
223
+ this.$nextTick(() => this.$el.querySelector('.vfp-urlInput').focus())
224
+ },
225
+
226
+ activateFileDialog () {
227
+ this.$el.querySelector('#vfp-filePicker').click()
228
+ },
229
+
230
+ clearUrl () {
231
+ this.showUrlInput = false
232
+ // this.urlModel = ''
233
+ // this.urlSuccess = false
234
+ // this.urlError = false
235
+ // this.$emit('update:url', '')
236
+ // this.$emit('update:modelValue', '')
237
+ }
238
+ }
239
+ }
240
+ </script>
241
+
242
+ <style lang="scss">
243
+ .vfp {
244
+ display: flex;
245
+ min-height: 130px;
246
+ width: 100%;
247
+
248
+ .vfp-bgArea {
249
+ transition: 0.3s;
250
+ background: #F2F3F4;
251
+ display: grid;
252
+ grid-template-rows: 40% 32% 28%;
253
+ padding: 20px 10px;
254
+ width: 100%;
255
+ outline: 1px dashed #CACFD2;
256
+ outline-offset: -10px;
257
+ color: #3b3e40;
258
+ text-align: center;
259
+ }
260
+
261
+ .vfp-inputfile {
262
+ width: 0.1px;
263
+ height: 0.1px;
264
+ opacity: 0;
265
+ position: absolute;
266
+ }
267
+
268
+ .vfp-gridItem { align-self: center; justify-self: center; }
269
+
270
+ .vfp-label { cursor: pointer; font-size: 0.9rem; font-family: monospace; display:block; }
271
+ .vfp-info {
272
+ font-size: 9px;
273
+ color: #7f8c8d;
274
+ margin-top: 1px;
275
+ margin-bottom: 5px;
276
+ display: block;
277
+ }
278
+ .vfp-urlToggle { cursor: pointer; font-size: 0.8rem; }
279
+ .vfp-urlInput {
280
+ margin-top: 10px;
281
+ width: calc(100% - 20px);
282
+ padding: 6px 6px !important;
283
+ border: 1px solid #CACFD2;
284
+ border-radius: 4px;
285
+ font-size: 0.9rem;
286
+ }
287
+
288
+ .vfp-button {
289
+ cursor: pointer;
290
+ border: none;
291
+ padding: 4px 10px;
292
+ border-radius: 4px;
293
+
294
+ // hover effect
295
+ background-color: #E4E7EA;
296
+ &:hover {
297
+ background-color: #D7DBDD;
298
+ }
299
+ }
300
+
301
+ button.vfp-urlInput {
302
+ margin-top: 2px;
303
+ }
304
+
305
+ .vfp-active {
306
+ background-color: #D7DBDD;
307
+ outline-color: #F2F3F4;
308
+ }
309
+
310
+ @media only screen and (max-width: 440px) {
311
+ .vfp-bgArea {
312
+ padding: 18px 10px;
313
+ grid-template-rows: 45% 25% 30%;
314
+ grid-row-gap: 5px;
315
+ }
316
+ }
317
+ }
318
+ </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)
@@ -0,0 +1,3 @@
1
+ name,age
2
+ Alice,30
3
+ Bob,25