@jseeio/jsee 0.4.1 → 0.8.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/LICENSE +21 -0
  3. package/README.md +583 -55
  4. package/dist/2b3e1faf89f94a483539.png +0 -0
  5. package/dist/416d91365b44e4b4f477.png +0 -0
  6. package/dist/8f2c4d11474275fbc161.png +0 -0
  7. package/dist/jsee.core.js +1 -0
  8. package/dist/jsee.full.js +1 -0
  9. package/dist/jsee.runtime.js +2 -1
  10. package/package.json +84 -18
  11. package/src/app.js +130 -33
  12. package/src/cli.js +474 -64
  13. package/src/extended-imports.js +11 -0
  14. package/src/main.js +264 -45
  15. package/src/overlay.js +26 -1
  16. package/src/utils.js +390 -12
  17. package/templates/common-inputs.js +88 -0
  18. package/templates/common-outputs.js +340 -4
  19. package/templates/minimal-app.vue +367 -0
  20. package/templates/minimal-input.vue +573 -0
  21. package/templates/minimal-output.vue +426 -0
  22. package/templates/virtual-table.vue +194 -0
  23. package/.claude/settings.local.json +0 -13
  24. package/.eslintrc.js +0 -38
  25. package/AGENTS.md +0 -65
  26. package/CLAUDE.md +0 -5
  27. package/CNAME +0 -1
  28. package/_config.yml +0 -26
  29. package/dist/jsee.js +0 -1
  30. package/jest-puppeteer.config.js +0 -14
  31. package/jest.config.js +0 -8
  32. package/jest.unit.config.js +0 -8
  33. package/load/index.html +0 -52
  34. package/templates/bulma-app.vue +0 -242
  35. package/templates/bulma-input.vue +0 -125
  36. package/templates/bulma-output.vue +0 -101
  37. package/test/class.html +0 -22
  38. package/test/code.html +0 -25
  39. package/test/codew.html +0 -25
  40. package/test/example-class.js +0 -8
  41. package/test/example-sum.js +0 -3
  42. package/test/fixtures/lodash-like.js +0 -15
  43. package/test/fixtures/upload-sample.csv +0 -3
  44. package/test/importw.html +0 -28
  45. package/test/minimal.html +0 -14
  46. package/test/minimal1.html +0 -13
  47. package/test/minimal2.html +0 -15
  48. package/test/minimal3.html +0 -10
  49. package/test/minimal4.html +0 -22
  50. package/test/pipeline.html +0 -52
  51. package/test/python.html +0 -41
  52. package/test/string.html +0 -26
  53. package/test/stringw.html +0 -29
  54. package/test/sum.schema.json +0 -17
  55. package/test/sumw.schema.json +0 -15
  56. package/test/test-basic.test.js +0 -603
  57. package/test/test-python.test.js +0 -23
  58. package/test/unit/cli-fetch.test.js +0 -229
  59. package/test/unit/utils.test.js +0 -888
  60. package/webpack.config.js +0 -101
package/src/utils.js CHANGED
@@ -105,7 +105,14 @@ const VALID_INPUT_TYPES = [
105
105
  'file',
106
106
  'group',
107
107
  'action',
108
- 'button'
108
+ 'button',
109
+ 'slider',
110
+ 'radio',
111
+ 'toggle',
112
+ 'date',
113
+ 'multi-select',
114
+ 'range',
115
+ 'folder'
109
116
  ]
110
117
 
111
118
  const VALID_MODEL_TYPES = [
@@ -710,7 +717,18 @@ async function importScripts (...imports) {
710
717
  }
711
718
  }
712
719
 
