Skip to content

Commit

Permalink
fix: use decorations for heading anchors
Browse files Browse the repository at this point in the history
This avoids transactions that actually change the document state.

Fixes #5861.

Signed-off-by: Max <[email protected]>
  • Loading branch information
max-nextcloud committed Jun 17, 2024
1 parent 9523e0a commit a1d2552
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 230 deletions.
14 changes: 7 additions & 7 deletions cypress/e2e/sections.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Content Sections', () => {
cy.openFile(fileName, { force: true })
cy.getContent().type('# Heading 1{enter}')
cy.getContent()
.find('h1')
.find('h1 > a')
.should('have.attr', 'id')
.and('equal', 'h-heading-1')
cy.getContent()
Expand All @@ -61,7 +61,7 @@ describe('Content Sections', () => {
.and('equal', '#h-heading-1')
cy.getContent().type('{backspace}{backspace}2{enter}')
cy.getContent()
.find('h1')
.find('h1 > a')
.should('have.attr', 'id')
.and('equal', 'h-heading-2')
cy.getContent()
Expand All @@ -75,13 +75,13 @@ describe('Content Sections', () => {
cy.visitTestFolder()
cy.openFile('anchors.md')
cy.getContent()
.get('h2[id="h-bottom"]')
.get('h2 > a[id="h-bottom"]')
.should('not.be.inViewport')
cy.getContent()
.find('a[href="#h-bottom"]:not(.heading-anchor)')
.click()
cy.getContent()
.get('h2[id="h-bottom"]')
.get('h2 > a[id="h-bottom"]')
.should('be.inViewport')
})

Expand All @@ -92,15 +92,15 @@ describe('Content Sections', () => {
cy.getContent()
.type('# Heading 1{enter}')
cy.getContent()
.find('h1')
.find('h1 > a')
.should('have.attr', 'id')
.and('equal', 'h-heading-1')
cy.getContent()
.find('h1 [data-node-view-content]')
.find('h1')
.click({ force: true, position: 'center' })
cy.getActionEntry('headings').click()
cy.get('.v-popper__wrapper .open').getActionEntry('headings-h3').click()
cy.getContent().find('h3')
cy.getContent().find('h3 > a')
.should('have.attr', 'id')
.and('equal', 'h-heading-1')
})
Expand Down
9 changes: 2 additions & 7 deletions src/components/Editor/TableOfContents.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div data-text-el="editor-table-of-contents" :class="{ '--initial-render': initialRender }" class="editor--toc">
<ul class="editor--toc__list">
<li v-for="(heading) in headings"
:key="heading.uuid"
:key="heading.id"
:data-toc-level="heading.level"
class="editor--toc__item"
:class="{
Expand Down Expand Up @@ -45,12 +45,7 @@ export default {
},
methods: {
goto(heading) {
this.$editor
.chain()
.focus()
.setTextSelection(heading.position)
.scrollIntoView()
.run()
document.getElementById(heading.id).scrollIntoView()
this.$nextTick(() => {
window.location.hash = heading.id
Expand Down
2 changes: 1 addition & 1 deletion src/css/prosemirror.scss
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ div.ProseMirror {
opacity: 0;
padding: 0;
left: -1em;
bottom: 0;
top: 0;
font-size: max(1em, 16px);
position: absolute;
text-decoration: none;
Expand Down
2 changes: 1 addition & 1 deletion src/extensions/RichText.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import EmojiSuggestion from './../components/Suggestion/Emoji/suggestions.js'
import FrontMatter from './../nodes/FrontMatter.js'
import Gapcursor from '@tiptap/extension-gapcursor'
import HardBreak from './../nodes/HardBreak.js'
import Heading from '../nodes/Heading/index.js'
import Heading from '../nodes/Heading.js'
import HorizontalRule from '@tiptap/extension-horizontal-rule'
import Image from './../nodes/Image.js'
import ImageInline from './../nodes/ImageInline.js'
Expand Down
34 changes: 34 additions & 0 deletions src/nodes/Heading.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import TipTapHeading from '@tiptap/extension-heading'
import headingAnchor, { headingAnchorPluginKey } from '../plugins/headingAnchor.js'
import store from '../store/index.js'

const setHeadings = (val) => store.dispatch('text/setHeadings', val)

const Heading = TipTapHeading.extend({

addKeyboardShortcuts() {
return this.options.levels.reduce((items, level) => ({
...items,
[`Mod-Shift-${level}`]: () => this.editor.commands.toggleHeading({ level }),
}), {})
},

// sync heading data structure to the vuex store
onUpdate({ editor }) {
const headings = headingAnchorPluginKey
.getState(editor.state)?.headings ?? []
setHeadings(headings)
},

addProseMirrorPlugins() {
return [headingAnchor()]
},

})

export default Heading
68 changes: 0 additions & 68 deletions src/nodes/Heading/HeadingView.vue

This file was deleted.

72 changes: 0 additions & 72 deletions src/nodes/Heading/extractor.js

This file was deleted.

74 changes: 0 additions & 74 deletions src/nodes/Heading/index.js

This file was deleted.

48 changes: 48 additions & 0 deletions src/plugins/extractHeadings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { slugify } from './slug.js'

/**
* Extract heading data structure from doc
*
* @param {Document} doc - the prosemirror doc
* @return {Array} headings found in the doc
*/
export default function extractHeadings(doc) {
const counter = new Map()
const headings = []

const getId = text => {
const id = slugify(text)
if (counter.has(id)) {
const next = counter.get(id)
// increment counter
counter.set(id, next + 1)
return `h-${id}--${next}`
}
// define counter
counter.set(id, 1)
return 'h-' + id
}

doc.descendants((node, offset) => {
if (node.type.name !== 'heading') {
return
}
const text = node.textContent
// ignore empty headings
if (!text) return
const id = getId(text)
headings.push(Object.freeze({
level: node.attrs.level,
text,
id,
offset,
}))
})

return headings
}
Loading

0 comments on commit a1d2552

Please sign in to comment.