Skip to content

Commit

Permalink
feat: Move search highlighting to editor API (#6199)
Browse files Browse the repository at this point in the history
* feat(search): Move to editor API instead of event bus

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>
  • Loading branch information
elzody authored Aug 13, 2024
1 parent 7ba7183 commit 3a4d611
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 18 deletions.
92 changes: 92 additions & 0 deletions cypress/component/editor/search.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* 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'
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', () => {
let editor = null

before(() => {
cy.fixture('lorem.txt').then((text) => {
editor = new Editor({
element: document.querySelector('div[data-cy-root]'),
content: text,
extensions: [Document, Text, Search, Paragraph, HardBreak],
})
})
})

it('can highlight a match', () => {
const searchQuery = 'Lorem ipsum dolor sit amet'
editor.commands.setSearchQuery(searchQuery)

const highlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]')
expect(highlightedElements).to.have.lengthOf(1)
verifyHighlights(highlightedElements, searchQuery)
})

it('can highlight multiple matches', () => {
const searchQuery = 'quod'
editor.commands.setSearchQuery(searchQuery)

const highlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]')
expect(highlightedElements).to.have.lengthOf(3)
verifyHighlights(highlightedElements, searchQuery)
})

it('can toggle highlight all', () => {
const searchQuery = 'quod'
let highlightedElements = []

// Highlight only first occurrence
editor.commands.setSearchQuery(searchQuery, false)
highlightedElements = document.querySelectorAll('span[data-text-el="search-decoration"]')

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="search-decoration"]')

editor.commands.nextMatch()
const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el="search-decoration"]')

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="search-decoration"]')

editor.commands.previousMatch()
const currentlyHighlightedElement = document.querySelectorAll('span[data-text-el="search-decoration"]')

expect(currentlyHighlightedElement).to.have.lengthOf(1)
expect(allHighlightedElements[0]).to.deep.equal(currentlyHighlightedElement[0])
})
})

/**
* 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())
}
}
7 changes: 7 additions & 0 deletions cypress/fixtures/lorem.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<p>
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.
</p>

<p>
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.
</p>
23 changes: 23 additions & 0 deletions src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand All @@ -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?.()
}
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -212,5 +234,6 @@ window.OCA.Text.createEditor = async function({
.onLoaded(onLoaded)
.onUpdate(onUpdate)
.onOutlineToggle(onOutlineToggle)
.onSearch(onSearch)
.render(el)
}
15 changes: 0 additions & 15 deletions src/extensions/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import { Extension } from '@tiptap/core'
import { subscribe } from '@nextcloud/event-bus'
import searchDecorations from '../plugins/searchDecorations.js'
import {
setSearchQuery,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/searchDecorations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 24 additions & 1 deletion src/tests/plugins/searchQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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() {
Expand Down

0 comments on commit 3a4d611

Please sign in to comment.