diff --git a/examples/Components/Routes/Search/index.vue b/examples/Components/Routes/Search/index.vue new file mode 100644 index 000000000..379dac653 --- /dev/null +++ b/examples/Components/Routes/Search/index.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/examples/Components/Subnavigation/index.vue b/examples/Components/Subnavigation/index.vue index 953b8a6d5..a871baf0e 100644 --- a/examples/Components/Subnavigation/index.vue +++ b/examples/Components/Subnavigation/index.vue @@ -24,6 +24,9 @@ Tables + + Search + Suggestions diff --git a/examples/main.js b/examples/main.js index 933ff7c0a..705007f77 100644 --- a/examples/main.js +++ b/examples/main.js @@ -68,6 +68,13 @@ const routes = [ githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/TodoList', }, }, + { + path: '/search', + component: () => import('Components/Routes/Search'), + meta: { + githubUrl: 'https://github.com/scrumpy/tiptap/tree/master/examples/Components/Routes/Search', + }, + }, { path: '/suggestions', component: () => import('Components/Routes/Suggestions'), diff --git a/packages/tiptap-extensions/src/extensions/Search.js b/packages/tiptap-extensions/src/extensions/Search.js new file mode 100644 index 000000000..fad8cdfa7 --- /dev/null +++ b/packages/tiptap-extensions/src/extensions/Search.js @@ -0,0 +1,109 @@ +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 + } + + get name() { + return 'search' + } + + get defaultOptions() { + return { + autoSelectNext: true, + findClass: 'find', + searching: false, + caseSensitive: false, + } + } + + toggleSearch() { + return () => { + this.options.searching = !this.options.searching + return true + } + } + + keys() { + return { + 'Mod-f': this.toggleSearch(), + } + } + + commands() { + return { + find: attrs => this.find(attrs), + toggleSearch: () => this.toggleSearch(), + } + } + + get findRegExp() { + return RegExp(this.searchTerm, !this.options.caseSensitive ? 'gi' : 'g') + } + + get decorations() { + return this.results.map(deco => ( + Decoration.inline(deco.from, deco.to, { class: this.options.findClass }) + )) + } + + _search(doc) { + this.results = [] + + if (!this.searchTerm) { + return + } + + const search = this.findRegExp + + doc.descendants((node, pos) => { + if (node.isText) { + let m + while (m = search.exec(node.text)) { + this.results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length, + }) + } + } + }) + } + + find(searchTerm) { + return ({ doc, tr }, dispatch) => { + this.options.searching = true + this.searchTerm = searchTerm + + this._search(doc) + + dispatch(tr) + } + } + + createDeco(doc) { + return this.decorations ? DecorationSet.create(doc, this.decorations) : [] + } + + get plugins() { + return [ + new Plugin({ + state: { + init: (_, { doc }) => this.createDeco(doc), + apply: (tr, old) => ( + (tr.docChanged || this.options.searching) ? this.createDeco(tr.doc) : old + ), + }, + props: { + decorations(state) { return this.getState(state) }, + }, + }), + ] + } + +} diff --git a/packages/tiptap-extensions/src/index.js b/packages/tiptap-extensions/src/index.js index 4851bb515..b38176962 100644 --- a/packages/tiptap-extensions/src/index.js +++ b/packages/tiptap-extensions/src/index.js @@ -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'