From 92297494c2b7da26c4946d4064dbdcab5513faf9 Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 13:16:48 -0500 Subject: [PATCH 1/9] adds extentions for suggestions --- package.json | 3 +++ yarn.lock | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/package.json b/package.json index 9f72a5b3..77d2c4a9 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,14 @@ "@tiptap/extension-italic": "^2.0.3", "@tiptap/extension-link": "^2.0.3", "@tiptap/extension-list-item": "^2.0.3", + "@tiptap/extension-mention": "^2.1.13", "@tiptap/extension-ordered-list": "^2.0.3", "@tiptap/extension-paragraph": "^2.0.3", "@tiptap/extension-placeholder": "^2.0.3", "@tiptap/extension-text": "^2.0.3", "@tiptap/pm": "^2.0.3", "@tiptap/react": "^2.0.3", + "@tiptap/suggestion": "^2.1.13", "@types/testing-library__jest-dom": "^6.0.0", "@typescript-eslint/eslint-plugin": "^2", "@typescript-eslint/parser": "^2", @@ -29,6 +31,7 @@ "react-toggle": "4.1.1", "react-transition-group": "^4.3.0", "sanitize-html": "^2.11.0", + "tippy.js": "^6.3.7", "uuid": "^7.0.2" }, "scripts": { diff --git a/yarn.lock b/yarn.lock index caf6f4c7..b202104f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3502,6 +3502,11 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.10.tgz#0615e4fb68161e6457e6041e195f454bfd537d44" integrity sha512-rRRyB14vOcSjTMAh8Y+50TRC/jO469CelGwFjOLrK1ZSEag5wmLDaqpWOOb52BFYnvCHuIm1HqZtdL5bTI/J1w== +"@tiptap/extension-mention@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.1.13.tgz#6359c563268c46539660958847fe76c22131f2c8" + integrity sha512-OYqaucyBiCN/CmDYjpOVX74RJcIEKmAqiZxUi8Gfaq7ryEO5a8Gk93nK+8uZ0onaqHE+mHpoLFFbcAFbOPgkUQ== + "@tiptap/extension-ordered-list@^2.0.3": version "2.1.10" resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.10.tgz#ef5d5ba68baf86e9b66c1b2c1cec458aa111ad44" @@ -3554,6 +3559,11 @@ "@tiptap/extension-bubble-menu" "^2.1.10" "@tiptap/extension-floating-menu" "^2.1.10" +"@tiptap/suggestion@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@tiptap/suggestion/-/suggestion-2.1.13.tgz#0a8317260baed764a523a09099c0889a0e5b507e" + integrity sha512-Y05TsiXTFAJ5SrfoV+21MAxig5UNbY0AVa03lQlh/yicTRPpIc6hgZzblB0uxDSYoj6+kaHE4MIZvPvhUD8BJQ== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" From d98458894b6b11bc780430ae6ceda6bbe3589dca Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 13:21:50 -0500 Subject: [PATCH 2/9] adds template variables to RichTextEditor --- src/RichTextEditor/RichTextEditor.jsx | 24 +- src/RichTextEditor/RichTextEditor.scss | 4 + src/RichTextEditor/RichTextEditor.stories.jsx | 9 + src/RichTextEditor/TemplateVariable.scss | 27 ++ src/RichTextEditor/TemplateVariable.tsx | 317 ++++++++++++++++++ 5 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 src/RichTextEditor/TemplateVariable.scss create mode 100644 src/RichTextEditor/TemplateVariable.tsx diff --git a/src/RichTextEditor/RichTextEditor.jsx b/src/RichTextEditor/RichTextEditor.jsx index ca72e4a3..b33c8da7 100644 --- a/src/RichTextEditor/RichTextEditor.jsx +++ b/src/RichTextEditor/RichTextEditor.jsx @@ -24,6 +24,7 @@ import Text from '@tiptap/extension-text'; import sanitizeHtml from 'sanitize-html'; import { LoadingSkeleton } from 'src/LoadingSkeleton'; +import { TemplateVariable, buildSuggestions } from './TemplateVariable'; import RichTextEditorMenuBar from './RichTextEditorMenuBar'; @@ -55,6 +56,7 @@ const RichTextEditor = ({ isOneLine, onChange, placeholder, + templateVariables, }) => { const oneLineExtension = isOneLine ? [OneLineLimit] : []; @@ -73,6 +75,14 @@ const RichTextEditor = ({ }), ]; + const templateVariablesExtension = templateVariables.length > 0 ? + [TemplateVariable.configure({ + HTMLAttributes: { + class: 'RichTextEditor__TemplateVariable', + }, + suggestion: buildSuggestions(templateVariables), + })] : []; + const optionalExtensions = [ { name: RichTextEditorActions.BOLD, @@ -100,6 +110,7 @@ const RichTextEditor = ({ const extensions = [ ...oneLineExtension, ...requiredExtensions, + ...templateVariablesExtension, ...optionalExtensions, ]; @@ -135,15 +146,18 @@ const RichTextEditor = ({ className="RichTextEditor" id={id} > - + {availableActions.length > 0 && ( + + )} ( /> ); +export const TemplateVariables = () => ( + null} + /> +); + export const Error = () => ( ; + renderText: (props: { options: TemplateVariableOptions; node: ProseMirrorNode }) => string; + renderHTML: (props: { options: TemplateVariableOptions; node: ProseMirrorNode }) => DOMOutputSpec; + suggestion: Omit; +} + +export const TemplateVariablePluginKey = new PluginKey('template_variable'); + +export const TemplateVariable = Node.create({ + name: 'template_variable', + + addOptions() { + return { + HTMLAttributes: {}, + renderText({ node }) { + return `{{ ${node.attrs.label ?? node.attrs.id} }}`; + }, + renderHTML({ node }) { + return [ + 'span', + this.HTMLAttributes, + `{{ ${node.attrs.label ?? node.attrs.id} }}`, + ]; + }, + suggestion: { + char: '/', + pluginKey: TemplateVariablePluginKey, + command: ({ editor, range, props }) => { + // increase range.to by one when the next node is of type "text" + // and starts with a space character + const { nodeAfter } = editor.view.state.selection.$to; + const overrideSpace = nodeAfter?.text?.startsWith(' '); + + if (overrideSpace) { + range.to += 1; + } + + editor + .chain() + .focus() + .insertContentAt(range, [ + { + type: this.name, + attrs: props, + }, + { + type: 'text', + text: ' ', + }, + ]) + .run(); + + window.getSelection()?.collapseToEnd(); + }, + allow: ({ state, range }) => { + const $from = state.doc.resolve(range.from); + const type = state.schema.nodes[this.name]; + const allow = !!$from.parent.type.contentMatch.matchType(type); + + return allow; + }, + }, + }; + }, + + group: 'inline', + inline: true, + selectable: false, + atom: true, + + addAttributes() { + return { + id: { + default: null, + parseHTML: (element) => element.getAttribute('data-id'), + renderHTML: (attributes) => { + if (!attributes.id) { + return {}; + } + + return { + 'data-id': attributes.id, + }; + }, + }, + + label: { + default: null, + parseHTML: (element) => element.getAttribute('data-label'), + renderHTML: (attributes) => { + if (!attributes.label) { + return {}; + } + + return { + 'data-label': attributes.label, + }; + }, + }, + }; + }, + + parseHTML() { + return [ + { + tag: `span[data-type="${this.name}"]`, + }, + ]; + }, + + renderHTML({ node, HTMLAttributes }) { + const html = this.options.renderHTML({ + options: this.options, + node, + }); + + if (typeof html === 'string') { + return [ + 'span', + mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + html, + ]; + } + return html; + }, + + renderText({ node }) { + return this.options.renderText({ + options: this.options, + node, + }); + }, + + addKeyboardShortcuts() { + return { + Backspace: () => this.editor.commands.command(({ tr, state }) => { + let isTemplateVariable = false; + const { selection } = state; + const { empty, anchor } = selection; + + if (!empty) { + return false; + } + + state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { + if (node.type.name === this.name) { + isTemplateVariable = true; + tr.insertText(this.options.suggestion.char || '', pos, pos + node.nodeSize); + + return false; + } + + return undefined; + }); + + return isTemplateVariable; + }), + }; + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + ...this.options.suggestion, + }), + ]; + }, +}); + +type TemplateVariableListProps = { + items: string[]; + command: (arg0: {id: string}) => void; +} + +export const TemplateVariableList = forwardRef((props: TemplateVariableListProps, ref) => { + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectItem = (index) => { + const item = props.items[index]; + + if (item) { + props.command({ id: item }); + } + }; + + const upHandler = () => { + setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length); + }; + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length); + }; + + const enterHandler = () => { + selectItem(selectedIndex); + }; + + useEffect(() => setSelectedIndex(0), [props.items]); + + useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }) => { + if (event.key === 'ArrowUp') { + upHandler(); + return true; + } + + if (event.key === 'ArrowDown') { + downHandler(); + return true; + } + + if (event.key === 'Enter') { + enterHandler(); + return true; + } + + return false; + }, + })); + + return ( +
+ {props.items.length ? + props.items.map((item, index) => ( + + )) : +
No result
} +
+ ); +}); + +export function buildSuggestions(variables: string[]) { + return { + items: ({ query }) => variables + .filter((item) => item.toLowerCase().startsWith(query.toLowerCase())), + + render: () => { + let component; + let popup; + + return { + onStart: (props) => { + component = new ReactRenderer(TemplateVariableList, { + props, + editor: props.editor, + }); + + if (!props.clientRect) { + return; + } + + popup = tippy('body', { + getReferenceClientRect: props.clientRect, + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + }); + }, + + onUpdate(props) { + component.updateProps(props); + + if (!props.clientRect) { + return; + } + + popup[0].setProps({ + getReferenceClientRect: props.clientRect, + }); + }, + + onKeyDown(props) { + if (props.event.key === 'Escape') { + popup[0].hide(); + + return true; + } + + return component.ref?.onKeyDown(props); + }, + + onExit() { + popup[0].destroy(); + }, + }; + }, + }; +} From 4f8d6a7db57f4fecbe01019b253b0ae3671f3058 Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 13:24:37 -0500 Subject: [PATCH 3/9] removes dependency --- package.json | 3 +-- yarn.lock | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/package.json b/package.json index 77d2c4a9..24d3310f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@tiptap/extension-italic": "^2.0.3", "@tiptap/extension-link": "^2.0.3", "@tiptap/extension-list-item": "^2.0.3", - "@tiptap/extension-mention": "^2.1.13", "@tiptap/extension-ordered-list": "^2.0.3", "@tiptap/extension-paragraph": "^2.0.3", "@tiptap/extension-placeholder": "^2.0.3", @@ -193,4 +192,4 @@ "readme": "https://github.com/user-interviews/ui-design-system#readme", "homepage": "https://github.com/user-interviews/ui-design-system", "_id": "@user-interviews/ui-design-system@1.32.0" -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b202104f..650e28d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3502,11 +3502,6 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-list-item/-/extension-list-item-2.1.10.tgz#0615e4fb68161e6457e6041e195f454bfd537d44" integrity sha512-rRRyB14vOcSjTMAh8Y+50TRC/jO469CelGwFjOLrK1ZSEag5wmLDaqpWOOb52BFYnvCHuIm1HqZtdL5bTI/J1w== -"@tiptap/extension-mention@^2.1.13": - version "2.1.13" - resolved "https://registry.yarnpkg.com/@tiptap/extension-mention/-/extension-mention-2.1.13.tgz#6359c563268c46539660958847fe76c22131f2c8" - integrity sha512-OYqaucyBiCN/CmDYjpOVX74RJcIEKmAqiZxUi8Gfaq7ryEO5a8Gk93nK+8uZ0onaqHE+mHpoLFFbcAFbOPgkUQ== - "@tiptap/extension-ordered-list@^2.0.3": version "2.1.10" resolved "https://registry.yarnpkg.com/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.10.tgz#ef5d5ba68baf86e9b66c1b2c1cec458aa111ad44" From b77ef4f352432b088257fa311c22c206dafa66fd Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 13:32:22 -0500 Subject: [PATCH 4/9] use scss vars --- src/RichTextEditor/TemplateVariable.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RichTextEditor/TemplateVariable.scss b/src/RichTextEditor/TemplateVariable.scss index 3d1594a8..3f447007 100644 --- a/src/RichTextEditor/TemplateVariable.scss +++ b/src/RichTextEditor/TemplateVariable.scss @@ -4,18 +4,18 @@ border: 1px solid $synth-div-stroke-neutral; background-color: $ux-white; border-radius: $ux-border-radius; - padding: 0.5rem 0; + padding: $ux-spacing-20 0; } .RichTextEditor__TemplateVariables__Item { @include synth-font-type-30; - background: #fff; + background: $ux-white; text-align: left; width: 100%; border: 0; outline: 0; color: $ux-gray-900; - padding: 4px 16px; + padding: $ux-spacing-10 $ux-spacing-40; } .RichTextEditor__TemplateVariables__Item:active, From 09a0fd4dac0110d2faba140c708d6b77d93466d7 Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 14:11:34 -0500 Subject: [PATCH 5/9] Fixes placeholder text Co-authored-by: Brian Collins <84730553+brianCollinsUI@users.noreply.github.com> --- src/RichTextEditor/RichTextEditor.stories.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RichTextEditor/RichTextEditor.stories.jsx b/src/RichTextEditor/RichTextEditor.stories.jsx index 41329367..b63f467d 100644 --- a/src/RichTextEditor/RichTextEditor.stories.jsx +++ b/src/RichTextEditor/RichTextEditor.stories.jsx @@ -56,7 +56,7 @@ export const OneLine = () => ( export const TemplateVariables = () => ( null} /> From d9a913f7557b4630c3e804022b2189a5c2ecfc55 Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 14:11:54 -0500 Subject: [PATCH 6/9] Simplifies border radius Co-authored-by: Brian Collins <84730553+brianCollinsUI@users.noreply.github.com> --- src/RichTextEditor/RichTextEditor.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RichTextEditor/RichTextEditor.scss b/src/RichTextEditor/RichTextEditor.scss index ec421112..dc5aa9bc 100644 --- a/src/RichTextEditor/RichTextEditor.scss +++ b/src/RichTextEditor/RichTextEditor.scss @@ -54,5 +54,5 @@ } .RichTextEditor__field--without-menu-bar .ProseMirror { - border-radius: $ux-border-radius $ux-border-radius; + border-radius: $ux-border-radius; } From dc3343948e62be5e44d507c7c18bd1383c86a748 Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 14:18:09 -0500 Subject: [PATCH 7/9] adds newline char to package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 24d3310f..825e63c1 100644 --- a/package.json +++ b/package.json @@ -192,4 +192,4 @@ "readme": "https://github.com/user-interviews/ui-design-system#readme", "homepage": "https://github.com/user-interviews/ui-design-system", "_id": "@user-interviews/ui-design-system@1.32.0" -} \ No newline at end of file +} From 109aea7aa156fb343bc6394f1cca0353ddc274cc Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 14:22:21 -0500 Subject: [PATCH 8/9] updates no result text --- src/RichTextEditor/TemplateVariable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RichTextEditor/TemplateVariable.tsx b/src/RichTextEditor/TemplateVariable.tsx index 6e064f13..c63425c4 100644 --- a/src/RichTextEditor/TemplateVariable.tsx +++ b/src/RichTextEditor/TemplateVariable.tsx @@ -250,7 +250,7 @@ export const TemplateVariableList = forwardRef((props: TemplateVariableListProps {item} )) : -
No result
} +
No variables found
} ); }); From 042a1a310ca928bfcf567335273af26f43f70219 Mon Sep 17 00:00:00 2001 From: Kyle Shike Date: Thu, 28 Dec 2023 14:29:26 -0500 Subject: [PATCH 9/9] prevent hover state of no results --- src/RichTextEditor/TemplateVariable.scss | 6 ++++-- src/RichTextEditor/TemplateVariable.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/RichTextEditor/TemplateVariable.scss b/src/RichTextEditor/TemplateVariable.scss index 3f447007..19a14372 100644 --- a/src/RichTextEditor/TemplateVariable.scss +++ b/src/RichTextEditor/TemplateVariable.scss @@ -22,6 +22,8 @@ .RichTextEditor__TemplateVariables__Item:hover, .RichTextEditor__TemplateVariables__Item:focus, .RichTextEditor__TemplateVariables__Item--selected { - background-color: $synth-hover-state; - text-decoration: none; + &:not(.RichTextEditor__TemplateVariables__Item--no-results) { + background-color: $synth-hover-state; + text-decoration: none; + } } diff --git a/src/RichTextEditor/TemplateVariable.tsx b/src/RichTextEditor/TemplateVariable.tsx index c63425c4..36998097 100644 --- a/src/RichTextEditor/TemplateVariable.tsx +++ b/src/RichTextEditor/TemplateVariable.tsx @@ -250,7 +250,7 @@ export const TemplateVariableList = forwardRef((props: TemplateVariableListProps {item} )) : -
No variables found
} +
No variables found
} ); });