/**
* CefNavigation.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
import Env from '../api/Env';
import * as CaretContainer from '../caret/CaretContainer';
import CaretPosition from '../caret/CaretPosition';
import * as CaretUtils from '../caret/CaretUtils';
import { HDirection, CaretWalker } from '../caret/CaretWalker';
import * as LineUtils from '../caret/LineUtils';
import * as LineWalker from '../caret/LineWalker';
import NodeType from '../dom/NodeType';
import * as CefUtils from './CefUtils';
import * as RangeNodes from '../selection/RangeNodes';
import Arr from '../util/Arr';
import Fun from '../util/Fun';
const isContentEditableFalse = NodeType.isContentEditableFalse;
const getSelectedNode = RangeNodes.getSelectedNode;
const isAfterContentEditableFalse = CaretUtils.isAfterContentEditableFalse;
const isBeforeContentEditableFalse = CaretUtils.isBeforeContentEditableFalse;
const getVisualCaretPosition = (walkFn, caretPosition: CaretPosition): CaretPosition => {
while ((caretPosition = walkFn(caretPosition))) {
if (caretPosition.isVisible()) {
return caretPosition;
}
}
return caretPosition;
};
const isMoveInsideSameBlock = (from: CaretPosition, to: CaretPosition): boolean => {
const inSameBlock = CaretUtils.isInSameBlock(from, to);
// Handle bogus BR
abc|
if (!inSameBlock && NodeType.isBr(from.getNode())) {
return true;
}
return inSameBlock;
};
const moveToCeFalseHorizontally = (direction: HDirection, editor, getNextPosFn, range): Range => {
let node, caretPosition, peekCaretPosition, rangeIsInContainerBlock;
const forwards = direction === HDirection.Forwards;
const isBeforeContentEditableFalseFn = forwards ? isBeforeContentEditableFalse : isAfterContentEditableFalse;
if (!range.collapsed) {
node = getSelectedNode(range);
if (isContentEditableFalse(node)) {
return CefUtils.showCaret(direction, editor, node, direction === HDirection.Backwards, true);
}
}
rangeIsInContainerBlock = CaretContainer.isRangeInCaretContainerBlock(range);
caretPosition = CaretUtils.getNormalizedRangeEndPoint(direction, editor.getBody(), range);
if (isBeforeContentEditableFalseFn(caretPosition)) {
return CefUtils.selectNode(editor, caretPosition.getNode(!forwards));
}
caretPosition = getNextPosFn(caretPosition);
if (!caretPosition) {
if (rangeIsInContainerBlock) {
return range;
}
return null;
}
if (isBeforeContentEditableFalseFn(caretPosition)) {
return CefUtils.showCaret(direction, editor, caretPosition.getNode(!forwards), forwards, true);
}
// Peek ahead for handling of ab|c -> abc|
peekCaretPosition = getNextPosFn(caretPosition);
if (isBeforeContentEditableFalseFn(peekCaretPosition)) {
if (isMoveInsideSameBlock(caretPosition, peekCaretPosition)) {
return CefUtils.showCaret(direction, editor, peekCaretPosition.getNode(!forwards), forwards, true);
}
}
if (rangeIsInContainerBlock) {
return CefUtils.renderRangeCaret(editor, caretPosition.toRange(), true);
}
return null;
};
const moveToCeFalseVertically = function (direction: LineWalker.VDirection, editor, walkerFn, range: Range) {
let caretPosition, linePositions, nextLinePositions;
let closestNextLineRect, caretClientRect, clientX;
let dist1, dist2, contentEditableFalseNode;
contentEditableFalseNode = getSelectedNode(range);
caretPosition = CaretUtils.getNormalizedRangeEndPoint(direction, editor.getBody(), range);
linePositions = walkerFn(editor.getBody(), LineWalker.isAboveLine(1), caretPosition);
nextLinePositions = Arr.filter(linePositions, LineWalker.isLine(1));
caretClientRect = Arr.last(caretPosition.getClientRects());
if (isBeforeContentEditableFalse(caretPosition) || CaretUtils.isBeforeTable(caretPosition)) {
contentEditableFalseNode = caretPosition.getNode();
}
if (isAfterContentEditableFalse(caretPosition) || CaretUtils.isAfterTable(caretPosition)) {
contentEditableFalseNode = caretPosition.getNode(true);
}
if (!caretClientRect) {
return null;
}
clientX = caretClientRect.left;
closestNextLineRect = LineUtils.findClosestClientRect(nextLinePositions, clientX);
if (closestNextLineRect) {
if (isContentEditableFalse(closestNextLineRect.node)) {
dist1 = Math.abs(clientX - closestNextLineRect.left);
dist2 = Math.abs(clientX - closestNextLineRect.right);
return CefUtils.showCaret(direction, editor, closestNextLineRect.node, dist1 < dist2, true);
}
}
if (contentEditableFalseNode) {
const caretPositions = LineWalker.positionsUntil(direction, editor.getBody(), LineWalker.isAboveLine(1), contentEditableFalseNode);
closestNextLineRect = LineUtils.findClosestClientRect(Arr.filter(caretPositions, LineWalker.isLine(1)), clientX);
if (closestNextLineRect) {
return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange(), true);
}
closestNextLineRect = Arr.last(Arr.filter(caretPositions, LineWalker.isLine(0)));
if (closestNextLineRect) {
return CefUtils.renderRangeCaret(editor, closestNextLineRect.position.toRange(), true);
}
}
};
const createTextBlock = (editor): Element => {
const textBlock = editor.dom.create(editor.settings.forced_root_block);
if (!Env.ie || Env.ie >= 11) {
textBlock.innerHTML = '
';
}
return textBlock;
};
const exitPreBlock = (editor, direction: HDirection, range: Range): void => {
let pre, caretPos, newBlock;
const caretWalker = CaretWalker(editor.getBody());
const getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next);
const getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev);
if (range.collapsed && editor.settings.forced_root_block) {
pre = editor.dom.getParent(range.startContainer, 'PRE');
if (!pre) {
return;
}
if (direction === 1) {
caretPos = getNextVisualCaretPosition(CaretPosition.fromRangeStart(range));
} else {
caretPos = getPrevVisualCaretPosition(CaretPosition.fromRangeStart(range));
}
if (!caretPos) {
newBlock = createTextBlock(editor);
if (direction === 1) {
editor.$(pre).after(newBlock);
} else {
editor.$(pre).before(newBlock);
}
editor.selection.select(newBlock, true);
editor.selection.collapse();
}
}
};
const getHorizontalRange = (editor, forward: boolean): Range => {
const caretWalker = CaretWalker(editor.getBody());
const getNextVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.next);
const getPrevVisualCaretPosition = Fun.curry(getVisualCaretPosition, caretWalker.prev);
let newRange;
const direction = forward ? HDirection.Forwards : HDirection.Backwards;
const getNextPosFn = forward ? getNextVisualCaretPosition : getPrevVisualCaretPosition;
const range = editor.selection.getRng();
newRange = moveToCeFalseHorizontally(direction, editor, getNextPosFn, range);
if (newRange) {
return newRange;
}
newRange = exitPreBlock(editor, direction, range);
if (newRange) {
return newRange;
}
return null;
};
const getVerticalRange = (editor, down: boolean): Range => {
let newRange;
const direction = down ? 1 : -1;
const walkerFn = down ? LineWalker.downUntil : LineWalker.upUntil;
const range = editor.selection.getRng();
newRange = moveToCeFalseVertically(direction, editor, walkerFn, range);
if (newRange) {
return newRange;
}
newRange = exitPreBlock(editor, direction, range);
if (newRange) {
return newRange;
}
return null;
};
// Changed return type from () => boolean to any
// Lambda notation started throwing errors after
// typescript version change.
const moveH = (editor, forward: boolean): any => {
return () => {
const newRng = getHorizontalRange(editor, forward);
if (newRng) {
editor.selection.setRng(newRng);
return true;
} else {
return false;
}
};
};
// Changed return type from () => boolean to any
// Lambda notation started throwing errors after
// typescript version change.
const moveV = (editor, down: boolean): any => {
return () => {
const newRng = getVerticalRange(editor, down);
if (newRng) {
editor.selection.setRng(newRng);
return true;
} else {
return false;
}
};
};
export {
moveH,
moveV
};