@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.
- package/README.md +144 -0
- package/client-lib/base-image-name.mjs +41 -0
- package/client-lib/data-to-image.mjs +45 -0
- package/client-lib/dynamic-load.mjs +21 -0
- package/client-lib/file-select-dialog.mjs +70 -0
- package/client-lib/form-answer-dialog.mjs +65 -0
- package/client-lib/format-bytes.mjs +10 -0
- package/client-lib/get-extension-from-mime.mjs +17 -0
- package/client-lib/get-file-image-stats.mjs +17 -0
- package/client-lib/get-image-stats.mjs +11 -0
- package/client-lib/image-browser-view-methods/create-directory.mjs +23 -0
- package/client-lib/image-browser-view-methods/delete.mjs +100 -0
- package/client-lib/image-browser-view-methods/drag-and-drop.mjs +57 -0
- package/client-lib/image-browser-view-methods/file-obj-manipulation.mjs +190 -0
- package/client-lib/image-browser-view-methods/sink.mjs +20 -0
- package/client-lib/image-browser-view-methods/upload.mjs +159 -0
- package/client-lib/image-browser-view-methods/utils.mjs +108 -0
- package/client-lib/image-browser-view-methods/view-interactions.mjs +96 -0
- package/client-lib/image-browser-view.mjs +227 -0
- package/client-lib/image-resize.mjs +32 -0
- package/client-lib/info-dialog.mjs +36 -0
- package/client-lib/load-styles.mjs +14 -0
- package/client-lib/make-image-set.mjs +75 -0
- package/client-lib/name-parts.mjs +24 -0
- package/client-lib/styles-loaded.mjs +18 -0
- package/less/components.less +357 -0
- package/package.json +80 -0
- package/public/tree-file-browser/resources/css/tree-browser.css +394 -0
- package/public/tree-file-browser/resources/css/tree-browser.css.map +1 -0
- package/public/tree-file-browser/resources/js/tree-file-browser.js +2 -0
- package/public/tree-file-browser/resources/js/tree-file-browser.js.map +1 -0
- package/server-lib/initialize-tree-browser-resources.cjs +17 -0
- package/server-lib/initialize-tree-browser-resources.mjs +15 -0
- package/views/load-browser-views.js +13 -0
- package/views/webhandle-tree-image-browser/extension-pill.tri +1 -0
- package/views/webhandle-tree-image-browser/guilded-file-upload-form.tri +11 -0
- package/views/webhandle-tree-image-browser/guilded-image-upload-form.tri +33 -0
- package/views/webhandle-tree-image-browser/image-browser-frame.tri +122 -0
- package/views/webhandle-tree-image-browser/variant-choice-box.tri +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Webhandle / Tree Image Browser
|
|
2
|
+
|
|
3
|
+
Mostly client side html/css/js components to browse, select, upload, and delete files
|
|
4
|
+
on a server from a browser. It uses the [File Sink](https://www.npmjs.com/package/file-sink)
|
|
5
|
+
family of packages to provide access to server files.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @webhandle/tree-file-browser
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
This package can be used either as something built into a single page app or as a piece of
|
|
17
|
+
code loaded on demand.
|
|
18
|
+
|
|
19
|
+
### Single page app
|
|
20
|
+
|
|
21
|
+
Set up a file manager
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { ImageBrowserView, FileSelectDialog, loadStyles } from '@webhandle/tree-file-browser/client-lib/dynamic-load.mjs'
|
|
25
|
+
|
|
26
|
+
let treeHolder = document.querySelector('.webhandle-file-tree-image-browser')
|
|
27
|
+
if(treeHolder) {
|
|
28
|
+
loadStyles()
|
|
29
|
+
let imageBrowserView = new ImageBrowserView({
|
|
30
|
+
// The source of the files
|
|
31
|
+
sink: the file sink
|
|
32
|
+
// optional, an panel to post events
|
|
33
|
+
, eventNotificationPanel: eventPanel
|
|
34
|
+
// optional, the directory which to open to
|
|
35
|
+
, startingDirectory: 'img/empty'
|
|
36
|
+
})
|
|
37
|
+
imageBrowserView.appendTo(treeHolder)
|
|
38
|
+
imageBrowserView.render()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`loadStyles` causes css to be added to the page which support the browser. Different setups are possible,
|
|
44
|
+
but by default, the styles expect the content to be a child of an element with a class `webhandle-file-tree-image-browser`.
|
|
45
|
+
Alternatively, the styles can be built into the pages stylesheet by including the file
|
|
46
|
+
`@webhandle/tree-file-browser/less/components.less`. `loadStyles` attempts to determine if the css is already in place,
|
|
47
|
+
so there's no harm in calling it if the css has already been included.
|
|
48
|
+
|
|
49
|
+
Loading the css dynamically or loading the file browser dynamically requires that the compiled resources have been added to
|
|
50
|
+
files available to the browser. These are at `@webhandle/tree-file-browser/resources` and must be available at the url
|
|
51
|
+
`/@webhandle/tree-file-browser/resources`
|
|
52
|
+
|
|
53
|
+
If you're using webhandle, you can set up all the server side resources, including kapla-tree-on-page and the material icons,
|
|
54
|
+
by using:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import webhandle from "webhandle"
|
|
58
|
+
import initializeTreeBrowserResources from "@webhandle/tree-file-browser/server-lib/initialize-tree-browser-resources.mjs"
|
|
59
|
+
initializeTreeBrowserResources(webhandle)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or the equivalent common js include:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const webhandle = require('webhandle')
|
|
66
|
+
const initializeTreeBrowserResources = require("@webhandle/tree-file-browser/server-lib/initialize-tree-browser-resources.cjs")
|
|
67
|
+
initializeTreeBrowserResources(webhandle)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This code can also be used as a file selection dialog like:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { ImageBrowserView, FileSelectDialog, loadStyles } from '@webhandle/tree-file-browser/client-lib/dynamic-load.mjs'
|
|
74
|
+
let selectButton = document.querySelector('.select-image')
|
|
75
|
+
if(selectButton) {
|
|
76
|
+
selectButton.addEventListener('click', async function(evt) {
|
|
77
|
+
evt.preventDefault()
|
|
78
|
+
let diag = new FileSelectDialog({
|
|
79
|
+
sink: the-file-sink
|
|
80
|
+
, startingDirectory: 'img'
|
|
81
|
+
, imagesOnly: true
|
|
82
|
+
})
|
|
83
|
+
let result = await diag.open()
|
|
84
|
+
console.log(result)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
### Dynmaically load
|
|
92
|
+
|
|
93
|
+
Using it independently on a single page is also pretty easy
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<script type="module">
|
|
97
|
+
import { ImageBrowserView, loadStyles } from '/@webhandle/tree-file-browser/resources/js/tree-file-browser.js'
|
|
98
|
+
loadStyles()
|
|
99
|
+
|
|
100
|
+
let treeHolder = document.querySelector('.webhandle-file-tree-image-browser')
|
|
101
|
+
if(treeHolder) {
|
|
102
|
+
let imageBrowserView = new ImageBrowserView({
|
|
103
|
+
sink: the-file-sink
|
|
104
|
+
, startingDirectory: 'img'
|
|
105
|
+
})
|
|
106
|
+
imageBrowserView.appendTo(treeHolder)
|
|
107
|
+
imageBrowserView.render()
|
|
108
|
+
|
|
109
|
+
imageBrowserView.emitter.on('select', async function(evt) {
|
|
110
|
+
console.log(await imageBrowserView.getSelectedUrl())
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Selections
|
|
118
|
+
|
|
119
|
+
You can listen for selections like:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
imageBrowserView.emitter.on('select', async function(evt) {
|
|
123
|
+
console.log(await imageBrowserView.getSelectedUrl())
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Options
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
/**
|
|
131
|
+
* Construct a new file browser
|
|
132
|
+
* @param {object} options
|
|
133
|
+
* @param {FileSink} options.sink The file to use as a file source
|
|
134
|
+
* @param {boolean} [options.imagesOnly] Set to true if you would like to display only images
|
|
135
|
+
* @param {boolean} [options.allowFileSelection] Set to true so that selected files are marked
|
|
136
|
+
* @param {EventNotificationPanel} [options.eventNotificationPanel] The panel which status messages will be added to.
|
|
137
|
+
* @param {string} [options.startingDirectory] Opens to that directory path if it exists
|
|
138
|
+
* @param {boolean} [options.deleteWithoutConfirm] False by default
|
|
139
|
+
* @param {boolean} [options.ignoreGlobalEvents] False by default, if true it will not listen to events like paste or keypresses
|
|
140
|
+
* which occur on the document
|
|
141
|
+
* @param {Emitter} [options.emitter] Emitter for various file events
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* @param {File,string} file
|
|
5
|
+
*/
|
|
6
|
+
export default function baseImageName(file) {
|
|
7
|
+
let name
|
|
8
|
+
if(typeof file === 'string') {
|
|
9
|
+
name = file
|
|
10
|
+
}
|
|
11
|
+
else if(file instanceof File) {
|
|
12
|
+
name = file.name
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let parts = name.split('.')
|
|
16
|
+
|
|
17
|
+
if(parts.length > 1) {
|
|
18
|
+
parts.pop()
|
|
19
|
+
}
|
|
20
|
+
name = parts.join('.')
|
|
21
|
+
name = name.replace(/-@2x$/, '')
|
|
22
|
+
name = name.replace(/@2x$/, '')
|
|
23
|
+
name = name.replace(/-2x$/, '')
|
|
24
|
+
name = name.replace(/2x$/, '')
|
|
25
|
+
|
|
26
|
+
let chars = [name.substring(0, 1)]
|
|
27
|
+
for(let char of name.substring(1)) {
|
|
28
|
+
if(char.match(/[A-Z]/)) {
|
|
29
|
+
chars.push('-')
|
|
30
|
+
}
|
|
31
|
+
chars.push(char)
|
|
32
|
+
}
|
|
33
|
+
name = chars.join('')
|
|
34
|
+
|
|
35
|
+
name = name.toLowerCase()
|
|
36
|
+
name = name.replace(/[^1234567890a-z-]/g, '-')
|
|
37
|
+
name = name.replace(/--+/g, '-')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
return name
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Takes data, perhaps even an image, and resolves to a fully
|
|
4
|
+
* loaded image.
|
|
5
|
+
* @param {Blob,ArrayBuffer,string,Image} data
|
|
6
|
+
*/
|
|
7
|
+
export default async function dataToImage(data) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
let finalImage
|
|
10
|
+
let objectUrl
|
|
11
|
+
if (data instanceof Image) {
|
|
12
|
+
finalImage = data
|
|
13
|
+
}
|
|
14
|
+
else if (typeof data === 'string') {
|
|
15
|
+
finalImage = new Image();
|
|
16
|
+
finalImage.src = data;
|
|
17
|
+
}
|
|
18
|
+
// ArrayBuffer.isView(data) might be true or could be a Blob or File
|
|
19
|
+
else {
|
|
20
|
+
// let's make a blob first. This works if it's an ArrayBuffer of a Blob
|
|
21
|
+
let imageBlob = new Blob([data])
|
|
22
|
+
objectUrl = URL.createObjectURL(imageBlob)
|
|
23
|
+
|
|
24
|
+
finalImage = new Image()
|
|
25
|
+
finalImage.src = objectUrl
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function finish() {
|
|
29
|
+
if (objectUrl) {
|
|
30
|
+
URL.revokeObjectURL(objectUrl)
|
|
31
|
+
}
|
|
32
|
+
resolve(finalImage)
|
|
33
|
+
}
|
|
34
|
+
if (finalImage.complete) {
|
|
35
|
+
finish()
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// we'll have to wait till it's loaded
|
|
39
|
+
finalImage.addEventListener('load', () => {
|
|
40
|
+
finish()
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ibv from './image-browser-view.mjs';
|
|
2
|
+
import { FileSelectDialog as fsd } from './file-select-dialog.mjs'
|
|
3
|
+
|
|
4
|
+
import ls from './load-styles.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export let ImageBrowserView = ibv
|
|
9
|
+
export let FileSelectDialog = fsd
|
|
10
|
+
export let loadStyles = ls
|
|
11
|
+
|
|
12
|
+
if(typeof window !== 'undefined') {
|
|
13
|
+
window['@webhandle/tree-file-browser'] = {
|
|
14
|
+
ImageBrowserView
|
|
15
|
+
, FileSelectDialog
|
|
16
|
+
, loadStyles
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export default ImageBrowserView
|
|
20
|
+
|
|
21
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import Dialog from 'ei-dialog'
|
|
2
|
+
import ImageBrowserView from './image-browser-view.mjs'
|
|
3
|
+
|
|
4
|
+
export class FileSelectDialog extends Dialog {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
super(Object.assign({
|
|
7
|
+
title: 'Select A File'
|
|
8
|
+
, body: `<div class="webhandle-file-tree-image-browser" style="width: 87vw;"> </div>`
|
|
9
|
+
, afterOpen: function (bodyElement, dialog) {
|
|
10
|
+
|
|
11
|
+
let treeHolder = bodyElement.querySelector('.webhandle-file-tree-image-browser')
|
|
12
|
+
if (treeHolder) {
|
|
13
|
+
let options = {
|
|
14
|
+
sink: dialog.sink
|
|
15
|
+
, imagesOnly: dialog.imagesOnly
|
|
16
|
+
, eventNotificationPanel: dialog.eventNotificationPanel
|
|
17
|
+
, startingDirectory: dialog.startingDirectory
|
|
18
|
+
, deleteWithoutConfirm: dialog.deleteWithoutConfirm
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let imageBrowserView = this.imageBrowserView = new ImageBrowserView(options)
|
|
22
|
+
if(dialog._createAccessUrl) {
|
|
23
|
+
imageBrowserView._createAccessUrl = dialog._createAccessUrl
|
|
24
|
+
}
|
|
25
|
+
if(dialog._transformRelativeUrlToPublic) {
|
|
26
|
+
imageBrowserView._transformRelativeUrlToPublic = dialog._transformRelativeUrlToPublic
|
|
27
|
+
}
|
|
28
|
+
imageBrowserView.appendTo(treeHolder)
|
|
29
|
+
imageBrowserView.render()
|
|
30
|
+
|
|
31
|
+
imageBrowserView.emitter.on('select', async function (evt) {
|
|
32
|
+
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, options,
|
|
37
|
+
{
|
|
38
|
+
on: {
|
|
39
|
+
'.btn-ok': async () => {
|
|
40
|
+
let result = {
|
|
41
|
+
selection: this.imageBrowserView.getSelectedFiles()
|
|
42
|
+
}
|
|
43
|
+
result.url = await this.imageBrowserView.getSelectedUrl(result.selection)
|
|
44
|
+
this.resolve(result)
|
|
45
|
+
|
|
46
|
+
return true
|
|
47
|
+
},
|
|
48
|
+
'.mask': () => {
|
|
49
|
+
this.resolve()
|
|
50
|
+
return true
|
|
51
|
+
},
|
|
52
|
+
'.btn-cancel': () => {
|
|
53
|
+
this.resolve()
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async open() {
|
|
62
|
+
this.promise = new Promise((resolve, reject) => {
|
|
63
|
+
this.resolve = resolve
|
|
64
|
+
this.reject = reject
|
|
65
|
+
})
|
|
66
|
+
super.open()
|
|
67
|
+
|
|
68
|
+
return this.promise
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Dialog from 'ei-dialog'
|
|
2
|
+
import formValueInjector from 'form-value-injector'
|
|
3
|
+
import gatherFormData from '@webhandle/gather-form-data'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class FormAnswerDialog extends Dialog {
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} options Properties to create the dialog box. In addition to the properties from Dialog
|
|
10
|
+
* there those below.
|
|
11
|
+
* @param {Object} options.data The data which will be used to populate the controls in the dialog
|
|
12
|
+
*/
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(Object.assign({}, options,
|
|
15
|
+
{
|
|
16
|
+
on: {
|
|
17
|
+
'.btn-ok': () => {
|
|
18
|
+
this.resolve(this.gatherData())
|
|
19
|
+
return true
|
|
20
|
+
},
|
|
21
|
+
'.mask': () => {
|
|
22
|
+
this.resolve()
|
|
23
|
+
return true
|
|
24
|
+
},
|
|
25
|
+
'.btn-cancel': () => {
|
|
26
|
+
this.resolve()
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
}
|
|
32
|
+
))
|
|
33
|
+
if (this.afterOpen) {
|
|
34
|
+
this.afterOpenOriginal = this.afterOpen
|
|
35
|
+
}
|
|
36
|
+
this.afterOpen = function (bodyElement, self) {
|
|
37
|
+
if (this.data) {
|
|
38
|
+
bodyElement.innerHTML = formValueInjector(bodyElement.innerHTML, this.data)
|
|
39
|
+
}
|
|
40
|
+
let firstInput = bodyElement.querySelector('input, textarea')
|
|
41
|
+
if (firstInput) {
|
|
42
|
+
firstInput.focus()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (this.afterOpenOriginal) {
|
|
46
|
+
this.afterOpenOriginal(bodyElement, self)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
gatherData() {
|
|
51
|
+
let dialogBody = document.querySelector(this.getBodySelector())
|
|
52
|
+
return gatherFormData(dialogBody)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async open() {
|
|
56
|
+
this.promise = new Promise((resolve, reject) => {
|
|
57
|
+
this.resolve = resolve
|
|
58
|
+
this.reject = reject
|
|
59
|
+
})
|
|
60
|
+
super.open()
|
|
61
|
+
|
|
62
|
+
return this.promise
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
export default function formatBytes(bytes, decimals) {
|
|
3
|
+
if (bytes == 0)
|
|
4
|
+
return '0 Bytes'
|
|
5
|
+
var k = 1024,
|
|
6
|
+
dm = decimals || 2,
|
|
7
|
+
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
|
8
|
+
i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
9
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
let types = {
|
|
3
|
+
"image/png": "png"
|
|
4
|
+
, "image/jpeg": "jpg"
|
|
5
|
+
, "image/jpg": "jpg"
|
|
6
|
+
, "image/webp": "webp"
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export default function getExtension(mime) {
|
|
11
|
+
if (mime in types) {
|
|
12
|
+
return types[mime]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let ext = mime.split('/').pop()
|
|
16
|
+
return ext
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import dataToImage from './data-to-image.mjs'
|
|
2
|
+
import getImageStats from './get-image-stats.mjs'
|
|
3
|
+
import getExtension from './get-extension-from-mime.mjs'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param {File} file
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export default async function getFileImageStats(file) {
|
|
11
|
+
let source = await dataToImage(file)
|
|
12
|
+
let stats = await getImageStats(source)
|
|
13
|
+
stats.ratio = stats.width / stats.height
|
|
14
|
+
stats.ext = getExtension(file.type)
|
|
15
|
+
|
|
16
|
+
return stats
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FormAnswerDialog } from '../form-answer-dialog.mjs'
|
|
2
|
+
|
|
3
|
+
export function createDirectory(evt, selected) {
|
|
4
|
+
let dialog = new FormAnswerDialog({
|
|
5
|
+
title: 'Create Directory'
|
|
6
|
+
, body: '<label>Directory name <input type="text" name="name" /></label>'
|
|
7
|
+
})
|
|
8
|
+
let prom = dialog.open()
|
|
9
|
+
prom.then(async data => {
|
|
10
|
+
if (data) {
|
|
11
|
+
let directoryPath = this.currentNode.file.relPath + '/' + data.name
|
|
12
|
+
await this.sink.mkdir(directoryPath)
|
|
13
|
+
let file = await this.sink.getFullFileInfo(directoryPath)
|
|
14
|
+
let node = this._fileToKalpaNode(file)
|
|
15
|
+
this.tree.options.stream.emit('data', node)
|
|
16
|
+
let cur = this.tree.selected()
|
|
17
|
+
if (cur) {
|
|
18
|
+
this.tree.expand(cur.id)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { FormAnswerDialog } from '../form-answer-dialog.mjs'
|
|
2
|
+
|
|
3
|
+
export async function deleteFile(evt, selected) {
|
|
4
|
+
let sel = this.getSelectedFiles()
|
|
5
|
+
|
|
6
|
+
if (sel.files.length > 0) {
|
|
7
|
+
|
|
8
|
+
let files = sel.files
|
|
9
|
+
let names = sel.names
|
|
10
|
+
|
|
11
|
+
if (!this.deleteWithoutConfirm) {
|
|
12
|
+
let dialog = new FormAnswerDialog({
|
|
13
|
+
title: 'Delete File' + (files.length > 1 ? 's' : '')
|
|
14
|
+
, body: '<p>' + names.join(', ') + '</p>'
|
|
15
|
+
})
|
|
16
|
+
let prom = dialog.open()
|
|
17
|
+
let ans = await prom
|
|
18
|
+
if (!ans) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (let file of files) {
|
|
24
|
+
let path = file.relPath
|
|
25
|
+
let note
|
|
26
|
+
if (this.eventNotificationPanel) {
|
|
27
|
+
note = this.eventNotificationPanel.addNotification({
|
|
28
|
+
model: {
|
|
29
|
+
status: 'pending',
|
|
30
|
+
headline: `deleting ${file.name}`
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
await this.sink.rm(path)
|
|
35
|
+
if (this.eventNotificationPanel) {
|
|
36
|
+
note.remove()
|
|
37
|
+
note = this.eventNotificationPanel.addNotification({
|
|
38
|
+
model: {
|
|
39
|
+
status: 'success',
|
|
40
|
+
headline: `removed ${file.name}`
|
|
41
|
+
}
|
|
42
|
+
, ttl: 2000
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (let item of sel.boxes) {
|
|
47
|
+
item.remove()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.emitter.emit('delete', {
|
|
51
|
+
type: 'delete'
|
|
52
|
+
, selected: sel
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function deleteDirectory(evt, selected) {
|
|
57
|
+
let path = this.currentNode.file.relPath
|
|
58
|
+
let name = this.currentNode.file.name
|
|
59
|
+
|
|
60
|
+
if (!path) {
|
|
61
|
+
// probably the root, just cancel
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let dialog = new FormAnswerDialog({
|
|
66
|
+
title: 'Delete Directory'
|
|
67
|
+
, body: '<p>' + name + '</p>'
|
|
68
|
+
})
|
|
69
|
+
let prom = dialog.open()
|
|
70
|
+
let ans = await prom
|
|
71
|
+
if (!ans) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
let note
|
|
75
|
+
if (this.eventNotificationPanel) {
|
|
76
|
+
note = this.eventNotificationPanel.addNotification({
|
|
77
|
+
model: {
|
|
78
|
+
status: 'pending',
|
|
79
|
+
headline: `deleting ${name}`
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
await this.sink.rm(path, { recursive: true })
|
|
84
|
+
let curSelected = this.tree.selected()
|
|
85
|
+
let parent = this.tree.parent(curSelected)
|
|
86
|
+
|
|
87
|
+
this.tree.removeNode(curSelected)
|
|
88
|
+
this.tree.select(parent.id)
|
|
89
|
+
|
|
90
|
+
if (this.eventNotificationPanel) {
|
|
91
|
+
note.remove()
|
|
92
|
+
note = this.eventNotificationPanel.addNotification({
|
|
93
|
+
model: {
|
|
94
|
+
status: 'success',
|
|
95
|
+
headline: `removed ${name}`
|
|
96
|
+
}
|
|
97
|
+
, ttl: 2000
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
export function getDropCoverSelector() {
|
|
3
|
+
return '.img-drop-cover'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export async function handleDrop(evt, selected) {
|
|
7
|
+
let uploadType = 'literal'
|
|
8
|
+
let dropSquare = evt.target.closest('.drop-type')
|
|
9
|
+
if (dropSquare) {
|
|
10
|
+
if (dropSquare.classList.contains('guided-upload')) {
|
|
11
|
+
uploadType = 'guided'
|
|
12
|
+
}
|
|
13
|
+
else if (dropSquare.classList.contains('automatic')) {
|
|
14
|
+
uploadType = 'automatic'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this._cleanupDropDone()
|
|
19
|
+
evt.preventDefault()
|
|
20
|
+
let files = await this._getFilesFromEvent(evt)
|
|
21
|
+
this.uploadFiles(files, { uploadType })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isFileTypeDrag(evt) {
|
|
25
|
+
let fileType = true
|
|
26
|
+
if (evt.dataTransfer) {
|
|
27
|
+
if (evt.dataTransfer.items[0].kind === 'string') {
|
|
28
|
+
fileType = false
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fileType
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function dragEnter(evt, selected) {
|
|
36
|
+
let overlay = this.isFileTypeDrag(evt)
|
|
37
|
+
if (overlay) {
|
|
38
|
+
this.overCount++
|
|
39
|
+
this.el.querySelector(this.getDropCoverSelector()).classList.add('file-dropping')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function dragLeave(evt, selected) {
|
|
43
|
+
if (this.isFileTypeDrag(evt)) {
|
|
44
|
+
this.overCount--
|
|
45
|
+
if (this.overCount == 0) {
|
|
46
|
+
this._cleanupDropDone()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function dragOver(evt, selected) {
|
|
51
|
+
evt.preventDefault()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function _cleanupDropDone() {
|
|
55
|
+
this.overCount = 0;
|
|
56
|
+
[...this.el.querySelectorAll('.file-dropping')].forEach(cover => cover.classList.remove('file-dropping'))
|
|
57
|
+
}
|