diff --git a/package.json b/package.json index 7e9aa1e..fe1c6d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "draft-js-markdown-plugin", - "version": "2.1.1", + "version": "3.0.0", "description": "A DraftJS plugin for supporting Markdown syntax shortcuts, fork of draft-js-markdown-shortcuts-plugin", "main": "lib/index.js", "scripts": { diff --git a/src/__test__/plugin.test.js b/src/__test__/plugin.test.js index 22f7dc9..a8c6388 100755 --- a/src/__test__/plugin.test.js +++ b/src/__test__/plugin.test.js @@ -542,7 +542,8 @@ describe("draft-js-markdown-plugin", () => { expect(modifierSpy).toHaveBeenCalledWith( defaultInlineWhitelist, currentEditorState, - " " + " ", + undefined ); }); it("unstickys inline style", () => { @@ -660,115 +661,6 @@ describe("draft-js-markdown-plugin", () => { expect(store.setEditorState).toHaveBeenCalledWith(newEditorState); }); }); - describe("handlePastedText", () => { - let pastedText; - let html; - beforeEach(() => { - pastedText = `_hello world_ - Hello`; - html = undefined; - subject = () => - plugin.handlePastedText( - pastedText, - html, - store.getEditorState(), - store - ); - }); - [ - "replaceText", - // TODO(@mxstbr): This broke when switching mocha->jest, fix it! - // 'insertEmptyBlock', - "handleBlockType", - "handleImage", - "handleLink", - "handleInlineStyle", - ].forEach(modifier => { - describe(modifier, () => { - beforeEach(() => { - createMarkdownPlugin.__Rewire__(modifier, modifierSpy); // eslint-disable-line no-underscore-dangle - }); - it("returns handled", () => { - expect(subject()).toBe("handled"); - expect(modifierSpy).toHaveBeenCalled(); - }); - }); - }); - describe("nothing in clipboard", () => { - beforeEach(() => { - pastedText = ""; - }); - it("returns not-handled", () => { - expect(subject()).toBe("not-handled"); - }); - }); - describe("pasted just text", () => { - beforeEach(() => { - pastedText = "hello"; - createMarkdownPlugin.__Rewire__("replaceText", modifierSpy); // eslint-disable-line no-underscore-dangle - }); - it("returns handled", () => { - expect(subject()).toBe("handled"); - expect(modifierSpy).toHaveBeenCalledWith( - currentEditorState, - "hello" - ); - }); - }); - describe("pasted just text with new line code", () => { - beforeEach(() => { - pastedText = "hello\nworld"; - const rawContentState = { - entityMap: {}, - blocks: [ - { - key: "item1", - text: "", - type: "unstyled", - depth: 0, - inlineStyleRanges: [], - entityRanges: [], - data: {}, - }, - ], - }; - const otherRawContentState = { - entityMap: {}, - blocks: [ - { - key: "item2", - text: "H1", - type: "header-one", - depth: 0, - inlineStyleRanges: [], - entityRanges: [], - data: {}, - }, - ], - }; - /* eslint-disable no-underscore-dangle */ - createMarkdownPlugin.__Rewire__("replaceText", () => - createEditorState(rawContentState, currentSelectionState) - ); - createMarkdownPlugin.__Rewire__("checkReturnForState", () => - createEditorState(otherRawContentState, currentSelectionState) - ); - /* eslint-enable no-underscore-dangle */ - }); - it("return handled", () => { - expect(subject()).toBe("handled"); - }); - }); - describe("passed `html` argument", () => { - beforeEach(() => { - pastedText = "# hello"; - html = "<h1>hello</h1>"; - }); - it("returns not-handled", () => { - expect(subject()).toBe("not-handled"); - }); - }); - }); }); }); }); diff --git a/src/index.js b/src/index.js index f824b5e..ba85f2e 100755 --- a/src/index.js +++ b/src/index.js @@ -131,7 +131,8 @@ function checkCharacterForState(config, editorState, character) { newEditorState = handleInlineStyle( config.features.inline, editorState, - character + character, + config.customInlineMatchers ); } return newEditorState; @@ -415,64 +416,6 @@ const createMarkdownPlugin = (_config = {}) => { } } }, - handlePastedText(text, html, editorState, { setEditorState }) { - let newEditorState = editorState; - let buffer = []; - - if (html) { - return "not-handled"; - } - - // If we're in a code block don't add markdown to it - if (inCodeBlock(editorState)) { - setEditorState(insertText(editorState, text)); - return "handled"; - } - - for (let i = 0; i < text.length; i++) { - // eslint-disable-line no-plusplus - if (INLINE_STYLE_CHARACTERS.indexOf(text[i]) >= 0) { - newEditorState = replaceText( - newEditorState, - buffer.join("") + text[i] - ); - newEditorState = checkCharacterForState( - config, - newEditorState, - text[i] - ); - buffer = []; - } else if (text[i].charCodeAt(0) === 10) { - newEditorState = replaceText(newEditorState, buffer.join("")); - const tmpEditorState = checkReturnForState( - config, - newEditorState, - {} - ); - if (newEditorState === tmpEditorState) { - newEditorState = insertEmptyBlock(tmpEditorState); - } else { - newEditorState = tmpEditorState; - } - buffer = []; - } else if (i === text.length - 1) { - newEditorState = replaceText( - newEditorState, - buffer.join("") + text[i] - ); - buffer = []; - } else { - buffer.push(text[i]); - } - } - - if (editorState !== newEditorState) { - setEditorState(newEditorState); - return "handled"; - } - - return "not-handled"; - }, }; }; diff --git a/src/modifiers/__test__/changeCurrentInlineStyle-test.js b/src/modifiers/__test__/changeCurrentInlineStyle-test.js index 7f45d70..8424e68 100644 --- a/src/modifiers/__test__/changeCurrentInlineStyle-test.js +++ b/src/modifiers/__test__/changeCurrentInlineStyle-test.js @@ -43,6 +43,51 @@ describe("changeCurrentInlineStyle", () => { "CODE" ); expect(newEditorState).not.toEqual(editorState); + expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual( + rawContentState("foo bar baz", [ + { + length: 3, + offset: 4, + style: "CODE", + }, + ]) + ); + }); + it("removes inline styles when applying code style", () => { + const text = "`some bold text`"; + const editorState = createEditorState(text, [ + { + length: 4, + offset: 6, + style: "BOLD", + }, + ]); + const matchArr = ["`some bold text`", "some bold text"]; + matchArr.index = 0; + matchArr.input = text; + let newEditorState = changeCurrentInlineStyle( + editorState, + matchArr, + "CODE" + ); + expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual( + rawContentState("some bold text", [ + { length: 14, offset: 0, style: "CODE" }, + ]) + ); + }); + it("handles a style terminator properly", () => { + const text = "foo **bar** baz"; + const editorState = createEditorState(text, []); + const matchArr = ["**bar** ", "bar", " "]; + matchArr.index = 4; + matchArr.input = text; + const newEditorState = changeCurrentInlineStyle( + editorState, + matchArr, + "BOLD" + ); + expect(newEditorState).not.toEqual(editorState); expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual( rawContentState( "foo bar baz", @@ -50,10 +95,10 @@ describe("changeCurrentInlineStyle", () => { { length: 3, offset: 4, - style: "CODE", + style: "BOLD", }, ], - "CODE" + "BOLD" ) ); }); diff --git a/src/modifiers/__test__/handleInlineStyle-test.js b/src/modifiers/__test__/handleInlineStyle-test.js index a4f3bcd..a04fce9 100644 --- a/src/modifiers/__test__/handleInlineStyle-test.js +++ b/src/modifiers/__test__/handleInlineStyle-test.js @@ -47,14 +47,14 @@ describe("handleInlineStyle", () => { }); const testCases = { - "converts a mix of code, bold and italic and strikethrough in one go": { - character: "`", + "converts a mix of bold and italic and strikethrough in one go": { + character: "*", before: { entityMap: {}, blocks: [ { key: "item1", - text: "`h~el*lo _inline~_* style", + text: "*h~ello _inline~_ style", type: "unstyled", depth: 0, inlineStyleRanges: [], @@ -72,32 +72,55 @@ describe("handleInlineStyle", () => { type: "unstyled", depth: 0, inlineStyleRanges: [ - { - length: 12, - offset: 0, - style: "CODE", - }, - { - length: 11, - offset: 1, - style: "STRIKETHROUGH", - }, - { - length: 9, - offset: 3, - style: "BOLD", - }, - { - length: 6, - offset: 6, - style: "ITALIC", - }, + { length: 12, offset: 0, style: "BOLD" }, + { length: 11, offset: 1, style: "STRIKETHROUGH" }, + { length: 6, offset: 6, style: "ITALIC" }, ], entityRanges: [], data: {}, }, ], }, + selection: new SelectionState({ + anchorKey: "item1", + anchorOffset: 17, + focusKey: "item1", + focusOffset: 17, + isBackward: false, + hasFocus: true, + }), + }, + + "should not covert inside the code style": { + character: "`", + before: { + entityMap: {}, + blocks: [ + { + key: "item1", + text: "`h~el*lo _inline~_* style", + type: "unstyled", + depth: 0, + inlineStyleRanges: [], + entityRanges: [], + data: {}, + }, + ], + }, + after: { + entityMap: {}, + blocks: [ + { + key: "item1", + text: "h~el*lo _inline~_* style", + type: "unstyled", + depth: 0, + inlineStyleRanges: [{ length: 18, offset: 0, style: "CODE" }], + entityRanges: [], + data: {}, + }, + ], + }, selection: new SelectionState({ anchorKey: "item1", anchorOffset: 19, diff --git a/src/modifiers/changeCurrentInlineStyle.js b/src/modifiers/changeCurrentInlineStyle.js index c611c76..e76215f 100644 --- a/src/modifiers/changeCurrentInlineStyle.js +++ b/src/modifiers/changeCurrentInlineStyle.js @@ -1,5 +1,6 @@ import { OrderedSet } from "immutable"; import { EditorState, SelectionState, Modifier } from "draft-js"; +import removeInlineStyles from "./removeInlineStyles"; const changeCurrentInlineStyle = (editorState, matchArr, style) => { const currentContent = editorState.getCurrentContent(); @@ -8,8 +9,12 @@ const changeCurrentInlineStyle = (editorState, matchArr, style) => { const { index } = matchArr; const blockMap = currentContent.getBlockMap(); const block = blockMap.get(key); - const currentInlineStyle = block.getInlineStyleAt(index).merge(); - const newStyle = currentInlineStyle.merge([style]); + const currentInlineStyle = block.getInlineStyleAt(index); + // do not modify the text if it is inside code style + const hasCodeStyle = currentInlineStyle.find(style => style === "CODE"); + if (hasCodeStyle) { + return editorState; + } const focusOffset = index + matchArr[0].length; const wordSelection = SelectionState.createEmpty(key).merge({ @@ -17,16 +22,32 @@ const changeCurrentInlineStyle = (editorState, matchArr, style) => { focusOffset, }); - const inlineStyles = []; - const markdownCharacterLength = (matchArr[0].length - matchArr[1].length) / 2; + let newEditorState = editorState; - let newContentState = currentContent; + // remove all styles if applying code style + if (style === "CODE") { + newEditorState = removeInlineStyles(newEditorState, wordSelection); + } + + let newContentState = newEditorState.getCurrentContent(); + + // check if match contains a terminator group at the end + let matchTerminatorLength = 0; + if (matchArr.length == 3) { + matchTerminatorLength = matchArr[2].length; + } + + const markdownCharacterLength = + (matchArr[0].length - matchArr[1].length - matchTerminatorLength) / 2; // remove markdown delimiter at end newContentState = Modifier.removeRange( newContentState, wordSelection.merge({ - anchorOffset: wordSelection.getFocusOffset() - markdownCharacterLength, + anchorOffset: + wordSelection.getFocusOffset() - + markdownCharacterLength - + matchTerminatorLength, }) ); @@ -50,12 +71,23 @@ const changeCurrentInlineStyle = (editorState, matchArr, style) => { newContentState, wordSelection.merge({ anchorOffset: index, - focusOffset: focusOffset - markdownCharacterLength * 2, + focusOffset: + focusOffset - markdownCharacterLength * 2 - matchTerminatorLength, }), style ); - const newEditorState = EditorState.push( + // Check if a terminator exists and re-add it after the styled text + if (matchTerminatorLength > 0) { + newContentState = Modifier.insertText( + newContentState, + afterSelection, + matchArr[2] + ); + afterSelection = newContentState.getSelectionAfter(); + } + + newEditorState = EditorState.push( editorState, newContentState, "change-inline-style" diff --git a/src/modifiers/handleInlineStyle.js b/src/modifiers/handleInlineStyle.js index f494425..bead9e6 100644 --- a/src/modifiers/handleInlineStyle.js +++ b/src/modifiers/handleInlineStyle.js @@ -4,12 +4,14 @@ import { inlineMatchers } from "../constants"; import insertText from "./insertText"; import { getCurrentLine as getLine } from "../utils"; -const handleChange = (editorState, line, whitelist) => { +const handleChange = (editorState, line, whitelist, customInlineMatchers) => { let newEditorState = editorState; - Object.keys(inlineMatchers) + const matchers = Object.assign({}, inlineMatchers, customInlineMatchers); + + Object.keys(matchers) .filter(matcher => whitelist.includes(matcher)) .some(k => { - inlineMatchers[k].some(re => { + matchers[k].some(re => { let matchArr; do { matchArr = re.exec(line); @@ -31,19 +33,30 @@ const handleChange = (editorState, line, whitelist) => { const handleInlineStyle = ( whitelist, editorStateWithoutCharacter, - character + character, + customInlineMatchers = {} ) => { const editorState = insertText(editorStateWithoutCharacter, character); let selection = editorState.getSelection(); let line = getLine(editorState); - let newEditorState = handleChange(editorState, line, whitelist); + let newEditorState = handleChange( + editorState, + line, + whitelist, + customInlineMatchers + ); let lastEditorState = editorState; // Recursively resolve markdown, e.g. _*text*_ should turn into both italic and bold while (newEditorState !== lastEditorState) { lastEditorState = newEditorState; line = getLine(newEditorState); - newEditorState = handleChange(newEditorState, line, whitelist); + newEditorState = handleChange( + newEditorState, + line, + whitelist, + customInlineMatchers + ); } if (newEditorState !== editorState) { diff --git a/src/modifiers/removeInlineStyles.js b/src/modifiers/removeInlineStyles.js new file mode 100644 index 0000000..6d70038 --- /dev/null +++ b/src/modifiers/removeInlineStyles.js @@ -0,0 +1,17 @@ +import { EditorState, RichUtils, Modifier } from "draft-js"; + +export default (editorState, selection = editorState.getSelection()) => { + const styles = ["BOLD", "ITALIC", "STRIKETHROUGH", "CODE"]; + + let newEditorState = EditorState.push( + editorState, + styles.reduce( + (newContentState, style) => + Modifier.removeInlineStyle(newContentState, selection, style), + editorState.getCurrentContent() + ), + "change-inline-style" + ); + + return RichUtils.toggleLink(newEditorState, selection, null); +};