Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/text.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulius Härtl <jus@bitgrid.net>2019-08-19 14:40:42 +0300
committerJulius Härtl <jus@bitgrid.net>2019-11-04 21:35:10 +0300
commitd8cc1e80d6ce7401310eca1d532d731a4ff507b1 (patch)
treedab5621c440d42cddd42882bdd58d5a9738a993a
parentf5e23e2566f3037c2434030d73b7e5a7f799c544 (diff)
Rich workspace
Signed-off-by: Julius Härtl <jus@bitgrid.net>
-rw-r--r--css/prosemirror.scss9
-rw-r--r--src/EditorFactory.js10
-rw-r--r--src/components/EditorWrapper.vue14
-rw-r--r--src/components/MenuBar.vue16
-rw-r--r--src/components/ReadOnlyEditor.vue5
-rw-r--r--src/files.js41
-rw-r--r--src/services/PollingBackend.js14
-rw-r--r--src/services/SyncService.js1
-rw-r--r--src/views/RichWorkspace.vue142
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>