Merge branch 'rewriting-version2.0' into Split-Merge-blocks

This commit is contained in:
Nikita 2018-01-05 18:04:49 +03:00
commit ef24b23f31
12 changed files with 1495 additions and 490 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

41
docs/caret.md Normal file
View file

@ -0,0 +1,41 @@
# CodeX Editor Caret Module
The `Caret` module contains methods working with caret. Uses [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) methods to navigate caret
between blocks.
Caret class implements basic Module class that holds User configuration
and default CodeXEditor instances
You can read more about Module class [here]()
## Properties
## Methods
### setToBlock
```javascript
Caret.setToBlock(block, offset, atEnd)
```
> Method gets Block instance and puts caret to the text node with offset
#### params
| Param | Type | Description|
| -------------|------ |:-------------:|
| block | Object | Block instance that BlockManager created|
| offset | Number | caret offset regarding to the text node (Default: 0)|
| atEnd | Boolean | puts caret at the end of last text node|
### setToTheLastBlock
```javascript
Caret.setToTheLastBlock()
```
> sets Caret at the end of last Block
If last block is not empty, inserts another empty Block which is passed as initial

View file

@ -82,9 +82,9 @@
{
type : 'text',
data : {
text : 'Пишите нам на team@ifmo.su'
text : '<p><b>CodeX</b> Привет!!!</p>'
}
},
}
]
}
});

View file

@ -183,7 +183,7 @@ export default class Block {
}
let emptyText = this._html.textContent.trim().length === 0,
let emptyText = $.isEmpty(this.pluginsContent),
emptyMedia = !this.hasMedia;
return emptyText && emptyMedia;

View file

