@webhandle/tree-file-browser 1.0.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 (39) hide show
  1. package/README.md +144 -0
  2. package/client-lib/base-image-name.mjs +41 -0
  3. package/client-lib/data-to-image.mjs +45 -0
  4. package/client-lib/dynamic-load.mjs +21 -0
  5. package/client-lib/file-select-dialog.mjs +70 -0
  6. package/client-lib/form-answer-dialog.mjs +65 -0
  7. package/client-lib/format-bytes.mjs +10 -0
  8. package/client-lib/get-extension-from-mime.mjs +17 -0
  9. package/client-lib/get-file-image-stats.mjs +17 -0
  10. package/client-lib/get-image-stats.mjs +11 -0
  11. package/client-lib/image-browser-view-methods/create-directory.mjs +23 -0
  12. package/client-lib/image-browser-view-methods/delete.mjs +100 -0
  13. package/client-lib/image-browser-view-methods/drag-and-drop.mjs +57 -0
  14. package/client-lib/image-browser-view-methods/file-obj-manipulation.mjs +190 -0
  15. package/client-lib/image-browser-view-methods/sink.mjs +20 -0
  16. package/client-lib/image-browser-view-methods/upload.mjs +159 -0
  17. package/client-lib/image-browser-view-methods/utils.mjs +108 -0
  18. package/client-lib/image-browser-view-methods/view-interactions.mjs +96 -0
  19. package/client-lib/image-browser-view.mjs +227 -0
  20. package/client-lib/image-resize.mjs +32 -0
  21. package/client-lib/info-dialog.mjs +36 -0
  22. package/client-lib/load-styles.mjs +14 -0
  23. package/client-lib/make-image-set.mjs +75 -0
  24. package/client-lib/name-parts.mjs +24 -0
  25. package/client-lib/styles-loaded.mjs +18 -0
  26. package/less/components.less +357 -0
  27. package/package.json +80 -0
  28. package/public/tree-file-browser/resources/css/tree-browser.css +394 -0
  29. package/public/tree-file-browser/resources/css/tree-browser.css.map +1 -0
  30. package/public/tree-file-browser/resources/js/tree-file-browser.js +2 -0
  31. package/public/tree-file-browser/resources/js/tree-file-browser.js.map +1 -0
  32. package/server-lib/initialize-tree-browser-resources.cjs +17 -0
  33. package/server-lib/initialize-tree-browser-resources.mjs +15 -0
  34. package/views/load-browser-views.js +13 -0
  35. package/views/webhandle-tree-image-browser/extension-pill.tri +1 -0
  36. package/views/webhandle-tree-image-browser/guilded-file-upload-form.tri +11 -0
  37. package/views/webhandle-tree-image-browser/guilded-image-upload-form.tri +33 -0
  38. package/views/webhandle-tree-image-browser/image-browser-frame.tri +122 -0
  39. package/views/webhandle-tree-image-browser/variant-choice-box.tri +25 -0
