123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- /**
- * FileExplorer.js
- *
- * @author realor
- */
- import { Panel } from './Panel.js'
- import { Controls } from './Controls.js'
- import { Dialog } from './Dialog.js'
- import { LoginDialog } from './LoginDialog.js'
- import { ServiceDialog } from './ServiceDialog.js'
- import { MessageDialog } from './MessageDialog.js'
- import { ConfirmDialog } from './ConfirmDialog.js'
- import { Toast } from './Toast.js'
- import { ServiceManager } from '../io/ServiceManager.js'
- import { FileService, Metadata, Result } from '../io/FileService.js'
- import { I18N } from '../i18n/I18N.js'
- class FileExplorer extends Panel {
- constructor(application, createContextButtons = true) {
- super(application)
- this.id = 'file_explorer'
- this.title = 'title.file_explorer'
- this.position = 'left'
- this.group = 'model' // service group
- this.minimumHeight = 200
- this.service = null // current service
- this.basePath = '/'
- this.entryName = ''
- this.entryType = null // COLLECTION or FILE
- this.showFileSize = true
- this.serviceElem = document.createElement('div')
- this.serviceElem.className = 'service_panel'
- this.headerElem = document.createElement('div')
- this.headerElem.className = 'header'
- this.homeButtonElem = Controls.addImageButton(this.headerElem, 'home', 'button.home', event => this.goHome(), 'image_button home')
- this.backButtonElem = Controls.addImageButton(this.headerElem, 'home', 'button.back', event => this.goBack(), 'image_button back')
- this.directoryElem = document.createElement('div')
- this.directoryElem.className = 'directory'
- this.entriesElem = document.createElement('ul')
- this.entriesElem.className = 'path_entries'
- this.footerElem = document.createElement('div')
- this.footerElem.className = 'footer'
- this.buttonsPanelElem = document.createElement('div')
- this.buttonsPanelElem.className = 'buttons_panel'
- this.bodyElem.appendChild(this.serviceElem)
- this.serviceElem.appendChild(this.headerElem)
- this.serviceElem.appendChild(this.entriesElem)
- this.serviceElem.appendChild(this.footerElem)
- this.headerElem.appendChild(this.homeButtonElem)
- this.headerElem.appendChild(this.backButtonElem)
- this.headerElem.appendChild(this.directoryElem)
- this.footerElem.appendChild(this.buttonsPanelElem)
- this.showButtonsPanel()
- if (createContextButtons) {
- this.addContextButtons()
- }
- }
- addContextButton(name, label, action, isVisible) {
- const buttonElem = Controls.addButton(this.buttonsPanelElem, name, label, action)
- buttonElem._isVisible = isVisible
- }
- addContextButtons() {
- this.addContextButton(
- 'open',
- 'button.open',
- () => this.openEntry(),
- () => this.isEntrySelected()
- )
- this.addCommonContextButtons()
- this.addServiceContextButtons()
- }
- addCommonContextButtons() {
- this.addContextButton(
- 'delete',
- 'button.delete',
- () => this.showDeleteDialog(),
- () => this.isEntrySelected()
- )
- this.addContextButton(
- 'folder',
- 'button.folder',
- () => this.showFolderDialog(),
- () => this.isDirectoryList()
- )
- this.addContextButton(
- 'upload',
- 'button.upload',
- () => this.showUploadDialog(),
- () => this.isDirectoryList()
- )
- this.addContextButton(
- 'download',
- 'button.download',
- () => this.download(this.basePath + '/' + this.entryName),
- () => this.isDirectoryList() && this.isFileEntrySelected()
- )
- }
- addServiceContextButtons() {
- this.addContextButton(
- 'add',
- 'button.add',
- () => this.showAddDialog(),
- () => this.isServiceList()
- )
- this.addContextButton(
- 'edit',
- 'button.edit',
- () => this.showEditDialog(),
- () => this.isServiceList() && this.isEntrySelected()
- )
- }
- isServiceList() {
- return this.service === null
- }
- isDirectoryList() {
- return this.service !== null
- }
- isEntrySelected() {
- return this.entryName !== ''
- }
- isFileEntrySelected() {
- return this.entryType === Metadata.FILE
- }
- isCollectionEntrySelected() {
- return this.entryType === Metadata.COLLECTION
- }
- showAddDialog() {
- const serviceTypes = ServiceManager.getTypesOf(FileService)
- let dialog = new ServiceDialog('title.add_cloud_service', serviceTypes)
- dialog.setI18N(this.application.i18n)
- dialog.onSave = (serviceType, name, description, url, username, password) => {
- const service = new ServiceManager.classes[serviceType]()
- service.name = name
- service.description = description
- service.url = url
- service.username = username
- service.password = password
- this.application.addService(service, this.group)
- this.showServices()
- }
- dialog.show()
- }
- showEditDialog() {
- const service = this.application.services[this.group][this.entryName]
- const serviceTypes = ServiceManager.getTypesOf(FileService)
- let dialog = new ServiceDialog('title.edit_cloud_service', serviceTypes, service.constructor.type, service.name, service.description, service.url, service.username, service.password)
- dialog.setI18N(this.application.i18n)
- dialog.serviceTypeSelect.disabled = true
- dialog.nameElem.readOnly = true
- dialog.onSave = (serviceType, name, description, url, username, password) => {
- service.description = description
- service.url = url
- service.username = username
- service.password = password
- this.application.addService(service, this.group)
- this.showServices()
- }
- dialog.show()
- }
- showDeleteDialog() {
- const application = this.application
- let name = this.entryName
- if (this.service === null) {
- ConfirmDialog.create('title.delete_cloud_service', 'question.delete_service', name)
- .setAction(() => {
- let service = application.services[this.group][name]
- application.removeService(service, this.group)
- this.entryName = ''
- this.entryType = null
- this.showServices()
- })
- .setAcceptLabel('button.delete')
- .setI18N(application.i18n)
- .show()
- } else {
- let question = this.entryType === Metadata.FILE ? 'question.delete_file' : 'question.delete_folder'
- ConfirmDialog.create('title.delete_from_cloud', question, name)
- .setAction(() => this.deletePath(this.basePath + '/' + name))
- .setAcceptLabel('button.delete')
- .setI18N(application.i18n)
- .show()
- }
- }
- showFolderDialog() {
- const application = this.application
- const dialog = new Dialog('title.create_folder_in_cloud')
- dialog.setSize(250, 130)
- dialog.setI18N(application.i18n)
- let nameElem = dialog.addTextField('folder_name', 'label.folder_name')
- nameElem.setAttribute('spellcheck', 'false')
- dialog.addButton('folder_accept', 'button.create', () => dialog.onAccept())
- dialog.addButton('folder_cancel', 'button.cancel', () => dialog.onCancel())
- dialog.onAccept = () => {
- this.makeFolder(this.basePath + '/' + nameElem.value)
- dialog.hide()
- }
- dialog.onCancel = () => {
- dialog.hide()
- }
- dialog.onShow = () => {
- nameElem.focus()
- }
- dialog.show()
- }
- showUploadDialog() {
- let inputFile = document.createElement('input')
- inputFile.type = 'file'
- inputFile.addEventListener('change', event => this.upload(inputFile.files))
- inputFile.click()
- }
- openPath(path, onSuccess) {
- this.showProgressBar('Reading...')
- this.service.open(
- path,
- result => this.handleOpenResult(path, result, onSuccess),
- data => this.setProgress(data.progress, data.message)
- )
- }
- savePath(path, data, onSuccess) {
- this.showProgressBar('Saving...')
- this.service.save(
- path,
- data,
- result => this.handleSaveResult(path, data, result, onSuccess),
- data => this.setProgress(data.progress, data.message)
- )
- }
- deletePath(path, onSuccess) {
- this.showProgressBar('Deleting...')
- this.service.remove(path, result => this.handleDeleteResult(path, result, onSuccess))
- }
- makeFolder(path, onSuccess) {
- this.showProgressBar('Creating folder...')
- this.service.makeCollection(path, result => this.handleMakeFolderResult(path, result, onSuccess))
- }
- download(path, onSuccess) {
- this.showProgressBar('Downloading file...')
- this.service.open(
- path,
- result => this.handleDownloadResult(path, result, onSuccess),
- data => this.setProgress(data.progress)
- )
- }
- upload(files, onSuccess) {
- if (files.length > 0) {
- const application = this.application
- let file = files[0]
- let reader = new FileReader()
- reader.onload = event => {
- const data = event.target.result
- const path = this.basePath + '/' + file.name
- this.showProgressBar('Uploading file...')
- this.service.save(
- path,
- data,
- result => {
- this.entryName = file.name
- this.entryType = Metadata.FILE
- this.handleUploadResult(files, result, onSuccess)
- },
- data => this.setProgress(data.progress)
- )
- }
- reader.readAsText(file)
- }
- }
- handleOpenResult(path, result, onSuccess) {
- this.showButtonsPanel()
- if (result.status === Result.OK) {
- if (onSuccess) onSuccess()
- if (result.entries) {
- this.entryName = ''
- this.entryType = null
- this.showDirectory(path, result)
- } else {
- this.openFile(this.service.url + path, result.data)
- }
- } else {
- this.handleError(
- result,
- false,
- () => this.openPath(path),
- () => {
- if (path === '/') this.service = null
- }
- )
- }
- }
- handleSaveResult(path, data, result, onSuccess) {
- const application = this.application
- this.showButtonsPanel()
- if (result.status === Result.OK) {
- if (onSuccess) onSuccess()
- Toast.create('message.file_saved')
- .setI18N(application.i18n)
- .show()
- // reload current directory
- this.openPath(this.basePath)
- } else {
- this.handleError(result, true, () => this.savePath(path, data, onSuccess))
- }
- }
- handleDeleteResult(path, result, onSuccess) {
- const application = this.application
- this.showButtonsPanel()
- if (result.status === Result.OK) {
- if (onSuccess) onSuccess()
- if (this.entryType === Metadata.COLLECTION) {
- Toast.create('message.folder_deleted')
- .setI18N(application.i18n)
- .show()
- } else {
- Toast.create('message.file_deleted')
- .setI18N(application.i18n)
- .show()
- }
- this.entryName = ''
- this.entryType = null
- // reload basePath
- this.openPath(this.basePath)
- } else {
- this.handleError(result, true, () => this.deletePath(path))
- }
- }
- handleMakeFolderResult(path, result, onSuccess) {
- const application = this.application
- this.showButtonsPanel()
- if (result.status === Result.OK) {
- if (onSuccess) onSuccess()
- Toast.create('message.folder_created')
- .setI18N(application.i18n)
- .show()
- this.entryName = ''
- this.entryType = null
- // reload basePath
- this.openPath(this.basePath)
- } else {
- this.handleError(result, true, () => this.makeFolder(path))
- }
- }
- handleDownloadResult(path, result, onSuccess) {
- this.showButtonsPanel()
- if (result.status === Result.OK) {
- if (onSuccess) onSuccess()
- const data = result.data
- if (this.downloadUrl) {
- window.URL.revokeObjectURL(this.downloadUrl)
- }
- const blob = new Blob([data], { type: 'application/octet-stream' })
- this.downloadUrl = window.URL.createObjectURL(blob)
- let linkElem = document.createElement('a')
- linkElem.download = this.entryName
- linkElem.target = '_blank'
- linkElem.href = this.downloadUrl
- linkElem.style.display = 'block'
- linkElem.click()
- } else {
- this.handleError(result, false, () => this.download(path))
- }
- }
- handleUploadResult(files, result, onSuccess) {
- const application = this.application
- this.showButtonsPanel()
- if (result.status === Result.OK) {
- if (onSuccess) onSuccess()
- Toast.create('message.file_saved')
- .setI18N(application.i18n)
- .show()
- // reload current directory
- this.openPath(this.basePath)
- } else {
- this.handleError(result, true, () => this.upload(files))
- }
- }
- showServices() {
- const application = this.application
- this.basePath = '/'
- const COLLECTION = Metadata.COLLECTION
- this.directoryElem.innerHTML = '/'
- this.entriesElem.innerHTML = ''
- let firstLink = null
- for (let serviceName in application.services[this.group]) {
- let service = application.services[this.group][serviceName]
- let entryElem = document.createElement('li')
- entryElem.className = 'entry service'
- entryElem.entryName = service.name
- let linkElem = document.createElement('a')
- linkElem.href = '#'
- linkElem.innerHTML = service.description || service.name
- linkElem.addEventListener('click', event => this.onEntry(service.name, COLLECTION))
- linkElem.addEventListener('dblclick', event => this.openEntry())
- entryElem.appendChild(linkElem)
- if (firstLink === null) firstLink = linkElem
- this.entriesElem.appendChild(entryElem)
- }
- this.highlight()
- this.updateButtons()
- if (firstLink) firstLink.focus()
- }
- showDirectory(path, result) {
- const application = this.application
- const service = this.service
- this.basePath = path
- const FILE = Metadata.FILE
- if (path === '/') {
- // service home
- this.directoryElem.innerHTML = service.description || service.name
- } else {
- this.directoryElem.innerHTML = result.metadata.name
- }
- let entries = result.entries
- entries.sort(this.entryComparator)
- this.entriesElem.innerHTML = ''
- let firstLink = null
- for (let entry of entries) {
- let entryElem = document.createElement('li')
- let className = 'entry ' + (entry.type === FILE ? 'file' : 'collection')
- entryElem.className = className
- entryElem.entryName = entry.name
- let linkElem = document.createElement('a')
- linkElem.href = '#'
- let label = entry.description
- let size = entry.size
- if (entry.type === FILE && size > 0 && this.showFileSize) {
- label += ' ('
- if (size > 1000000) label += (size / 1000000).toFixed(0) + ' Mb'
- else if (size > 1000) label += (size / 1000).toFixed(0) + ' Kb'
- else label += '1 Kb'
- label += ')'
- }
- linkElem.innerHTML = label
- linkElem.addEventListener('click', event => this.onEntry(entry.name, entry.type))
- linkElem.addEventListener('dblclick', event => this.openEntry())
- entryElem.appendChild(linkElem)
- if (firstLink === null) firstLink = linkElem
- this.entriesElem.appendChild(entryElem)
- }
- this.highlight()
- this.updateButtons()
- if (firstLink) firstLink.focus()
- }
- highlight() {
- const entriesElem = this.entriesElem
- for (let i = 0; i < entriesElem.childNodes.length; i++) {
- let childNode = entriesElem.childNodes[i]
- if (childNode.nodeName === 'LI') {
- if (childNode.entryName === this.entryName) {
- childNode.classList.add('selected')
- } else {
- childNode.classList.remove('selected')
- }
- }
- }
- }
- goHome() {
- this.entryName = ''
- this.entryType = null
- this.service = null
- this.showServices()
- }
- goBack() {
- if (this.service === null) return
- this.entryName = ''
- this.entryType = null
- if (this.basePath === '/') {
- this.service = null
- this.showServices()
- } else {
- const index = this.basePath.lastIndexOf('/')
- if (index <= 0) {
- this.openPath('/')
- } else {
- this.openPath(this.basePath.substring(0, index))
- }
- }
- }
- onEntry(entryName, entryType) {
- this.entryName = entryName
- this.entryType = entryType
- this.highlight()
- this.updateButtons()
- }
- openEntry() {
- let path
- if (this.service === null) {
- this.service = this.application.services[this.group][this.entryName]
- path = '/'
- } else {
- path = this.basePath
- if (!this.basePath.endsWith('/')) path += '/'
- path += this.entryName
- }
- this.openPath(path)
- }
- openFile(url, data) {
- this.application.progressBar.visible = false
- this.showButtonsPanel()
- }
- updateButtons() {
- const children = this.buttonsPanelElem.children
- let firstVisibleButton = null
- for (let child of children) {
- if (child._isVisible()) {
- child.style.display = 'inline'
- if (firstVisibleButton === null) firstVisibleButton = child
- } else {
- child.style.display = 'none'
- }
- }
- if (firstVisibleButton) {
- firstVisibleButton.focus()
- }
- }
- showButtonsPanel() {
- this.buttonsPanelElem.style.display = 'block'
- this.application.progressBar.visible = false
- }
- showProgressBar(message = '') {
- this.buttonsPanelElem.style.display = 'none'
- this.application.progressBar.message = message
- this.application.progressBar.progress = undefined
- this.application.progressBar.visible = true
- }
- setProgress(progress, message) {
- this.application.progressBar.progress = progress
- if (message) {
- this.application.progressBar.message = message
- }
- }
- entryComparator(a, b) {
- const COLLECTION = Metadata.COLLECTION
- const FILE = Metadata.FILE
- if (a.type === COLLECTION && b.type === FILE) return -1
- if (a.type === FILE && b.type === COLLECTION) return 1
- if (a.name < b.name) return -1
- if (a.name > b.name) return 1
- return 0
- }
- handleError(result, isWriteAction, onLogin, onFailed) {
- if (result.status === Result.INVALID_CREDENTIALS) {
- this.requestCredentials('message.invalid_credentials', onLogin, onFailed)
- } else if (result.status === Result.FORBIDDEN) {
- this.requestCredentials(isWriteAction ? 'message.action_denied' : 'message.access_denied', onLogin, onFailed)
- } else {
- if (onFailed) onFailed()
- MessageDialog.create('ERROR', result.message)
- .setClassName('error')
- .setI18N(this.application.i18n)
- .show()
- }
- }
- requestCredentials(message, onLogin, onFailed) {
- const loginDialog = new LoginDialog(this.application, message)
- loginDialog.login = (username, password) => {
- this.service.username = username
- this.service.password = password
- if (onLogin) onLogin()
- }
- loginDialog.onCancel = () => {
- loginDialog.hide()
- if (onFailed) onFailed()
- }
- loginDialog.show()
- }
- }
- export { FileExplorer }
|