Skip to content
Permalink
Browse files

feat: backspace merge codeblock (#355)

* feat: merge codeblocks when position of cursor is between same codeblocks

* fix: save undoState for merge codeblock

* refactor: imporve destructuring

* fix: reflect code review

* fix: merge codeblocks when code blocks are side by side and backspace is pressed in front of the second codeblock

* fix: reflect code review
  • Loading branch information...
sohee-lee7 committed Jan 10, 2019
1 parent 00566a6 commit c586b6059caa8e8c877013efa1505d382f3d2260
Showing with 170 additions and 33 deletions.
  1. +117 −31 src/js/wwCodeBlockManager.js
  2. +53 −2 test/unit/wwCodeBlockManager.spec.js
@@ -57,7 +57,7 @@ class WwCodeBlockManager {
* @private
*/
_initKeyHandler() {
this._onKeyEventHandler = this._removeCodeblockFirstLine.bind(this);
this._onKeyEventHandler = this._onBackspaceKeyEventHandler.bind(this);
this.wwe.addKeyEventHandler('BACK_SPACE', this._onKeyEventHandler);
}

@@ -192,10 +192,6 @@ class WwCodeBlockManager {
});
}

_isCodeBlockFirstLine(range) {
return this.isInCodeBlock(range) && range.collapsed && range.startOffset === 0;
}

