editor.js/modules/callbacks.js
2017-01-25 19:32:14 +03:00

869 lines
24 KiB
JavaScript

/**
* Codex Editor callbacks module
*
* @author Codex Team
* @version 1.3.4
*/
var callbacks = (function(callbacks) {
callbacks.redactorSyncTimeout = null;
callbacks.globalKeydown = function(event){
switch (event.keyCode){
case codex.core.keys.ENTER : codex.callback.enterKeyPressed(event); break;
}
};
callbacks.redactorKeyDown = function(event) {
switch (event.keyCode){
case codex.core.keys.TAB : codex.callback.tabKeyPressed(event); break;
case codex.core.keys.ENTER : codex.callback.enterKeyPressedOnRedactorZone(event); break;
case codex.core.keys.ESC : codex.callback.escapeKeyPressed(event); break;
default : codex.callback.defaultKeyPressed(event); break;
}
};
callbacks.globalKeyup = function(event){
switch (event.keyCode){
case codex.core.keys.UP :
case codex.core.keys.LEFT :
case codex.core.keys.RIGHT :
case codex.core.keys.DOWN : codex.callback.arrowKeyPressed(event); break;
}
};
callbacks.tabKeyPressed = function(event){
if ( !codex.toolbar.opened ) {
codex.toolbar.open();
}
if (codex.toolbar.opened && !codex.toolbar.toolbox.opened) {
codex.toolbar.toolbox.open();
} else {
codex.toolbar.toolbox.leaf();
}
event.preventDefault();
};
callbacks.enterKeyPressed = function(event) {
if (codex.content.editorAreaHightlighted) {
/**
* it means that we lose input index, saved index before is not correct
* therefore we need to set caret when we insert new block
*/
codex.caret.inputIndex = -1;
codex.callback.enterPressedOnBlock();
}
};
/**
* ENTER key handler
* Makes new paragraph block
*/
callbacks.enterKeyPressedOnRedactorZone = function(event){
if (event.target.contentEditable == 'true') {
/** Update input index */
codex.caret.saveCurrentInputIndex();
}
var currentInputIndex = codex.caret.getCurrentInputIndex() || 0,
workingNode = codex.content.currentNode,
tool = workingNode.dataset.tool,
isEnterPressedOnToolbar = codex.toolbar.opened &&
codex.toolbar.current &&
event.target == codex.state.inputs[currentInputIndex];
/** The list of tools which needs the default browser behaviour */
var enableLineBreaks = codex.tools[tool].enableLineBreaks;
/** This type of block creates when enter is pressed */
var NEW_BLOCK_TYPE = codex.settings.initialBlockPlugin;
/**
* When toolbar is opened, select tool instead of making new paragraph
*/
if ( isEnterPressedOnToolbar ) {
event.preventDefault();
codex.toolbar.toolbox.toolClicked(event);
codex.toolbar.close();
/**
* Stop other listeners callback executions
*/
event.stopPropagation();
event.stopImmediatePropagation();
return;
}
/**
* Allow paragraph lineBreaks with shift enter
* Or if shiftkey pressed and enter and enabledLineBreaks, the let new block creation
*/
if ( event.shiftKey || enableLineBreaks ){
event.stopPropagation();
event.stopImmediatePropagation();
return;
}
var isLastTextNode = false,
currentSelection = window.getSelection(),
currentSelectedNode = currentSelection.anchorNode,
caretAtTheEndOfText = codex.caret.position.atTheEnd(),
isTextNodeHasParentBetweenContenteditable = false;
/**
* Allow making new <p> in same block by SHIFT+ENTER and forbids to prevent default browser behaviour
*/
if ( event.shiftKey && !enableLineBreaks ) {
codex.callback.enterPressedOnBlock(codex.content.currentBlock, event);
event.preventDefault();
return;
}
/**
* Workaround situation when caret at the Text node that has some wrapper Elements
* Split block cant handle this.
* We need to save default behavior
*/
isTextNodeHasParentBetweenContenteditable = currentSelectedNode && currentSelectedNode.parentNode.contentEditable != "true";
/**
* Split blocks when input has several nodes and caret placed in textNode
*/
if (
currentSelectedNode.nodeType == codex.core.nodeTypes.TEXT &&
!isTextNodeHasParentBetweenContenteditable &&
!caretAtTheEndOfText
){
event.preventDefault();
codex.core.log('Splitting Text node...');
codex.content.splitBlock(currentInputIndex);
/** Show plus button when next input after split is empty*/
if (!codex.state.inputs[currentInputIndex + 1].textContent.trim()) {
codex.toolbar.showPlusButton();
}
} else {
var islastNode = codex.content.isLastNode(currentSelectedNode);
if ( islastNode && caretAtTheEndOfText ) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
codex.core.log('ENTER clicked in last textNode. Create new BLOCK');
codex.content.insertBlock({
type: NEW_BLOCK_TYPE,
block: codex.tools[NEW_BLOCK_TYPE].render()
}, true);
codex.toolbar.move();
codex.toolbar.open();
/** Show plus button with empty block */
codex.toolbar.showPlusButton();
}
}
/** get all inputs after new appending block */
codex.ui.saveInputs();
};
callbacks.escapeKeyPressed = function(event){
/** Close all toolbar */
codex.toolbar.close();
/** Close toolbox */
codex.toolbar.toolbox.close();
event.preventDefault();
};
callbacks.arrowKeyPressed = function(event){
codex.content.workingNodeChanged();
/* Closing toolbar */
codex.toolbar.close();
codex.toolbar.move();
};
callbacks.defaultKeyPressed = function(event) {
codex.toolbar.close();
if (!codex.toolbar.inline.actionsOpened) {
codex.toolbar.inline.close();
codex.content.clearMark();
}
};
callbacks.redactorClicked = function (event) {
callbacks.detectWhenClickedOnFirstLevelBlockArea();
codex.content.workingNodeChanged(event.target);
codex.ui.saveInputs();
var selectedText = codex.toolbar.inline.getSelectionText();
/**
* If selection range took off, then we hide inline toolbar
*/
if (selectedText.length === 0) {
codex.toolbar.inline.close();
}
/** Update current input index in memory when caret focused into existed input */
if (event.target.contentEditable == 'true') {
codex.caret.saveCurrentInputIndex();
}
if (codex.content.currentNode === null) {
/**
* If inputs in redactor does not exits, then we put input index 0 not -1
*/
var indexOfLastInput = codex.state.inputs.length > 0 ? codex.state.inputs.length - 1 : 0;
/** If we have any inputs */
if (codex.state.inputs.length) {
/** getting firstlevel parent of input */
var firstLevelBlock = codex.content.getFirstLevelBlock(codex.state.inputs[indexOfLastInput]);
}
/** If input is empty, then we set caret to the last input */
if (codex.state.inputs.length && codex.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == codex.settings.initialBlockPlugin) {
codex.caret.setToBlock(indexOfLastInput);
} else {
/** Create new input when caret clicked in redactors area */
var NEW_BLOCK_TYPE = codex.settings.initialBlockPlugin;
codex.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : codex.tools[NEW_BLOCK_TYPE].render()
});
/** If there is no inputs except inserted */
if (codex.state.inputs.length === 1) {
codex.caret.setToBlock(indexOfLastInput);
} else {
/** Set caret to this appended input */
codex.caret.setToNextBlock(indexOfLastInput);
}
}
/**
* Move toolbar to the right position and open
*/
codex.toolbar.move();
codex.toolbar.open();
} else {
/**
* Move toolbar to the new position and open
*/
codex.toolbar.move();
codex.toolbar.open();
/** Close all panels */
codex.toolbar.settings.close();
codex.toolbar.toolbox.close();
}
var inputIsEmpty = !codex.content.currentNode.textContent.trim();
if (inputIsEmpty) {
/** Show plus button */
codex.toolbar.showPlusButton();
} else {
/** Hide plus buttons */
codex.toolbar.hidePlusButton();
}
var currentNodeType = codex.content.currentNode.dataset.tool;
/** Mark current block*/
if (currentNodeType != codex.settings.initialBlockPlugin || !inputIsEmpty) {
codex.content.markBlock();
}
};
/**
* This method allows to define, is caret in contenteditable element or not.
* Otherwise, if we get TEXT node from range container, that will means we have input index.
* In this case we use default browsers behaviour (if plugin allows that) or overwritten action.
* Therefore, to be sure that we've clicked first-level block area, we should have currentNode, which always
* specifies to the first-level block. Other cases we just ignore.
*/
callbacks.detectWhenClickedOnFirstLevelBlockArea = function() {
var selection = window.getSelection(),
anchorNode = selection.anchorNode,
flag = false;
if (selection.rangeCount == 0) {
codex.content.editorAreaHightlighted = true;
} else {
if (!codex.core.isDomNode(anchorNode)) {
anchorNode = anchorNode.parentNode;
}
/** Already founded, without loop */
if (anchorNode.contentEditable == 'true') {
flag = true;
}
while (anchorNode.contentEditable != 'true') {
anchorNode = anchorNode.parentNode;
if (anchorNode.contentEditable == 'true') {
flag = true;
}
if (anchorNode == document.body) {
break;
}
}
/** If editable element founded, flag is "TRUE", Therefore we return "FALSE" */
codex.content.editorAreaHightlighted = flag ? false : true;
}
};
/**
* Toolbar button click handler
* @param this - cursor to the button
*/
callbacks.toolbarButtonClicked = function (event) {
var button = this;
codex.toolbar.current = button.dataset.type;
codex.toolbar.toolbox.toolClicked(event);
codex.toolbar.close();
};
callbacks.redactorInputEvent = function (event) {
/**
* Clear previous sync-timeout
*/
if (this.redactorSyncTimeout){
clearTimeout(this.redactorSyncTimeout);
}
/**
* Start waiting to input finish and sync redactor
*/
this.redactorSyncTimeout = setTimeout(function() {
codex.content.sync();
}, 500);
};
/** Show or Hide toolbox when plus button is clicked */
callbacks.plusButtonClicked = function() {
if (!codex.nodes.toolbox.classList.contains('opened')) {
codex.toolbar.toolbox.open();
} else {
codex.toolbar.toolbox.close();
}
};
/**
* Block handlers for KeyDown events
*/
callbacks.blockKeydown = function(event, block) {
switch (event.keyCode){
case codex.core.keys.DOWN:
case codex.core.keys.RIGHT:
codex.callback.blockRightOrDownArrowPressed(block);
break;
case codex.core.keys.BACKSPACE:
codex.callback.backspacePressed(block);
break;
case codex.core.keys.UP:
case codex.core.keys.LEFT:
codex.callback.blockLeftOrUpArrowPressed(block);
break;
}
};
/**
* RIGHT or DOWN keydowns on block
*/
callbacks.blockRightOrDownArrowPressed = function (block) {
var selection = window.getSelection(),
inputs = codex.state.inputs,
focusedNode = selection.anchorNode,
focusedNodeHolder;
/** Check for caret existance */
if (!focusedNode){
return false;
}
/** Looking for closest (parent) contentEditable element of focused node */
while (focusedNode.contentEditable != 'true') {
focusedNodeHolder = focusedNode.parentNode;
focusedNode = focusedNodeHolder;
}
/** Input index in DOM level */
var editableElementIndex = 0;
while (focusedNode != inputs[editableElementIndex]) {
editableElementIndex ++;
}
/**
* Founded contentEditable element doesn't have childs
* Or maybe New created block
*/
if (!focusedNode.textContent)
{
codex.caret.setToNextBlock(editableElementIndex);
return;
}
/**
* Do nothing when caret doesn not reaches the end of last child
*/
var caretInLastChild = false,
caretAtTheEndOfText = false;
var lastChild,
deepestTextnode;
lastChild = focusedNode.childNodes[focusedNode.childNodes.length - 1 ];
if (codex.core.isDomNode(lastChild)) {
deepestTextnode = codex.content.getDeepestTextNodeFromPosition(lastChild, lastChild.childNodes.length);
} else {
deepestTextnode = lastChild;
}
caretInLastChild = selection.anchorNode == deepestTextnode;
caretAtTheEndOfText = deepestTextnode.length == selection.anchorOffset;
if ( !caretInLastChild || !caretAtTheEndOfText ) {
codex.core.log('arrow [down|right] : caret does not reached the end');
return false;
}
codex.caret.setToNextBlock(editableElementIndex);
};
/**
* LEFT or UP keydowns on block
*/
callbacks.blockLeftOrUpArrowPressed = function (block) {
var selection = window.getSelection(),
inputs = codex.state.inputs,
focusedNode = selection.anchorNode,
focusedNodeHolder;
/** Check for caret existance */
if (!focusedNode){
return false;
}
/**
* LEFT or UP not at the beginning
*/
if ( selection.anchorOffset !== 0) {
return false;
}
/** Looking for parent contentEditable block */
while (focusedNode.contentEditable != 'true') {
focusedNodeHolder = focusedNode.parentNode;
focusedNode = focusedNodeHolder;
}
/** Input index in DOM level */
var editableElementIndex = 0;
while (focusedNode != inputs[editableElementIndex]) {
editableElementIndex ++;
}
/**
* Do nothing if caret is not at the beginning of first child
*/
var caretInFirstChild = false,
caretAtTheBeginning = false;
var firstChild,
deepestTextnode;
/**
* Founded contentEditable element doesn't have childs
* Or maybe New created block
*/
if (!focusedNode.textContent) {
codex.caret.setToPreviousBlock(editableElementIndex);
return;
}
firstChild = focusedNode.childNodes[0];
if (codex.core.isDomNode(firstChild)) {
deepestTextnode = codex.content.getDeepestTextNodeFromPosition(firstChild, 0);
} else {
deepestTextnode = firstChild;
}
caretInFirstChild = selection.anchorNode == deepestTextnode;
caretAtTheBeginning = selection.anchorOffset === 0;
if ( caretInFirstChild && caretAtTheBeginning ) {
codex.caret.setToPreviousBlock(editableElementIndex);
}
};
/**
* Callback for enter key pressing in first-level block area
*/
callbacks.enterPressedOnBlock = function (event) {
var NEW_BLOCK_TYPE = codex.settings.initialBlockPlugin;
codex.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : codex.tools[NEW_BLOCK_TYPE].render()
}, true );
codex.toolbar.move();
codex.toolbar.open();
};
callbacks.backspacePressed = function (block) {
var currentInputIndex = codex.caret.getCurrentInputIndex(),
range,
selectionLength,
firstLevelBlocksCount;
if (block.textContent.trim()) {
range = codex.content.getRange();
selectionLength = range.endOffset - range.startOffset;
if (codex.caret.position.atStart() && !selectionLength && codex.state.inputs[currentInputIndex - 1]) {
codex.content.mergeBlocks(currentInputIndex);
} else {
return;
}
}
if (!selectionLength) {
block.remove();
}
firstLevelBlocksCount = codex.nodes.redactor.childNodes.length;
/**
* If all blocks are removed
*/
if (firstLevelBlocksCount === 0) {
/** update currentNode variable */
codex.content.currentNode = null;
/** Inserting new empty initial block */
codex.ui.addInitialBlock();
/** Updating inputs state after deleting last block */
codex.ui.saveInputs();
/** Set to current appended block */
setTimeout(function () {
codex.caret.setToPreviousBlock(1);
}, 10);
} else {
if (codex.caret.inputIndex !== 0) {
/** Target block is not first */
codex.caret.setToPreviousBlock(codex.caret.inputIndex);
} else {
/** If we try to delete first block */
codex.caret.setToNextBlock(codex.caret.inputIndex);
}
}
codex.toolbar.move();
if (!codex.toolbar.opened) {
codex.toolbar.open();
}
/** Updating inputs state */
codex.ui.saveInputs();
/** Prevent default browser behaviour */
event.preventDefault();
};
/**
* @deprecated
*
* @param event
*/
callbacks.blockPaste = function(event) {
var currentInputIndex = codex.caret.getCurrentInputIndex(),
node = codex.state.inputs[currentInputIndex];
setTimeout(function() {
codex.content.sanitize(node);
event.preventDefault();
}, 10);
event.stopImmediatePropagation();
};
/**
* This method is used to observe pasted dirty data.
*
* Mutation handlers send to separate observers each mutation (added, changed and so on), which will be
* passed from handler that sanitizes and replaces data.
*
* Probably won't be used
*
* @deprecated
*
* @param event
* @private
*/
callbacks._blockPasteCallback = function(event) {
var currentInputIndex = codex.caret.getCurrentInputIndex();
/**
* create an observer instance
*/
var observer = new MutationObserver(codex.callback.handleMutationsOnPaste);
/**
* configuration of the observer:
*/
var config = {
attributes: true,
childList: false,
characterData: false,
subtree : true
};
// pass in the target node, as well as the observer options
observer.observe(codex.state.inputs[currentInputIndex], config);
};
/**
* This method prevents default behaviour.
*
* We get from clipboard pasted data, sanitize, make a fragment that contains of this sanitized nodes.
* Firstly, we need to memorize the caret position. We can do that by getting the range of selection.
* After all, we insert clear fragment into caret placed position. Then, we should move the caret to the last node
*
* @param event
*/
callbacks.blockPasteCallback = function(event) {
/** Prevent default behaviour */
event.preventDefault();
/** get html pasted data - dirty data */
var data = event.clipboardData.getData('text/html');
/** Temporary DIV that is used to work with childs as arrays item */
var div = codex.draw.node('DIV', '', {}),
cleaner = new codex.sanitizer.init(codex.sanitizer.Config.BASIC),
cleanData,
fragment;
/** Create fragment, that we paste to range after proccesing */
fragment = document.createDocumentFragment();
cleanData = cleaner.clean(data);
div.innerHTML = cleanData;
var node, lastNode;
/**
* and fill in fragment
*/
while (( node = div.firstChild) ) {
lastNode = fragment.appendChild(node);
}
/**
* work with selection and range
*/
var selection, range;
selection = window.getSelection();
range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(fragment);
// document.execCommand('insertParagraph', false, "<p>");
/** Preserve the selection */
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
};
/**
* Sends all mutations to paste handler
*/
callbacks.handleMutationsOnPaste = function(mutations) {
var self = this;
/**
* Calling function with context of this function.
* Also, we should sanitize pasted or changed data one time and ignore
* changings which makes sanitize method.
* For that, we need to send Context, MutationObserver.__proto__ that contains
* observer disconnect method.
*/
mutations.forEach(function(mutation) {
codex.content.paste.call(self, mutation);
});
};
/**
* Clicks on block settings button
*/
callbacks.showSettingsButtonClicked = function(){
/**
* Get type of current block
* It uses to append settings from tool.settings property.
* ...
* Type is stored in data-type attribute on block
*/
var currentToolType = codex.content.currentNode.dataset.tool;
codex.toolbar.settings.toggle(currentToolType);
/** Close toolbox when settings button is active */
codex.toolbar.toolbox.close();
codex.toolbar.settings.hideRemoveActions();
};
return callbacks;
})({});
module.exports = callbacks;