Source: xxe/view/CaretPlaceholder.js

/**
 * Draw a caret placeholder when the document view looses the keyboard focus.
 */
class CaretPlaceholder {
    constructor(docView) {
        this._docView = docView;
        
        this._disabled = false;
        this._caret = null;
        
        const activeElem = document.activeElement;
        this._docViewHasFocus = (activeElem !== null &&
                                 docView.contains(activeElem));

        this._onFocusin = this.onFocusin.bind(this);
        docView.addEventListener("focusin", this._onFocusin);
        this._onFocusout = this.onFocusout.bind(this);
        docView.addEventListener("focusout", this._onFocusout);
    }

    dispose() {
        if (this._onFocusin) {
            this._docView.removeEventListener("focusin", this._onFocusin);
            this._onFocusin = null;
        }
        if (this._onFocusout) {
            this._docView.removeEventListener("focusout", this._onFocusout);
            this._onFocusout = null;
        }
    }

    onFocusin(event) {
        // Do not consume event.
        if (!this._docViewHasFocus) {
            this._docViewHasFocus = true;

            if (!this._disabled) {
                this.hide();
            }
        }
    }

    onFocusout(event) {
        // Do not consume event.
        if (this._docViewHasFocus) {
            let receivingFocus = event.relatedTarget;
            if (receivingFocus === null ||
                !this._docView.contains(receivingFocus)) {
                this._docViewHasFocus = false;

                if (!this._disabled) {
                    this.show();
                }
            }
        }
    }

    get disabled() {
        return this._disabled;
    }
    
    set disabled(disable) {
        this._disabled = disable;
    }
    
    hide() {
        this.erase();
        
        // "Redraw" actual caret because in most cases it is lost.
        TextHighlight.drawDot(this._docView.dot, this._docView.dotOffset);
    }

    erase() {
        // Do not make any assumption about caret and its the parent.
        
        let caret = this._caret;
        if (caret !== null) {
            this._caret = null;

            let container = caret.parentNode;
            if (container !== null && container.isConnected) {
                container.removeChild(caret);

                container = NodeView.getTextualContent(container);
                if (container !== null) {
                    NodeView.normalizeTextualContent(container);
                }
            }
        }
    }
    
    show() {
        const dot = this._docView.dot;
        if (dot !== null && !this._docView.hasTextSelection()) {
            this.erase();
            this.draw(dot, this._docView.dotOffset);
        }
    }
    
    draw(dot, dotOffset) {
        // Do not make any assumption about the contents of dotContent.
        // dotContent may even be null in case of a TextNode's view having
        // replaced content.
        
        let dotContent = NodeView.getTextualContent(dot);
        if (dotContent !== null) {
            let [charsNode, charOffset] =
                NodeView.textualContentToCharOffset(dotContent, dotOffset);
            if (charsNode !== null) {
                this._caret = document.createElement("a");
                this._caret.classList.add("xxe-caret");

                if (charOffset === 0) {
                    charsNode.parentNode.insertBefore(this._caret, charsNode);
                } else if (charOffset === charsNode.length) {
                    charsNode.parentNode.insertBefore(this._caret,
                                                      charsNode.nextSibling);
                } else {
                    let beforeNode = charsNode.splitText(charOffset);
                    charsNode.parentNode.insertBefore(this._caret, beforeNode);
                }

                if (dotContent.textContent.length === 0) {
                    this._caret.classList.add("xxe-caret-empty");
                }
            } else {
                console.error(`CaretPlaceholder.draw: INTERNAL ERROR: \
cannot convert dot offset ${dotOffset} to char offset in \
${DOMUtil.dumpNode(dotContent)}`);
            }
        }
    }
}