diff options
author | Ferdinand Thiessen <rpm@fthiessen.de> | 2022-05-30 22:01:21 +0300 |
---|---|---|
committer | Vinicius Reis <vinicius.reis@nextcloud.com> | 2022-09-01 15:34:41 +0300 |
commit | ddc73ff28f76077c73b91b10f6671ffd383e8bf6 (patch) | |
tree | 976f261484c6005f887ded59b0f9ed1314a7f45f /src/nodes | |
parent | 9973d1fb5230d8c9fb46394a14fbdcc90d339a3a (diff) |
Add visible and clickable links for the page anchors
Handle inter-page links properly and use a heading view
for displaying links and link anchors. Implements #2173
Also fix link handling, to scroll to selected header when
clicking on an anchor link (`#some-id`).
Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
Diffstat (limited to 'src/nodes')
-rw-r--r-- | src/nodes/Heading/HeadingView.vue | 112 | ||||
-rw-r--r-- | src/nodes/Heading/index.js | 15 |
2 files changed, 127 insertions, 0 deletions
diff --git a/src/nodes/Heading/HeadingView.vue b/src/nodes/Heading/HeadingView.vue new file mode 100644 index 000000000..c2a728a3d --- /dev/null +++ b/src/nodes/Heading/HeadingView.vue @@ -0,0 +1,112 @@ +<!-- + - @copyright Copyright (c) 2022 + - + - @license AGPL-3.0-or-later + - + - 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> + <NodeViewWrapper ref="content" + :as="domElement" + v-bind="node.attrs" + tabindex="-1"> + <a aria-hidden="true" + class="anchor-link" + :href="href" + :title="t('text', 'Link to this section')" + @click.stop="click">{{ linkSymbol }}</a> + <NodeViewContent /> + </NodeViewWrapper> +</template> + +<script> +import Vue from 'vue' +import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2' +import { useEditorMixin } from '../../components/Editor.provider.js' + +export default Vue.extend({ + name: 'HeadingView', + components: { + NodeViewWrapper, + NodeViewContent, + }, + mixins: [useEditorMixin], + props: { + node: { + type: Object, + required: true, + }, + extension: { + type: Object, + required: true, + }, + }, + + computed: { + href() { + return `#${this.node.attrs.id}` + }, + domElement() { + const hasLevel = this.extension.options.levels.includes(this.node.attrs.level) + const level = hasLevel ? this.node.attrs.level : this.extension.options.levels[0] + return `h${level}` + }, + linkSymbol() { + return this.extension.options.linkSymbol + }, + t: () => window.t, + }, + + methods: { + click() { + this.$refs.content.$el.scrollIntoView() + window.location.hash = this.href + }, + }, +}) +</script> + +<style scoped lang="scss"> +div.ProseMirror { + /* Anchor links */ + h1, h2, h3, h4, h5, h6 { + .anchor-link { + opacity: 0; + padding: 0; + left: -18px; + font-size: max(1em, 16px); + position: absolute; + text-decoration: none; + transition-duration: .15s; + transition-property: opacity; + transition-timing-function: cubic-bezier(.4,0,.2,1); + } + + &:hover .anchor-link { + opacity: 0.45; + } + } + + // Shrink clickable area of anchor permalinks while editing + &.ProseMirror-focused[contenteditable="true"] { + h1,h2,h3,h4,h5,h6 { + .anchor-link { + width: fit-content; + } + } + } +} +</style> diff --git a/src/nodes/Heading/index.js b/src/nodes/Heading/index.js index ce7e73e78..63614da7e 100644 --- a/src/nodes/Heading/index.js +++ b/src/nodes/Heading/index.js @@ -1,8 +1,18 @@ import TipTapHeading from '@tiptap/extension-heading' +import { VueNodeViewRenderer } from '@tiptap/vue-2' import debounce from 'debounce' + +import HeadingView from './HeadingView.vue' import { setHeadings, extractHeadings } from './extractor.js' const Heading = TipTapHeading.extend({ + addOptions() { + return { + ...this.parent?.(), + linkSymbol: '#', + } + }, + addAttributes() { return { ...this.parent(), @@ -16,6 +26,11 @@ const Heading = TipTapHeading.extend({ }, } }, + + addNodeView() { + return VueNodeViewRenderer(HeadingView) + }, + addKeyboardShortcuts() { return this.options.levels.reduce((items, level) => ({ ...items, |