From 945da9cf921278bce1d93b45f7eea63767f79e5a Mon Sep 17 00:00:00 2001 From: Devesh Date: Tue, 21 Dec 2021 22:04:50 +0530 Subject: [PATCH 1/3] feat(ui-markdown-editor): inline wysiwyg - #360 Signed-off-by: Devesh --- .../src/plugins/withText.js | 13 ++- .../src/utilities/constants.js | 2 + .../src/utilities/matchCases.js | 97 +++++++++++++++++-- 3 files changed, 104 insertions(+), 8 deletions(-) diff --git a/packages/ui-markdown-editor/src/plugins/withText.js b/packages/ui-markdown-editor/src/plugins/withText.js index dbf850e..8dee44b 100644 --- a/packages/ui-markdown-editor/src/plugins/withText.js +++ b/packages/ui-markdown-editor/src/plugins/withText.js @@ -1,6 +1,7 @@ import { Node } from 'slate'; import { isBlockHeading } from '../utilities/toolbarHelpers'; import { matchCases } from "utilities/matchCases"; +import { SPACE_BAR } from "utilities/constants"; export const withText = (editor) => { const { insertText } = editor; @@ -10,8 +11,16 @@ export const withText = (editor) => { if(isBlockHeading(editor)){ return; } - const currentLine = currentNode.text; - matchCases(editor, currentLine); + + const currentLine = currentNode.text + + onkeyup = (ev) => { + switch (ev.key) { + case SPACE_BAR: + matchCases(editor, currentLine); + break; + } + } } return editor; } \ No newline at end of file diff --git a/packages/ui-markdown-editor/src/utilities/constants.js b/packages/ui-markdown-editor/src/utilities/constants.js index e1fa4a3..015dede 100644 --- a/packages/ui-markdown-editor/src/utilities/constants.js +++ b/packages/ui-markdown-editor/src/utilities/constants.js @@ -4,6 +4,8 @@ const MISC_CONSTANTS = { DROPDOWN_NORMAL: 'Normal', }; +export const SPACE_BAR = " "; + export const BUTTON_COLORS = { BACKGROUND_INACTIVE: '#FFFFFF', BACKGROUND_ACTIVE: '#F0F0F0', diff --git a/packages/ui-markdown-editor/src/utilities/matchCases.js b/packages/ui-markdown-editor/src/utilities/matchCases.js index 76bd3b6..cd85b7b 100644 --- a/packages/ui-markdown-editor/src/utilities/matchCases.js +++ b/packages/ui-markdown-editor/src/utilities/matchCases.js @@ -4,7 +4,11 @@ import { insertThematicBreak } from "./toolbarHelpers"; export const matchCases = (editor, currentLine) => { - const matchHeadings = (editor, currentLine) => { + const offsetBeforeSpace = editor.selection.anchor.offset - 2; + const lastChar = currentLine.charAt(offsetBeforeSpace); + const prevTextFromSpace = currentLine.substr(0, offsetBeforeSpace + 1); + + const matchHeadings = () => { const headingMatchCase = currentLine.match(/(^\s*)#{1,6}\s/m); if(!headingMatchCase) return; @@ -20,8 +24,8 @@ export const matchCases = (editor, currentLine) => { return; } - const matchPageBreak = (editor, currentLine)=>{ - const pageBreakMatchCase = currentLine.match(/(^\s*)([*-])(?:[\t ]*\2){2,}/m); + const matchPageBreak = () => { + const pageBreakMatchCase = currentLine.match(/(^\s*)([-])(?:[\t ]*\2){2,}/m); if(!pageBreakMatchCase) return Editor.deleteBackward(editor, { unit: 'word' }); @@ -30,6 +34,87 @@ export const matchCases = (editor, currentLine) => { return; } - matchHeadings(editor, currentLine); - matchPageBreak(editor, currentLine); -} + /** + * + * @param {string} textToInsert The text that we want to format + * @param {Object} textFormats This is the format style of the text (bold, italic, code) we want to apply or remove as the key in an object + * and a boolean as the value. i.e. `{ bold: true }` + * @param {Integer} markdownCharacters The number of markdown characters that the user has to type to trigger WYSIWYG + */ + const insertFormattedInlineText = (textToInsert, textFormats, markdownCharacters) => { + const currentRange = { + anchor: editor.selection.anchor, + focus: { + path: editor.selection.focus.path, + offset: editor.selection.focus.offset - (textToInsert.length + 1 + markdownCharacters) + } + } + + Transforms.insertText(editor, " "); + + Transforms.insertNodes( + editor, { + text: textToInsert, + + ...textFormats + + }, { + at: currentRange + } + ) + } + + const matchCodeInline = () => { + const codeInlineMatchCase = prevTextFromSpace.match(/\s?(`|``)((?!\1).)+?\1$/m); + if (!lastChar === "`") return; + if (!codeInlineMatchCase) return; + + const codeText = codeInlineMatchCase[0].trim().replace(new RegExp(codeInlineMatchCase[1], "g"), ""); + + insertFormattedInlineText(codeText, { + code: true + }, 2); + + return; + } + + const matchBoldAndItalic = () => { + + let boldAndItalicMatchCase; + let boldMatchCase; + let italicMatchCase; + + if (lastChar === "*" || lastChar === "_") { + if ((boldAndItalicMatchCase = prevTextFromSpace.match(/\s?(\*\*\*|___)((?!\1).)+?\1$/m))) { + // ***[bold + italic]***, ___[bold + italic]___ + const reg = boldAndItalicMatchCase[1] === "***" ? /\*\*\*/ : boldAndItalicMatchCase[1]; + const boldAndItalicText = boldAndItalicMatchCase[0].trim().replace(new RegExp(reg, "g"), ""); + insertFormattedInlineText(boldAndItalicText, { + bold: true, + italic: true + }, 6) + } else if ((boldMatchCase = prevTextFromSpace.match(/\s?(\*\*|__)((?!\1).)+?\1$/m))) { + // **bold**, __bold__ + const reg = boldMatchCase[1] === "**" ? /\*\*/ : boldMatchCase[1]; + const boldText = boldMatchCase[0].replace(new RegExp(reg, "g"), ""); + insertFormattedInlineText(boldText, { + bold: true + }, 4) + } else if ((italicMatchCase = prevTextFromSpace.match(/\s?(\*|_)((?!\1).)+?\1$/m))) { + // *italic*, _italic_ + const reg = italicMatchCase[1] === "*" ? /\*/ : italicMatchCase[1]; + const italicText = italicMatchCase[0].replace(new RegExp(reg, "g"), ""); + insertFormattedInlineText(italicText, { + italic: true + }, 2) + } + + } + } + + matchHeadings(); + matchPageBreak(); + matchBoldAndItalic() + matchCodeInline(); + return; +} \ No newline at end of file From 1bc8bd8dd6788f108d11e1162ca022274b25288e Mon Sep 17 00:00:00 2001 From: Devesh Date: Wed, 22 Dec 2021 14:43:26 +0530 Subject: [PATCH 2/3] feat(ui-markdown-editor): updates - #360 Signed-off-by: Devesh --- .../src/plugins/withText.js | 8 +- .../src/utilities/constants.js | 2 +- .../src/utilities/matchCases.js | 184 ++++++++++-------- 3 files changed, 110 insertions(+), 84 deletions(-) diff --git a/packages/ui-markdown-editor/src/plugins/withText.js b/packages/ui-markdown-editor/src/plugins/withText.js index 8dee44b..1e4fa2a 100644 --- a/packages/ui-markdown-editor/src/plugins/withText.js +++ b/packages/ui-markdown-editor/src/plugins/withText.js @@ -1,7 +1,7 @@ import { Node } from 'slate'; import { isBlockHeading } from '../utilities/toolbarHelpers'; import { matchCases } from "utilities/matchCases"; -import { SPACE_BAR } from "utilities/constants"; +import { SPACE_CHARACTER } from "utilities/constants"; export const withText = (editor) => { const { insertText } = editor; @@ -15,10 +15,8 @@ export const withText = (editor) => { const currentLine = currentNode.text onkeyup = (ev) => { - switch (ev.key) { - case SPACE_BAR: - matchCases(editor, currentLine); - break; + if(ev.key === SPACE_CHARACTER){ + matchCases(editor, currentLine); } } } diff --git a/packages/ui-markdown-editor/src/utilities/constants.js b/packages/ui-markdown-editor/src/utilities/constants.js index 015dede..cb2ed75 100644 --- a/packages/ui-markdown-editor/src/utilities/constants.js +++ b/packages/ui-markdown-editor/src/utilities/constants.js @@ -4,7 +4,7 @@ const MISC_CONSTANTS = { DROPDOWN_NORMAL: 'Normal', }; -export const SPACE_BAR = " "; +export const SPACE_CHARACTER = " "; export const BUTTON_COLORS = { BACKGROUND_INACTIVE: '#FFFFFF', diff --git a/packages/ui-markdown-editor/src/utilities/matchCases.js b/packages/ui-markdown-editor/src/utilities/matchCases.js index cd85b7b..6aa25db 100644 --- a/packages/ui-markdown-editor/src/utilities/matchCases.js +++ b/packages/ui-markdown-editor/src/utilities/matchCases.js @@ -1,120 +1,148 @@ -import { Transforms, Editor } from 'slate'; +import { Transforms, Editor } from "slate"; import { H1, H2, H3, H4, H5, H6, HR } from "./schema"; import { insertThematicBreak } from "./toolbarHelpers"; export const matchCases = (editor, currentLine) => { + const offsetBeforeSpace = editor.selection.anchor.offset - 2; + const lastChar = currentLine.charAt(offsetBeforeSpace); + const prevTextFromSpace = currentLine.substr(0, offsetBeforeSpace + 1); + + const matchHeadings = () => { + const headingMatchCase = currentLine.match(/(^\s*)#{1,6}\s/m); + if (!headingMatchCase) return; + + const count = (headingMatchCase[0].match(/#/g) || []).length; + if (count === 1) Transforms.setNodes(editor, { type: H1 }); + else if (count === 2) Transforms.setNodes(editor, { type: H2 }); + else if (count === 3) Transforms.setNodes(editor, { type: H3 }); + else if (count === 4) Transforms.setNodes(editor, { type: H4 }); + else if (count === 5) Transforms.setNodes(editor, { type: H5 }); + else if (count === 6) Transforms.setNodes(editor, { type: H6 }); + + Editor.deleteBackward(editor, { unit: "word" }); + return; + }; + + const matchPageBreak = () => { + const pageBreakMatchCase = currentLine.match(/(^\s*)([-])(?:[\t ]*\2){2,}/m); + if (!pageBreakMatchCase) return; + + Editor.deleteBackward(editor, { unit: "word" }); + insertThematicBreak(editor, HR); - const offsetBeforeSpace = editor.selection.anchor.offset - 2; - const lastChar = currentLine.charAt(offsetBeforeSpace); - const prevTextFromSpace = currentLine.substr(0, offsetBeforeSpace + 1); - - const matchHeadings = () => { - const headingMatchCase = currentLine.match(/(^\s*)#{1,6}\s/m); - if(!headingMatchCase) return; - - const count = (headingMatchCase[0].match(/#/g) || []).length; - if (count === 1) Transforms.setNodes(editor, { type: H1 }); - else if (count === 2) Transforms.setNodes(editor, { type: H2 }); - else if (count === 3) Transforms.setNodes(editor, { type: H3 }); - else if (count === 4) Transforms.setNodes(editor, { type: H4 }); - else if (count === 5) Transforms.setNodes(editor, { type: H5 }); - else if (count === 6) Transforms.setNodes(editor, { type: H6 }); - - Editor.deleteBackward(editor, { unit: 'word' }); - return; - } - - const matchPageBreak = () => { - const pageBreakMatchCase = currentLine.match(/(^\s*)([-])(?:[\t ]*\2){2,}/m); - if(!pageBreakMatchCase) return - - Editor.deleteBackward(editor, { unit: 'word' }); - insertThematicBreak(editor, HR); - - return; - } - - /** - * - * @param {string} textToInsert The text that we want to format - * @param {Object} textFormats This is the format style of the text (bold, italic, code) we want to apply or remove as the key in an object - * and a boolean as the value. i.e. `{ bold: true }` - * @param {Integer} markdownCharacters The number of markdown characters that the user has to type to trigger WYSIWYG - */ - const insertFormattedInlineText = (textToInsert, textFormats, markdownCharacters) => { + return; + }; + + /** + * + * @param {string} textToInsert The text that we want to format + * @param {Object} textFormats This is the format style of the text (bold, italic, code) we want to apply or remove as the key in an object and a boolean as the value. i.e. `{ bold: true }` + * @param {Integer} markdownCharacters The number of markdown characters that the user has to type to trigger WYSIWYG + */ + const insertFormattedInlineText = (textToInsert, textFormats, markdownCharacters) => { const currentRange = { anchor: editor.selection.anchor, focus: { path: editor.selection.focus.path, - offset: editor.selection.focus.offset - (textToInsert.length + 1 + markdownCharacters) - } - } + offset: + editor.selection.focus.offset - + (textToInsert.length + 1 + markdownCharacters), + }, + }; Transforms.insertText(editor, " "); Transforms.insertNodes( - editor, { + editor, + { text: textToInsert, - ...textFormats - - }, { - at: currentRange + ...textFormats, + }, + { + at: currentRange, } - ) - } + ); + }; const matchCodeInline = () => { const codeInlineMatchCase = prevTextFromSpace.match(/\s?(`|``)((?!\1).)+?\1$/m); if (!lastChar === "`") return; if (!codeInlineMatchCase) return; - const codeText = codeInlineMatchCase[0].trim().replace(new RegExp(codeInlineMatchCase[1], "g"), ""); + const codeText = codeInlineMatchCase[0] + .trim() + .replace(new RegExp(codeInlineMatchCase[1], "g"), ""); - insertFormattedInlineText(codeText, { - code: true - }, 2); + insertFormattedInlineText( + codeText, + { + code: true, + }, + 2 + ); return; - } + }; const matchBoldAndItalic = () => { - let boldAndItalicMatchCase; let boldMatchCase; let italicMatchCase; if (lastChar === "*" || lastChar === "_") { - if ((boldAndItalicMatchCase = prevTextFromSpace.match(/\s?(\*\*\*|___)((?!\1).)+?\1$/m))) { - // ***[bold + italic]***, ___[bold + italic]___ - const reg = boldAndItalicMatchCase[1] === "***" ? /\*\*\*/ : boldAndItalicMatchCase[1]; - const boldAndItalicText = boldAndItalicMatchCase[0].trim().replace(new RegExp(reg, "g"), ""); - insertFormattedInlineText(boldAndItalicText, { - bold: true, - italic: true - }, 6) - } else if ((boldMatchCase = prevTextFromSpace.match(/\s?(\*\*|__)((?!\1).)+?\1$/m))) { - // **bold**, __bold__ - const reg = boldMatchCase[1] === "**" ? /\*\*/ : boldMatchCase[1]; + if ( + (boldAndItalicMatchCase = prevTextFromSpace.match( + /\s?(\*\*\*|___)((?!\1).)+?\1$/m + )) + ) { + // ***[bold + italic]***, ___[bold + italic]___ + const reg = + boldAndItalicMatchCase[1] === "***" ? /\*\*\*/ : boldAndItalicMatchCase[1]; + const boldAndItalicText = boldAndItalicMatchCase[0] + .trim() + .replace(new RegExp(reg, "g"), ""); + insertFormattedInlineText( + boldAndItalicText, + { + bold: true, + italic: true, + }, + 6 + ); + } else if ( + (boldMatchCase = prevTextFromSpace.match(/\s?(\*\*|__)((?!\1).)+?\1$/m)) + ) { + // **bold**, __bold__ + const reg = boldMatchCase[1] === "**" ? /\*\*/ : boldMatchCase[1]; const boldText = boldMatchCase[0].replace(new RegExp(reg, "g"), ""); - insertFormattedInlineText(boldText, { - bold: true - }, 4) - } else if ((italicMatchCase = prevTextFromSpace.match(/\s?(\*|_)((?!\1).)+?\1$/m))) { - // *italic*, _italic_ + insertFormattedInlineText( + boldText, + { + bold: true, + }, + 4 + ); + } else if ( + (italicMatchCase = prevTextFromSpace.match(/\s?(\*|_)((?!\1).)+?\1$/m)) + ) { + // *italic*, _italic_ const reg = italicMatchCase[1] === "*" ? /\*/ : italicMatchCase[1]; const italicText = italicMatchCase[0].replace(new RegExp(reg, "g"), ""); - insertFormattedInlineText(italicText, { - italic: true - }, 2) + insertFormattedInlineText( + italicText, + { + italic: true, + }, + 2 + ); } - } - } + }; matchHeadings(); matchPageBreak(); - matchBoldAndItalic() + matchBoldAndItalic(); matchCodeInline(); return; -} \ No newline at end of file +}; \ No newline at end of file From 90f7ef87ab53638117b67ab22bdfafde34a75dba Mon Sep 17 00:00:00 2001 From: Devesh Date: Tue, 25 Jan 2022 18:03:11 +0530 Subject: [PATCH 3/3] Implemented withShortcuts Signed-off-by: Devesh --- packages/ui-markdown-editor/src/index.js | 6 +- .../src/plugins/withShortcuts.js | 180 ++++++++++++++++++ .../src/plugins/withText.js | 24 --- 3 files changed, 183 insertions(+), 27 deletions(-) create mode 100644 packages/ui-markdown-editor/src/plugins/withShortcuts.js delete mode 100644 packages/ui-markdown-editor/src/plugins/withText.js diff --git a/packages/ui-markdown-editor/src/index.js b/packages/ui-markdown-editor/src/index.js index 6c04f80..1d4f4cb 100644 --- a/packages/ui-markdown-editor/src/index.js +++ b/packages/ui-markdown-editor/src/index.js @@ -21,7 +21,7 @@ import { withLinks, isSelectionLinkBody } from './plugins/withLinks'; import { withHtml } from './plugins/withHtml'; import { withLists } from './plugins/withLists'; import FormatBar from './FormattingToolbar'; -import { withText } from './plugins/withText'; +import { withShortcuts } from './plugins/withShortcuts'; export const markdownToSlate = (markdown) => { const slateTransformer = new SlateTransformer(); @@ -41,12 +41,12 @@ export const MarkdownEditor = (props) => { const editor = useMemo(() => { if (augmentEditor) { return augmentEditor( - withLists(withLinks(withHtml(withImages(withText( + withLists(withLinks(withHtml(withImages(withShortcuts( withSchema(withHistory(withReact(createEditor()))) ))))) ); } - return withLists(withLinks(withHtml(withImages(withText( + return withLists(withLinks(withHtml(withImages(withShortcuts( withSchema(withHistory(withReact(createEditor()))) ))))); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/ui-markdown-editor/src/plugins/withShortcuts.js b/packages/ui-markdown-editor/src/plugins/withShortcuts.js new file mode 100644 index 0000000..3225392 --- /dev/null +++ b/packages/ui-markdown-editor/src/plugins/withShortcuts.js @@ -0,0 +1,180 @@ +import { Transforms, Editor } from "slate"; +import { SPACE_CHARACTER } from "../utilities/constants"; +import { H1, H2, H3, H4, H5, H6, HR, BLOCK_QUOTE } from "../utilities/schema"; +import { insertThematicBreak } from "../utilities/toolbarHelpers"; + +const SHORTCUTS = { + '>': BLOCK_QUOTE, + '#': H1, + '##': H2, + '###': H3, + '####': H4, + '#####': H5, + '######': H6, +} + +/** +* +* @param {Object} editor The editor Object +* @param {string} textToInsert The text that we want to format +* @param {Object} textFormats This is the format style of the text (bold, italic, code) we want to apply or remove as the key in an object and a boolean as the value. i.e. `{ bold: true }` +* @param {Integer} markdownCharacters The number of markdown characters that the user has to type to trigger WYSIWYG +*/ +const insertFormattedInlineText = (editor, textToInsert, textFormats, markdownCharacters) => { + const currentRange = { + anchor: editor.selection.anchor, + focus: { + path: editor.selection.focus.path, + offset: + editor.selection.focus.offset - + (textToInsert.length + 1 + markdownCharacters), + }, + }; + + Transforms.insertText(editor, " "); + + Transforms.insertNodes( + editor, + { + text: textToInsert, + + ...textFormats, + }, + { + at: currentRange, + } + ); +}; + +export const withShortcuts = (editor) => { + const { insertText } = editor; + editor.insertText = (text) => { + const { selection } = editor + + onkeyup = (ev) => { + if(ev.key === SPACE_CHARACTER){ + const { anchor } = selection + const block = Editor.above(editor, { + match: n => Editor.isBlock(editor, n), + }) + const path = block ? block[1] : [] + const start = Editor.start(editor, path) + const range = { anchor, focus: start } + const prevTextFromSpace = Editor.string(editor, range) + + const offsetBeforeSpace = range.anchor.offset; + const lastChar = prevTextFromSpace.charAt(offsetBeforeSpace-1); + + const type = SHORTCUTS[prevTextFromSpace] + + if (type) { + Transforms.select(editor, range) + Transforms.delete(editor) + const newProperties = { + type, + } + Transforms.setNodes(editor, newProperties, { + match: n => Editor.isBlock(editor, n), + }) + return + } + + const matchPageBreak = () => { + const pageBreakMatchCase = prevTextFromSpace.match(/(^\s*)([-])(?:[\t ]*\2){2,}/m); + if (!pageBreakMatchCase) return; + + Editor.deleteBackward(editor, { unit: "word" }); + insertThematicBreak(editor, HR); + + return; + }; + + const matchCodeInline = () => { + const codeInlineMatchCase = prevTextFromSpace.match(/\s?(`|``)((?!\1).)+?\1$/m); + if (!lastChar === "`") return; + if (!codeInlineMatchCase) return; + + const codeText = codeInlineMatchCase[0] + .trim() + .replace(new RegExp(codeInlineMatchCase[1], "g"), ""); + + insertFormattedInlineText( + editor, + codeText, + { + code: true, + }, + 2 + ); + + return; + }; + + const matchBoldAndItalic = () => { + let boldAndItalicMatchCase; + let boldMatchCase; + let italicMatchCase; + + if (lastChar === "*" || lastChar === "_") { + if ( + (boldAndItalicMatchCase = prevTextFromSpace.match( + /\s?(\*\*\*|___)((?!\1).)+?\1$/m + )) + ) { + // ***[bold + italic]***, ___[bold + italic]___ + const reg = + boldAndItalicMatchCase[1] === "***" ? /\*\*\*/ : boldAndItalicMatchCase[1]; + const boldAndItalicText = boldAndItalicMatchCase[0] + .trim() + .replace(new RegExp(reg, "g"), ""); + insertFormattedInlineText( + editor, + boldAndItalicText, + { + bold: true, + italic: true, + }, + 6 + ); + } else if ( + (boldMatchCase = prevTextFromSpace.match(/\s?(\*\*|__)((?!\1).)+?\1$/m)) + ) { + // **bold**, __bold__ + const reg = boldMatchCase[1] === "**" ? /\*\*/ : boldMatchCase[1]; + const boldText = boldMatchCase[0].replace(new RegExp(reg, "g"), ""); + insertFormattedInlineText( + editor, + boldText, + { + bold: true, + }, + 4 + ); + } else if ( + (italicMatchCase = prevTextFromSpace.match(/\s?(\*|_)((?!\1).)+?\1$/m)) + ) { + // *italic*, _italic_ + const reg = italicMatchCase[1] === "*" ? /\*/ : italicMatchCase[1]; + const italicText = italicMatchCase[0].replace(new RegExp(reg, "g"), ""); + insertFormattedInlineText( + editor, + italicText, + { + italic: true, + }, + 2 + ); + } + } + }; + + matchPageBreak(); + matchBoldAndItalic(); + matchCodeInline(); + return; + } + } + insertText(text); + } + return editor; +} \ No newline at end of file diff --git a/packages/ui-markdown-editor/src/plugins/withText.js b/packages/ui-markdown-editor/src/plugins/withText.js deleted file mode 100644 index 1e4fa2a..0000000 --- a/packages/ui-markdown-editor/src/plugins/withText.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Node } from 'slate'; -import { isBlockHeading } from '../utilities/toolbarHelpers'; -import { matchCases } from "utilities/matchCases"; -import { SPACE_CHARACTER } from "utilities/constants"; - -export const withText = (editor) => { - const { insertText } = editor; - editor.insertText = (text) => { - insertText(text); - const currentNode = Node.get(editor, editor.selection.focus.path); - if(isBlockHeading(editor)){ - return; - } - - const currentLine = currentNode.text - - onkeyup = (ev) => { - if(ev.key === SPACE_CHARACTER){ - matchCases(editor, currentLine); - } - } - } - return editor; -} \ No newline at end of file