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'