diff options
-rw-r--r-- | css/style.scss | 1 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/components/Editor.vue | 285 | ||||
-rw-r--r-- | src/files.js | 4 | ||||
-rw-r--r-- | src/main.js | 1 | ||||
-rw-r--r-- | src/public.js | 2 | ||||
-rw-r--r-- | webpack.common.js | 7 |
7 files changed, 207 insertions, 96 deletions
diff --git a/css/style.scss b/css/style.scss index 226342e83..11afde3b4 100644 --- a/css/style.scss +++ b/css/style.scss @@ -358,6 +358,7 @@ li.ProseMirror-selectednode:after { border-radius: 4px; padding: 0; position: relative; + overflow: scroll; } div[contenteditable=true], diff --git a/package.json b/package.json index ac90384ee..a7e4b79c9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "lodash": "^4.17.11", "nextcloud-axios": "^0.1.3", - "nextcloud-vue": "^0.9.7", + "nextcloud-vue": "^0.11.2", "prosemirror-collab": "^1.1.1", "prosemirror-example-setup": "^1.0.1", "prosemirror-history": "^1.0.4", @@ -81,6 +81,7 @@ "stylelint": "^8.4.0", "stylelint-config-recommended-scss": "^3.3.0", "stylelint-webpack-plugin": "^0.10.5", + "url-loader": "^1.1.2", "vue-jest": "^3.0.4", "vue-loader": "^15.7.0", "vue-style-loader": "^4.1.1", diff --git a/src/components/Editor.vue b/src/components/Editor.vue index 14a5e7649..eca54d76a 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -38,93 +38,46 @@ <div id="editor"> <editor-menu-bar :editor="tiptap" v-slot="{ commands, isActive }"> <div class="menubar"> + <button class="icon-bold" :class="{ 'is-active': isActive.bold() }" @click="commands.bold"></button> + <button class="icon-italic" :class="{ 'is-active': isActive.italic() }" @click="commands.italic"></button> + <button class="icon-underline" :class="{ 'is-active': isActive.underline() }" @click="commands.underline"></button> + + <button :class="{ 'is-active': isActive.heading({ level: 1 }) }" @click="commands.heading({ level: 1 })">H1</button> + <button :class="{ 'is-active': isActive.heading({ level: 2 }) }" @click="commands.heading({ level: 2 })">H2</button> + <button :class="{ 'is-active': isActive.heading({ level: 3 }) }" @click="commands.heading({ level: 3 })">H3</button> <Actions> - <ActionButton - icon="icon-info" - :class="{ 'is-active': isActive.bold() }" - @click="commands.bold"> - B </ActionButton> + <ActionButton @click="commands.heading({ level: 4 })">H4</ActionButton> + <ActionButton @click="commands.heading({ level: 5 })">H5</ActionButton> + <ActionButton @click="commands.heading({ level: 6 })">H6</ActionButton> </Actions> - <button - class="menubar__button" - :class="{ 'is-active': isActive.bold() }" - @click="commands.bold" - > - B - </button> - - <button - class="menubar__button" - :class="{ 'is-active': isActive.italic() }" - @click="commands.italic" - > - I - </button> - - <button - class="menubar__button" - :class="{ 'is-active': isActive.heading({ level: 1 }) }" - @click="commands.heading({ level: 1 })" - > - H1 - </button> - - <button - class="menubar__button" - :class="{ 'is-active': isActive.heading({ level: 2 }) }" - @click="commands.heading({ level: 2 })" - > - H2 - </button> - - <button - class="menubar__button" - :class="{ 'is-active': isActive.heading({ level: 3 }) }" - @click="commands.heading({ level: 3 })" - > - H3 - </button> - - <button - class="menubar__button" - :class="{ 'is-active': isActive.bullet_list() }" - @click="commands.bullet_list" - > - - - </button> - <button - class="menubar__button" - :class="{ 'is-active': isActive.ordered_list() }" - @click="commands.ordered_list" - > - 1. - </button> - - <button - class="menubar__button" - :class="{ 'is-active': isActive.blockquote() }" - @click="commands.blockquote" - > - " - </button> - <button - class="menubar__button" - :class="{ 'is-active': isActive.code() }" - @click="commands.code" - > - {} - </button> - - <button - class="menubar__button" - :class="{ 'is-active': isActive.paragraph() }" - @click="commands.paragraph" - > - P - </button> + + <button class="icon-ul" :class="{ 'is-active': isActive.bullet_list() }" @click="commands.bullet_list"></button> + <button class="icon-ol" :class="{ 'is-active': isActive.ordered_list() }" @click="commands.ordered_list"></button> + <button class="icon-quote" :class="{ 'is-active': isActive.blockquote() }" @click="commands.blockquote"></button> + + <button class="icon-code" :class="{ 'is-active': isActive.code() }" @click="commands.code"></button> + <button class="icon-image" @click="showImagePrompt(commands.image)"></button> </div> </editor-menu-bar> - <editor-content class="editor__content" :editor="tiptap" /> + <editor-menu-bubble class="menububble" :editor="tiptap" @hide="hideLinkMenu" v-slot="{ commands, isActive, getMarkAttrs, menu }"> + <div class="menububble" :class="{ 'is-active': menu.isActive }" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"> + + <form class="menububble__form" v-if="linkMenuIsActive" @submit.prevent="setLinkUrl(commands.link, linkUrl)"> + <input class="menububble__input" type="text" v-model="linkUrl" placeholder="https://" ref="linkInput" @keydown.esc="hideLinkMenu"/> + <button class="menububble__button" @click="setLinkUrl(commands.link, null)" type="button"></button> + </form> + + <template v-else> + <button + class="menububble__button" + @click="showLinkMenu(getMarkAttrs('link'))" + :class="{ 'is-active': isActive.link() }" + ><span>{{ isActive.link() ? 'Update Link' : 'Add Link'}}</span></button> + </template> + + </div> + </editor-menu-bubble> + <editor-content class="editor__content" :editor="tiptap" /> </div> <read-only-editor v-if="hasSyncCollission" :content="syncError.data.outsideChange" /> </div> @@ -140,7 +93,7 @@ </template> <script> - import Vue from 'vue' +import Vue from 'vue' import debounce from 'lodash/debounce' import { EditorSync, ERROR_TYPE } from './../EditorSync' @@ -150,7 +103,7 @@ import { endpointUrl } from './../helpers' import { getVersion } from 'prosemirror-collab' import { defaultMarkdownParser, defaultMarkdownSerializer } from 'prosemirror-markdown' -import { Editor, EditorContent, EditorMenuBar } from 'tiptap' +import { Editor, EditorContent, EditorMenuBar, EditorMenuBubble } from 'tiptap' import { HardBreak, Heading, @@ -165,6 +118,7 @@ import { ListItem, Blockquote, CodeBlock, + Image, History, Collaboration, } from 'tiptap-extensions' @@ -189,6 +143,7 @@ export default { ReadOnlyEditor, EditorContent, EditorMenuBar, + EditorMenuBubble, ActionButton }, directives: { @@ -228,7 +183,10 @@ export default { dirty: false, initialLoading: false, lastSavedString: '', - syncError: null + syncError: null, + + linkUrl: null, + linkMenuIsActive: false, } }, computed: { @@ -329,14 +287,14 @@ export default { /** tiptap */ this.markdownit = MarkdownIt('commonmark', {html: false}); this.tiptap = new Editor({ - content: "<p>Hello world</p>", //this.markdownit.render(documentSource), + content: "<p> </p>", //this.markdownit.render(documentSource), onUpdate: ({state}) => { console.log(defaultMarkdownSerializer.serialize(state.doc)) this.syncService.state = state }, extensions: [ new HardBreak(), - new Heading({ levels: [1, 2, 3] }), + new Heading(), new Bold(), new Underline, new Strike, @@ -348,6 +306,7 @@ export default { new CodeBlock(), new ListItem, new Link, + new Image, new History(), new Collaboration({ // the initial version we start with @@ -425,7 +384,35 @@ export default { Vue.set(this.filteredSessions, session.userId, session) } } - } + }, + + showLinkMenu(attrs) { + this.linkUrl = attrs.href + this.linkMenuIsActive = true + this.$nextTick(() => { + this.$refs.linkInput.focus() + }) + }, + hideLinkMenu() { + this.linkUrl = null + this.linkMenuIsActive = false + }, + + setLinkUrl(command, url) { + command({ href: url }) + this.hideLinkMenu() + this.tiptap.focus() + }, + + showImagePrompt(command) { + const _command = command + OC.dialogs.filepicker('Insert an image', (file) => { + const src = OC.generateUrl('/core/preview.png?') + `file=${file}&x=1024&y=1024` + _command({ src }) + // TODO: check permissions + // TODO: check for available preview + }, false, false) + }, } } </script> @@ -508,8 +495,9 @@ export default { position: absolute; top: 0; right: 0; - z-index: 100; + z-index: 110; padding: 6px; + padding-right: 16px; display: flex; input, div { @@ -518,6 +506,123 @@ export default { } } + .menubar { + width: 100%; + position: fixed; + position: sticky; + top: 0; + z-index: 100; + background-color: var(--color-main-background-translucent); + } + + .menubar button { + width: 44px; + height: 44px; + background-size: 16px; + border: 0; + background-color: var(--color-main-background); + opacity: 0.6; + background-position: center center; + vertical-align: top; + &:hover, &:focus, &:active { + background-color: var(--color-background-dark); + } + &.is-active { + opacity: 1; + } + } + + .icon-bold { + background-image: url('./../../img/icons/bold.svg'); + } + .icon-italic { + background-image: url('./../../img/icons/italic.svg'); + } + .icon-underline { + background-image: url('./../../img/icons/underline.svg'); + } + .icon-link { + background-image: url('./../../img/icons/link.svg'); + } + .icon-ol { + background-image: url('./../../img/icons/ol.svg'); + } + .icon-ul { + background-image: url('./../../img/icons/ul.svg'); + } + .icon-hr { + background-image: url('./../../img/icons/hr.svg'); + } + .icon-quote { + background-image: url('./../../img/icons/quote.svg'); + } + .icon-paragraph { + background-image: url('./../../img/icons/paragraph.svg'); + } + .icon-code { + background-image: url('./../../img/icons/code.svg'); + } + .icon-image { + background-image: url('./../../img/icons/image.svg'); + } + + + $color-white: #fff; + $color-black: #000; + + .menububble { + position: absolute; + display: flex; + z-index: 220; + background: $color-black; + border-radius: 5px; + padding: 0.3rem; + margin-bottom: 0.5rem; + visibility: hidden; + opacity: 0; + transition: opacity 0.2s, visibility 0.2s; + + &.is-active { + opacity: 1; + visibility: visible; + } + + &__button { + display: inline-flex; + background: transparent; + border: 0; + color: $color-white; + padding: 0.2rem 0.5rem; + margin-right: 0.2rem; + border-radius: 3px; + cursor: pointer; + + &:last-child { + margin-right: 0; + } + + &:hover { + background-color: rgba($color-white, 0.1); + } + + &.is-active { + background-color: rgba($color-white, 0.2); + } + } + + &__form { + display: flex; + align-items: center; + } + + &__input { + font: inherit; + border: none; + background: transparent; + color: $color-white; + } + } + </style> <style lang="scss"> @import './../../css/style'; diff --git a/src/files.js b/src/files.js index ee936bc6f..b2a70a2d2 100644 --- a/src/files.js +++ b/src/files.js @@ -51,6 +51,10 @@ const newFileMenuPlugin = { OC.Plugins.register('OCA.Files.NewFileMenu', newFileMenuPlugin) documentReady(() => { + if (typeof OCA.Viewer === 'undefined') { + console.error('Viewer app is not installed') + return + } OCA.Viewer.registerHandler({ id: 'text', mimes: ['text/markdown'], diff --git a/src/main.js b/src/main.js index f663d615c..f1afc053e 100644 --- a/src/main.js +++ b/src/main.js @@ -3,6 +3,7 @@ import Vue from 'vue' import Editor from './components/Editor' __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line +__webpack_public_path__ = OC.linkTo('text', 'js/') // eslint-disable-line Vue.prototype.t = t Vue.prototype.OCA = OCA diff --git a/src/public.js b/src/public.js index 4ee978cd9..dda717a0a 100644 --- a/src/public.js +++ b/src/public.js @@ -3,6 +3,8 @@ import Editor from './components/Editor' import { documentReady } from './helpers' __webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line +__webpack_public_path__ = OC.linkTo('text', 'js') // eslint-disable-line +console.log(__webpack_public_path__) Vue.prototype.t = t Vue.prototype.OCA = OCA diff --git a/webpack.common.js b/webpack.common.js index b4f92be4a..2d2ba44a3 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -4,7 +4,7 @@ const VueLoaderPlugin = require('vue-loader/lib/plugin'); module.exports = { entry: { - type: path.join(__dirname, 'src', 'main.js'), + text: path.join(__dirname, 'src', 'main.js'), files: path.join(__dirname, 'src', 'files.js'), public: path.join(__dirname, 'src', 'public.js'), }, @@ -42,10 +42,7 @@ module.exports = { }, { test: /\.(png|jpg|gif|svg)$/, - loader: 'file-loader', - options: { - name: '[name].[ext]?[hash]' - } + loader: 'url-loader' } ] }, |