Skip to content

Commit

Permalink
Support paste (#17)
Browse files Browse the repository at this point in the history
* Support Pasting markdown

* Fix single line paste

* Reset buffer using assignment

* Move eslint rules to eslintrc

* Revert eslint config

* Added Test case for utils

* Specify disable rule

* Added test case for handlePastedText

* remove redundant check
  • Loading branch information
ngs authored Apr 16, 2017
1 parent 3b67f7c commit b6c821a
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 36 deletions.
45 changes: 45 additions & 0 deletions src/__test__/plugin-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,51 @@ describe('draft-js-markdown-shortcuts-plugin', () => {
});
});
});
describe('handlePastedText', () => {
let pastedText;
let html;
beforeEach(() => {
pastedText = `_hello world_
Hello`;
subject = () => plugin.handlePastedText(pastedText, html, store);
});
[
'addText',
'addEmptyBlock',
'handleBlockType',
'handleImage',
'handleLink',
'handleInlineStyle'
].forEach((modifier) => {
describe(modifier, () => {
beforeEach(() => {
createMarkdownShortcutsPlugin.__Rewire__(modifier, modifierSpy); // eslint-disable-line no-underscore-dangle
});
it('returns handled', () => {
expect(subject()).to.equal('handled');
expect(modifierSpy).to.have.been.called();
});
});
});
describe('nothing in clipboard', () => {
beforeEach(() => {
pastedText = '';
});
it('returns not-handled', () => {
expect(subject()).to.equal('not-handled');
});
});
describe('pasted just text', () => {
beforeEach(() => {
pastedText = 'hello';
createMarkdownShortcutsPlugin.__Rewire__('addText', modifierSpy); // eslint-disable-line no-underscore-dangle
});
it('returns handled', () => {
expect(subject()).to.equal('handled');
expect(modifierSpy).to.have.been.calledWith(currentEditorState, 'hello');
});
});
});
});
});
});
48 changes: 48 additions & 0 deletions src/__test__/utils-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect } from 'chai';
import Draft, { EditorState } from 'draft-js';
import { addText, addEmptyBlock } from '../utils';

describe('utils test', () => {
it('is loaded', () => {
expect(addText).to.be.a('function');
expect(addEmptyBlock).to.be.a('function');
});

const newRawContentState = {
entityMap: {},
blocks: [{
key: 'item1',
text: 'altered!!',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {}
}]
};
it('should add empty block', () => {
let newEditorState = EditorState.createWithContent(Draft.convertFromRaw(newRawContentState));
const initialBlockSize = newEditorState.getCurrentContent().getBlockMap().size;
const randomBlockSize = Math.floor((Math.random() * 50) + 1); // random number bettween 1 to 50
for (let i = 0; i < randomBlockSize; i++) { // eslint-disable-line no-plusplus
newEditorState = addEmptyBlock(newEditorState);
}
const finalBlockSize = newEditorState.getCurrentContent().getBlockMap().size;
expect(finalBlockSize - initialBlockSize).to.equal(randomBlockSize);

const lastBlock = newEditorState.getCurrentContent().getLastBlock();
expect(lastBlock.getType()).to.equal('unstyled');
expect(lastBlock.getText()).to.have.lengthOf(0);
});

it('should addText', () => {
let newEditorState = EditorState.createWithContent(Draft.convertFromRaw(newRawContentState));
const randomText = Date.now().toString(32);
newEditorState = addEmptyBlock(newEditorState);
newEditorState = addText(newEditorState, randomText);
const currentContent = newEditorState.getCurrentContent();
expect(currentContent.hasText()).to.equal(true);
const lastBlock = currentContent.getLastBlock();
expect(lastBlock.getText()).to.equal(randomText);
});
});
113 changes: 77 additions & 36 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,48 @@ import leaveList from './modifiers/leaveList';
import insertText from './modifiers/insertText';
import createLinkDecorator from './decorators/link';
import createImageDecorator from './decorators/image';
import { addText, addEmptyBlock } from './utils';

const INLINE_STYLE_CHARACTERS = [' ', '*', '_'];

function checkCharacterForState(editorState, character) {
let newEditorState = handleBlockType(editorState, character);
if (editorState === newEditorState) {
newEditorState = handleImage(editorState, character);
}
if (editorState === newEditorState) {
newEditorState = handleLink(editorState, character);
}
if (editorState === newEditorState) {
newEditorState = handleInlineStyle(editorState, character);
}
return newEditorState;
}

function checkReturnForState(editorState, ev) {
let newEditorState = editorState;
const contentState = editorState.getCurrentContent();
const selection = editorState.getSelection();
const key = selection.getStartKey();
const currentBlock = contentState.getBlockForKey(key);
const type = currentBlock.getType();
const text = currentBlock.getText();
if (/-list-item$/.test(type) && text === '') {
newEditorState = leaveList(editorState);
}
if (newEditorState === editorState &&
(ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey || /^header-/.test(type))) {
newEditorState = insertEmptyBlock(editorState);
}
if (newEditorState === editorState && type === 'code-block') {
newEditorState = insertText(editorState, '\n');
}
if (newEditorState === editorState) {
newEditorState = handleNewCodeBlock(editorState);
}

return newEditorState;
}

