diff --git a/src/web/app/desktop/-tags/drive/browser.tag b/src/web/app/desktop/views/components/drive.vue similarity index 51% rename from src/web/app/desktop/-tags/drive/browser.tag rename to src/web/app/desktop/views/components/drive.vue index 7aaedab82..5d398dab9 100644 --- a/src/web/app/desktop/-tags/drive/browser.tag +++ b/src/web/app/desktop/views/components/drive.vue @@ -1,6 +1,7 @@ -<mk-drive-browser> +<template> +<div class="mk-drive"> <nav> - <div class="path" oncontextmenu={ pathOncontextmenu }> + <div class="path" @contextmenu.prevent.stop="() => {}"> <mk-drive-browser-nav-folder :class="{ current: folder == null }" folder={ null }/> <template each={ folder in hierarchyFolders }> <span class="separator">%fa:angle-right%</span> @@ -11,7 +12,15 @@ </div> <input class="search" type="search" placeholder=" %i18n:desktop.tags.mk-drive-browser.search%"/> </nav> - <div class="main { uploading: uploads.length > 0, fetching: fetching }" ref="main" onmousedown={ onmousedown } ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop } oncontextmenu={ oncontextmenu }> + <div class="main { uploading: uploads.length > 0, fetching: fetching }" + ref="main" + @mousedown="onMousedown" + @dragover.prevent.stop="onDragover" + @dragenter.prevent="onDragenter" + @dragleave="onDragleave" + @drop.prevent.stop="onDrop" + @contextmenu.prevent.stop="onContextmenu" + > <div class="selection" ref="selection"></div> <div class="contents" ref="contents"> <div class="folders" ref="foldersContainer" v-if="folders.length > 0"> @@ -44,323 +53,142 @@ </div> </div> <div class="dropzone" v-if="draghover"></div> - <mk-uploader ref="uploader"/> - <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" onchange={ changeFileInput }/> - <style lang="stylus" scoped> - :scope - display block + <mk-uploader @change="onChangeUploaderUploads" @uploaded="onUploaderUploaded"/> + <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/> +</div> +</template> - > nav - display block - z-index 2 - width 100% - overflow auto - font-size 0.9em - color #555 - background #fff - //border-bottom 1px solid #dfdfdf - box-shadow 0 1px 0 rgba(0, 0, 0, 0.05) +<script lang="ts"> +import Vue from 'vue'; +import contains from '../../../common/scripts/contains'; +import dialog from '../../scripts/dialog'; +import inputDialog from '../../scripts/input-dialog'; - &, * - user-select none +export default Vue.extend({ + props: { + initFolder: { + required: false + }, + multiple: { + default: false + } + }, + data() { + return { + /** + * 現在の階層(フォルダ) + * * null でルートを表す + */ + folder: null, - > .path - display inline-block - vertical-align bottom - margin 0 - padding 0 8px - width calc(100% - 200px) - line-height 38px - white-space nowrap + files: [], + folders: [], + moreFiles: false, + moreFolders: false, + hierarchyFolders: [], + selectedFiles: [], + uploadings: [], + connection: null, + connectionId: null, - > * - display inline-block - margin 0 - padding 0 8px - line-height 38px - cursor pointer + /** + * ドロップされようとしているか + */ + draghover: false, - i - margin-right 4px + /** + * 自信の所有するアイテムがドラッグをスタートさせたか + * (自分自身の階層にドロップできないようにするためのフラグ) + */ + isDragSource: false, - * - pointer-events none - - &:hover - text-decoration underline - - &.current - font-weight bold - cursor default - - &:hover - text-decoration none - - &.separator - margin 0 - padding 0 - opacity 0.5 - cursor default - - > [data-fa] - margin 0 - - > .search - display inline-block - vertical-align bottom - user-select text - cursor auto - margin 0 - padding 0 18px - width 200px - font-size 1em - line-height 38px - background transparent - outline none - //border solid 1px #ddd - border none - border-radius 0 - box-shadow none - transition color 0.5s ease, border 0.5s ease - font-family FontAwesome, sans-serif - - &[data-active='true'] - background #fff - - &::-webkit-input-placeholder, - &:-ms-input-placeholder, - &:-moz-placeholder - color $ui-control-foreground-color - - > .main - padding 8px - height calc(100% - 38px) - overflow auto - - &, * - user-select none - - &.fetching - cursor wait !important - - * - pointer-events none - - > .contents - opacity 0.5 - - &.uploading - height calc(100% - 38px - 100px) - - > .selection - display none - position absolute - z-index 128 - top 0 - left 0 - border solid 1px $theme-color - background rgba($theme-color, 0.5) - pointer-events none - - > .contents - - > .folders - > .files - display flex - flex-wrap wrap - - > .folder - > .file - flex-grow 1 - width 144px - margin 4px - - > .padding - flex-grow 1 - pointer-events none - width 144px + 8px // 8px is margin - - > .empty - padding 16px - text-align center - color #999 - pointer-events none - - > p - margin 0 - - > .fetching - .spinner - margin 100px auto - width 40px - height 40px - text-align center - - animation sk-rotate 2.0s infinite linear - - .dot1, .dot2 - width 60% - height 60% - display inline-block - position absolute - top 0 - background-color rgba(0, 0, 0, 0.3) - border-radius 100% - - animation sk-bounce 2.0s infinite ease-in-out - - .dot2 - top auto - bottom 0 - animation-delay -1.0s - - @keyframes sk-rotate { 100% { transform: rotate(360deg); }} - - @keyframes sk-bounce { - 0%, 100% { - transform: scale(0.0); - } 50% { - transform: scale(1.0); - } - } - - > .dropzone - position absolute - left 0 - top 38px - width 100% - height calc(100% - 38px) - border dashed 2px rgba($theme-color, 0.5) - pointer-events none - - > mk-uploader - height 100px - padding 16px - background #fff - - > input - display none - - </style> - <script lang="typescript"> - import contains from '../../../common/scripts/contains'; - import dialog from '../../scripts/dialog'; - import inputDialog from '../../scripts/input-dialog'; - - this.mixin('i'); - this.mixin('api'); - - this.mixin('drive-stream'); - this.connection = this.driveStream.getConnection(); - this.connectionId = this.driveStream.use(); - - this.files = []; - this.folders = []; - this.hierarchyFolders = []; - this.selectedFiles = []; - - this.uploads = []; - - // 現在の階層(フォルダ) - // * null でルートを表す - this.folder = null; - - this.multiple = this.opts.multiple != null ? this.opts.multiple : false; - - // ドロップされようとしているか - this.draghover = false; - - // 自信の所有するアイテムがドラッグをスタートさせたか - // (自分自身の階層にドロップできないようにするためのフラグ) - this.isDragSource = false; - - this.on('mount', () => { - this.$refs.uploader.on('uploaded', file => { - this.addFile(file, true); - }); - - this.$refs.uploader.on('change-uploads', uploads => { - this.update({ - uploads: uploads - }); - }); - - this.connection.on('file_created', this.onStreamDriveFileCreated); - this.connection.on('file_updated', this.onStreamDriveFileUpdated); - this.connection.on('folder_created', this.onStreamDriveFolderCreated); - this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); - - if (this.opts.folder) { - this.move(this.opts.folder); - } else { - this.fetch(); - } - }); - - this.on('unmount', () => { - this.connection.off('file_created', this.onStreamDriveFileCreated); - this.connection.off('file_updated', this.onStreamDriveFileUpdated); - this.connection.off('folder_created', this.onStreamDriveFolderCreated); - this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); - this.driveStream.dispose(this.connectionId); - }); - - this.onStreamDriveFileCreated = file => { - this.addFile(file, true); + fetching: true }; + }, + mounted() { + this.connection = this.$root.$data.os.streams.driveStream.getConnection(); + this.connectionId = this.$root.$data.os.streams.driveStream.use(); - this.onStreamDriveFileUpdated = file => { + this.connection.on('file_created', this.onStreamDriveFileCreated); + this.connection.on('file_updated', this.onStreamDriveFileUpdated); + this.connection.on('folder_created', this.onStreamDriveFolderCreated); + this.connection.on('folder_updated', this.onStreamDriveFolderUpdated); + + if (this.initFolder) { + this.move(this.initFolder); + } else { + this.fetch(); + } + }, + beforeDestroy() { + this.connection.off('file_created', this.onStreamDriveFileCreated); + this.connection.off('file_updated', this.onStreamDriveFileUpdated); + this.connection.off('folder_created', this.onStreamDriveFolderCreated); + this.connection.off('folder_updated', this.onStreamDriveFolderUpdated); + this.$root.$data.os.streams.driveStream.dispose(this.connectionId); + }, + methods: { + onStreamDriveFileCreated(file) { + this.addFile(file, true); + }, + onStreamDriveFileUpdated(file) { const current = this.folder ? this.folder.id : null; if (current != file.folder_id) { this.removeFile(file); } else { this.addFile(file, true); } - }; - - this.onStreamDriveFolderCreated = folder => { + }, + onStreamDriveFolderCreated(folder) { this.addFolder(folder, true); - }; - - this.onStreamDriveFolderUpdated = folder => { + }, + onStreamDriveFolderUpdated(folder) { const current = this.folder ? this.folder.id : null; if (current != folder.parent_id) { this.removeFolder(folder); } else { this.addFolder(folder, true); } - }; - - this.onmousedown = e => { + }, + onChangeUploaderUploads(uploads) { + this.uploadings = uploads; + }, + onUploaderUploaded(file) { + this.addFile(file, true); + }, + onMousedown(e): any { if (contains(this.$refs.foldersContainer, e.target) || contains(this.$refs.filesContainer, e.target)) return true; - const rect = this.$refs.main.getBoundingClientRect(); + const main = this.$refs.main as any; + const selection = this.$refs.selection as any; - const left = e.pageX + this.$refs.main.scrollLeft - rect.left - window.pageXOffset - const top = e.pageY + this.$refs.main.scrollTop - rect.top - window.pageYOffset + const rect = main.getBoundingClientRect(); + + const left = e.pageX + main.scrollLeft - rect.left - window.pageXOffset + const top = e.pageY + main.scrollTop - rect.top - window.pageYOffset const move = e => { - this.$refs.selection.style.display = 'block'; + selection.style.display = 'block'; - const cursorX = e.pageX + this.$refs.main.scrollLeft - rect.left - window.pageXOffset; - const cursorY = e.pageY + this.$refs.main.scrollTop - rect.top - window.pageYOffset; + const cursorX = e.pageX + main.scrollLeft - rect.left - window.pageXOffset; + const cursorY = e.pageY + main.scrollTop - rect.top - window.pageYOffset; const w = cursorX - left; const h = cursorY - top; if (w > 0) { - this.$refs.selection.style.width = w + 'px'; - this.$refs.selection.style.left = left + 'px'; + selection.style.width = w + 'px'; + selection.style.left = left + 'px'; } else { - this.$refs.selection.style.width = -w + 'px'; - this.$refs.selection.style.left = cursorX + 'px'; + selection.style.width = -w + 'px'; + selection.style.left = cursorX + 'px'; } if (h > 0) { - this.$refs.selection.style.height = h + 'px'; - this.$refs.selection.style.top = top + 'px'; + selection.style.height = h + 'px'; + selection.style.top = top + 'px'; } else { - this.$refs.selection.style.height = -h + 'px'; - this.$refs.selection.style.top = cursorY + 'px'; + selection.style.height = -h + 'px'; + selection.style.top = cursorY + 'px'; } }; @@ -368,23 +196,13 @@ document.documentElement.removeEventListener('mousemove', move); document.documentElement.removeEventListener('mouseup', up); - this.$refs.selection.style.display = 'none'; + selection.style.display = 'none'; }; document.documentElement.addEventListener('mousemove', move); document.documentElement.addEventListener('mouseup', up); - }; - - this.pathOncontextmenu = e => { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - }; - - this.ondragover = e => { - e.preventDefault(); - e.stopPropagation(); - + }, + onDragover(e): any { // ドラッグ元が自分自身の所有するアイテムかどうか if (!this.isDragSource) { // ドラッグされてきたものがファイルだったら @@ -395,21 +213,14 @@ e.dataTransfer.dropEffect = 'none'; return false; } - }; - - this.ondragenter = e => { - e.preventDefault(); + }, + onDragenter(e) { if (!this.isDragSource) this.draghover = true; - }; - - this.ondragleave = e => { + }, + onDragleave(e) { this.draghover = false; - }; - - this.ondrop = e => { - e.preventDefault(); - e.stopPropagation(); - + }, + onDrop(e): any { this.draghover = false; // ドロップされてきたものがファイルだったら @@ -433,7 +244,7 @@ const file = obj.id; if (this.files.some(f => f.id == file)) return false; this.removeFile(file); - this.api('drive/files/update', { + this.$root.$data.os.api('drive/files/update', { file_id: file, folder_id: this.folder ? this.folder.id : null }); @@ -444,7 +255,7 @@ if (this.folder && folder == this.folder.id) return false; if (this.folders.some(f => f.id == folder)) return false; this.removeFolder(folder); - this.api('drive/folders/update', { + this.$root.$data.os.api('drive/folders/update', { folder_id: folder, parent_id: this.folder ? this.folder.id : null }).then(() => { @@ -464,32 +275,26 @@ } return false; - }; - - this.oncontextmenu = e => { - e.preventDefault(); - e.stopImmediatePropagation(); - - const ctx = riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-base-contextmenu')), { - browser: this - })[0]; - ctx.open({ - x: e.pageX - window.pageXOffset, - y: e.pageY - window.pageYOffset - }); + }, + onContextmenu(e) { + document.body.appendChild(new MkDriveContextmenu({ + propsData: { + browser: this, + x: e.pageX - window.pageXOffset, + y: e.pageY - window.pageYOffset + } + }).$mount().$el); return false; - }; - - this.selectLocalFile = () => { - this.$refs.fileInput.click(); - }; - - this.urlUpload = () => { + }, + selectLocalFile() { + (this.$refs.fileInput as any).click(); + }, + urlUpload() { inputDialog('%i18n:desktop.tags.mk-drive-browser.url-upload%', '%i18n:desktop.tags.mk-drive-browser.url-of-file%', null, url => { - this.api('drive/files/upload_from_url', { + this.$root.$data.os.api('drive/files/upload_from_url', { url: url, folder_id: this.folder ? this.folder.id : undefined }); @@ -499,34 +304,29 @@ text: '%i18n:common.ok%' }]); }); - }; - - this.createFolder = () => { + }, + createFolder() { inputDialog('%i18n:desktop.tags.mk-drive-browser.create-folder%', '%i18n:desktop.tags.mk-drive-browser.folder-name%', null, name => { - this.api('drive/folders/create', { + this.$root.$data.os.api('drive/folders/create', { name: name, folder_id: this.folder ? this.folder.id : undefined }).then(folder => { this.addFolder(folder, true); - this.update(); }); }); - }; - - this.changeFileInput = () => { - Array.from(this.$refs.fileInput.files).forEach(file => { + }, + onChangeFileInput() { + Array.from((this.$refs.fileInput as any).files).forEach(file => { this.upload(file, this.folder); }); - }; - - this.upload = (file, folder) => { + }, + upload(file, folder) { if (folder && typeof folder == 'object') folder = folder.id; - this.$refs.uploader.upload(file, folder); - }; - - this.chooseFile = file => { + (this.$refs.uploader as any).upload(file, folder); + }, + chooseFile(file) { const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id); if (this.multiple) { if (isAlreadySelected) { @@ -534,7 +334,6 @@ } else { this.selectedFiles.push(file); } - this.update(); this.$emit('change-selection', this.selectedFiles); } else { if (isAlreadySelected) { @@ -544,15 +343,15 @@ this.$emit('change-selection', [file]); } } - }; - - this.newWindow = folderId => { - riot.mount(document.body.appendChild(document.createElement('mk-drive-browser-window')), { - folder: folderId - }); - }; - - this.move = target => { + }, + newWindow(folderId) { + document.body.appendChild(new MkDriveWindow({ + propsData: { + folder: folderId + } + }).$mount().$el); + }, + move(target) { if (target == null) { this.goRoot(); return; @@ -560,11 +359,9 @@ target = target.id; } - this.update({ - fetching: true - }); + this.fetching = true; - this.api('drive/folders/show', { + this.$root.$data.os.api('drive/folders/show', { folder_id: target }).then(folder => { this.folder = folder; @@ -577,20 +374,17 @@ if (folder.parent) dive(folder.parent); - this.update(); this.$emit('open-folder', folder); this.fetch(); }); - }; - - this.addFolder = (folder, unshift = false) => { + }, + addFolder(folder, unshift = false) { const current = this.folder ? this.folder.id : null; if (current != folder.parent_id) return; if (this.folders.some(f => f.id == folder.id)) { const exist = this.folders.map(f => f.id).indexOf(folder.id); - this.folders[exist] = folder; - this.update(); + this.folders[exist] = folder; // TODO return; } @@ -599,18 +393,14 @@ } else { this.folders.push(folder); } - - this.update(); - }; - - this.addFile = (file, unshift = false) => { + }, + addFile(file, unshift = false) { const current = this.folder ? this.folder.id : null; if (current != file.folder_id) return; if (this.files.some(f => f.id == file.id)) { const exist = this.files.map(f => f.id).indexOf(file.id); - this.files[exist] = file; - this.update(); + this.files[exist] = file; // TODO return; } @@ -619,47 +409,42 @@ } else { this.files.push(file); } - - this.update(); - }; - - this.removeFolder = folder => { + }, + removeFolder(folder) { if (typeof folder == 'object') folder = folder.id; this.folders = this.folders.filter(f => f.id != folder); - this.update(); - }; - - this.removeFile = file => { + }, + removeFile(file) { if (typeof file == 'object') file = file.id; this.files = this.files.filter(f => f.id != file); - this.update(); - }; - - this.appendFile = file => this.addFile(file); - this.appendFolder = file => this.addFolder(file); - this.prependFile = file => this.addFile(file, true); - this.prependFolder = file => this.addFolder(file, true); - - this.goRoot = () => { + }, + appendFile(file) { + this.addFile(file); + }, + appendFolder(folder) { + this.addFolder(folder); + }, + prependFile(file) { + this.addFile(file, true); + }, + prependFolder(folder) { + this.addFolder(folder, true); + }, + goRoot() { // 既にrootにいるなら何もしない if (this.folder == null) return; - this.update({ - folder: null, - hierarchyFolders: [] - }); + this.folder = null; + this.hierarchyFolders = []; this.$emit('move-root'); this.fetch(); - }; - - this.fetch = () => { - this.update({ - folders: [], - files: [], - moreFolders: false, - moreFiles: false, - fetching: true - }); + }, + fetch() { + this.folders = []; + this.files = []; + this.moreFolders = false; + this.moreFiles = false; + this.fetching = true; let fetchedFolders = null; let fetchedFiles = null; @@ -668,7 +453,7 @@ const filesMax = 30; // フォルダ一覧取得 - this.api('drive/folders', { + this.$root.$data.os.api('drive/folders', { folder_id: this.folder ? this.folder.id : null, limit: foldersMax + 1 }).then(folders => { @@ -681,7 +466,7 @@ }); // ファイル一覧取得 - this.api('drive/files', { + this.$root.$data.os.api('drive/files', { folder_id: this.folder ? this.folder.id : null, limit: filesMax + 1 }).then(files => { @@ -698,24 +483,19 @@ if (flag) { fetchedFolders.forEach(this.appendFolder); fetchedFiles.forEach(this.appendFile); - this.update({ - fetching: false - }); + this.fetching = false; } else { flag = true; } }; - }; - - this.fetchMoreFiles = () => { - this.update({ - fetching: true - }); + }, + fetchMoreFiles() { + this.fetching = true; const max = 30; // ファイル一覧取得 - this.api('drive/files', { + this.$root.$data.os.api('drive/files', { folder_id: this.folder ? this.folder.id : null, limit: max + 1 }).then(files => { @@ -726,11 +506,205 @@ this.moreFiles = false; } files.forEach(this.appendFile); - this.update({ - fetching: false - }); + this.fetching = false; }); - }; + } + } +}); +</script> - </script> -</mk-drive-browser> +<style lang="stylus" scoped> +.mk-drive + + > nav + display block + z-index 2 + width 100% + overflow auto + font-size 0.9em + color #555 + background #fff + //border-bottom 1px solid #dfdfdf + box-shadow 0 1px 0 rgba(0, 0, 0, 0.05) + + &, * + user-select none + + > .path + display inline-block + vertical-align bottom + margin 0 + padding 0 8px + width calc(100% - 200px) + line-height 38px + white-space nowrap + + > * + display inline-block + margin 0 + padding 0 8px + line-height 38px + cursor pointer + + i + margin-right 4px + + * + pointer-events none + + &:hover + text-decoration underline + + &.current + font-weight bold + cursor default + + &:hover + text-decoration none + + &.separator + margin 0 + padding 0 + opacity 0.5 + cursor default + + > [data-fa] + margin 0 + + > .search + display inline-block + vertical-align bottom + user-select text + cursor auto + margin 0 + padding 0 18px + width 200px + font-size 1em + line-height 38px + background transparent + outline none + //border solid 1px #ddd + border none + border-radius 0 + box-shadow none + transition color 0.5s ease, border 0.5s ease + font-family FontAwesome, sans-serif + + &[data-active='true'] + background #fff + + &::-webkit-input-placeholder, + &:-ms-input-placeholder, + &:-moz-placeholder + color $ui-control-foreground-color + + > .main + padding 8px + height calc(100% - 38px) + overflow auto + + &, * + user-select none + + &.fetching + cursor wait !important + + * + pointer-events none + + > .contents + opacity 0.5 + + &.uploading + height calc(100% - 38px - 100px) + + > .selection + display none + position absolute + z-index 128 + top 0 + left 0 + border solid 1px $theme-color + background rgba($theme-color, 0.5) + pointer-events none + + > .contents + + > .folders + > .files + display flex + flex-wrap wrap + + > .folder + > .file + flex-grow 1 + width 144px + margin 4px + + > .padding + flex-grow 1 + pointer-events none + width 144px + 8px // 8px is margin + + > .empty + padding 16px + text-align center + color #999 + pointer-events none + + > p + margin 0 + + > .fetching + .spinner + margin 100px auto + width 40px + height 40px + text-align center + + animation sk-rotate 2.0s infinite linear + + .dot1, .dot2 + width 60% + height 60% + display inline-block + position absolute + top 0 + background-color rgba(0, 0, 0, 0.3) + border-radius 100% + + animation sk-bounce 2.0s infinite ease-in-out + + .dot2 + top auto + bottom 0 + animation-delay -1.0s + + @keyframes sk-rotate { 100% { transform: rotate(360deg); }} + + @keyframes sk-bounce { + 0%, 100% { + transform: scale(0.0); + } 50% { + transform: scale(1.0); + } + } + + > .dropzone + position absolute + left 0 + top 38px + width 100% + height calc(100% - 38px) + border dashed 2px rgba($theme-color, 0.5) + pointer-events none + + > .mk-uploader + height 100px + padding 16px + background #fff + + > input + display none + +</style>