/**
* Remove codeblock of first line when press backspace in first line
* @memberof WwCodeBlockManager
@@ -204,37 +200,127 @@ class WwCodeBlockManager {
* @returns {boolean}
* @private
*/
_removeCodeblockFirstLine(ev, range) {
if (this._isCodeBlockFirstLine(range)) {
const sq = this.wwe.getEditor();
const container = range.commonAncestorContainer;
const preNode = container.nodeName === 'PRE' ? container : container.parentNode;
const codeContent = preNode.textContent.replace(FIND_ZWS_RX, '');
sq.modifyBlocks(() => {
const newFrag = this.wwe.getEditor().getDocument().createDocumentFragment();
let strArray = codeContent.split('\n');

const firstDiv = document.createElement('div');
const firstLine = strArray.shift();
firstDiv.innerHTML += `${firstLine}<br>`;
newFrag.appendChild(firstDiv);

if (strArray.length) {
const newPreNode = preNode.cloneNode();
newPreNode.textContent = strArray.join('\n');
newFrag.appendChild(newPreNode);
}

return newFrag;
});
_onBackspaceKeyEventHandler(ev, range) {
let isNeedNext = true;
const sq = this.wwe.getEditor();
const {commonAncestorContainer: container} = range;
if (this._isCodeBlockFirstLine(range) && !this._isFrontCodeblock(range)) {
this._removeCodeblockFirstLine(container);
range.collapse(true);
isNeedNext = false;
} else if (
range.collapsed && this._isEmptyLine(container) &&
this._isBetweenSameCodeblocks(container)
) {
const {previousSibling, nextSibling} = container;
const prevTextLength = previousSibling.textContent.length;

sq.saveUndoState(range);

container.parentNode.removeChild(container);
this._mergeCodeblocks(previousSibling, nextSibling);

range.setStart(previousSibling.childNodes[0], prevTextLength);
range.collapse(true);
isNeedNext = false;
}

if (!isNeedNext) {
sq.setSelection(range);
ev.preventDefault();

return false;
}

return true;
return isNeedNext;
}

/**
* Check node is empty line
* @memberof WwCodeBlockManager
* @param {Node} node node
* @returns {boolean}
* @private
*/
_isEmptyLine(node) {
const {nodeName, childNodes} = node;

return nodeName === 'DIV' && childNodes.length === 1 && childNodes[0].nodeName === 'BR';
}

/**
* Check whether node is between same codeblocks
* @memberof WwCodeBlockManager
* @param {Node} node Node
* @returns {boolean}
* @private
*/
_isBetweenSameCodeblocks(node) {
const {previousSibling, nextSibling} = node;

return (
domUtils.getNodeName(previousSibling) === 'PRE' &&
domUtils.getNodeName(nextSibling) === 'PRE' &&
previousSibling.getAttribute('data-language') === nextSibling.getAttribute('data-language')
);
}

_mergeCodeblocks(frontCodeblock, backCodeblock) {
const postText = backCodeblock.textContent;
frontCodeblock.childNodes[0].textContent += `\n${postText}`;
backCodeblock.parentNode.removeChild(backCodeblock);
}

/**
* Check whether range is first line of code block
* @memberof WwCodeBlockManager
* @param {Range} range Range object
* @returns {boolean}
* @private
*/
_isCodeBlockFirstLine(range) {
return this.isInCodeBlock(range) && range.collapsed && range.startOffset === 0;
}

/**
* Check whether front block of range is code block
* @memberof WwCodeBlockManager
* @param {Range} range Range object
* @returns {boolean}
* @private
*/
_isFrontCodeblock(range) {
const block = domUtils.getParentUntil(range.startContainer, this.wwe.getEditor().getRoot());
const {previousSibling} = block;

return previousSibling && previousSibling.nodeName === 'PRE';
}

/**
* Remove codeblock first line of codeblock
* @memberof WwCodeBlockManager
* @param {Node} node Pre Node
* @private
*/
_removeCodeblockFirstLine(node) {
const sq = this.wwe.getEditor();
const preNode = node.nodeName === 'PRE' ? node : node.parentNode;
const codeContent = preNode.textContent.replace(FIND_ZWS_RX, '');
sq.modifyBlocks(() => {
const newFrag = sq.getDocument().createDocumentFragment();
let strArray = codeContent.split('\n');

const firstDiv = document.createElement('div');
const firstLine = strArray.shift();
firstDiv.innerHTML = `${firstLine}<br>`;
newFrag.appendChild(firstDiv);

if (strArray.length) {
const newPreNode = preNode.cloneNode();
newPreNode.textContent = strArray.join('\n');
newFrag.appendChild(newPreNode);
}

return newFrag;
});
}

/**
@@ -50,7 +50,7 @@ describe('WwCodeBlockManager', () => {

describe('key handlers', () => {
describe('BACKSPACE', () => {
it('_removeCodeblockIfNeed() remove codeblock if codeblock has one code line when offset is 0', () => {
it('_onBackspaceKeyEvnetHandler() remove codeblock if codeblock has one code line when offset is 0', () => {
const range = wwe.getEditor().getSelection().cloneRange();

wwe.setValue('<pre>test</pre>');
@@ -72,7 +72,7 @@ describe('WwCodeBlockManager', () => {
expect(wwe.get$Body().find('pre').length).toEqual(0);
});

it('_removeCodeblockIfNeed() remove codeblock and make one empty line if there is no content', () => {
it('_onBackspaceKeyEvnetHandler() remove codeblock and make one empty line if there is no content', () => {
const range = wwe.getEditor().getSelection().cloneRange();

wwe.setValue('<pre>\n</pre>');
@@ -92,6 +92,57 @@ describe('WwCodeBlockManager', () => {
expect(wwe.get$Body().find('div').length).toEqual(2);
expect(wwe.get$Body().find('pre').length).toEqual(0);
});

it('_onBackspaceKeyEventHandler() merge same codeblocks if backspace key is pressed in empty line between same codeblock', () => {
const range = wwe.getEditor().getSelection().cloneRange();

wwe.setValue([
'<pre>test1</pre>',
'<div><br></div>',
'<pre>test2</pre>'
].join(''));

range.setStart(wwe.get$Body().find('div')[0], 0);
range.collapse(true);

wwe.getEditor().setSelection(range);

em.emit('wysiwygKeyEvent', {
keyMap: 'BACK_SPACE',
data: {
preventDefault: () => {}
}
});

expect(wwe.get$Body().find('div').length).toEqual(1);
expect(wwe.get$Body().find('pre').length).toEqual(1);
expect(wwe.get$Body().find('pre').text()).toEqual('test1\ntest2');
});

it('_onBackspaceKeyEventHandler() do not merge codeblocks that has different data-language attribute if backspace key is pressed in empty line between different codeblock', () => {
const range = wwe.getEditor().getSelection().cloneRange();

wwe.setValue([
'<pre data-language="uml">test1</pre>',
'<div><br></div>',
'<pre data-language="chart">test2</pre>'
].join(''));

range.setStart(wwe.get$Body().find('div')[0], 0);
range.collapse(true);

wwe.getEditor().setSelection(range);

em.emit('wysiwygKeyEvent', {
keyMap: 'BACK_SPACE',
data: {
preventDefault: () => {}
}
});

expect(wwe.get$Body().find('div').length).toEqual(2);
expect(wwe.get$Body().find('pre').length).toEqual(2);
});
});
});

0 comments on commit c586b60

Please sign in to comment.
You can’t perform that action at this time.