const createMarkdownShortcutsPlugin = (config = {}) => {
const store = {};
Expand All @@ -36,34 +77,6 @@ const createMarkdownShortcutsPlugin = (config = {}) => {
store.setEditorState = setEditorState;
store.getEditorState = getEditorState;
},
handleReturn(ev, { setEditorState, getEditorState }) {
const editorState = getEditorState();
let newEditorState = editorState;
const contentState = editorState.getCurrentContent();
const selection = editorState.getSelection();
const key = selection.getStartKey();
const currentBlock = contentState.getBlockForKey(key);
const type = currentBlock.getType();
const text = currentBlock.getText();
if (/-list-item$/.test(type) && text === '') {
newEditorState = leaveList(editorState);
}
if (newEditorState === editorState &&
(ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey || /^header-/.test(type))) {
newEditorState = insertEmptyBlock(editorState);
}
if (newEditorState === editorState && type === 'code-block') {
newEditorState = insertText(editorState, '\n');
}
if (newEditorState === editorState) {
newEditorState = handleNewCodeBlock(editorState);
}
if (editorState !== newEditorState) {
setEditorState(newEditorState);
return 'handled';
}
return 'not-handled';
},
blockStyleFn(block) {
switch (block.getType()) {
case CHECKABLE_LIST_ITEM:
Expand Down Expand Up @@ -100,21 +113,49 @@ const createMarkdownShortcutsPlugin = (config = {}) => {
}
return 'not-handled';
},
handleReturn(ev, { setEditorState, getEditorState }) {
const editorState = getEditorState();
const newEditorState = checkReturnForState(editorState, ev);
if (editorState !== newEditorState) {
setEditorState(newEditorState);
return 'handled';
}
return 'not-handled';
},
handleBeforeInput(character, { getEditorState, setEditorState }) {
if (character !== ' ') {
return 'not-handled';
}
const editorState = getEditorState();
let newEditorState = handleBlockType(editorState, character);
if (editorState === newEditorState) {
newEditorState = handleImage(editorState, character);
}
if (editorState === newEditorState) {
newEditorState = handleLink(editorState, character);
const newEditorState = checkCharacterForState(editorState, character);
if (editorState !== newEditorState) {
setEditorState(newEditorState);
return 'handled';
}
if (editorState === newEditorState) {
newEditorState = handleInlineStyle(editorState, character);
return 'not-handled';
},
handlePastedText(text, html, { getEditorState, setEditorState }) {
const editorState = getEditorState();
let newEditorState = editorState;
let buffer = [];
for (let i = 0; i < text.length; i++) { // eslint-disable-line no-plusplus
if (INLINE_STYLE_CHARACTERS.indexOf(text[i]) >= 0) {
newEditorState = addText(newEditorState, buffer.join('') + text[i]);
newEditorState = checkCharacterForState(newEditorState, text[i]);
buffer = [];
} else if (text[i].charCodeAt(0) === 10) {
newEditorState = addText(newEditorState, buffer.join(''));
newEditorState = addEmptyBlock(newEditorState);
newEditorState = checkReturnForState(newEditorState, {});
buffer = [];
} else if (i === text.length - 1) {
newEditorState = addText(newEditorState, buffer.join('') + text[i]);
buffer = [];
} else {
buffer.push(text[i]);
}
}

if (editorState !== newEditorState) {
setEditorState(newEditorState);
return 'handled';
Expand Down
32 changes: 32 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { genKey, ContentBlock, Modifier, EditorState } from 'draft-js';
import { List } from 'immutable';

function getEmptyContentBlock() {
return new ContentBlock({
key: genKey(),
text: '',
characterList: List(),
});
}

export function addText(editorState, bufferText) {
const contentState = Modifier.insertText(editorState.getCurrentContent(), editorState.getSelection(), bufferText);
return EditorState.push(editorState, contentState, 'insert-characters');
}

export function addEmptyBlock(editorState) {
let contentState = editorState.getCurrentContent();
const emptyBlock = getEmptyContentBlock();
const blockMap = contentState.getBlockMap();
const selectionState = editorState.getSelection();
contentState = contentState.merge({
blockMap: blockMap.set(emptyBlock.getKey(), emptyBlock),
selectionAfter: selectionState.merge({
anchorKey: emptyBlock.getKey(),
focusKey: emptyBlock.getKey(),
anchorOffset: 0,
focusOffset: 0,
}),
});
return EditorState.push(editorState, contentState, 'insert-characters');
}

0 comments on commit b6c821a

Please sign in to comment.