/**
* The mark manager used by a {@link DocumentView}.
*/
class MarkManager {
constructor(docView, marks) {
this._docView = docView;
this._contenteditableState = docView.contenteditableState;
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
this._markUID = null;
this._mark = null;
this._markOffset = -1;
this._selectedUID = null;
this._selected = null;
this._selected2UID = null;
this._selected2 = null;
this._textHighlight = new TextHighlight(docView);
this._nodeHighlight = new NodeHighlight(docView);
// The caret placeholder when not focused.
this._caretPlaceholder = new CaretPlaceholder(docView);
this._caretPlaceholder.disabled = true;
if (marks !== null) {
for (let mark of marks) {
if ("offset" in mark) { // offset is often 0.
mark.changeType = CHANGE_TEXT_LOCATION_ADDED;
} else {
mark.changeType = CHANGE_NODE_MARK_ADDED;
}
}
this.applyMarkChanges(marks);
this.makeSelectionVisible(/*expand*/ true);
this.ensureDocumentViewHasFocus();
}
this._caretPlaceholder.disabled = false;
}
dispose() {
if (this._caretPlaceholder) {
this._caretPlaceholder.dispose();
this._caretPlaceholder = null;
}
}
// -----------------------------------------------------------------------
// API used by DocumentView
// -----------------------------------------------------------------------
/**
* Return the text node view containing <tt>dot</tt>
* if any; <code>null</code> otherwise.
*/
get dot() {
return this._dot;
}
/**
* Return the character offset of <tt>dot</tt> if any;
* -1 otherwise.
*/
get dotOffset() {
return this._dotOffset;
}
/**
* Set the character offset of <tt>dot</tt>.
*/
set dotOffset(offset) {
this._dotOffset = offset;
}
/**
* Return the text node view containing <tt>mark</tt>
* if any; <code>null</code> otherwise.
*/
get mark() {
return this._mark;
}
/**
* Return the character offset of <tt>mark</tt> if any;
* -1 otherwise.
*/
get markOffset() {
return this._markOffset;
}
/**
* Return the <tt>selected</tt> node view dot if any;
* <code>null</code> otherwise.
*/
get selected() {
return this._selected;
}
/**
* Return the <tt>selected2</tt> node view dot if any;
* <code>null</code> otherwise.
*/
get selected2() {
return this._selected2;
}
/**
* Tests whether specified point is contained in text or node selection
* (if any).
*/
selectionContains(clientX, clientY) {
if (this.hasTextSelection() &&
this._textHighlight.contains(clientX, clientY)) {
return true;
}
if (this._selected !== null &&
this._nodeHighlight.contains(clientX, clientY)) {
return true;
}
return false;
}
/**
* Tests whether there is a text selection.
*/
hasTextSelection() {
return (this._dot !== null &&
this._mark !== null &&
(this._mark !== this._dot ||
this._markOffset !== this._dotOffset));
}
// -----------------------------------------------------------------------
// DocumentMarksChanged
// -----------------------------------------------------------------------
documentMarksChanged(event) {
this._caretPlaceholder.disabled = true;
this._caretPlaceholder.erase();
this.applyMarkChanges(event.changes);
if (!event.dragging) {
this.makeSelectionVisible(/*expand*/ event.showing);
}
this.ensureDocumentViewHasFocus();
this._caretPlaceholder.disabled = false;
}
applyMarkChanges(changes) {
let drawNodeSelection = false;
let drawTextSelection = false;
let dotChanged = false;
let node = null;
// Do not report an error in getNodeView. A mark may be added/removed
// to/from a node having no view (ancestor has display:none or has
// replaced content).
//
// Unlike what happens for DocumentViewChangedEvents, the server side
// does not filter out this kind of DocumentMarksChangedEvents. Which
// is a good thing because we currently don't know how to display
// hidden marks. So we just erase them here.
for (let change of changes) {
switch (change.changeType) {
case CHANGE_NODE_MARK_ADDED:
case CHANGE_NODE_MARK_MOVED:
if (change.mark === "SELECTED") {
node = this._docView.getNodeView(change.uid,
/*reportError*/ false);
if (node === null) {
this._selectedUID = null;
this._selected = null;
} else {
this._selectedUID = change.uid;
this._selected = node;
}
drawNodeSelection = true;
} else if (change.mark === "SELECTED2") {
node = this._docView.getNodeView(change.uid,
/*reportError*/ false);
if (node === null) {
this._selected2UID = null;
this._selected2 = null;
} else {
this._selected2UID = change.uid;
this._selected2 = node;
}
drawNodeSelection = true;
}
break;
case CHANGE_NODE_MARK_REMOVED:
if (change.mark === "SELECTED") {
this._selectedUID = null;
this._selected = null;
drawNodeSelection = true;
} else if (change.mark === "SELECTED2") {
this._selected2UID = null;
this._selected2 = null;
drawNodeSelection = true;
}
break;
case CHANGE_TEXT_LOCATION_ADDED:
case CHANGE_TEXT_LOCATION_MOVED:
if (change.mark === "DOT") {
node = this._docView.getNodeView(change.uid,
/*reportError*/ false);
if (node === null) {
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
} else {
this._dotUID = change.uid;
this._dot = node;
this._dotOffset = change.offset;
}
drawTextSelection = dotChanged = true;
} else if (change.mark === "MARK") {
node = this._docView.getNodeView(change.uid,
/*reportError*/ false);
if (node === null) {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
} else {
this._markUID = change.uid;
this._mark = node;
this._markOffset = change.offset;
}
drawTextSelection = true;
}
break;
case CHANGE_TEXT_LOCATION_REMOVED:
if (change.mark === "DOT") {
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
drawTextSelection = dotChanged = true;
} else if (change.mark === "MARK") {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
drawTextSelection = true;
}
break;
}
}
if (dotChanged) {
this._docView.magicX = -1;
}
if (drawNodeSelection || drawTextSelection) {
this._contenteditableState.reset(this._dot, this._dotOffset);
}
// Otherwise could be changes to marks we do not support here.
this.drawHighlights(drawNodeSelection, drawTextSelection);
}
drawHighlights(drawNodeSelection, drawTextSelection) {
if (this._selected2 !== null && this._selected === null) {
this._selected2UID = null;
this._selected2 = null;
drawNodeSelection = true;
}
if (this._mark !== null && this._dot === null) {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
drawTextSelection = true;
}
if (drawNodeSelection) {
this._nodeHighlight.draw(this._selected, this._selected2);
}
if (drawTextSelection) {
this._textHighlight.draw(this._dot, this._dotOffset,
this._mark, this._markOffset);
}
}
makeSelectionVisible(expand) {
let selection = null;
let sel = this._selected;
let dot = this._dot;
if (sel !== null) {
selection = sel;
if (dot !== null && selection.contains(dot)) {
selection = dot;
}
} else {
if (dot !== null) {
selection = dot;
}
}
if (selection !== null) {
if (expand) {
NodeView.expandViewBranch(selection,
this._docView.viewContainer);
} else {
// Why this?
// When the element to be scrolled is not visible, Firefox
// scrolls the scrollable to the top (which is annoying),
// while Chrome does nothing at all.
selection =
NodeView.lookupVisibleView(selection,
this._docView.viewContainer);
}
if (selection !== null) {
selection.scrollIntoViewIfNeeded(/*center*/ true);
}
}
}
ensureDocumentViewHasFocus() {
let focused = null;
const activeElem = document.activeElement;
const docViewContainer = this._docView.viewContainer;
if (activeElem === null ||
!this._docView.viewContainer.contains(activeElem) || // Not focused.
!DOMUtil.isDisplayedNode(activeElem, docViewContainer)) {
let focused = null;
if (this._dot !== null &&
DOMUtil.isDisplayedNode(this._dot, docViewContainer)) {
focused = NodeView.getTextualContent(this._dot);
}
if (focused === null) {
focused = docViewContainer;
}
focused.focus({ preventScroll: true });
}
return focused;
}
changingDocumentView() {
this._caretPlaceholder.disabled = true;
this._caretPlaceholder.erase();
}
documentViewChanged(redrawMarks) {
if (redrawMarks) {
this.redrawMarks();
}
this._caretPlaceholder.disabled = false;
}
redrawMarks() {
let drawNodeSelection = false;
let node = null;
if (this._selectedUID !== null) {
node = this._docView.getNodeView(this._selectedUID, /*err*/ false);
if (node === null) {
this._selectedUID = null;
this._selected = null;
drawNodeSelection = true;
} else {
if (node !== this._selected) {
this._selected = node;
drawNodeSelection = true;
}
}
}
if (this._selected2UID !== null) {
node = this._docView.getNodeView(this._selected2UID, /*err*/ false);
if (node === null) {
this._selected2UID = null;
this._selected2 = null;
drawNodeSelection = true;
} else {
if (node !== this._selected2) {
this._selected2 = node;
drawNodeSelection = true;
}
}
}
let drawTextSelection = false;
let dotChanged = false;
if (this._dotUID !== null) {
node = this._docView.getNodeView(this._dotUID, /*err*/ false);
if (node === null) {
this._dotUID = null;
this._dot = null;
this._dotOffset = -1;
drawTextSelection = dotChanged = true;
} else {
if (node !== this._dot) {
this._dot = node;
this._dotOffset =
MarkManager.checkTextualContentOffset(node,
this._dotOffset);
drawTextSelection = dotChanged = true;
}
}
if (drawTextSelection) {
// Dot has changed. Update contenteditableState.
this._contenteditableState.reset(this._dot, this._dotOffset);
}
}
if (this._markUID !== null) {
node = this._docView.getNodeView(this._markUID, /*err*/ false);
if (node === null) {
this._markUID = null;
this._mark = null;
this._markOffset = -1;
drawTextSelection = true;
} else {
if (node !== this._mark) {
this._mark = node;
this._markOffset =
MarkManager.checkTextualContentOffset(node,
this._markOffset);
drawTextSelection = true;
}
}
}
if (dotChanged) {
this._docView.magicX = -1;
}
this.drawHighlights(drawNodeSelection, drawTextSelection);
if (!drawTextSelection && this._dot !== null) {
// Always redraw dot (if any).
TextHighlight.drawDot(this._dot, this._dotOffset);
}
this.ensureDocumentViewHasFocus();
}
static checkTextualContentOffset(node, offset) {
let content = NodeView.getTextualContent(node);
if (content !== null) {
const textLength = content.textContent.length;
if (offset < 0) {
offset = 0;
} else if (offset > textLength) {
offset = textLength;
}
}
// Otherwise, not the view of a TextNode. Cannot check.
// (For example a TextNode's view having replaced content.)
return offset;
}
redrawDot() {
// Used by DocumentView to implement requestFocus.
this._caretPlaceholder.erase();
if (this._docView.dot !== null) {
TextHighlight.drawDot(this._docView.dot, this._docView.dotOffset);
}
this.ensureDocumentViewHasFocus();
}
}