Merge pull request #324 from Chrissi2812/search
add text search and replace
This commit is contained in:
198
packages/tiptap-extensions/src/extensions/Search.js
Normal file
198
packages/tiptap-extensions/src/extensions/Search.js
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Extension, Plugin } from 'tiptap'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
|
||||
export default class Search extends Extension {
|
||||
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
|
||||
this.results = []
|
||||
this.searchTerm = null
|
||||
this._updating = false
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'search'
|
||||
}
|
||||
|
||||
init() {
|
||||
this.editor.events.push('toggleSearch')
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
autoSelectNext: true,
|
||||
findClass: 'find',
|
||||
searching: false,
|
||||
caseSensitive: false,
|
||||
disableRegex: true,
|
||||
alwaysSearch: false,
|
||||
}
|
||||
}
|
||||
|
||||
toggleSearch() {
|
||||
return () => {
|
||||
this.options.searching = !this.options.searching
|
||||
this.editor.emit('toggleSearch', this.options.searching)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
keys() {
|
||||
return {
|
||||
'Mod-f': this.toggleSearch(),
|
||||
}
|
||||
}
|
||||
|
||||
commands() {
|
||||
return {
|
||||
find: attrs => this.find(attrs),
|
||||
replace: attrs => this.replace(attrs),
|
||||
replaceAll: attrs => this.replaceAll(attrs),
|
||||
clearSearch: () => this.clear(),
|
||||
toggleSearch: () => this.toggleSearch(),
|
||||
}
|
||||
}
|
||||
|
||||
get findRegExp() {
|
||||
return RegExp(this.searchTerm, !this.options.caseSensitive ? 'gui' : 'gu')
|
||||
}
|
||||
|
||||
get decorations() {
|
||||
return this.results.map(deco => (
|
||||
Decoration.inline(deco.from, deco.to, { class: this.options.findClass })
|
||||
))
|
||||
}
|
||||
|
||||
_search(doc) {
|
||||
this.results = []
|
||||
const mergedTextNodes = []
|
||||
let index = 0
|
||||
|
||||
if (!this.searchTerm) {
|
||||
return
|
||||
}
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
if (node.isText) {
|
||||
if (mergedTextNodes[index]) {
|
||||
mergedTextNodes[index] = {
|
||||
text: mergedTextNodes[index].text + node.text,
|
||||
pos: mergedTextNodes[index].pos,
|
||||
}
|
||||
} else {
|
||||
mergedTextNodes[index] = {
|
||||
text: node.text,
|
||||
pos,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
index += 1
|
||||
}
|
||||
})
|
||||
|
||||
mergedTextNodes.forEach(({ text, pos }) => {
|
||||
const search = this.findRegExp
|
||||
let m
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((m = search.exec(text))) {
|
||||
if (m[0] === '') {
|
||||
break
|
||||
}
|
||||
this.results.push({
|
||||
from: pos + m.index,
|
||||
to: pos + m.index + m[0].length,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
replace(replace) {
|
||||
return (state, dispatch) => {
|
||||
const { from, to } = this.results[0]
|
||||
|
||||
dispatch(state.tr.insertText(replace, from, to))
|
||||
}
|
||||
}
|
||||
|
||||
rebaseNextResult(replace, index, lastOffset = 0) {
|
||||
const nextIndex = index + 1
|
||||
if (!this.results[nextIndex]) return null
|
||||
|
||||
const { from: currentFrom, to: currentTo } = this.results[index]
|
||||
const offset = (currentTo - currentFrom - replace.length) + lastOffset
|
||||
|
||||
const { from, to } = this.results[nextIndex]
|
||||
this.results[nextIndex] = {
|
||||
to: to - offset,
|
||||
from: from - offset,
|
||||
}
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
replaceAll(replace) {
|
||||
return ({ tr }, dispatch) => {
|
||||
let offset
|
||||
this.results.forEach(({ from, to }, index) => {
|
||||
tr.insertText(replace, from, to)
|
||||
|
||||
offset = this.rebaseNextResult(replace, index, offset)
|
||||
})
|
||||
|
||||
dispatch(tr)
|
||||
}
|
||||
}
|
||||
|
||||
find(searchTerm) {
|
||||
return (state, dispatch) => {
|
||||
this.searchTerm = (this.options.disableRegex) ? searchTerm.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') : searchTerm
|
||||
|
||||
this.updateView(state, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
return (state, dispatch) => {
|
||||
this.searchTerm = null
|
||||
|
||||
this.updateView(state, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
updateView({ tr }, dispatch) {
|
||||
this._updating = true
|
||||
dispatch(tr)
|
||||
this._updating = false
|
||||
}
|
||||
|
||||
createDeco(doc) {
|
||||
this._search(doc)
|
||||
return this.decorations ? DecorationSet.create(doc, this.decorations) : []
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
state: {
|
||||
init() { return DecorationSet.empty },
|
||||
apply: (tr, old) => {
|
||||
if (this._updating
|
||||
|| this.options.searching
|
||||
|| (tr.docChanged && this.options.alwaysSearch)) {
|
||||
return this.createDeco(tr.doc)
|
||||
}
|
||||
if (tr.docChanged) {
|
||||
return old.map(tr.mapping, tr.doc)
|
||||
}
|
||||
return old
|
||||
},
|
||||
},
|
||||
props: {
|
||||
decorations(state) { return this.getState(state) },
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export { default as Underline } from './marks/Underline'
|
||||
export { default as Collaboration } from './extensions/Collaboration'
|
||||
export { default as History } from './extensions/History'
|
||||
export { default as Placeholder } from './extensions/Placeholder'
|
||||
export { default as Search } from './extensions/Search'
|
||||
|
||||
export { default as Suggestions } from './plugins/Suggestions'
|
||||
export { default as Highlight } from './plugins/Highlight'
|
||||
|
||||
Reference in New Issue
Block a user