From d444c32bd65476497fcb1ea1cb5956b99e410394 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Thu, 8 Aug 2024 15:39:37 -0400 Subject: [PATCH 01/10] feat(search): Move to editor API instead of event bus Signed-off-by: Elizabeth Danzberger --- src/editor.js | 23 +++++++++++++++++++++++ src/extensions/Search.js | 15 --------------- src/plugins/searchDecorations.js | 4 ++-- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/editor.js b/src/editor.js index 8ce4ff561f1..19e06b47619 100644 --- a/src/editor.js +++ b/src/editor.js @@ -5,6 +5,7 @@ import Vue from 'vue' import store from './store/index.js' +import { subscribe } from '@nextcloud/event-bus' import { EDITOR_UPLOAD, HOOK_MENTION_SEARCH, HOOK_MENTION_INSERT, ATTACHMENT_RESOLVER } from './components/Editor.provider.js' import { ACTION_ATTACHMENT_PROMPT } from './components/Editor/MediaHandler.provider.js' // eslint-disable-next-line import/no-unresolved, n/no-missing-import @@ -56,6 +57,11 @@ class TextEditorEmbed { return this } + onSearch(onSearchCallback = () => {}) { + subscribe('text:editor:search-results', onSearchCallback) + return this + } + render(el) { el.innerHTML = '' const element = document.createElement('div') @@ -77,6 +83,21 @@ class TextEditorEmbed { return this } + setSearchQuery(query, matchAll) { + const editor = this.#getEditorComponent()?.$editor + editor.commands.setSearchQuery(query, matchAll) + } + + searchNext() { + const editor = this.#getEditorComponent()?.$editor + editor.commands.nextMatch() + } + + searchPrevious() { + const editor = this.#getEditorComponent()?.$editor + editor.commands.previousMatch() + } + async save() { return this.#getEditorComponent().save?.() } @@ -138,6 +159,7 @@ window.OCA.Text.createEditor = async function({ onFileInsert = undefined, onMentionSearch = undefined, onMentionInsert = undefined, + onSearch = undefined, }) { const { default: MarkdownContentEditor } = await import(/* webpackChunkName: "editor" */'./components/Editor/MarkdownContentEditor.vue') const { default: Editor } = await import(/* webpackChunkName: "editor" */'./components/Editor.vue') @@ -212,5 +234,6 @@ window.OCA.Text.createEditor = async function({ .onLoaded(onLoaded) .onUpdate(onUpdate) .onOutlineToggle(onOutlineToggle) + .onSearch(onSearch) .render(el) } diff --git a/src/extensions/Search.js b/src/extensions/Search.js index 3363c29eb82..8ec66ebd5f8 100644 --- a/src/extensions/Search.js +++ b/src/extensions/Search.js @@ -4,7 +4,6 @@ */ import { Extension } from '@tiptap/core' -import { subscribe } from '@nextcloud/event-bus' import searchDecorations from '../plugins/searchDecorations.js' import { setSearchQuery, @@ -16,20 +15,6 @@ import { export default Extension.create({ name: 'Search', - onCreate() { - subscribe('text:editor:search', ({ query, matchAll }) => { - this.editor.commands.setSearchQuery(query, matchAll) - }) - - subscribe('text:editor:search-next', () => { - this.editor.commands.nextMatch() - }) - - subscribe('text:editor:search-previous', () => { - this.editor.commands.previousMatch() - }) - }, - addCommands() { return { setSearchQuery, diff --git a/src/plugins/searchDecorations.js b/src/plugins/searchDecorations.js index 1f0a66b7438..0e5af5d802d 100644 --- a/src/plugins/searchDecorations.js +++ b/src/plugins/searchDecorations.js @@ -37,8 +37,8 @@ export default function searchDecorations() { }) emit('text:editor:search-results', { - results: (newSearch.query === '' ? null : total), - index, + totalMatches: (newSearch.query === '' ? null : total), + matchIndex: index, }) return highlightResults(tr.doc, results) From 6f678c9df8091f27e6f29ca651b71e65f971fe0b Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 10:19:16 -0400 Subject: [PATCH 02/10] feat(test): add previous match test Signed-off-by: Elizabeth Danzberger --- src/tests/plugins/searchQuery.spec.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/tests/plugins/searchQuery.spec.js b/src/tests/plugins/searchQuery.spec.js index 1ed5059770e..7082e96f3ae 100644 --- a/src/tests/plugins/searchQuery.spec.js +++ b/src/tests/plugins/searchQuery.spec.js @@ -5,7 +5,7 @@ import { searchQuery } from '../../plugins/searchQuery.js' import { Plugin, EditorState } from '@tiptap/pm/state' import { schema } from '@tiptap/pm/schema-basic' -import { setSearchQuery, nextMatch } from '../../plugins/searchQuery.js' +import { setSearchQuery, nextMatch, previousMatch } from '../../plugins/searchQuery.js' describe('searchQuery plugin', () => { it('can set up plugin and state', () => { @@ -61,6 +61,29 @@ describe('searchQuery plugin', () => { index: 1, // index is incremented to the next match }) }) + + it ('can accept previous match state', () => { + const { plugin, state } = pluginSetup() + + const setSearch = setSearchQuery('lorem')(state) + const previousSearch = previousMatch()(state) + + let newState = state.apply(setSearch) + + expect(plugin.getState(newState)).toEqual({ + query: 'lorem', + matchAll: true, + index: 0, + }) + + newState = newState.apply(previousSearch) + + expect(plugin.getState(newState)).toEqual({ + query: 'lorem', + matchAll: false, + index: -1, + }) + }) }) function pluginSetup() { From 061176b761cca5805aa00d830b750f1ab3a58754 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 11:03:28 -0400 Subject: [PATCH 03/10] feat(test): add cypress component test for editor Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 cypress/component/editor/search.cy.js diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js new file mode 100644 index 00000000000..3535fa9be24 --- /dev/null +++ b/cypress/component/editor/search.cy.js @@ -0,0 +1,25 @@ +import { createCustomEditor } from '../../support/components.js' +import Search from '../../../src/extensions/Search.js' + +describe('editor search highlighting', () => { + const editor = createCustomEditor({ + content: 'lorem ipsum', + extensions: [Search], + }) + + before(() => { + editor.setOptions({ + element: document.querySelector('div[data-cy-root]') + }) + }) + + beforeEach(() => editor.createView()) + + it('can highlight a search', () => { + editor.commands.setSearchQuery('lorem') + + const highlightElements = document.querySelector('span[data-text-el]') + + expect(highlightElements) + }) +}) \ No newline at end of file From 713707c828f89910ea877472fb32eaddc98e3174 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 11:56:13 -0400 Subject: [PATCH 04/10] feat(test): load fixture into editor Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 31 +++++++++++++++------------ cypress/fixtures/lorem.txt | 7 ++++++ 2 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 cypress/fixtures/lorem.txt diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js index 3535fa9be24..5ace1d3fcb9 100644 --- a/cypress/component/editor/search.cy.js +++ b/cypress/component/editor/search.cy.js @@ -1,25 +1,28 @@ -import { createCustomEditor } from '../../support/components.js' +import { Editor } from '@tiptap/core' +import { Document } from '@tiptap/extension-document' +import { Text } from '@tiptap/extension-text' import Search from '../../../src/extensions/Search.js' +import Paragraph from '../../../src/nodes/Paragraph.js' +import HardBreak from '../../../src/nodes/HardBreak.js' describe('editor search highlighting', () => { - const editor = createCustomEditor({ - content: 'lorem ipsum', - extensions: [Search], - }) + let editor = null before(() => { - editor.setOptions({ - element: document.querySelector('div[data-cy-root]') + cy.fixture('lorem.txt').then((text) => { + editor = new Editor({ + element: document.querySelector('div[data-cy-root]'), + content: text, + extensions: [Document, Text, Search, Paragraph, HardBreak], + }) }) }) - beforeEach(() => editor.createView()) - it('can highlight a search', () => { - editor.commands.setSearchQuery('lorem') - - const highlightElements = document.querySelector('span[data-text-el]') + const searchQuery = 'Lorem ipsum dolor sit amet' + editor.commands.setSearchQuery(searchQuery) - expect(highlightElements) + const highlightedElement = document.querySelector('span[data-text-el]') + expect(highlightedElement.innerText).to.equal(searchQuery) }) -}) \ No newline at end of file +}) diff --git a/cypress/fixtures/lorem.txt b/cypress/fixtures/lorem.txt new file mode 100644 index 00000000000..5774a99e277 --- /dev/null +++ b/cypress/fixtures/lorem.txt @@ -0,0 +1,7 @@ +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim aeque doleamus animo, cum corpore dolemus, fieri tamen permagna accessio potest, si aliquod aeternum et infinitum impendere malum nobis opinemur. Quod idem licet transferre in voluptatem, ut postea variari voluptas distinguique possit, augeri amplificarique non possit. At. +

