Source: xxe/view/MarkManager.js

/**
 * 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();
    }
}