@jseeio/jsee 0.4.2 → 0.8.1

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 (66) hide show
  1. package/CHANGELOG.md +96 -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 +127 -32
  12. package/src/browser-bundle-node.js +9 -0
  13. package/src/cli.js +479 -67
  14. package/src/extended-imports.js +11 -0
  15. package/src/main.js +232 -44
  16. package/src/overlay.js +26 -1
  17. package/src/utils.js +386 -16
  18. package/templates/common-inputs.js +88 -0
  19. package/templates/common-outputs.js +340 -4
  20. package/templates/minimal-app.vue +367 -0
  21. package/templates/minimal-input.vue +573 -0
  22. package/templates/minimal-output.vue +426 -0
  23. package/templates/virtual-table.vue +194 -0
  24. package/.claude/settings.local.json +0 -15
  25. package/.eslintrc.js +0 -38
  26. package/AGENTS.md +0 -65
  27. package/CLAUDE.md +0 -5
  28. package/CNAME +0 -1
  29. package/_config.yml +0 -26
  30. package/dist/jsee.js +0 -1
  31. package/dump.sh +0 -23
  32. package/jest-puppeteer.config.js +0 -14
  33. package/jest.config.js +0 -8
  34. package/jest.unit.config.js +0 -8
  35. package/jsee.dump.txt +0 -5459
  36. package/load/index.html +0 -52
  37. package/templates/bulma-app.vue +0 -242
  38. package/templates/bulma-input.vue +0 -125
  39. package/templates/bulma-output.vue +0 -101
  40. package/test/arrow-main.html +0 -18
  41. package/test/arrow-worker.html +0 -18
  42. package/test/class.html +0 -22
  43. package/test/code.html +0 -25
  44. package/test/codew.html +0 -25
  45. package/test/example-class.js +0 -8
  46. package/test/example-sum.js +0 -3
  47. package/test/fixtures/lodash-like.js +0 -15
  48. package/test/fixtures/upload-sample.csv +0 -3
  49. package/test/importw.html +0 -28
  50. package/test/minimal.html +0 -14
  51. package/test/minimal1.html +0 -13
  52. package/test/minimal2.html +0 -15
  53. package/test/minimal3.html +0 -10
  54. package/test/minimal4.html +0 -22
  55. package/test/pipeline.html +0 -52
  56. package/test/python.html +0 -41
  57. package/test/runtime-arrow.html +0 -18
  58. package/test/string.html +0 -26
  59. package/test/stringw.html +0 -29
  60. package/test/sum.schema.json +0 -17
  61. package/test/sumw.schema.json +0 -15
  62. package/test/test-basic.test.js +0 -630
  63. package/test/test-python.test.js +0 -23
  64. package/test/unit/cli-fetch.test.js +0 -229
  65. package/test/unit/utils.test.js +0 -908
  66. 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
  }
@@ -776,12 +888,16 @@ function getName (code) {
776
888
  switch (typeof code) {
777
889
  case 'function':
778
890
  if (!code.name) return undefined
779
- // Arrow functions get an inferred .name from property assignment
780
- // (e.g. { code: (a) => a } code.name === "code") which is misleading.
781
- // Only trust .name when toString() confirms a real named declaration.
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.
782
894
  const src = code.toString().trimStart()
783
- if (src.startsWith('function') || src.startsWith('async function')) {
784
- return code.name
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
785
901
  }
786
902
  return undefined
787
903
  case 'string':
@@ -882,8 +998,9 @@ function validateSchema (schema) {
882
998
  const hasModel = typeof schema.model !== 'undefined'
883
999
  const hasView = (typeof schema.view !== 'undefined') || (typeof schema.render !== 'undefined')
884
1000
 
885
- if (!hasModel && !hasView) {
886
- 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`')
887
1004
  } else if (!hasModel && hasView) {
888
1005
  report.warnings.push('Schema has no `model`, using `view`/`render` only')
889
1006
  }
@@ -912,8 +1029,12 @@ function validateSchema (schema) {
912
1029
 
913
1030
  // Convert a URL parameter string to the appropriate type
914
1031
  function coerceParam (value, type, name) {
915
- if (type === 'number') return Number(value)
916
- 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
+ }
917
1038
  if (type === 'json') {
918
1039
  try { return JSON.parse(value) }
919
1040
  catch (e) { console.error(`Failed to parse JSON for input ${name}:`, e) }
@@ -923,6 +1044,7 @@ function coerceParam (value, type, name) {
923
1044
 
924
1045
  // Extract URL parameter value for an input, checking name, sanitized name, and aliases
925
1046
  function getUrlParam (urlParams, input) {
1047
+ if (!input.name) return null
926
1048
  if (urlParams.has(input.name)) return urlParams.get(input.name)
927
1049
  if (urlParams.has(sanitizeName(input.name))) return urlParams.get(sanitizeName(input.name))
928
1050
  if (!input.alias) return null
@@ -933,6 +1055,240 @@ function getUrlParam (urlParams, input) {
933
1055
  return null
934
1056
  }
935
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
+
936
1292
  module.exports = {
937
1293
  isObject,
938
1294
  loadFromDOM,
@@ -956,5 +1312,19 @@ module.exports = {
956
1312
  isCssImport,
957
1313
  isRelativeImport,
958
1314
  getUrlParam,
959
- 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
960
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
  }