+ +

+ Ullus investigandi veri, nisi inveneris, et quaerendi defatigatio turpis est, cum esset accusata et vituperata ab Hortensio. Qui liber cum et mortem contemnit, qua qui est imbutus quietus esse numquam potest. Praeterea bona praeterita grata recordatione renovata delectant. Est autem situm in nobis ut et voluptates et dolores nasci fatemur e corporis voluptatibus et doloribus -- itaque concedo, quod modo dicebas, cadere. +

\ No newline at end of file From a28b08d85e81a2a48039d0da32470d8c6316c2d2 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 12:10:05 -0400 Subject: [PATCH 05/10] feat(test): test highlighting multiple matches Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js index 5ace1d3fcb9..6e83082dc55 100644 --- a/cypress/component/editor/search.cy.js +++ b/cypress/component/editor/search.cy.js @@ -18,11 +18,24 @@ describe('editor search highlighting', () => { }) }) - it('can highlight a search', () => { + it('can highlight a match', () => { const searchQuery = 'Lorem ipsum dolor sit amet' editor.commands.setSearchQuery(searchQuery) - const highlightedElement = document.querySelector('span[data-text-el]') - expect(highlightedElement.innerText).to.equal(searchQuery) + const highlightedElements = document.querySelectorAll('span[data-text-el]') + expect(highlightedElements.length).to.equal(1) + expect(highlightedElements[0].innerText).to.equal(searchQuery) + }) + + it('can highlight multiple matches', () => { + const searchQuery = 'et' + editor.commands.setSearchQuery(searchQuery) + + const highlightedElements = document.querySelectorAll('span[data-text-el]') + expect(highlightedElements.length).to.equal(16) + + for (const element of highlightedElements) { + expect(element.innerText).to.equal(searchQuery) + } }) }) From bf5461455b44de07bee0417245cf4fd768a4a9cd Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 12:26:44 -0400 Subject: [PATCH 06/10] feat(test): test for toggling highlight all Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 30 +++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js index 6e83082dc55..643860f1e91 100644 --- a/cypress/component/editor/search.cy.js +++ b/cypress/component/editor/search.cy.js @@ -24,7 +24,7 @@ describe('editor search highlighting', () => { const highlightedElements = document.querySelectorAll('span[data-text-el]') expect(highlightedElements.length).to.equal(1) - expect(highlightedElements[0].innerText).to.equal(searchQuery) + verifyHighlights(highlightedElements, searchQuery) }) it('can highlight multiple matches', () => { @@ -33,9 +33,31 @@ describe('editor search highlighting', () => { const highlightedElements = document.querySelectorAll('span[data-text-el]') expect(highlightedElements.length).to.equal(16) + verifyHighlights(highlightedElements, searchQuery) + }) + + it('can toggle highlight all', () => { + const searchQuery = 'quod' + let highlightedElements = [] + + // Highlight all occurrences + editor.commands.setSearchQuery(searchQuery, true) + highlightedElements = document.querySelectorAll('span[data-text-el]') + + expect(highlightedElements.length).to.equal(3) + verifyHighlights(highlightedElements, searchQuery) - for (const element of highlightedElements) { - expect(element.innerText).to.equal(searchQuery) - } + // Highlight only first occurrence + editor.commands.setSearchQuery(searchQuery, false) + highlightedElements = document.querySelectorAll('span[data-text-el]') + + expect(highlightedElements.length).to.equal(1) + verifyHighlights(highlightedElements, searchQuery) }) }) + +function verifyHighlights(highlightedElements, searchQuery) { + for (const element of highlightedElements) { + expect(element.innerText.toLowerCase()).to.equal(searchQuery.toLowerCase()) + } +} From 557753c284a31f1616005e12524626909dd7a66a Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 13:01:05 -0400 Subject: [PATCH 07/10] feat(test): add tests for moving to next/previous match Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 41 ++++++++++++++++++++------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js index 643860f1e91..6ad8271c4a4 100644 --- a/cypress/component/editor/search.cy.js +++ b/cypress/component/editor/search.cy.js @@ -23,16 +23,16 @@ describe('editor search highlighting', () => { editor.commands.setSearchQuery(searchQuery) const highlightedElements = document.querySelectorAll('span[data-text-el]') - expect(highlightedElements.length).to.equal(1) + expect(highlightedElements).to.have.lengthOf(1) verifyHighlights(highlightedElements, searchQuery) }) it('can highlight multiple matches', () => { - const searchQuery = 'et' + const searchQuery = 'quod' editor.commands.setSearchQuery(searchQuery) const highlightedElements = document.querySelectorAll('span[data-text-el]') - expect(highlightedElements.length).to.equal(16) + expect(highlightedElements).to.have.lengthOf(3) verifyHighlights(highlightedElements, searchQuery) }) @@ -40,20 +40,39 @@ describe('editor search highlighting', () => { const searchQuery = 'quod' let highlightedElements = [] - // Highlight all occurrences - editor.commands.setSearchQuery(searchQuery, true) - highlightedElements = document.querySelectorAll('span[data-text-el]') - - expect(highlightedElements.length).to.equal(3) - verifyHighlights(highlightedElements, searchQuery) - // Highlight only first occurrence editor.commands.setSearchQuery(searchQuery, false) highlightedElements = document.querySelectorAll('span[data-text-el]') - expect(highlightedElements.length).to.equal(1) + expect(highlightedElements).to.have.lengthOf(1) verifyHighlights(highlightedElements, searchQuery) }) + + it('can move to next occurrence', () => { + const searchQuery = 'quod' + + editor.commands.setSearchQuery(searchQuery, true) + const allHighlightedElements = document.querySelectorAll('span[data-text-el]') + + editor.commands.nextMatch() + const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el]') + + expect(currentlyHighlightedElement).to.have.lengthOf(1) + expect(allHighlightedElements[1]).to.deep.equal(currentlyHighlightedElement[0]) + }) + + it('can move to previous occurrence', () => { + const searchQuery = 'quod' + + editor.commands.setSearchQuery(searchQuery, true) + const allHighlightedElements = document.querySelectorAll('span[data-text-el]') + + editor.commands.previousMatch() + const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el]') + + expect(currentlyHighlightedElement).to.have.lengthOf(1) + expect(allHighlightedElements[0]).to.deep.equal(currentlyHighlightedElement[0]) + }) }) function verifyHighlights(highlightedElements, searchQuery) { From 379609711231a2158d6b76ec7d450a5681ceb77d Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 13:03:48 -0400 Subject: [PATCH 08/10] fix: formatting/jsdoc Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js index 6ad8271c4a4..13bf5a5a4e8 100644 --- a/cypress/component/editor/search.cy.js +++ b/cypress/component/editor/search.cy.js @@ -75,6 +75,11 @@ describe('editor search highlighting', () => { }) }) +/** + * Verifies the Nodes in the given NodeList match the search query + * @param {NodeList} highlightedElements - NodeList of highlighted elements + * @param {string} searchQuery - search query + */ function verifyHighlights(highlightedElements, searchQuery) { for (const element of highlightedElements) { expect(element.innerText.toLowerCase()).to.equal(searchQuery.toLowerCase()) From 62fcf605fc898a260a7416f56667b7953f631393 Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Mon, 12 Aug 2024 13:06:15 -0400 Subject: [PATCH 09/10] chore: add spdx header to new test file Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js index 13bf5a5a4e8..05babead8b9 100644 --- a/cypress/component/editor/search.cy.js +++ b/cypress/component/editor/search.cy.js @@ -1,3 +1,8 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + import { Editor } from '@tiptap/core' import { Document } from '@tiptap/extension-document' import { Text } from '@tiptap/extension-text' From 944fdb4d6917ff1e8f95ef88fadb57ac06e47f6a Mon Sep 17 00:00:00 2001 From: Elizabeth Danzberger Date: Tue, 13 Aug 2024 09:29:29 -0400 Subject: [PATCH 10/10] fix(test): make sure to only get search decorations Signed-off-by: Elizabeth Danzberger --- cypress/component/editor/search.cy.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cypress/component/editor/search.cy.js b/cypress/component/editor/search.cy.js index 05babead8b9..7819608b016 100644 --- a/cypress/component/editor/search.cy.js +++ b/cypress/component/editor/search.cy.js @@ -27,7 +27,7 @@ describe('editor search highlighting', () => { const searchQuery = 'Lorem ipsum dolor sit amet' editor.commands.setSearchQuery(searchQuery) - const highlightedElements = document.querySelectorAll('span[data-text-el]') + const highlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]') expect(highlightedElements).to.have.lengthOf(1) verifyHighlights(highlightedElements, searchQuery) }) @@ -36,7 +36,7 @@ describe('editor search highlighting', () => { const searchQuery = 'quod' editor.commands.setSearchQuery(searchQuery) - const highlightedElements = document.querySelectorAll('span[data-text-el]') + const highlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]') expect(highlightedElements).to.have.lengthOf(3) verifyHighlights(highlightedElements, searchQuery) }) @@ -47,7 +47,7 @@ describe('editor search highlighting', () => { // Highlight only first occurrence editor.commands.setSearchQuery(searchQuery, false) - highlightedElements = document.querySelectorAll('span[data-text-el]') + highlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]') expect(highlightedElements).to.have.lengthOf(1) verifyHighlights(highlightedElements, searchQuery) @@ -57,10 +57,10 @@ describe('editor search highlighting', () => { const searchQuery = 'quod' editor.commands.setSearchQuery(searchQuery, true) - const allHighlightedElements = document.querySelectorAll('span[data-text-el]') + const allHighlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]') editor.commands.nextMatch() - const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el]') + const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el="search-decoration"]') expect(currentlyHighlightedElement).to.have.lengthOf(1) expect(allHighlightedElements[1]).to.deep.equal(currentlyHighlightedElement[0]) @@ -70,10 +70,10 @@ describe('editor search highlighting', () => { const searchQuery = 'quod' editor.commands.setSearchQuery(searchQuery, true) - const allHighlightedElements = document.querySelectorAll('span[data-text-el]') + const allHighlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]') editor.commands.previousMatch() - const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el]') + const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el="search-decoration"]') expect(currentlyHighlightedElement).to.have.lengthOf(1) expect(allHighlightedElements[0]).to.deep.equal(currentlyHighlightedElement[0])