diff --git a/cypress/e2e/nodes/Links.spec.js b/cypress/e2e/nodes/Links.spec.js index 40518b7b937..0bcf40d6f64 100644 --- a/cypress/e2e/nodes/Links.spec.js +++ b/cypress/e2e/nodes/Links.spec.js @@ -27,7 +27,7 @@ describe('test link marks', function() { describe('link bubble', function() { - function clickLink(link, options = {}) { + const clickLink = (link, options = {}) => { cy.getContent() .find(`a[href*="${link}"]`) .click(options) diff --git a/src/EditorFactory.js b/src/EditorFactory.js index 586654a4eaf..86feee804ca 100644 --- a/src/EditorFactory.js +++ b/src/EditorFactory.js @@ -32,10 +32,15 @@ const loadSyntaxHighlight = async (language) => { } } -const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, extensions, enableRichEditing, session, relativePath, isEmbedded = false }) => { - let defaultExtensions - if (enableRichEditing) { - defaultExtensions = [ +const editorProps = { + scrollMargin: 50, + scrollThreshold: 50, +} + +const createRichEditor = ({ extensions = [], session, relativePath, isEmbedded = false } = {}) => { + return new Editor({ + editorProps, + extensions: [ RichText.configure({ relativePath, isEmbedded, @@ -49,19 +54,23 @@ const createEditor = ({ language, onCreate = () => {}, onUpdate = () => {}, exte ], }), FocusTrap, - ] - } else { - defaultExtensions = [PlainText, CodeBlockLowlight.configure({ lowlight, defaultLanguage: language })] - } + ...extensions, + ], + }) +} +const createPlainEditor = ({ language, extensions = [] } = {}) => { return new Editor({ - onCreate, - onUpdate, - editorProps: { - scrollMargin: 50, - scrollThreshold: 50, - }, - extensions: defaultExtensions.concat(extensions || []), + editorProps, + extensions: [ + PlainText, + CodeBlockLowlight.configure({ + lowlight, + defaultLanguage: language, + exitOnTripleEnter: false, + }), + ...extensions, + ], }) } @@ -69,5 +78,4 @@ const serializePlainText = (doc) => { return doc.textContent } -export default createEditor -export { createEditor, serializePlainText, loadSyntaxHighlight } +export { createRichEditor, createPlainEditor, serializePlainText, loadSyntaxHighlight } diff --git a/src/components/Editor.vue b/src/components/Editor.vue index df97144a5f3..73b223a2a99 100644 --- a/src/components/Editor.vue +++ b/src/components/Editor.vue @@ -98,7 +98,12 @@ import { SyncService, ERROR_TYPE, IDLE_TIMEOUT } from './../services/SyncService import createSyncServiceProvider from './../services/SyncServiceProvider.js' import AttachmentResolver from './../services/AttachmentResolver.js' import { extensionHighlight } from '../helpers/mappings.js' -import { createEditor, serializePlainText, loadSyntaxHighlight } from './../EditorFactory.js' +import { + createRichEditor, + createPlainEditor, + serializePlainText, + loadSyntaxHighlight, +} from './../EditorFactory.js' import { createMarkdownSerializer } from './../extensions/Markdown.js' import markdownit from './../markdownit/index.js' @@ -401,10 +406,15 @@ export default { listenEditorEvents() { this.$editor.on('focus', this.onFocus) this.$editor.on('blur', this.onBlur) + this.$editor.on('create', this.onCreate) + this.$editor.on('update', this.onUpdate) }, + unlistenEditorEvents() { this.$editor.off('focus', this.onFocus) this.$editor.off('blur', this.onBlur) + this.$editor.off('create', this.onCreate) + this.$editor.off('update', this.onUpdate) }, listenSyncServiceEvents() { @@ -504,59 +514,46 @@ export default { this.$baseVersionEtag = document.baseVersionEtag this.hasConnectionIssue = false - const language = extensionHighlight[this.fileExtension] || this.fileExtension; - - (this.isRichEditor ? Promise.resolve() : loadSyntaxHighlight(language)) - .then(() => { - const session = this.currentSession - if (!this.$editor) { - this.$editor = createEditor({ - language, - relativePath: this.relativePath, - session, - onCreate: ({ editor }) => { - this.$syncService.startSync() - const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc) - this.emit('create:content', { - markdown: proseMirrorMarkdown, - }) - }, - onUpdate: ({ editor }) => { - // this.debugContent(editor) - const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc) - this.emit('update:content', { - markdown: proseMirrorMarkdown, - }) - }, - extensions: [ - Autofocus.configure({ - fileId: this.fileId, - }), - Collaboration.configure({ - document: this.$ydoc, - }), - CollaborationCursor.configure({ - provider: this.$providers[0], - user: { - name: session?.userId - ? session.displayName - : (session?.guestName || t('text', 'Guest')), - color: session?.color, - clientId: this.$ydoc.clientID, - }, - }), - ], - enableRichEditing: this.isRichEditor, - isEmbedded: this.isEmbedded, - }) - this.hasEditor = true - this.listenEditorEvents() - } else { - // $editor already existed. So this is a reconnect. - this.$syncService.startSync() - } + if (this.$editor) { + // $editor already existed. So this is a reconnect. + this.$syncService.startSync() + return + } + const session = this.currentSession + const extensions = [ + Autofocus.configure({ fileId: this.fileId }), + Collaboration.configure({ document: this.$ydoc }), + CollaborationCursor.configure({ + provider: this.$providers[0], + user: { + name: session?.userId + ? session.displayName + : (session?.guestName || t('text', 'Guest')), + color: session?.color, + clientId: this.$ydoc.clientID, + }, + }), + ] + if (this.isRichEditor) { + this.$editor = createRichEditor({ + relativePath: this.relativePath, + session, + extensions, + isEmbedded: this.isEmbedded, }) + this.hasEditor = true + this.listenEditorEvents() + } else { + const language = extensionHighlight[this.fileExtension] + || this.fileExtension + loadSyntaxHighlight(language) + .then(() => { + this.$editor = createPlainEditor({ language, extensions }) + this.hasEditor = true + this.listenEditorEvents() + }) + } }, @@ -571,6 +568,22 @@ export default { } }, + onCreate({ editor }) { + this.$syncService.startSync() + const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc) + this.emit('create:content', { + markdown: proseMirrorMarkdown, + }) + }, + + onUpdate({ editor }) { + // this.debugContent(editor) + const proseMirrorMarkdown = this.$syncService.serialize(editor.state.doc) + this.emit('update:content', { + markdown: proseMirrorMarkdown, + }) + }, + onSync({ steps, document }) { this.hasConnectionIssue = this.$syncService.backend.fetcher === 0 || !this.$providers[0].wsconnected || this.$syncService.pushError > 0 this.$nextTick(() => { diff --git a/src/mixins/setContent.js b/src/mixins/setContent.js index addf6aee02c..5af31521720 100644 --- a/src/mixins/setContent.js +++ b/src/mixins/setContent.js @@ -9,7 +9,7 @@ import { Doc, encodeStateAsUpdate, XmlFragment, applyUpdate } from 'yjs' import { generateJSON } from '@tiptap/core' import { prosemirrorToYXmlFragment } from 'y-prosemirror' import { Node } from '@tiptap/pm/model' -import { createEditor } from '../EditorFactory.js' +import { createRichEditor, createPlainEditor } from '../EditorFactory.js' export default { methods: { @@ -31,9 +31,10 @@ export default { ? markdownit.render(content) + '

' : `

${escapeHtml(content)}
` - const editor = createEditor({ - enableRichEditing: isRichEditor, - }) + const editor = isRichEditor + ? createRichEditor() + : createPlainEditor() + const json = generateJSON(html, editor.extensionManager.extensions) const doc = Node.fromJSON(editor.schema, json) diff --git a/src/tests/builders.js b/src/tests/builders.js index c0e8da5c4a5..ce209b26866 100644 --- a/src/tests/builders.js +++ b/src/tests/builders.js @@ -6,14 +6,11 @@ import { expect } from '@jest/globals'; import { Mark, Node } from '@tiptap/pm/model' import { builders } from 'prosemirror-test-builder' -import createEditor from '../EditorFactory' +import { createRichEditor } from '../EditorFactory' export function getBuilders() { - const editor = createEditor({ - content: '', - enableRichEditing: true - }) + const editor = createRichEditor() return builders(editor.schema, { tr: { nodeType: 'tableRow' }, td: { nodeType: 'tableCell' }, @@ -84,7 +81,7 @@ function createDocumentString(node) { * @param {Node} subject The editor document * @param {Node} expected The expected document * @example - * const editor = createEditor() + * const editor = createRichEditor() * expectDocument(editor.state.doc, table( * tr( * td('foo') diff --git a/src/tests/helpers.js b/src/tests/helpers.js index a1c980439bf..53a44ac18fb 100644 --- a/src/tests/helpers.js +++ b/src/tests/helpers.js @@ -10,7 +10,7 @@ import Document from '@tiptap/extension-document' import Paragraph from '../nodes/Paragraph' import Text from '@tiptap/extension-text' -import createEditor from '../EditorFactory' +import { createRichEditor } from '../EditorFactory' import markdownit from '../markdownit' export function createCustomEditor({ content, extensions }) { @@ -32,9 +32,7 @@ export function createCustomEditor({ content, extensions }) { * @returns {string} */ export function markdownThroughEditor(markdown) { - const tiptap = createEditor({ - enableRichEditing: true - }) + const tiptap = createRichEditor() tiptap.commands.setContent(markdownit.render(markdown)) const serializer = createMarkdownSerializer(tiptap.schema) return serializer.serialize(tiptap.state.doc) @@ -47,9 +45,7 @@ export function markdownThroughEditor(markdown) { * @returns {string} */ export function markdownThroughEditorHtml(html) { - const tiptap = createEditor({ - enableRichEditing: true - }) + const tiptap = createRichEditor() tiptap.commands.setContent(html) const serializer = createMarkdownSerializer(tiptap.schema) return serializer.serialize(tiptap.state.doc) @@ -62,9 +58,7 @@ export function markdownThroughEditorHtml(html) { * @returns {string} */ export function markdownFromPaste(html) { - const tiptap = createEditor({ - enableRichEditing: true - }) + const tiptap = createRichEditor() tiptap.commands.insertContent(html) const serializer = createMarkdownSerializer(tiptap.schema) return serializer.serialize(tiptap.state.doc) diff --git a/src/tests/markdown.spec.js b/src/tests/markdown.spec.js index 6f1b6258a01..f15cec281c7 100644 --- a/src/tests/markdown.spec.js +++ b/src/tests/markdown.spec.js @@ -11,7 +11,7 @@ import { markdownFromPaste } from './helpers.js' import { createMarkdownSerializer } from "../extensions/Markdown"; -import createEditor from "../EditorFactory"; +import { createRichEditor } from "../EditorFactory"; /* * This file is for various markdown tests, mainly testing if input and output stays the same. @@ -212,9 +212,7 @@ describe('Markdown serializer from html', () => { describe('Trailing nodes', () => { test('No extra transaction is added after loading', () => { const source = "# My heading\n\n* test\n* test2" - const tiptap = createEditor({ - enableRichEditing: true, - }) + const tiptap = createRichEditor() tiptap.commands.setContent(markdownit.render(source)) const jsonBefore = tiptap.getJSON() diff --git a/src/tests/nodes/Table.spec.js b/src/tests/nodes/Table.spec.js index 19c843f3494..06dabbea0ef 100644 --- a/src/tests/nodes/Table.spec.js +++ b/src/tests/nodes/Table.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { createEditor } from '../../EditorFactory' +import { createRichEditor } from '../../EditorFactory' import { createMarkdownSerializer } from '../../extensions/Markdown' import { builders } from 'prosemirror-test-builder' @@ -75,9 +75,7 @@ describe('Table', () => { }) function editorWithContent(content) { - const editor = createEditor({ - enableRichEditing: true, - }) + const editor = createRichEditor() editor.commands.setContent(content) return editor } diff --git a/src/tests/plaintext.spec.js b/src/tests/plaintext.spec.js index 20c89bb0438..8fd957236fc 100644 --- a/src/tests/plaintext.spec.js +++ b/src/tests/plaintext.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { createEditor, serializePlainText } from './../EditorFactory'; +import { createPlainEditor, serializePlainText } from './../EditorFactory'; import spec from "./fixtures/spec" import xssFuzzVectors from './fixtures/xssFuzzVectors'; @@ -18,9 +18,7 @@ const escapeHTML = (s) => { const plaintextThroughEditor = (markdown) => { const content = '
' + escapeHTML(markdown) + '
' - const tiptap = createEditor({ - enableRichEditing: false - }) + const tiptap = createPlainEditor() tiptap.commands.setContent(content) return serializePlainText(tiptap.state.doc) || 'failed' } @@ -84,4 +82,14 @@ describe('html as plain text', () => { expect(plaintextThroughEditor('"\';&.-#><')).toBe('"\';&.-#><') expect(plaintextThroughEditor(xssFuzzVectors)).toBe(xssFuzzVectors) }) +} ) + +describe('regression tests', () => { + test('tripple enter creates new lines at end (#6507)', () => { + const tiptap = createPlainEditor() + tiptap.commands.enter() + tiptap.commands.enter() + tiptap.commands.enter() + expect(serializePlainText(tiptap.state.doc)).toEqual("\n\n\n") + }) }) diff --git a/src/tests/tiptap.spec.js b/src/tests/tiptap.spec.js index 4b6f4244496..b1adc70ed23 100644 --- a/src/tests/tiptap.spec.js +++ b/src/tests/tiptap.spec.js @@ -3,13 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import createEditor from '../EditorFactory' +import { createRichEditor } from '../EditorFactory' import markdownit from '../markdownit' const renderedHTML = ( markdown ) => { - const editor = createEditor({ - enableRichEditing: true - }) + const editor = createRichEditor() editor.commands.setContent(markdownit.render(markdown)) // Remove TrailingNode return editor.getHTML().replace(/

<\/p>$/, '')