From 0333ca39281b401e8323101b473d90309948c1f8 Mon Sep 17 00:00:00 2001 From: Sven Adlung Date: Mon, 15 Nov 2021 17:19:36 +0100 Subject: [PATCH] Add Savvy React example --- .../Examples/Savvy/React/ColorHighlighter.ts | 27 ++++ .../Examples/Savvy/React/SmilieReplacer.ts | 133 ++++++++++++++++++ demos/src/Examples/Savvy/React/findColors.ts | 28 ++++ demos/src/Examples/Savvy/React/index.html | 15 ++ demos/src/Examples/Savvy/React/index.jsx | 38 +++++ demos/src/Examples/Savvy/React/index.spec.js | 34 +++++ demos/src/Examples/Savvy/React/styles.scss | 38 +++++ 7 files changed, 313 insertions(+) create mode 100644 demos/src/Examples/Savvy/React/ColorHighlighter.ts create mode 100644 demos/src/Examples/Savvy/React/SmilieReplacer.ts create mode 100644 demos/src/Examples/Savvy/React/findColors.ts create mode 100644 demos/src/Examples/Savvy/React/index.html create mode 100644 demos/src/Examples/Savvy/React/index.jsx create mode 100644 demos/src/Examples/Savvy/React/index.spec.js create mode 100644 demos/src/Examples/Savvy/React/styles.scss diff --git a/demos/src/Examples/Savvy/React/ColorHighlighter.ts b/demos/src/Examples/Savvy/React/ColorHighlighter.ts new file mode 100644 index 00000000..33bf722c --- /dev/null +++ b/demos/src/Examples/Savvy/React/ColorHighlighter.ts @@ -0,0 +1,27 @@ +import { Extension } from '@tiptap/core' +import { Plugin } from 'prosemirror-state' +import findColors from './findColors' + +export const ColorHighlighter = Extension.create({ + name: 'colorHighlighter', + + addProseMirrorPlugins() { + return [ + new Plugin({ + state: { + init(_, { doc }) { + return findColors(doc) + }, + apply(transaction, oldState) { + return transaction.docChanged ? findColors(transaction.doc) : oldState + }, + }, + props: { + decorations(state) { + return this.getState(state) + }, + }, + }), + ] + }, +}) diff --git a/demos/src/Examples/Savvy/React/SmilieReplacer.ts b/demos/src/Examples/Savvy/React/SmilieReplacer.ts new file mode 100644 index 00000000..1c0a6944 --- /dev/null +++ b/demos/src/Examples/Savvy/React/SmilieReplacer.ts @@ -0,0 +1,133 @@ +import { Extension, textInputRule } from '@tiptap/core' + +export const SmilieReplacer = Extension.create({ + name: 'smilieReplacer', + + addInputRules() { + return [ + textInputRule({ find: /-___- $/, replace: '๐Ÿ˜‘ ' }), + textInputRule({ find: /:'-\) $/, replace: '๐Ÿ˜‚ ' }), + textInputRule({ find: /':-\) $/, replace: '๐Ÿ˜… ' }), + textInputRule({ find: /':-D $/, replace: '๐Ÿ˜… ' }), + textInputRule({ find: />:-\) $/, replace: '๐Ÿ˜† ' }), + textInputRule({ find: /-__- $/, replace: '๐Ÿ˜‘ ' }), + textInputRule({ find: /':-\( $/, replace: '๐Ÿ˜“ ' }), + textInputRule({ find: /:'-\( $/, replace: '๐Ÿ˜ข ' }), + textInputRule({ find: />:-\( $/, replace: '๐Ÿ˜  ' }), + textInputRule({ find: /O:-\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /0:-3 $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /0:-\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /0;\^\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /O;-\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /0;-\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /O:-3 $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /:'\) $/, replace: '๐Ÿ˜‚ ' }), + textInputRule({ find: /:-D $/, replace: '๐Ÿ˜ƒ ' }), + textInputRule({ find: /':\) $/, replace: '๐Ÿ˜… ' }), + textInputRule({ find: /'=\) $/, replace: '๐Ÿ˜… ' }), + textInputRule({ find: /':D $/, replace: '๐Ÿ˜… ' }), + textInputRule({ find: /'=D $/, replace: '๐Ÿ˜… ' }), + textInputRule({ find: />:\) $/, replace: '๐Ÿ˜† ' }), + textInputRule({ find: />;\) $/, replace: '๐Ÿ˜† ' }), + textInputRule({ find: />=\) $/, replace: '๐Ÿ˜† ' }), + textInputRule({ find: /;-\) $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /\*-\) $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /;-\] $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /;\^\) $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /B-\) $/, replace: '๐Ÿ˜Ž ' }), + textInputRule({ find: /8-\) $/, replace: '๐Ÿ˜Ž ' }), + textInputRule({ find: /B-D $/, replace: '๐Ÿ˜Ž ' }), + textInputRule({ find: /8-D $/, replace: '๐Ÿ˜Ž ' }), + textInputRule({ find: /:-\* $/, replace: '๐Ÿ˜˜ ' }), + textInputRule({ find: /:\^\* $/, replace: '๐Ÿ˜˜ ' }), + textInputRule({ find: /:-\) $/, replace: '๐Ÿ™‚ ' }), + textInputRule({ find: /-_- $/, replace: '๐Ÿ˜‘ ' }), + textInputRule({ find: /:-X $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /:-# $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /:-x $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: />.< $/, replace: '๐Ÿ˜ฃ ' }), + textInputRule({ find: /:-O $/, replace: '๐Ÿ˜ฎ ' }), + textInputRule({ find: /:-o $/, replace: '๐Ÿ˜ฎ ' }), + textInputRule({ find: /O_O $/, replace: '๐Ÿ˜ฎ ' }), + textInputRule({ find: />:O $/, replace: '๐Ÿ˜ฎ ' }), + textInputRule({ find: /:-P $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:-p $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:-รž $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:-รพ $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:-b $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: />:P $/, replace: '๐Ÿ˜œ ' }), + textInputRule({ find: /X-P $/, replace: '๐Ÿ˜œ ' }), + textInputRule({ find: /x-p $/, replace: '๐Ÿ˜œ ' }), + textInputRule({ find: /':\( $/, replace: '๐Ÿ˜“ ' }), + textInputRule({ find: /'=\( $/, replace: '๐Ÿ˜“ ' }), + textInputRule({ find: />:\\ $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: />:\/ $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /:-\/ $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /:-. $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: />:\[ $/, replace: '๐Ÿ˜ž ' }), + textInputRule({ find: /:-\( $/, replace: '๐Ÿ˜ž ' }), + textInputRule({ find: /:-\[ $/, replace: '๐Ÿ˜ž ' }), + textInputRule({ find: /:'\( $/, replace: '๐Ÿ˜ข ' }), + textInputRule({ find: /;-\( $/, replace: '๐Ÿ˜ข ' }), + textInputRule({ find: /#-\) $/, replace: '๐Ÿ˜ต ' }), + textInputRule({ find: /%-\) $/, replace: '๐Ÿ˜ต ' }), + textInputRule({ find: /X-\) $/, replace: '๐Ÿ˜ต ' }), + textInputRule({ find: />:\( $/, replace: '๐Ÿ˜  ' }), + textInputRule({ find: /0:3 $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /0:\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /O:\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /O=\) $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /O:3 $/, replace: '๐Ÿ˜‡ ' }), + textInputRule({ find: /<\/3 $/, replace: '๐Ÿ’” ' }), + textInputRule({ find: /:D $/, replace: '๐Ÿ˜ƒ ' }), + textInputRule({ find: /=D $/, replace: '๐Ÿ˜ƒ ' }), + textInputRule({ find: /;\) $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /\*\) $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /;\] $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /;D $/, replace: '๐Ÿ˜‰ ' }), + textInputRule({ find: /B\) $/, replace: '๐Ÿ˜Ž ' }), + textInputRule({ find: /8\) $/, replace: '๐Ÿ˜Ž ' }), + textInputRule({ find: /:\* $/, replace: '๐Ÿ˜˜ ' }), + textInputRule({ find: /=\* $/, replace: '๐Ÿ˜˜ ' }), + textInputRule({ find: /:\) $/, replace: '๐Ÿ™‚ ' }), + textInputRule({ find: /=\] $/, replace: '๐Ÿ™‚ ' }), + textInputRule({ find: /=\) $/, replace: '๐Ÿ™‚ ' }), + textInputRule({ find: /:\] $/, replace: '๐Ÿ™‚ ' }), + textInputRule({ find: /:X $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /:# $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /=X $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /=x $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /:x $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /=# $/, replace: '๐Ÿ˜ถ ' }), + textInputRule({ find: /:O $/, replace: '๐Ÿ˜ฎ ' }), + textInputRule({ find: /:o $/, replace: '๐Ÿ˜ฎ ' }), + textInputRule({ find: /:P $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /=P $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:p $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /=p $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:รž $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:รพ $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:b $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /d: $/, replace: '๐Ÿ˜› ' }), + textInputRule({ find: /:\/ $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /:\\ $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /=\/ $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /=\\ $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /:L $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /=L $/, replace: '๐Ÿ˜• ' }), + textInputRule({ find: /:\( $/, replace: '๐Ÿ˜ž ' }), + textInputRule({ find: /:\[ $/, replace: '๐Ÿ˜ž ' }), + textInputRule({ find: /=\( $/, replace: '๐Ÿ˜ž ' }), + textInputRule({ find: /;\( $/, replace: '๐Ÿ˜ข ' }), + textInputRule({ find: /D: $/, replace: '๐Ÿ˜จ ' }), + textInputRule({ find: /:\$ $/, replace: '๐Ÿ˜ณ ' }), + textInputRule({ find: /=\$ $/, replace: '๐Ÿ˜ณ ' }), + textInputRule({ find: /#\) $/, replace: '๐Ÿ˜ต ' }), + textInputRule({ find: /%\) $/, replace: '๐Ÿ˜ต ' }), + textInputRule({ find: /X\) $/, replace: '๐Ÿ˜ต ' }), + textInputRule({ find: /:@ $/, replace: '๐Ÿ˜  ' }), + textInputRule({ find: /<3 $/, replace: 'โค๏ธ ' }), + textInputRule({ find: /\/shrug $/, replace: 'ยฏ\\_(ใƒ„)_/ยฏ' }), + ] + }, +}) diff --git a/demos/src/Examples/Savvy/React/findColors.ts b/demos/src/Examples/Savvy/React/findColors.ts new file mode 100644 index 00000000..dd6be4ba --- /dev/null +++ b/demos/src/Examples/Savvy/React/findColors.ts @@ -0,0 +1,28 @@ +import { Decoration, DecorationSet } from 'prosemirror-view' +import { Node } from 'prosemirror-model' + +export default function (doc: Node): DecorationSet { + const hexColor = /(#[0-9a-f]{3,6})\b/gi + const decorations: Decoration[] = [] + + doc.descendants((node, position) => { + if (!node.text) { + return + } + + Array.from(node.text.matchAll(hexColor)).forEach(match => { + const color = match[0] + const index = match.index || 0 + const from = position + index + const to = from + color.length + const decoration = Decoration.inline(from, to, { + class: 'color', + style: `--color: ${color}`, + }) + + decorations.push(decoration) + }) + }) + + return DecorationSet.create(doc, decorations) +} diff --git a/demos/src/Examples/Savvy/React/index.html b/demos/src/Examples/Savvy/React/index.html new file mode 100644 index 00000000..c44f2578 --- /dev/null +++ b/demos/src/Examples/Savvy/React/index.html @@ -0,0 +1,15 @@ + + + + + + + +
+ + + diff --git a/demos/src/Examples/Savvy/React/index.jsx b/demos/src/Examples/Savvy/React/index.jsx new file mode 100644 index 00000000..37b8fcbf --- /dev/null +++ b/demos/src/Examples/Savvy/React/index.jsx @@ -0,0 +1,38 @@ +import React from 'react' +import { useEditor, EditorContent } from '@tiptap/react' +import Document from '@tiptap/extension-document' +import Paragraph from '@tiptap/extension-paragraph' +import Text from '@tiptap/extension-text' +import Code from '@tiptap/extension-code' +import Typography from '@tiptap/extension-typography' +import { ColorHighlighter } from './ColorHighlighter' +import { SmilieReplacer } from './SmilieReplacer' +import './styles.scss' + +export default () => { + const editor = useEditor({ + extensions: [Document, Paragraph, Text, Code, Typography, ColorHighlighter, SmilieReplacer], + content: ` +

+ โ†’ With the Typography extension, tiptap understands ยปwhat you meanยซ and adds correct characters to your text โ€” itโ€™s like a โ€œtypography nerdโ€ on your side. +

+

+ Try it out and type (c), ->, >>, 1/2, !=, -- or 1x1 here: +

+

+

+ Or add completely custom input rules. We added a custom extension here that replaces smilies like :-), <3 or >:P with emojis. Try it out: +

+

+

+ You can also teach the editor new things. For example to recognize hex colors and add a color swatch on the fly: #FFF, #0D0D0D, #616161, #A975FF, #FB5151, #FD9170, #FFCB6B, #68CEF8, #80cbc4, #9DEF8F +

+ `, + }) + + if (!editor) { + return null + } + + return +} diff --git a/demos/src/Examples/Savvy/React/index.spec.js b/demos/src/Examples/Savvy/React/index.spec.js new file mode 100644 index 00000000..437397cc --- /dev/null +++ b/demos/src/Examples/Savvy/React/index.spec.js @@ -0,0 +1,34 @@ +context('/src/Examples/Savvy/React/', () => { + before(() => { + cy.visit('/src/Examples/Savvy/React/') + }) + + beforeEach(() => { + cy.get('.ProseMirror').then(([{ editor }]) => { + editor.commands.clearContent() + }) + }) + + const tests = [ + ['(c)', 'ยฉ'], + ['->', 'โ†’'], + ['>>', 'ยป'], + ['1/2', 'ยฝ'], + ['!=', 'โ‰ '], + ['--', 'โ€”'], + ['1x1', '1ร—1'], + [':-) ', '๐Ÿ™‚'], + ['<3 ', 'โค๏ธ'], + ['>:P ', '๐Ÿ˜œ'], + ] + + tests.forEach(test => { + it(`should parse ${test[0]} correctly`, () => { + cy.get('.ProseMirror').type(test[0]).should('contain', test[1]) + }) + }) + + it('should parse hex colors correctly', () => { + cy.get('.ProseMirror').type('#FD9170').find('.color') + }) +}) diff --git a/demos/src/Examples/Savvy/React/styles.scss b/demos/src/Examples/Savvy/React/styles.scss new file mode 100644 index 00000000..e1019bba --- /dev/null +++ b/demos/src/Examples/Savvy/React/styles.scss @@ -0,0 +1,38 @@ +/* Basic editor styles */ +.ProseMirror { + > * + * { + margin-top: 0.75em; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + } + + code { + background-color: rgba(#616161, 0.1); + color: #616161; + } +} + +/* Color swatches */ +.color { + white-space: nowrap; + + &::before { + background-color: var(--color); + border: 1px solid rgba(128, 128, 128, 0.3); + border-radius: 2px; + content: " "; + display: inline-block; + height: 1em; + margin-bottom: 0.15em; + margin-right: 0.1em; + vertical-align: middle; + width: 1em; + } +}