diff options
author | Julius Härtl <jus@bitgrid.net> | 2019-08-19 14:40:42 +0300 |
---|---|---|
committer | Julius Härtl <jus@bitgrid.net> | 2019-11-04 21:35:10 +0300 |
commit | d8cc1e80d6ce7401310eca1d532d731a4ff507b1 (patch) | |
tree | dab5621c440d42cddd42882bdd58d5a9738a993a | |
parent | f5e23e2566f3037c2434030d73b7e5a7f799c544 (diff) |
Rich workspace
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r-- | css/prosemirror.scss | 9 | ||||
-rw-r--r-- | src/EditorFactory.js | 10 | ||||
-rw-r--r-- | src/components/EditorWrapper.vue | 14 | ||||
-rw-r--r-- | src/components/MenuBar.vue | 16 | ||||
-rw-r--r-- | src/components/ReadOnlyEditor.vue | 5 | ||||
-rw-r--r-- | src/files.js | 41 | ||||
-rw-r--r-- | src/services/PollingBackend.js | 14 | ||||
-rw-r--r-- | src/services/SyncService.js | 1 | ||||
-rw-r--r-- | src/views/RichWorkspace.vue | 142 |
9 files changed, 236 insertions, 16 deletions
diff --git a/css/prosemirror.scss b/css/prosemirror.scss index 0cd43af70..7b47a6e9d 100644 --- a/css/prosemirror.scss +++ b/css/prosemirror.scss @@ -205,4 +205,11 @@ } } - +.editor__content p.is-empty:first-child::before { + content: attr(data-empty-text); + float: left; + color: #aaa; + pointer-events: none; + height: 0; + font-style: italic; +} diff --git a/src/EditorFactory.js b/src/EditorFactory.js index e7dda9e79..4f778fbf0 100644 --- a/src/EditorFactory.js +++ b/src/EditorFactory.js @@ -32,7 +32,8 @@ import { CodeBlock, CodeBlockHighlight, HorizontalRule, - History + History, + Placeholder } from 'tiptap-extensions' import { Strong, Italic, Strike } from './marks' import { Image, PlainTextDocument } from './nodes' @@ -75,7 +76,12 @@ const createEditor = ({ content, onInit, onUpdate, extensions, enableRichEditing new CodeBlock(), new ListItem(), new Link(), - new Image() + new Image(), + new Placeholder({ + emptyNodeClass: 'is-empty', + emptyNodeText: 'Write something …', + showOnlyWhenEditable: true + }) ] } else { richEditingExtensions = [ diff --git a/src/components/EditorWrapper.vue b/src/components/EditorWrapper.vue index 8abaa0a3c..9de97bdfa 100644 --- a/src/components/EditorWrapper.vue +++ b/src/components/EditorWrapper.vue @@ -47,7 +47,10 @@ </MenuBar> <div class="editor__content"> <MenuBubble v-if="!readOnly && isRichEditor" :editor="tiptap" /> - <EditorContent v-show="initialLoading" :editor="tiptap" /> + <EditorContent v-show="initialLoading" + class="editor__content" + :editor="tiptap" + spellcheck="false" /> </div> </div> <ReadOnlyEditor v-if="hasSyncCollission" @@ -107,6 +110,10 @@ export default { type: Boolean, default: false }, + autofocus: { + type: Boolean, + default: true + }, shareToken: { type: String, default: null @@ -347,7 +354,10 @@ export default { .on('stateChange', (state) => { if (state.initialLoading && !this.initialLoading) { this.initialLoading = true - this.tiptap.focus('start') + if (this.autofocus) { + this.tiptap.focus('start') + } + this.$emit('ready') } if (state.hasOwnProperty('dirty')) { this.dirty = state.dirty diff --git a/src/components/MenuBar.vue b/src/components/MenuBar.vue index 37822a76f..bbf8db126 100644 --- a/src/components/MenuBar.vue +++ b/src/components/MenuBar.vue @@ -21,8 +21,8 @@ --> <template> - <EditorMenuBar v-slot="{ commands, isActive }" :editor="editor"> - <div class="menubar"> + <EditorMenuBar v-slot="{ commands, isActive, focused }" :editor="editor"> + <div class="menubar autohide" :class="{ 'is-focused': focused }"> <div v-if="isRichEditor" ref="menubar" class="menubar-icons"> <template v-for="(icon, $index) in allIcons"> <button v-if="icon.class" @@ -180,7 +180,7 @@ export default { iconCount() { this.forceRecompute // eslint-disable-line this.windowWidth // eslint-disable-line - const menuBarWidth = this.$refs.menubar ? this.$refs.menubar.clientWidth : this.windowWidth - 200 + const menuBarWidth = this.$refs.menubar && this.$refs.menubar.clientWidth > 100 ? this.$refs.menubar.clientWidth : this.windowWidth - 200 const iconCount = Math.max((Math.floor(menuBarWidth / 44) - 2), 0) return iconCount } @@ -261,6 +261,16 @@ export default { z-index: 10010; // above modal-header so buttons are clickable background-color: var(--color-main-background-translucent); height: 44px; + + &.autohide { + visibility: hidden; + opacity: 0; + transition: visibility 0.2s 0.4s, opacity 0.2s 0.4s; + &.is-focused { + visibility: visible; + opacity: 1; + } + } .menubar-icons { flex-grow: 1; margin-left: calc((100% - 660px) / 2); diff --git a/src/components/ReadOnlyEditor.vue b/src/components/ReadOnlyEditor.vue index 244ea812b..4273f79a7 100644 --- a/src/components/ReadOnlyEditor.vue +++ b/src/components/ReadOnlyEditor.vue @@ -39,7 +39,7 @@ export default { }, isRichEditor: { type: Boolean, - default: false + default: true } }, data: () => { @@ -78,3 +78,6 @@ export default { } </style> +<style lang="scss"> + @import './../../css/prosemirror'; +</style> diff --git a/src/files.js b/src/files.js index 0ab1a7d32..30057d040 100644 --- a/src/files.js +++ b/src/files.js @@ -24,6 +24,8 @@ import FilesEditor from './components/FilesEditor' import PreviewPlugin from './files/PreviewPlugin' import { registerFileActionFallback, registerFileCreate } from './helpers/files' import { openMimetypesMarkdown, openMimetypesPlainText } from './helpers/mime' +import RichWorkspace from './views/RichWorkspace' +import Vue from 'vue' __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line __webpack_public_path__ = OC.linkTo('text', 'js/') // eslint-disable-line @@ -46,6 +48,45 @@ document.addEventListener('DOMContentLoaded', () => { }) +const FilesPlugin = { + + el: null, + + attach: (fileList) => { + if (fileList.id !== 'files') { + return + } + + FilesPlugin.el = document.createElement('div') + fileList.registerHeader({ + id: 'workspace', + el: FilesPlugin.el, + render: FilesPlugin.render.bind(FilesPlugin), + priority: 10 + }) + }, + + render: (fileList) => { + FilesPlugin.el.id = 'files-workspace-wrapper' + Vue.prototype.t = window.t + Vue.prototype.n = window.n + Vue.prototype.OCA = window.OCA + const View = Vue.extend(RichWorkspace) + const vm = new View({ + propsData: { + path: fileList.getCurrentDirectory() + } + }).$mount(FilesPlugin.el) + + fileList.$el.on('changeDirectory', data => { + vm.path = data.dir.toString() + // TODO Switch to path/README.md + }) + } +} + +OC.Plugins.register('OCA.Files.FileList', FilesPlugin) + OCA.Text = { Editor: FilesEditor } diff --git a/src/services/PollingBackend.js b/src/services/PollingBackend.js index 6a8f80c45..9101e3ded 100644 --- a/src/services/PollingBackend.js +++ b/src/services/PollingBackend.js @@ -66,7 +66,7 @@ class PollingBackend { } connect() { - this.fetcher = setInterval(this._fetchSteps.bind(this), this.fetchInterval) + this.fetcher = setInterval(this._fetchSteps.bind(this), 0) } _isPublic() { @@ -215,9 +215,9 @@ class PollingBackend { if (this.fetcher === 0) { return } - this.fetchInverval = FETCH_INTERVAL + this.fetchInterval = FETCH_INTERVAL clearInterval(this.fetcher) - this.fetcher = setInterval(this._fetchSteps.bind(this), this.fetchInverval) + this.fetcher = setInterval(this._fetchSteps.bind(this), this.fetchInterval) } @@ -225,18 +225,18 @@ class PollingBackend { if (this.fetcher === 0) { return } - this.fetchInverval = Math.min(this.fetchInverval * 2, FETCH_INTERVAL_MAX) + this.fetchInterval = Math.min(this.fetchInterval * 2, FETCH_INTERVAL_MAX) clearInterval(this.fetcher) - this.fetcher = setInterval(this._fetchSteps.bind(this), this.fetchInverval) + this.fetcher = setInterval(this._fetchSteps.bind(this), this.fetchInterval) } maximumRefetchTimer() { if (this.fetcher === 0) { return } - this.fetchInverval = FETCH_INTERVAL_SINGLE_EDITOR + this.fetchInterval = FETCH_INTERVAL_SINGLE_EDITOR clearInterval(this.fetcher) - this.fetcher = setInterval(this._fetchSteps.bind(this), this.fetchInverval) + this.fetcher = setInterval(this._fetchSteps.bind(this), this.fetchInterval) } carefulRetry() { diff --git a/src/services/SyncService.js b/src/services/SyncService.js index fdec914a1..cc63e980d 100644 --- a/src/services/SyncService.js +++ b/src/services/SyncService.js @@ -91,6 +91,7 @@ class SyncService { session: this.session }) return this._fetchDocument().then(({ data }) => { + this.emit('loaded', { document: this.document, session: this.session, diff --git a/src/views/RichWorkspace.vue b/src/views/RichWorkspace.vue new file mode 100644 index 000000000..ed7dd7223 --- /dev/null +++ b/src/views/RichWorkspace.vue @@ -0,0 +1,142 @@ +<!-- + - @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> + - + - @author Julius Härtl <jus@bitgrid.net> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> + +<template> + <div id="rich-workspace" :class="{'icon-loading': !loaded || !ready }"> + <div v-if="!file && loaded" class="empty-workspace" @click="createNew"> + <i>Click to enter notes, lists or links</i> + </div> + <EditorWrapper v-if="file" + v-show="ready" + :key="file.id" + :file-id="file.id" + :active="true" + :mime="file.mimetype" + :autofocus="autofocus" + @ready="ready=true" /> + </div> +</template> + +<script> +import EditorWrapper from '../components/EditorWrapper' +export default { + name: 'RichWorkspace', + components: { + EditorWrapper + }, + props: { + path: { + type: String, + required: true + } + }, + data() { + return { + file: null, + loaded: false, + ready: false, + autofocus: false + } + }, + watch: { + path: function() { + this.getFileInfo() + } + }, + async mounted() { + this.getFileInfo() + }, + methods: { + getFileInfo() { + this.loaded = false + this.autofocus = false + this.ready = false + OCA.Files.App.fileList.filesClient.getFileInfo(this.path + '/README.md').then((status, fileInfo) => { + this.file = fileInfo + this.editing = true + this.loaded = true + }).fail(() => { + this.file = null + this.loaded = true + this.ready = true + }) + }, + createNew() { + window.FileList.createFile('README.md', { scrollTo: false }).then((status, data) => { + this.getFileInfo() + this.autofocus = true + }) + } + } +} +</script> + +<style scoped> + #rich-workspace { + padding: 20px; + min-height: 141px; + } + .empty-workspace { + opacity: 0.7; + } + #rich-workspace::v-deep div[contenteditable=false] { + width: 100%; + padding: 0px; + background-color: var(--color-main-background); + opacity: 1; + border: none; + } + + #rich-workspace::v-deep #read-only-editor { + margin-top: 44px; + } + + #rich-workspace::v-deep #editor-container { + height: 100%; + position: unset !important; + } + + #rich-workspace::v-deep #editor-wrapper { + position: unset !important; + } + + #rich-workspace::v-deep #editor-wrapper .ProseMirror { + padding: 0px; + margin: 0; + } + + #rich-workspace::v-deep .menubar .menubar-icons { + margin-left: 0; + } + + #rich-workspace::v-deep .editor__content { + margin: 0; + max-width: 100%; + } + .component-fade-enter-active, .component-fade-leave-active { + transition: opacity .3s ease; + } + .component-fade-enter, .component-fade-leave-to + /* .component-fade-leave-active below version 2.1.8 */ { + opacity: 0; + } +</style> |