Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ node_modules
demo/dist.js
demo/draft.css
demo/prism.css

# Editor settings
.idea
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Demo: [samypesse.github.io/draft-js-code/](http://samypesse.github.io/draft-js-c
- [x] Indent with <kbd>TAB</kbd>
- [x] Insert new line with correct indentation with <kbd>ENTER</kbd>
- [x] Remove indentation with <kbd>DELETE</kbd>
- [ ] Remove indentation with <kdb>SHIFT+TAB</kbd> ([#6](https://github.com/SamyPesse/draft-js-code/issues/6))
- [x] Remove indentation with <kdb>SHIFT+TAB</kbd> ([#6](https://github.com/SamyPesse/draft-js-code/issues/6))
- [ ] Handle input of pair characters like `()`, `[]`, `{}`, `""`, etc. ([#3](https://github.com/SamyPesse/draft-js-code/issues/3))

### Installation
Expand All @@ -33,15 +33,19 @@ Returns true if user is editing a code block. You should call this method to enc

##### `CodeUtils.handleKeyCommand(editorState, command)`

Handle key command for code blocks, returns a new `EditorState` or `null`.
Handles the key command for code blocks, returns a new `EditorState` or `null`.

##### `CodeUtils.onTab(e, editorState)`
##### `CodeUtils.onTab(e, editorState, tabSize)`

Handle user pressing tab, to insert indentation, it returns a new `EditorState`.
Handles the user pressing `Tab`, to insert indentation, it returns a new `EditorState`.

The `tabSize` parameter is optional and defaults to four (4) spaces.

Also handles the user pressing `Shift+Tab` to reduce indentation if possible.

##### `CodeUtils.handleReturn(e, editorState)`

Handle user pressing return, to insert a new line inside the code block, it returns a new `EditorState`.
Handles the user pressing `Return`, to insert a new line inside the code block, it returns a new `EditorState`.


### Usage
Expand Down
78 changes: 70 additions & 8 deletions lib/__tests__/onTab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,16 @@ const createWithText = text => {

const tabs = times => ' '.repeat(times || 1);

it('should insert a tab', () => {
it('should prevent the default event behavior', () => {
const preventDefault = jest.fn();
const evt = { preventDefault };
const before = EditorState.createEmpty();

onTab(evt, before);
expect(preventDefault).toHaveBeenCalled();
});

it('should insert a tab when shift is not pressed', () => {
const evt = { preventDefault: jest.fn() };
const initialText = '';
const before = createWithText(initialText);
Expand All @@ -20,16 +29,59 @@ it('should insert a tab', () => {
expect(toPlainText(after)).toEqual(tabs(1));
});

it('should prevent the default event behavior', () => {
const preventDefault = jest.fn();
const evt = { preventDefault };
const before = EditorState.createEmpty();
it('should insert a two-spaced tab when the tab size is set to two', () => {
const evt = { preventDefault: jest.fn() };
const initialText = '';
const before = createWithText(initialText);
const after = onTab(evt, before, 2);

onTab(evt, before);
expect(preventDefault).toHaveBeenCalled();
expect(toPlainText(before)).toEqual(initialText);
expect(toPlainText(after)).toEqual(' ');
});

it('should remove a tab on a single line string when shift is pressed', () => {
const evt = { preventDefault: jest.fn(), shiftKey: true };
const initialText = tabs(1) + 'hello';
const before = createWithText(initialText);
const after = onTab(evt, before);

expect(toPlainText(before)).toEqual(initialText);
expect(toPlainText(after)).toEqual('hello');
});

it('should remove a two-spaced tab on a single line string when shift is pressed and the tab size is set to two', () => {
const evt = { preventDefault: jest.fn(), shiftKey: true };
const initialText = ' ' + 'hello';
const before = createWithText(initialText);
const after = onTab(evt, before, 2);

expect(toPlainText(before)).toEqual(initialText);
expect(toPlainText(after)).toEqual('hello');
});

it('should remove a tab from a multi line string when shift is pressed', () => {
const evt = { preventDefault: jest.fn(), shiftKey: true };
const initialText =
'Multi-Line Text\n' +
tabs(1) +
'- tabbed line one\n' +
tabs(1) +
'- tabbed line two';
const before = createWithText(initialText);
const after = onTab(evt, before);

const expectedText =
'Multi-Line Text\n' +
tabs(0) +
'- tabbed line one\n' +
tabs(1) +
'- tabbed line two';

expect(toPlainText(before)).toEqual(initialText);
expect(toPlainText(after)).toEqual(expectedText);
});

it('should add a tab to an existing tab', () => {
it('should add a tab to an existing tab when shift is not pressed', () => {
const evt = { preventDefault: jest.fn() };
const initialText = tabs(1);
const before = createWithText(initialText);
Expand All @@ -39,6 +91,16 @@ it('should add a tab to an existing tab', () => {
expect(toPlainText(after)).toEqual(initialText + tabs(1));
});

it('should remove a tab from an existing tab when shift is pressed', () => {
const evt = { preventDefault: jest.fn(), shiftKey: true };
const initialText = tabs(1);
const before = createWithText(initialText);
const after = onTab(evt, before);

expect(toPlainText(before)).toEqual(initialText);
expect(toPlainText(after)).toEqual('');
});

it('should replace selected content with the tab', () => {
const evt = { preventDefault: jest.fn() };
const initialText = 'hello';
Expand Down
41 changes: 14 additions & 27 deletions lib/onTab.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
var Draft = require('draft-js');
var getIndentation = require('./utils/getIndentation');
var increaseIndentation = require('./utils/increaseIndentation');
var decreaseIndentation = require('./utils/decreaseIndentation');

// TODO: tab should complete indentation instead of just inserting one

var DEFAULT_TAB_SIZE = 4;

/**
* Handle pressing tab in the editor
*
* @param {SyntheticKeyboardEvent} event
* @param {Draft.EditorState} editorState
* @return {Draft.EditorState}
*/
function onTab(e, editorState) {
e.preventDefault();

var contentState = editorState.getCurrentContent();
var selection = editorState.getSelection();
var startKey = selection.getStartKey();
var currentBlock = contentState.getBlockForKey(startKey);
function onTab(event, editorState, tabSize) {
event.preventDefault();
tabSize = tabSize || DEFAULT_TAB_SIZE;

var indentation = getIndentation(currentBlock.getText());
var newContentState;
var indentation;
if (event.shiftKey) {
var decreasedIndentation = decreaseIndentation(editorState, tabSize);

if (selection.isCollapsed()) {
newContentState = Draft.Modifier.insertText(
contentState,
selection,
indentation
);
indentation =
decreasedIndentation !== undefined ? decreasedIndentation : editorState;
} else {
newContentState = Draft.Modifier.replaceText(
contentState,
selection,
indentation
);
indentation = increaseIndentation(editorState, tabSize);
}

return Draft.EditorState.push(
editorState,
newContentState,
'insert-characters'
);
return indentation;
}

module.exports = onTab;
87 changes: 87 additions & 0 deletions lib/utils/decreaseIndentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
var Draft = require('draft-js');
var { SelectionState } = require('draft-js');

var getNewLine = require('./getNewLine');
var getIndentation = require('./getIndentation');
var getLines = require('./getLines');
var getLineAnchorForOffset = require('./getLineAnchorForOffset');
var getStringLengthIncludingLineAnchor = require('./getStringLengthIncludingLineAnchor');

/**
* Remove last indentation before cursor, return undefined if no modification is done
*
* @param {Draft.EditorState} editorState
* @return {Draft.EditorState|undefined}
*/
function decreaseIndentation(editorState, tabSize) {
var contentState = editorState.getCurrentContent();
var selection = editorState.getSelection();

if (!selection.isCollapsed()) {
return;
}

var startKey = selection.getStartKey();
var startOffset = selection.getStartOffset();

var currentBlock = contentState.getBlockForKey(startKey);
var blockText = currentBlock.getText();

// Detect newline separator and indentation
var newLine = getNewLine(blockText);
var indent = getIndentation(blockText);
if (tabSize === null) {
tabSize = indent.length;
}
var tab = ' '.repeat(tabSize);

// Get current line
var lines = getLines(blockText, newLine);
var lineAnchor = getLineAnchorForOffset(blockText, startOffset, newLine);

var currentLine = lines.get(lineAnchor.getLine());

var lineAnchorLimit = getStringLengthIncludingLineAnchor(lines, lineAnchor);

// TODO: If the line starts with fewer spaces than the tabSize, remove them?
// If the line doesn't start with an 'indent', ignore it. (there's no indent to remove).
if (currentLine.slice(0, tabSize) !== tab) {
return;
}

var startOfLineAnchor = lineAnchorLimit - currentLine.length;

// Remove indent
var rangeToRemove = selection.merge({
focusKey: startKey,
focusOffset: startOfLineAnchor,
anchorKey: startKey,
anchorOffset: startOfLineAnchor + tabSize,
isBackward: true
});

var newContentState = Draft.Modifier.removeRange(
contentState,
rangeToRemove,
'backward'
);
var newEditorState = Draft.EditorState.push(
editorState,
newContentState,
'remove-range'
);

// Restore the previous cursor position.
var newContentDefaultSelectionState = newContentState.getSelectionAfter();
var updateSelection = new SelectionState({
anchorKey: newContentDefaultSelectionState.anchorKey,
anchorOffset: startOffset - tabSize,
focusKey: newContentDefaultSelectionState.anchorKey,
focusOffset: startOffset - tabSize,
isBackward: false
});

return Draft.EditorState.forceSelection(newEditorState, updateSelection);
}

module.exports = decreaseIndentation;
30 changes: 30 additions & 0 deletions lib/utils/getStringLengthIncludingLineAnchor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Return an anchor of a cursor in a block as a {line,limit} object
*
* @param {List} allLines
* @param {LineAnchor} lineAnchor
* @return {Number}
*/
function getStringLengthIncludingLineAnchor(allLines, lineAnchor) {
var stringLengthIncludingLineAnchor = 0;

// When the cursor is at the start of an empty line or a line with only spaces,
// its getLine() method returns -1. We will treat it as a 0.
var lineNumber = lineAnchor.getLine() >= 0 ? lineAnchor.getLine() : 0;
var lineIndex = 0;
while (lineIndex <= lineNumber) {
var currentLine = allLines.get(lineIndex);

// Newline characters are not included in string.length.
// So, for each new line, we need to add 1 to the newline offset.
var newlineOffset = lineIndex === lineNumber ? 0 : 1;

stringLengthIncludingLineAnchor += currentLine.length + newlineOffset;

lineIndex++;
}

return stringLengthIncludingLineAnchor;
}

module.exports = getStringLengthIncludingLineAnchor;
40 changes: 40 additions & 0 deletions lib/utils/increaseIndentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
var Draft = require('draft-js');
var getIndentation = require('./getIndentation');

function increaseIndentation(editorState, tabSize) {
var contentState = editorState.getCurrentContent();
var selection = editorState.getSelection();
var startKey = selection.getStartKey();
var currentBlock = contentState.getBlockForKey(startKey);

var indentation;
if (tabSize !== null) {
indentation = ' '.repeat(tabSize);
} else {
indentation = getIndentation(currentBlock.getText());
}

var newContentState;

if (selection.isCollapsed()) {
newContentState = Draft.Modifier.insertText(
contentState,
selection,
indentation
);
} else {
newContentState = Draft.Modifier.replaceText(
contentState,
selection,
indentation
);
}

return Draft.EditorState.push(
editorState,
newContentState,
'insert-characters'
);
}

module.exports = increaseIndentation;
Loading