@@ -0,0 +1,190 @@
1
+ import condense from '@dankolz/webp-detection/lib/condense-image-variants.js'
2
+
3
+ export function createVariantValues(info) {
4
+ let variants = condense(info.children)
5
+ let variantValues = Object.values(variants)
6
+
7
+ let used = []
8
+ for (let variant of variantValues) {
9
+ used.push(...this._getAssociatedRealFiles(variant).map(variant => variant.name))
10
+ }
11
+
12
+ let remainingChildren = info.children.filter(item => {
13
+ return !used.includes(item.name)
14
+ })
15
+ .filter(item => !item.directory)
16
+
17
+ // Add thumbnails
18
+ for (let child of variantValues) {
19
+ child.thumbnailIcon = 'image'
20
+ if (child.preview) {
21
+ child.thumbnail = this._createAccessUrl(child.preview.file)
22
+ }
23
+ }
24
+
25
+ if (!this.imagesOnly) {
26
+ for (let file of remainingChildren) {
27
+ let info = {
28
+ file: file
29
+ , thumbnailIcon: 'description'
30
+ }
31
+ let name = file.name
32
+ info.ext = name.substring(name.lastIndexOf('.') + 1)
33
+ info.baseName = name.substring(0, name.lastIndexOf('.'))
34
+ variantValues.push(info)
35
+ }
36
+ }
37
+
38
+
39
+ // Determine extensions
40
+ for (let item of variantValues) {
41
+ item.extensions = this._determineExtensions(item)
42
+ item.sizes = this._determineSizes(item)
43
+ if (item.sizes[0] == item.sizes[1]) {
44
+ item.size = this._formatBytes(item.sizes[0])
45
+ }
46
+ else {
47
+ item.size = this._formatBytes(item.sizes[0]) + ' - ' + this._formatBytes(item.sizes[1])
48
+ }
49
+ }
50
+
51
+ variantValues.sort(this._compareVariants)
52
+ return variantValues
53
+ }
54
+
55
+ export async function _getFilesFromEvent(evt) {
56
+ let files = []
57
+
58
+ // items is the new interface we should use if that's available
59
+ if (evt.dataTransfer.items) {
60
+ let foundItems = [];
61
+ [...evt.dataTransfer.items].forEach((item, i) => {
62
+ foundItems.push(item)
63
+ })
64
+ for (let item of foundItems) {
65
+ if (item.kind === "file") {
66
+ if (item.webkitGetAsEntry) {
67
+ let entry = item.webkitGetAsEntry()
68
+ if (entry) {
69
+ // if there's no entry, it's probably not a file, so we'll just ignore
70
+ if (entry.isDirectory) {
71
+ continue
72
+
73
+ // Evenually we'll want to handle directories too, but for now we'll just go
74
+ // on with the other items
75
+
76
+ // var dirReader = entry.createReader()
77
+ // dirReader.readEntries(function (entries) {
78
+ // console.log(entries)
79
+ // })
80
+ }
81
+ else {
82
+ files.push(item.getAsFile())
83
+ }
84
+
85
+ }
86
+ }
87
+ else {
88
+ files.push(item.getAsFile())
89
+ }
90
+ }
91
+ else if (item instanceof File) {
92
+ // Maybe from a file input element
93
+ files.push(item)
94
+ }
95
+ }
96
+ } else {
97
+ [...evt.dataTransfer.files].forEach((file, i) => {
98
+ files.push(file)
99
+ })
100
+ }
101
+ return files
102
+ }
103
+
104
+ export function _getAssociatedRealFiles(variant) {
105
+ let files = []
106
+ if (variant.variants) {
107
+ files.push(...variant.variants.map(variant => variant.file))
108
+ }
109
+ else {
110
+ files.push(variant.file)
111
+ }
112
+ if (variant.definitionFile) {
113
+ files.push(variant.definitionFile)
114
+ }
115
+
116
+ return files
117
+ }
118
+
119
+ export function _createAccessUrl(file) {
120
+ return file.accessUrl
121
+ }
122
+
123
+
124
+ export function getSelectedFiles() {
125
+ let result = {
126
+ boxes: []
127
+ , variants: []
128
+ , files: []
129
+ , names: []
130
+ }
131
+ let currentSelected = this.el.querySelectorAll('.choice-boxes .variant-choice-box.selected')
132
+ if (currentSelected.length > 0) {
133
+ for (let sel of currentSelected) {
134
+ result.boxes.push(sel)
135
+ result.variants.push(sel.variant)
136
+ result.files.push(...this._getAssociatedRealFiles(sel.variant))
137
+ }
138
+ let names = result.files.map(file => file.name)
139
+ result.names.push(...names)
140
+ }
141
+
142
+ return result
143
+ }
144
+
145
+ export function _transformRelativeUrlToPublic(url) {
146
+ if(url.startsWith('/') == false) {
147
+ url = '/' + url
148
+ }
149
+ return url
150
+ }
151
+
152
+ export async function getSelectedUrl(selectedFiles) {
153
+ if(!selectedFiles) {
154
+ selectedFiles = this.getSelectedFiles()
155
+ }
156
+
157
+ if(selectedFiles.variants.length == 0) {
158
+ return
159
+ }
160
+
161
+ let variant = selectedFiles.variants[0]
162
+ let chosen
163
+ if(variant.primary) {
164
+ chosen = variant.primary.file
165
+ }
166
+ else {
167
+ chosen = variant.file
168
+ }
169
+
170
+ let base = this._transformRelativeUrlToPublic(chosen.relPath)
171
+
172
+ if(variant.definitionFile) {
173
+ let defData = await this.sink.read(variant.definitionFile.relPath)
174
+ try {
175
+ let data = JSON.parse(defData)
176
+ let sizes = data.displaySize.split('x')
177
+ base += `#format=webp2x&width=${sizes[0]}&height=${sizes[1]}`
178
+
179
+ if(data.altText) {
180
+ base += '&alt=' + encodeURIComponent(data.altText)
181
+ }
182
+ }
183
+ catch(e) {
184
+
185
+ }
186
+ }
187
+
188
+ return base
189
+
190
+ }
@@ -0,0 +1,20 @@
1
+
2
+ export async function _uploadData(name, data) {
3
+ let path = this.currentNode.file.relPath + '/' + this.sanitizeFileName(name)
4
+ await this.sink.write(path, data)
5
+ }
6
+
7
+ export async function findDirectories() {
8
+ return new Promise((resolve, reject) => {
9
+ let results = []
10
+ let events = this.sink.find({
11
+ file: false
12
+ })
13
+ events.on('data', (item) => {
14
+ results.push(item)
15
+ })
16
+ events.on('done', () => {
17
+ resolve(results)
18
+ })
19
+ })
20
+ }
@@ -0,0 +1,159 @@
1
+ import { FormAnswerDialog } from '../form-answer-dialog.mjs'
2
+ import baseImageName from '../base-image-name.mjs'
3
+ import makeImageSet from '../make-image-set.mjs'
4
+ import nameParts from '../name-parts.mjs'
5
+ import getFileImageStats from '../get-file-image-stats.mjs'
6
+ import { guidedImageUploadForm, guidedFileUploadForm } from '../../views/load-browser-views.js'
7
+
8
+
9
+ export async function _uploadGuidedImageFile(file) {
10
+ let baseFileName = baseImageName(file)
11
+ let stats = await getFileImageStats(file)
12
+
13
+ let data = {
14
+ nativeName: file.name
15
+ , name: baseFileName
16
+ , outputFormat: file.type
17
+ , stats: stats
18
+ , width: Math.floor(stats.width / 2)
19
+ }
20
+
21
+
22
+ let dialog = new FormAnswerDialog({
23
+ title: 'Upload File'
24
+ , body: guidedImageUploadForm(data)
25
+ , data: data
26
+ , dialogFrameClass: 'webhandle-file-tree-image-browser'
27
+ })
28
+ let prom = dialog.open()
29
+ let result = await prom
30
+
31
+ if (result) {
32
+ let makeImageData = {
33
+ baseFileName: result.name
34
+ , outputFormat: result.outputFormat
35
+ , singleDensityWidth: parseInt(result.width)
36
+ , altText: result.altText
37
+ }
38
+
39
+ let note = this._addPending(file)
40
+ let files = await makeImageSet(file, makeImageData)
41
+
42
+ for (let fileName of Object.keys(files)) {
43
+ await this._uploadData(fileName, files[fileName])
44
+ }
45
+
46
+ if (note) {
47
+ note.remove()
48
+ }
49
+ return true
50
+ }
51
+ }
52
+
53
+ export async function _uploadGuidedFile(file) {
54
+ let parts = nameParts(file)
55
+
56
+ let data = {
57
+ nativeName: file.name
58
+ , name: parts.join('.')
59
+ }
60
+
61
+
62
+ let dialog = new FormAnswerDialog({
63
+ title: 'Upload File'
64
+ , body: guidedFileUploadForm(data)
65
+ , data: data
66
+ , dialogFrameClass: 'webhandle-file-tree-image-browser'
67
+ })
68
+ let prom = dialog.open()
69
+ let result = await prom
70
+
71
+ if (result) {
72
+ let note = this._addPending(file)
73
+
74
+ await this._uploadData(result.name, file)
75
+
76
+ if (note) {
77
+ note.remove()
78
+ }
79
+ return true
80
+ }
81
+ }
82
+
83
+ export async function _uploadAutomaticImageFile(file) {
84
+ let note = this._addPending(file)
85
+ let parts = nameParts(file)
86
+ let baseFileName = parts[0]
87
+
88
+ let files = await makeImageSet(file, {
89
+ baseFileName: baseFileName,
90
+ outputFormat: file.type
91
+ })
92
+
93
+ for (let fileName of Object.keys(files)) {
94
+ await this._uploadData(fileName, files[fileName])
95
+ }
96
+
97
+ if (note) {
98
+ note.remove()
99
+ }
100
+ return true
101
+ }
102
+
103
+ export async function uploadFiles(files, { uploadType } = {}) {
104
+ for (let file of files) {
105
+
106
+ let uploaded = false
107
+ if (uploadType === 'guided' && this._isImageFile(file)) {
108
+ uploaded = await this._uploadGuidedImageFile(file)
109
+ }
110
+ else if (uploadType === 'guided') {
111
+ uploaded = await this._uploadGuidedFile(file)
112
+ }
113
+ else if (uploadType === 'automatic' && this._isImageFile(file)) {
114
+ uploaded = await this._uploadAutomaticImageFile(file)
115
+ }
116
+ else {
117
+ let note = this._addPending(file)
118
+ if (uploadType === 'automatic') {
119
+ let parts = nameParts(file)
120
+ await this._uploadData(parts.join('.'), file)
121
+ }
122
+ else {
123
+ await this._uploadData(file.name, file)
124
+ }
125
+ if (note) {
126
+ note.remove()
127
+ }
128
+ uploaded = true
129
+ }
130
+ if (this.eventNotificationPanel && uploaded) {
131
+ this.eventNotificationPanel.addNotification({
132
+ model: {
133
+ status: 'success',
134
+ headline: `uploaded ${file.name}`
135
+ }
136
+ , ttl: 2000
137
+ })
138
+ }
139
+ }
140
+ this.setCurrentNode(this.currentNode)
141
+ }
142
+
143
+
144
+ export function _uploadFileButton(evt, selected) {
145
+ this.el.querySelector(this.fileUploadSelector).click()
146
+ }
147
+
148
+ export async function _uploadFile(evt, selected) {
149
+ evt.preventDefault()
150
+ let input = this.el.querySelector(this.fileUploadSelector)
151
+ evt.dataTransfer = {
152
+ files: input.files
153
+ }
154
+ let files = await this._getFilesFromEvent(evt)
155
+ if(files.length > 0) {
156
+ this.uploadFiles(files, { uploadType: 'guided' })
157
+ input.value = ''
158
+ }
159
+ }
@@ -0,0 +1,108 @@
1
+
2
+ export function _join(...parts) {
3
+ parts = parts.filter(part => !!part)
4
+ let path = parts.join('/')
5
+ return path
6
+ }
7
+
8
+ export function _determineParentPath(path) {
9
+ if(!path) {
10
+ return ''
11
+ }
12
+ let parts = path.split('/')
13
+ parts.pop()
14
+ return parts.join('/')
15
+ }
16
+
17
+ export function _fileToKalpaNode(file) {
18
+ let node = {
19
+ id: this.idInd++
20
+ , label: file.name
21
+ , directory: file.directory
22
+ , file: file
23
+ , loaded: false
24
+ }
25
+
26
+ let parent = this.nodes[this._determineParentPath(file.relPath)]
27
+ this.nodes[file.relPath] = node
28
+
29
+ if (parent) {
30
+ node.parentId = parent.id
31
+ node.path = file.relPath
32
+ }
33
+
34
+ file.path = node.path
35
+ return node
36
+ }
37
+
38
+ export function _determineExtensions(variant) {
39
+ let extensions = new Set()
40
+ if (variant.variants) {
41
+ for (let imgVariant of variant.variants) {
42
+ extensions.add(imgVariant.ext)
43
+ }
44
+ }
45
+ else {
46
+ extensions.add(variant.ext)
47
+ }
48
+
49
+ let result = Array.from(extensions).filter(item => !!item)
50
+ result.sort((a, b) => {
51
+ return a.toLowerCase().localeCompare(b.toLowerCase())
52
+ })
53
+
54
+ return result
55
+ }
56
+
57
+ export function _determineSizes(variant) {
58
+ let min = 2000000000
59
+ let max = 0
60
+ if (variant.variants) {
61
+ for (let imgVariant of variant.variants) {
62
+ let size = imgVariant.file.stat.size
63
+ if (size > max) {
64
+ max = size
65
+ }
66
+ if (size < min) {
67
+ min = size
68
+ }
69
+ }
70
+ }
71
+ else {
72
+ let size = variant.file.stat.size
73
+ if (size > max) {
74
+ max = size
75
+ }
76
+ if (size < min) {
77
+ min = size
78
+ }
79
+ }
80
+ return [min, max]
81
+ }
82
+
83
+ export function _sortFiles(files) {
84
+ files.sort((one, two) => {
85
+ return one.relPath.toLowerCase().localeCompare(two.relPath.toLowerCase())
86
+ })
87
+
88
+ return files
89
+ }
90
+
91
+ export function _compareVariants(one, two) {
92
+ return one.baseName.toLowerCase().localeCompare(two.baseName.toLowerCase())
93
+ }
94
+
95
+ export function sanitizeFileName(name) {
96
+ return name.split('/').join('-').split('..').join('-')
97
+ }
98
+
99
+
100
+ export function _isImageFile(file) {
101
+ if (!file.type.startsWith('image')) {
102
+ return false
103
+ }
104
+ if (file.type.includes('jpeg') || file.type.includes('png') || file.type.includes('webp')) {
105
+ return true
106
+ }
107
+ return false
108
+ }
@@ -0,0 +1,96 @@
1
+ import { InfoDialog } from '../info-dialog.mjs'
2
+
3
+ export function changeFilesView(evt, selected) {
4
+ let choiceBoxes = this.el.querySelector('.choice-boxes')
5
+ let classes = [...selected.closest('.view-icons').querySelectorAll('button')].map(button => button.getAttribute('data-show-class'))
6
+ classes.forEach(item => {
7
+ choiceBoxes.classList.remove(item)
8
+ })
9
+ choiceBoxes.classList.add(selected.getAttribute('data-show-class'))
10
+ }
11
+
12
+ export function applyFilter(evt, selected) {
13
+ setTimeout(() => {
14
+ let value = this.el.querySelector('[name="filter"]').value
15
+ let allVariants = this.el.querySelectorAll('.choice-boxes .variant-choice-box')
16
+ for (let variant of allVariants) {
17
+ variant.classList.remove('hidden')
18
+ if (value) {
19
+ let searchString = variant.variant.baseName + variant.variant.extensions.join()
20
+ if (searchString.indexOf(value) < 0) {
21
+ variant.classList.add('hidden')
22
+ }
23
+ }
24
+ }
25
+ })
26
+ }
27
+
28
+ export function clearFilter(evt, selected) {
29
+ this.el.querySelector('[name="filter"]').value = ''
30
+ this.applyFilter()
31
+ }
32
+
33
+ export function selectVariant(evt, selected) {
34
+ let currentSelected = this.el.querySelectorAll('.choice-boxes .variant-choice-box.selected')
35
+ if (!evt.ctrlKey && !evt.shiftKey) {
36
+ for (let sel of currentSelected) {
37
+ sel.classList.remove('selected')
38
+ }
39
+ }
40
+
41
+ if (evt.shiftKey) {
42
+ let cur = selected
43
+ do {
44
+ if (cur.classList.contains('selected')) {
45
+ break
46
+ }
47
+ cur.classList.add('selected')
48
+ cur = cur.previousElementSibling
49
+ }
50
+ while (cur);
51
+ }
52
+ else {
53
+ selected.classList.toggle('selected')
54
+ }
55
+
56
+
57
+ let sel = this.getSelectedFiles()
58
+ this.emitter.emit('select', {
59
+ type: 'select'
60
+ , selected: sel
61
+ })
62
+ }
63
+
64
+
65
+
66
+ export function showVariantDetails(evt, selected) {
67
+ let choiceBox = selected.closest('.variant-choice-box')
68
+ let variant = choiceBox.variant
69
+
70
+ let files = this._getAssociatedRealFiles(variant)
71
+
72
+ let content = '<ul>'
73
+ for (let file of files) {
74
+ content += '<li><a target="_blank" href="' + file.accessUrl + '">'
75
+ content += file.name + '</a> - ' + this._formatBytes(file.stat.size)
76
+ content += '</li>'
77
+ }
78
+ content += '</ul>'
79
+
80
+ let dialog = new InfoDialog({
81
+ title: 'File Details: ' + variant.baseName
82
+ , body: content
83
+ , buttons: [
84
+ {
85
+ classes: 'btn btn-primary btn-ok',
86
+ label: 'OK'
87
+ }
88
+ ]
89
+ })
90
+ let prom = dialog.open()
91
+ prom.then(async data => {
92
+ if (data) {
93
+ }
94
+ })
95
+
96
+ }