Skip to content

Commit

Permalink
Merge pull request #254 from codex-team/daily
Browse files Browse the repository at this point in the history
Backspace handler: Merge blocks
  • Loading branch information
khaydarov authored May 29, 2018
2 parents e6c3fc7 + 51a5ffe commit d8747e5
Show file tree
Hide file tree
Showing 11 changed files with 921 additions and 223 deletions.
651 changes: 494 additions & 157 deletions build/codex-editor.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/codex-editor.js.map

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ CodeX Editor is a block-oriented editor. It means that entry composed with the l

### Constructor

### Render

### Save

### Render
### Validate

### Merge (optional)

Method that specifies how to merge two `Blocks` of the same type, for example on `Backspace` keypress.
Method does accept data object in same format as the `Render` and it should provide logic how to combine new
data with the currently stored value.

### Available settings

Expand Down
12 changes: 12 additions & 0 deletions example/plugins/text/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ class Text {

}

/**
* Merge current data with passed data
* @param {TextData} data
*/
merge(data) {
let newData = {
text : this.data.text + data.text
};

this.data = newData;
}

/**
* Check if saved text is empty
*
Expand Down
25 changes: 25 additions & 0 deletions src/components/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ export default class Block {

}

/**
* is block mergeable
* We plugin have merge function then we call it mergable
* @return {boolean}
*/
get mergeable() {

return typeof this.tool.merge === 'function';

}

/**
* Call plugins merge method
* @param {Object} data
*/
mergeWith(data) {

return Promise.resolve()
.then(() => {

this.tool.merge(data);

});

}
/**
* Extracts data from Block
* Groups Tool's save processing time
Expand Down
94 changes: 70 additions & 24 deletions src/components/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
*/
export default class Dom {

/**
* Check if passed tag has no closed tag
* @param {Element} tag
* @return {Boolean}
*/
static isSingleTag(tag) {

return tag.tagName && ['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'].includes(tag.tagName);

};


/**
* Helper for making Elements with classname and attributes
*
Expand Down Expand Up @@ -103,43 +115,61 @@ export default class Dom {
*
* @description Method recursively goes throw the all Node until it finds the Leaf
*
* @param {Element} node - root Node. From this vertex we start Deep-first search {@link https://en.wikipedia.org/wiki/Depth-first_search}
* @param {Node} node - root Node. From this vertex we start Deep-first search {@link https://en.wikipedia.org/wiki/Depth-first_search}
* @param {Boolean} atLast - find last text node
* @return {Node} - it can be text Node or Element Node, so that caret will able to work with it
*/
static getDeepestNode(node, atLast = false) {

if (node.childNodes.length === 0) {
/**
* Current function have two directions:
* - starts from first child and every time gets first or nextSibling in special cases
* - starts from last child and gets last or previousSibling
* @type {string}
*/
let child = atLast ? 'lastChild' : 'firstChild',
sibling = atLast ? 'previousSibling' : 'nextSibling';

if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {

let nodeChild = node[child];

/**
* We need to return an empty text node
* But caret will not be placed in empty textNode, so we need textNode with zero-width char
* special case when child is single tag that can't contain any content
*/
if (this.isElement(node) && !this.isNativeInput(node)) {

let emptyTextNode = this.text('\u200B');
if (Dom.isSingleTag(nodeChild)) {

node.appendChild(emptyTextNode);
/**
* 1) We need to check the next sibling. If it is Node Element then continue searching for deepest
* from sibling
*
* 2) If single tag's next sibling is null, then go back to parent and check his sibling
* In case of Node Element continue searching
*
* 3) If none of conditions above happened return parent Node Element
*/
if (nodeChild[sibling]) {

}
nodeChild = nodeChild[sibling];

return node;
} else if (nodeChild.parentNode[sibling]) {

}
nodeChild = nodeChild.parentNode[sibling];

let childsLength = node.childNodes.length,
last = childsLength - 1;
} else {

if (atLast) {
return nodeChild.parentNode;

return this.getDeepestNode(node.childNodes[last], atLast);
}

} else {
}

return this.getDeepestNode(node.childNodes[0], false);
return this.getDeepestNode(nodeChild, atLast);

}

return node;

}

/**
Expand Down Expand Up @@ -230,18 +260,32 @@ export default class Dom {

if (!node) {

return false;
return true;

}

if (!node.childNodes.length) {

return this.isNodeEmpty(node);

}

treeWalker.push(node);
treeWalker.push(node.firstChild);

while ( treeWalker.length > 0 ) {

node = treeWalker.shift();

if (!node) continue;

if ( this.isLeaf(node) ) {

leafs.push(node);

} else {

treeWalker.push(node.firstChild);

}

while ( node && node.nextSibling ) {
Expand All @@ -254,16 +298,18 @@ export default class Dom {

}

node = treeWalker.shift();
/**
* If one of childs is not empty, checked Node is not empty too
*/
if (node && !this.isNodeEmpty(node)) {

if (!node) continue;
return false;

node = node.firstChild;
treeWalker.push(node);
}

}

return leafs.every( leaf => this.isNodeEmpty(leaf)) ;
return leafs.every( leaf => this.isNodeEmpty(leaf) );

}