713
- function getModelFuncAPI (model, log=console.log) {
720
+ function parseSSELine (line) {
721
+ if (!line.startsWith('data:')) return null
722
+ const payload = line.slice(5).trim()
723
+ if (payload === '[DONE]') return null
724
+ try {
725
+ return JSON.parse(payload)
726
+ } catch (e) {
727
+ return payload
728
+ }
729
+ }
730
+
731
+ function getModelFuncAPI (model, log=console.log, onChunk) {
714
732
  switch (model.type) {
715
733
  case 'get':
716
734
  return (data) => {
@@ -724,19 +742,113 @@ function getModelFuncAPI (model, log=console.log) {
724
742
  case 'post':
725
743
  return (data) => {
726
744
  log('Sending POST request to', model.url)
727
- const resPromise = fetch(model.url, {
745
+ const accept = model.stream ? 'text/event-stream' : 'application/json'
746
+ return fetch(model.url, {
728
747
  method: 'POST',
729
748
  headers: {
730
- 'Accept': 'application/json',
749
+ 'Accept': accept,
731
750
  'Content-Type': 'application/json'
732
751
  },
733
752
  body: JSON.stringify(data)
734
- }).then(response => response.json())
735
- return resPromise
753
+ }).then(async (response) => {
754
+ const contentType = response.headers.get('content-type') || ''
755
+ // SSE streaming response
756
+ if (contentType.includes('text/event-stream') && response.body && onChunk) {
757
+ const reader = response.body.getReader()
758
+ const decoder = new TextDecoder()
759
+ let buffer = ''
760
+ let lastResult = null
761
+ while (true) {
762
+ const { done, value } = await reader.read()
763
+ if (done) break
764
+ buffer += decoder.decode(value, { stream: true })
765
+ const lines = buffer.split('\n')
766
+ buffer = lines.pop()
767
+ for (const line of lines) {
768
+ const trimmed = line.trim()
769
+ if (!trimmed) continue
770
+ const parsed = parseSSELine(trimmed)
771
+ if (parsed !== null) {
772
+ lastResult = parsed
773
+ onChunk(parsed)
774
+ }
775
+ }
776
+ }
777
+ // Process any remaining buffer
778
+ buffer += decoder.decode()
779
+ if (buffer.trim()) {
780
+ const parsed = parseSSELine(buffer.trim())
781
+ if (parsed !== null) {
782
+ lastResult = parsed
783
+ onChunk(parsed)
784
+ }
785
+ }
786
+ return lastResult
787
+ }
788
+ return response.json()
789
+ })
736
790
  }
737
791
  }
738
792
  }
739
793
 
794
+ const TYPED_ARRAY_CONSTRUCTORS = {
795
+ float32: Float32Array,
796
+ float64: Float64Array,
797
+ int8: Int8Array,
798
+ int16: Int16Array,
799
+ int32: Int32Array,
800
+ uint8: Uint8Array,
801
+ uint16: Uint16Array,
802
+ uint32: Uint32Array,
803
+ }
804
+
805
+ function toTypedArray (value, dtype) {
806
+ if (!dtype || !TYPED_ARRAY_CONSTRUCTORS[dtype]) return value
807
+ const Ctor = TYPED_ARRAY_CONSTRUCTORS[dtype]
808
+ if (value instanceof Ctor) return value
809
+ if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
810
+ return new Ctor(value instanceof ArrayBuffer ? value : value.buffer)
811
+ }
812
+ if (Array.isArray(value)) return new Ctor(value)
813
+ return value
814
+ }
815
+
816
+ function fromTypedArray (value) {
817
+ if (ArrayBuffer.isView(value)) return Array.from(value)
818
+ return value
819
+ }
820
+
821
+ function collectTransferables (value, seen) {
822
+ if (!value || typeof value !== 'object') return []
823
+ if (!seen) seen = new WeakSet()
824
+ if (seen.has(value)) return []
825
+ seen.add(value)
826
+ const buffers = []
827
+ if (value instanceof ArrayBuffer) {
828
+ buffers.push(value)
829
+ } else if (ArrayBuffer.isView(value)) {
830
+ buffers.push(value.buffer)
831
+ } else if (Array.isArray(value)) {
832
+ value.forEach(item => buffers.push(...collectTransferables(item, seen)))
833
+ } else {
834
+ Object.keys(value).forEach(key => {
835
+ buffers.push(...collectTransferables(value[key], seen))
836
+ })
837
+ }
838
+ return buffers
839
+ }
840
+
841
+ function wrapTypedArrayInputs (inputs, inputConfigs) {
842
+ if (!isObject(inputs) || !Array.isArray(inputConfigs)) return inputs
843
+ const wrapped = Object.assign({}, inputs)
844
+ inputConfigs.forEach(cfg => {
845
+ if (cfg.arrayBuffer && cfg.name && typeof wrapped[cfg.name] !== 'undefined') {
846
+ wrapped[cfg.name] = toTypedArray(wrapped[cfg.name], cfg.dtype || 'float64')
847
+ }
848
+ })
849
+ return wrapped
850
+ }
851
+
740
852
  async function delay (ms) {
741
853
  return new Promise(resolve => setTimeout(resolve, ms || 1))
742
854
  }
@@ -775,7 +887,19 @@ function debounce (fn, ms) {
775
887
  function getName (code) {
776
888
  switch (typeof code) {
777
889
  case 'function':
778
- return code.name
890
+ if (!code.name) return undefined
891
+ // JS infers .name from property assignment for ALL function forms:
892
+ // { code: (a) => a } and { code: function(a) {} } both get .name === "code".
893
+ // Only trust .name when the name actually appears in the source text.
894
+ const src = code.toString().trimStart()
895
+ const keyword = src.startsWith('async function') ? 'async function'
896
+ : src.startsWith('function') ? 'function'
897
+ : null
898
+ if (keyword) {
899
+ const afterKeyword = src.slice(keyword.length).trimStart()
900
+ if (afterKeyword.startsWith(code.name)) return code.name
901
+ }
902
+ return undefined
779
903
  case 'string':
780
904
  const words = code.split(' ')
781
905
  const functionIndex = words.findIndex((word) => word === 'function')
@@ -874,8 +998,9 @@ function validateSchema (schema) {
874
998
  const hasModel = typeof schema.model !== 'undefined'
875
999
  const hasView = (typeof schema.view !== 'undefined') || (typeof schema.render !== 'undefined')
876
1000
 
877
- if (!hasModel && !hasView) {
878
- report.errors.push('Schema should define `model` (or `view`/`render`)')
1001
+ const hasInputs = Array.isArray(schema.inputs) && schema.inputs.length > 0
1002
+ if (!hasModel && !hasView && !hasInputs) {
1003
+ report.errors.push('Schema should define `model` (or `view`/`render`) or `inputs`')
879
1004
  } else if (!hasModel && hasView) {
880
1005
  report.warnings.push('Schema has no `model`, using `view`/`render` only')
881
1006
  }
@@ -904,8 +1029,12 @@ function validateSchema (schema) {
904
1029
 
905
1030
  // Convert a URL parameter string to the appropriate type
906
1031
  function coerceParam (value, type, name) {
907
- if (type === 'number') return Number(value)
908
- if (type === 'boolean') return value === 'true'
1032
+ if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') return Number(value)
1033
+ if (type === 'boolean' || type === 'bool' || type === 'checkbox' || type === 'toggle') return value === 'true'
1034
+ if (type === 'range') {
1035
+ try { return JSON.parse(value) }
1036
+ catch (e) { console.error(`Failed to parse range for input ${name}:`, e) }
1037
+ }
909
1038
  if (type === 'json') {
910
1039
  try { return JSON.parse(value) }
911
1040
  catch (e) { console.error(`Failed to parse JSON for input ${name}:`, e) }
@@ -915,6 +1044,7 @@ function coerceParam (value, type, name) {
915
1044
 
916
1045
  // Extract URL parameter value for an input, checking name, sanitized name, and aliases
917
1046
  function getUrlParam (urlParams, input) {
1047
+ if (!input.name) return null
918
1048
  if (urlParams.has(input.name)) return urlParams.get(input.name)
919
1049
  if (urlParams.has(sanitizeName(input.name))) return urlParams.get(sanitizeName(input.name))
920
1050
  if (!input.alias) return null
@@ -925,6 +1055,240 @@ function getUrlParam (urlParams, input) {
925
1055
  return null
926
1056
  }
927
1057
 
1058
+ function jseeInputsToJsonSchema (inputs) {
1059
+ const properties = {}
1060
+ const required = []
1061
+ for (const inp of (inputs || [])) {
1062
+ const prop = {}
1063
+ if (inp.description) prop.description = inp.description
1064
+ switch (inp.type) {
1065
+ case 'int':
1066
+ prop.type = 'integer'
1067
+ break
1068
+ case 'float': case 'number':
1069
+ prop.type = 'number'
1070
+ break
1071
+ case 'bool': case 'checkbox': case 'toggle':
1072
+ prop.type = 'boolean'
1073
+ break
1074
+ case 'select': case 'categorical': case 'radio':
1075
+ prop.type = 'string'
1076
+ if (inp.options) prop.enum = inp.options
1077
+ break
1078
+ case 'slider':
1079
+ prop.type = 'number'
1080
+ if (inp.min !== undefined) prop.minimum = inp.min
1081
+ if (inp.max !== undefined) prop.maximum = inp.max
1082
+ if (inp.step !== undefined) prop.multipleOf = inp.step
1083
+ break
1084
+ case 'range':
1085
+ prop.type = 'array'
1086
+ prop.items = { type: 'number' }
1087
+ prop.minItems = 2
1088
+ prop.maxItems = 2
1089
+ break
1090
+ case 'multi-select':
1091
+ prop.type = 'array'
1092
+ prop.items = { type: 'string' }
1093
+ if (inp.options) prop.items.enum = inp.options
1094
+ break
1095
+ default:
1096
+ prop.type = 'string'
1097
+ }
1098
+ if (inp.default !== undefined) prop.default = inp.default
1099
+ properties[inp.name] = prop
1100
+ if (inp.default === undefined) required.push(inp.name)
1101
+ }
1102
+ return { type: 'object', properties, required }
1103
+ }
1104
+
1105
+ function generateOpenAPISpec (schema) {
1106
+ const models = Array.isArray(schema.model) ? schema.model : (schema.model ? [schema.model] : [])
1107
+ const paths = {}
1108
+ const inputSchema = jseeInputsToJsonSchema(schema.inputs)
1109
+
1110
+ for (const m of models) {
1111
+ paths['/' + m.name] = {
1112
+ post: {
1113
+ summary: 'Run ' + m.name,
1114
+ operationId: m.name,
1115
+ requestBody: {
1116
+ required: true,
1117
+ content: { 'application/json': { schema: inputSchema } }
1118
+ },
1119
+ responses: {
1120
+ '200': {
1121
+ description: 'Model output',
1122
+ content: { 'application/json': { schema: { type: 'object' } } }
1123
+ }
1124
+ }
1125
+ }
1126
+ }
1127
+ }
1128
+
1129
+ const title = schema.title
1130
+ || (schema.page && schema.page.title)
1131
+ || (models[0] && models[0].name)
1132
+ || 'JSEE API'
1133
+
1134
+ return {
1135
+ openapi: '3.1.0',
1136
+ info: { title, version: '1.0.0' },
1137
+ paths
1138
+ }
1139
+ }
1140
+
1141
+ function serializeResult (result) {
1142
+ if (result === null || result === undefined) return { result: null }
1143
+ // Buffer or Uint8Array → base64 image
1144
+ if (Buffer.isBuffer(result) || result instanceof Uint8Array) {
1145
+ const b64 = Buffer.from(result).toString('base64')
1146
+ return { result: 'data:image/png;base64,' + b64 }
1147
+ }
1148
+ // Plain object or array — return as-is
1149
+ if (typeof result === 'object') return result
1150
+ // Primitives
1151
+ return { result }
1152
+ }
1153
+
1154
+ function parseMultipart (contentType, body) {
1155
+ const match = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/)
1156
+ if (!match) return {}
1157
+ const boundary = '--' + (match[1] || match[2])
1158
+ const buf = Buffer.isBuffer(body) ? body : Buffer.from(body)
1159
+ const data = {}
1160
+ let start = buf.indexOf(boundary) + boundary.length
1161
+ while (start < buf.length) {
1162
+ // Skip \r\n after boundary
1163
+ if (buf[start] === 0x0d) start += 2
1164
+ else if (buf[start] === 0x0a) start += 1
1165
+ // Check for closing boundary (--)
1166
+ if (buf[start] === 0x2d && buf[start + 1] === 0x2d) break
1167
+ // Find end of headers (\r\n\r\n)
1168
+ const headerEnd = buf.indexOf('\r\n\r\n', start)
1169
+ if (headerEnd === -1) break
1170
+ const headers = buf.slice(start, headerEnd).toString('utf-8')
1171
+ const bodyStart = headerEnd + 4
1172
+ // Find next boundary
1173
+ const nextBoundary = buf.indexOf(boundary, bodyStart)
1174
+ if (nextBoundary === -1) break
1175
+ // Body ends 2 bytes before boundary (\r\n)
1176
+ const bodyEnd = nextBoundary - 2
1177
+ const nameMatch = headers.match(/name="([^"]+)"/)
1178
+ if (nameMatch) {
1179
+ const name = nameMatch[1]
1180
+ const filenameMatch = headers.match(/filename="([^"]*)"/)
1181
+ if (filenameMatch) {
1182
+ // File field — keep as Buffer
1183
+ data[name] = buf.slice(bodyStart, bodyEnd)
1184
+ } else {
1185
+ // Text field — try to parse as JSON for numbers/booleans
1186
+ const val = buf.slice(bodyStart, bodyEnd).toString('utf-8')
1187
+ try { data[name] = JSON.parse(val) } catch (e) { data[name] = val }
1188
+ }
1189
+ }
1190
+ start = nextBoundary + boundary.length
1191
+ }
1192
+ return data
1193
+ }
1194
+
1195
+ // Convert column-oriented data {x: [1,2], y: [3,4]} to row-oriented [{x:1, y:3}, {x:2, y:4}]
1196
+ function columnsToRows (data) {
1197
+ if (!isObject(data)) return data
1198
+ const keys = Object.keys(data)
1199
+ if (keys.length === 0) return []
1200
+ const firstArray = data[keys[0]]
1201
+ if (!Array.isArray(firstArray)) return data
1202
+ // Ensure all values are arrays of the same length
1203
+ const len = firstArray.length
1204
+ if (!keys.every(k => Array.isArray(data[k]) && data[k].length === len)) return data
1205
+ const rows = []
1206
+ for (let i = 0; i < len; i++) {
1207
+ const row = {}
1208
+ keys.forEach(k => { row[k] = data[k][i] })
1209
+ rows.push(row)
1210
+ }
1211
+ return rows
1212
+ }
1213
+
1214
+ function createValidateFn (input, filtrexCompile, filtrexOptions) {
1215
+ if (input.validate) {
1216
+ const expr = input.validate.replace(/\'/g, '"')
1217
+ const f = filtrexCompile(expr, filtrexOptions)
1218
+ const msg = input.error || 'Invalid value'
1219
+ return function (value) {
1220
+ try {
1221
+ return f({ value }) ? null : msg
1222
+ } catch (e) {
1223
+ return msg
1224
+ }
1225
+ }
1226
+ } else if (input.required) {
1227
+ const msg = input.error || 'Required'
1228
+ return function (value) {
1229
+ if (value === null || value === undefined || value === '') return msg
1230
+ if (Array.isArray(value) && value.length === 0) return msg
1231
+ return null
1232
+ }
1233
+ }
1234
+ return null
1235
+ }
1236
+
1237
+ function runValidation (inputs, validateFunctions) {
1238
+ let hasErrors = false
1239
+ inputs.forEach((input, index) => {
1240
+ if (index < validateFunctions.length && validateFunctions[index]) {
1241
+ input._error = validateFunctions[index](input.value)
1242
+ if (input._error) hasErrors = true
1243
+ }
1244
+ })
1245
+ return hasErrors
1246
+ }
1247
+
1248
+ const EXT_TO_OUTPUT = {
1249
+ '.pdf': 'pdf',
1250
+ '.png': 'image', '.jpg': 'image', '.jpeg': 'image',
1251
+ '.gif': 'image', '.svg': 'image', '.webp': 'image',
1252
+ '.mp3': 'audio', '.wav': 'audio', '.ogg': 'audio', '.flac': 'audio',
1253
+ '.mp4': 'video', '.webm': 'video', '.mov': 'video',
1254
+ '.csv': 'table', '.tsv': 'table',
1255
+ '.md': 'markdown',
1256
+ '.html': 'html', '.htm': 'html',
1257
+ '.json': 'object',
1258
+ }
1259
+
1260
+ function fileExtToOutputType (filename) {
1261
+ if (typeof filename !== 'string') return 'code'
1262
+ const dotIdx = filename.lastIndexOf('.')
1263
+ if (dotIdx < 0) return 'code'
1264
+ const ext = filename.slice(dotIdx).toLowerCase().replace(/[?#].*$/, '')
1265
+ return EXT_TO_OUTPUT[ext] || 'code'
1266
+ }
1267
+
1268
+ function inferOutputType (key, value) {
1269
+ if (Array.isArray(value)) {
1270
+ if (value.length > 0 && typeof value[0] === 'object' && value[0] !== null) return 'table'
1271
+ if (value.length > 0 && typeof value[0] === 'string' && /\.(png|jpe?g|gif|svg|webp)([?#].*)?$/i.test(value[0])) return 'gallery'
1272
+ return 'object'
1273
+ }
1274
+ if (typeof value === 'string') {
1275
+ if (value.startsWith('data:image/')) return 'image'
1276
+ if (value.startsWith('data:audio/')) return 'audio'
1277
+ if (value.startsWith('data:video/')) return 'video'
1278
+ if (value.startsWith('data:application/pdf')) return 'pdf'
1279
+ if (/\.(png|jpe?g|gif|svg|webp)([?#].*)?$/i.test(value)) return 'image'
1280
+ if (/\.(mp3|wav|ogg|flac)([?#].*)?$/i.test(value)) return 'audio'
1281
+ if (/\.(mp4|webm|mov)([?#].*)?$/i.test(value)) return 'video'
1282
+ if (/\.pdf([?#].*)?$/i.test(value)) return 'pdf'
1283
+ if (/\.md([?#].*)?$/i.test(value)) return 'markdown'
1284
+ if (value.includes('\n') && value.length > 200) return 'code'
1285
+ return 'string'
1286
+ }
1287
+ if (typeof value === 'number' || typeof value === 'boolean') return 'string'
1288
+ if (typeof value === 'object' && value !== null) return 'object'
1289
+ return 'string'
1290
+ }
1291
+
928
1292
  module.exports = {
929
1293
  isObject,
930
1294
  loadFromDOM,
@@ -948,5 +1312,19 @@ module.exports = {
948
1312
  isCssImport,
949
1313
  isRelativeImport,
950
1314
  getUrlParam,
951
- coerceParam
1315
+ coerceParam,
1316
+ jseeInputsToJsonSchema,
1317
+ generateOpenAPISpec,
1318
+ serializeResult,
1319
+ parseMultipart,
1320
+ parseSSELine,
1321
+ toTypedArray,
1322
+ fromTypedArray,
1323
+ wrapTypedArrayInputs,
1324
+ collectTransferables,
1325
+ columnsToRows,
1326
+ createValidateFn,
1327
+ runValidation,
1328
+ fileExtToOutputType,
1329
+ inferOutputType
952
1330
  }
@@ -5,14 +5,102 @@ const component = {
5
5
  props: ['input'],
6
6
  emits: ['inchange'],
7
7
  components: { FilePicker },
8
+ data () {
9
+ return {
10
+ collapsed: this.input && this.input.collapsed === true,
11
+ activeTab: 0
12
+ }
13
+ },
14
+ computed: {
15
+ effectiveStyle () {
16
+ if (this.input.style) return this.input.style
17
+ if (this.input.collapsed !== undefined || this.input.label) return 'accordion'
18
+ return 'blocks'
19
+ }
20
+ },
8
21
  methods: {
9
22
  changeHandler () {
10
23
  if (this.input.reactive) {
11
24
  this.$emit('inchange')
12
25
  }
13
26
  },
27
+ toggleCollapsed () {
28
+ this.collapsed = !this.collapsed
29
+ },
30
+ autosize (e) {
31
+ const el = e.target
32
+ el.style.height = 'auto'
33
+ el.style.height = el.scrollHeight + 'px'
34
+ },
14
35
  call (method) {
15
36
  console.log('calling: ', method)
37
+ },
38
+ folderSelected (e) {
39
+ const files = Array.from(e.target.files)
40
+ this.input.value = files.map(f => ({
41
+ name: f.webkitRelativePath || f.name,
42
+ path: f.webkitRelativePath || f.name,
43
+ size: f.size,
44
+ type: f.type,
45
+ selected: true,
46
+ _file: f
47
+ }))
48
+ this.changeHandler()
49
+ },
50
+ folderDropped (e) {
51
+ const files = Array.from(e.dataTransfer.files)
52
+ this.input.value = files.map(f => ({
53
+ name: f.name,
54
+ path: f.name,
55
+ size: f.size,
56
+ type: f.type,
57
+ selected: true,
58
+ _file: f
59
+ }))
60
+ this.changeHandler()
61
+ },
62
+ rangeInput (e, which) {
63
+ const val = Number(e.target.value)
64
+ const v = this.input.value || [0, 100]
65
+ if (v[0] === v[1]) {
66
+ // Same point: direction determines which thumb moves
67
+ if (val > v[0]) this.input.value = [v[0], val]
68
+ else if (val < v[0]) this.input.value = [val, v[1]]
69
+ } else if (which === 'min') {
70
+ this.input.value = [Math.min(val, v[1]), v[1]]
71
+ } else {
72
+ this.input.value = [v[0], Math.max(val, v[0])]
73
+ }
74
+ this.changeHandler()
75
+ },
76
+ folderSelectOne (file) {
77
+ this.input.value.forEach(f => { f.selected = false })
78
+ file.selected = true
79
+ if (this.input.reactive) {
80
+ this.$emit('inchange')
81
+ }
82
+ },
83
+ folderSelectionChanged () {
84
+ if (this.input.reactive) {
85
+ this.$emit('inchange')
86
+ }
87
+ },
88
+ formatSize (bytes) {
89
+ if (!bytes) return ''
90
+ if (bytes < 1024) return bytes + ' B'
91
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
92
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
93
+ }
94
+ },
95
+ mounted () {
96
+ if (this.input.type === 'text' && this.input.value) {
97
+ this.$nextTick(() => {
98
+ const el = this.$el.querySelector('textarea')
99
+ if (el) {
100
+ el.style.height = 'auto'
101
+ el.style.height = el.scrollHeight + 'px'
102
+ }
103
+ })
16
104
  }
17
105
  }
18
106
  }