@ -1,5 +1,5 @@
/**
* DOM manupulations helper
* DOM manipulations helper
*/
export default class Dom {
@ -13,7 +13,7 @@ export default class Dom {
*/
static make(tagName, classNames = null, attributes = {}) {
var el = document.createElement(tagName);
let el = document.createElement(tagName);
if ( Array.isArray(classNames) ) {
@ -35,6 +35,17 @@ export default class Dom {
}
/**
* Creates Text Node with the passed content
* @param {String} content - text content
* @return {Text}
*/
static text(content) {
return document.createTextNode(content);
}
/**
* Append one or several elements to the parent
*
@ -86,6 +97,51 @@ export default class Dom {
}
/**
* Search for deepest node which is Leaf.
* Leaf is the vertex that doesn't have any child nodes
*
* @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 {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) {
/**
* 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
*/
if (this.isElement(node) && !this.isNativeInput(node)) {
let emptyTextNode = this.text('\u200B');
node.appendChild(emptyTextNode);
}
return node;
}
let childsLength = node.childNodes.length,
last = childsLength - 1;
if (atLast) {
return this.getDeepestNode(node.childNodes[last], atLast);
} else {
return this.getDeepestNode(node.childNodes[0], false);
}
}
/**
* Check if object is DOM node
*
@ -98,4 +154,117 @@ export default class Dom {
}
/**
* Checks target if it is native input
* @param {Element|String} target - HTML element or string
* @return {Boolean}
*/
static isNativeInput(target) {
let nativeInputs = [
'INPUT',
'TEXTAREA'
];
return target ? nativeInputs.includes(target.tagName) : false;
}
/**
* Checks node if it is empty
*
* @description Method checks simple Node without any childs for emptiness
* If you have Node with 2 or more children id depth, you better use {@link Dom#isEmpty} method
*
* @param {Node} node
* @return {Boolean} true if it is empty
*/
static isNodeEmpty(node) {
let nodeText;
if ( this.isElement(node) && this.isNativeInput(node) ) {
nodeText = node.value;
} else {
nodeText = node.textContent.replace('\u200B', '');
}
return nodeText.trim().length === 0;
}
/**
* checks node if it is doesn't have any child nodes
* @param {Node} node
* @return {boolean}
*/
static isLeaf(node) {
if (!node) {
return false;
}
return node.childNodes.length === 0;
}
/**
* breadth-first search (BFS)
* {@link https://en.wikipedia.org/wiki/Breadth-first_search}
*
* @description Pushes to stack all DOM leafs and checks for emptiness
*
* @param {Node} node
* @return {boolean}
*/
static isEmpty(node) {
let treeWalker = [],
leafs = [];
if (!node) {
return false;
}
treeWalker.push(node);
while ( treeWalker.length > 0 ) {
if ( this.isLeaf(node) ) {
leafs.push(node);
}
while ( node && node.nextSibling ) {
node = node.nextSibling;
if (!node) continue;
treeWalker.push(node);
}
node = treeWalker.shift();
if (!node) continue;
node = node.firstChild;
treeWalker.push(node);
}
return leafs.every( leaf => this.isNodeEmpty(leaf)) ;
}
};

View file

@ -3,9 +3,12 @@
* @classdesc Manage editor`s blocks storage and appearance
*
* @module BlockManager
*
* @version 2.0.0
*/
import Block from '../block';
import Selection from '../Selection';
/**
* @typedef {BlockManager} BlockManager
@ -90,6 +93,8 @@ export default class BlockManager extends Module {
let toolInstance = this.Editor.Tools.construct(toolName, data),
block = new Block(toolName, toolInstance);
this.bindEvents(block);
/**
* Apply callback before inserting html
*/
@ -99,6 +104,100 @@ export default class BlockManager extends Module {
}
/**
* Bind Events
* @param {Object} block
*/
bindEvents(block) {
/**
* keydown on block
* @todo move to the keydown module
*/
block.pluginsContent.addEventListener('keydown', (event) => this.keyDownOnBlock(event), false);
}
/**
* @todo move to the keydown module
* @param {MouseEvent} event
*/
keyDownOnBlock(event) {
switch(event.keyCode) {
case _.keyCodes.ENTER:
this.enterPressedOnPluginsContent(event);
break;
case _.keyCodes.DOWN:
case _.keyCodes.RIGHT:
this.navigateNext();
break;
case _.keyCodes.UP:
case _.keyCodes.LEFT:
this.navigatePrevious();
break;
}
}
/**
* Set's caret to the next Block
* Before moving caret, we should check if caret position is at the end of Plugins node
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
*/
navigateNext() {
let lastTextNode = $.getDeepestNode(this.currentBlock.pluginsContent, true),
textNodeLength = lastTextNode.length;
if (Selection.getAnchorNode() !== lastTextNode) {
return;
}
if (Selection.getAnchorOffset() === textNodeLength) {
let nextBlock = this.nextBlock;
if (!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
* 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;
if (Selection.getAnchorNode() !== firstTextNode) {
return;
}
if (Selection.getAnchorOffset() === 0) {
let previousBlock = this.previousBlock;
if (!previousBlock) return;
this.Editor.Caret.setToBlock( previousBlock, textNodeLength, true );
}
}
/**
* Insert new block into _blocks
*
@ -110,6 +209,7 @@ export default class BlockManager extends Module {
let block = this.composeBlock(toolName, data);
this._blocks[++this.currentBlockIndex] = block;
this.Editor.Caret.setToBlock(block);
}
@ -146,18 +246,37 @@ export default class BlockManager extends Module {
}
/**
* returns last Block
* @return {Block}
*/
get lastBlock() {
return this._blocks[this._blocks.length - 1];
}
/**
* Returns Block by passed index
* @param {Number} index
* @return {Block}
*/
getBlockByIndex(index) {
return this._blocks[index];
}
/**
* Get Block instance by html element
*
* @todo get first level block before searching
*
* @param {HTMLElement} element
* @returns {Block}
*/
getBlock(element) {
let nodes = this._blocks.nodes,
index = nodes.indexOf(element);
firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`),
index = nodes.indexOf(firstLevelBlock);
if (index >= 0) {
@ -178,6 +297,42 @@ export default class BlockManager extends Module {
}
/**
* Returns next Block instance
* @return {Block|null}
*/
get nextBlock() {
let isLastBlock = this.currentBlockIndex === (this._blocks.length - 1);
if (isLastBlock) {
return null;
}
return this._blocks[this.currentBlockIndex + 1];
}
/**
* Returns previous Block instance
* @return {Block|null}
*/
get previousBlock() {
let isFirstBlock = this.currentBlockIndex === 0;
if (isFirstBlock) {
return null;
}
return this._blocks[this.currentBlockIndex - 1];
}
/**
* Get working html element
*
@ -191,20 +346,18 @@ export default class BlockManager extends Module {
/**
* Set currentBlockIndex to passed block
*
* @todo get first level block before searching
*
* @param {HTMLElement} element
*/
set currentNode(element) {
let nodes = this._blocks.nodes;
let nodes = this._blocks.nodes,
firstLevelBlock = element.closest(`.${Block.CSS.wrapper}`);
/**
* Update current Block's index
* @type {number}
*/
this.currentBlockIndex = nodes.indexOf(element);
this.currentBlockIndex = nodes.indexOf(firstLevelBlock);
/**
* Remove previous selected Block's state

View file

@ -2,8 +2,18 @@
* @class Caret
* @classdesc Contains methods for working Caret
*
* Uses Range methods to manipulate with caret
*
* @module Caret
*
* @version 2.0.0
*/
/**
* @typedef {Caret} Caret
*/
import Selection from '../Selection';
export default class Caret extends Module {
/**
@ -16,20 +26,79 @@ export default class Caret extends Module {
}
/**
* Set Caret to the last Block
* Method gets Block instance and puts caret to the text node with offset
* There two ways that method applies caret position:
* - first found text node: sets at the beginning, but you can pass an offset
* - last found text node: sets at the end of the node. Also, you can customize the behaviour
*
* @param {Block} block - Block class
* @param {Number} offset - caret offset regarding to the text node
* @param {Boolean} atEnd - put caret at the end of the text node or not
*/
setToBlock(block, offset = 0, atEnd = false) {
let element = block.pluginsContent;
/** If Element is INPUT */
if ($.isNativeInput(element)) {
element.focus();
return;
}
let nodeToSet = $.getDeepestNode(element, atEnd);
if (atEnd || offset > nodeToSet.length) {
offset = nodeToSet.length;
}
/** if found deepest node is native input */
if ($.isNativeInput(nodeToSet)) {
nodeToSet.focus();
return;
}
/**
* @todo try to fix via Promises or use querySelectorAll to not to use timeout
*/
_.delay( () => this.set(nodeToSet, offset), 20)();
this.Editor.BlockManager.currentNode = block.wrapper;
}
/**
* Creates Document Range and sets caret to the element with offset
* @param {Element} element - target node.
* @param {Number} offset - offset
*/
set( element, offset = 0) {
let range = document.createRange(),
selection = Selection.get();
range.setStart(element, offset);
range.setEnd(element, offset);
selection.removeAllRanges();
selection.addRange(range);
};
/**
* Set Caret to the last Block
* If last block is not empty, append another empty block
*/
setToTheLastBlock() {
let blocks = this.Editor.BlockManager.blocks,
lastBlock;
let lastBlock = this.Editor.BlockManager.lastBlock;
if (blocks.length) {
lastBlock = blocks[blocks.length - 1];
}
if (!lastBlock) return;
/**
* If last block is empty and it is an initialBlock, set to that.
@ -37,7 +106,7 @@ export default class Caret extends Module {
*/
if (lastBlock.isEmpty) {
this.set(lastBlock.html);
this.setToBlock(lastBlock);
} else {
@ -45,65 +114,6 @@ export default class Caret extends Module {
}
/**
// * If inputs in redactor does not exits, then we put input index 0 not -1
// */
// var indexOfLastInput = editor.state.inputs.length > 0 ? editor.state.inputs.length - 1 : 0;
//
// /** If we have any inputs */
// if (editor.state.inputs.length) {
//
// /** getting firstlevel parent of input */
// firstLevelBlock = editor.content.getFirstLevelBlock(editor.state.inputs[indexOfLastInput]);
//
// }
//
// /** If input is empty, then we set caret to the last input */
// if (editor.state.inputs.length && editor.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == editor.settings.initialBlockPlugin) {
//
// editor.caret.setToBlock(indexOfLastInput);
//
// } else {
//
// /** Create new input when caret clicked in redactors area */
// var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
//
// editor.content.insertBlock({
// type : NEW_BLOCK_TYPE,
// block : editor.tools[NEW_BLOCK_TYPE].render()
// });
//
// /** If there is no inputs except inserted */
// if (editor.state.inputs.length === 1) {
//
// editor.caret.setToBlock(indexOfLastInput);
//
// } else {
//
// /** Set caret to this appended input */
// editor.caret.setToNextBlock(indexOfLastInput);
//
// }
//
// }
}
/**
* Set caret to the passed Node
* @param {Element} node - content-editable Element
*/
set(node) {
/**
* @todo add working with Selection
* tmp: work with textContent
*/
node.textContent += '|';
}
}

View file

@ -31,7 +31,7 @@
// SETTINGS_ITEM : 'ce-settings__item'
// };
import Block from '../block';
// import Block from '../block';
/**
* @class

View file

@ -0,0 +1,61 @@
/**
* Working with selection
*/
export default class Selection {
/**
* @constructor
*/
constructor() {
this.instance = null;
this.selection = null;
}
/**
* Returns window Selection
* {@link https://developer.mozilla.org/ru/docs/Web/API/Window/getSelection}
* @return {Selection}
*/
static get() {
return window.getSelection();
}
/**
* Returns selected anchor
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorNode}
* @return {Node}
*/
static getAnchorNode() {
let selection = window.getSelection();
if (selection) {
return selection.anchorNode;
}
}
/**
* Returns selection offset according to the anchor node
* {@link https://developer.mozilla.org/ru/docs/Web/API/Selection/anchorOffset}
* @return {Number}
*/
static getAnchorOffset() {
let selection = window.getSelection();
if (selection) {
return selection.anchorOffset;
}
}
}

View file

@ -40,6 +40,31 @@ export default class Util {
}
/**
* Returns basic keycodes as constants
* @return {{}}
*/
static get keyCodes() {
return {
BACKSPACE: 8,
TAB: 9,
ENTER: 13,
SHIFT: 16,
CTRL: 17,
ALT: 18,
ESC: 27,
SPACE: 32,
LEFT: 37,
UP: 38,
DOWN: 40,
RIGHT: 39,
DELETE: 46,
META: 91
};
}
/**
* @typedef {Object} ChainData
* @property {Object} data - data that will be passed to the success or fallback
@ -157,4 +182,34 @@ export default class Util {
}
/**
* Check if passed element is contenteditable
* @param element
* @return {boolean}
*/
static isContentEditable(element) {
return element.contentEditable === 'true';
}
/**
* Delays method execution
*
* @param method
* @param timeout
*/
static delay(method, timeout) {
return function () {
let context = this,
args = arguments;
window.setTimeout(() => method.apply(context, args), timeout);
};
}
};

View file

@ -3,14 +3,16 @@
left: 0;
right: 0;
top: 0;
opacity: 0;
visibility: hidden;
/*opacity: 0;*/
/*visibility: hidden;*/
transition: opacity 100ms ease;
will-change: opacity, transform;
display: none;
&--opened {
opacity: 1;
visibility: visible;
display: block;
/*opacity: 1;*/
/*visibility: visible;*/
}
&__content {