Expand Down
100 changes: 81 additions & 19 deletions src/components/modules/blockManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import Block from '../block';
import Selection from '../Selection';

/**
* @typedef {BlockManager} BlockManager
Expand Down Expand Up @@ -121,53 +120,52 @@ export default class BlockManager extends Module {
*/
navigateNext() {

let lastTextNode = $.getDeepestNode(this.currentBlock.pluginsContent, true),
textNodeLength = lastTextNode.length;
let caretAtEnd = this.Editor.Caret.isAtEnd;

if (Selection.getAnchorNode() !== lastTextNode) {
if (!caretAtEnd) {

return;

}

if (Selection.getAnchorOffset() === textNodeLength) {
let nextBlock = this.nextBlock;

let nextBlock = this.nextBlock;
if (!nextBlock) {

if (!nextBlock) return;

this.Editor.Caret.setToBlock( nextBlock );
return;

}

this.Editor.Caret.setToBlock( nextBlock );


}

/**
* Set's caret to the previous Block
* Before moving caret, we should check if caret position is at the end of Plugins node
* Before moving caret, we should check if caret position is start of the Plugins node
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
*/
navigatePrevious() {

let firstTextNode = $.getDeepestNode(this.currentBlock.pluginsContent, false),
textNodeLength = firstTextNode.length;
let caretAtStart = this.Editor.Caret.isAtStart;

if (Selection.getAnchorNode() !== firstTextNode) {
if (!caretAtStart) {

return;

}

if (Selection.getAnchorOffset() === 0) {

let previousBlock = this.previousBlock;
let previousBlock = this.previousBlock;

if (!previousBlock) return;
if (!previousBlock) {

this.Editor.Caret.setToBlock( previousBlock, textNodeLength, true );
return;

}

this.Editor.Caret.setToBlock( previousBlock, 0, true );

}

/**
Expand All @@ -185,6 +183,53 @@ export default class BlockManager extends Module {

}

/**
* Merge two blocks
* @param {Block} targetBlock - previous block will be append to this block
* @param {Block} blockToMerge - block that will be merged with target block
*
* @return {Promise} - the sequence that can be continued
*/
mergeBlocks(targetBlock, blockToMerge) {

let blockToMergeIndex = this._blocks.indexOf(blockToMerge);

return Promise.resolve()
.then( () => {

if (blockToMerge.isEmpty) {

return;

}

return blockToMerge.data
.then((blockToMergeInfo) => {

targetBlock.mergeWith(blockToMergeInfo.data);

});

})
.then( () => {

this.removeBlock(blockToMergeIndex);
this.currentBlockIndex = this._blocks.indexOf(targetBlock);

});


}

/**
* Remove block with passed index or remove last
* @param {Number|null} index
*/
removeBlock(index) {

this._blocks.remove(index);

}
/**
* Split current Block
* 1. Extract content from Caret position to the Block`s end
Expand All @@ -201,7 +246,7 @@ export default class BlockManager extends Module {
* @todo make object in accordance with Tool
*/
let data = {
text: wrapper.innerHTML,
text: $.isEmpty(wrapper) ? '' : wrapper.innerHTML,
};

this.insert(this.config.initialBlock, data);
Expand Down Expand Up @@ -484,6 +529,23 @@ class Blocks {

}

/**
* Remove block
* @param {Number|null} index
*/
remove(index) {

if (!index) {

index = this.length - 1;

}

this.blocks[index].html.remove();
this.blocks.splice(index, 1);

}

/**
* Insert Block after passed target
*
Expand Down
Loading

0 comments on commit d8747e5

Please sign in to comment.