Source: view/NodeView.js

/**
 * A (not yet documentd) collection of low-level utitity functions
 * related to <em>node views</em>, client-side <em>views</em> of
 * server-side XML <em>nodes</em>. Node views
 * are contained in the {@link DocumentView}.
 * <p>A node view is represented on the client side by an HTML element
 * (mainly <code>div</code>; possibly having descendant HTML elements)
 * having the following internal structure/attributes:
 * <ul>
 * <li>Text view:
 * 
 *     <pre>&lt;span data-t="" id=UID contenteditable&gt;TEXT&lt;span&gt;</pre>
 * 
 *     <p>No generated content. 
 * 
 *  <li>Comment view:
 * 
 *      <pre>data-wc=UID
 *   [ + data-bc=UID ]
 *   + data-c="" id=UID contenteditable
 *        + TEXT
 *   [ + data-ac=UID ]</pre>
 *
 *      <p><strong>But</strong> when styled:
 *
 *      <pre>data-c="" id=UID contenteditable
 *     + TEXT</pre>
 *
 *      <pre>data-rc="" id=UID
 *     + REPLACED_CONTENT</pre>
 * 
 *  <li>Processing-instruction view:
 * 
 *      <pre>data-wp=UID
 *   [ + data-bc=UID ]
 *   + data-p="" id=UID contenteditable
 *        + TEXT
 *   [ + data-ap=UID ]</pre>
 *
 *      BUT when styled:
 *
 *      <pre>data-p="" id=UID contenteditable
 *     + TEXT</pre>
 *
 *      <pre>data-rp="" id=UID
 *     + REPLACED_CONTENT</pre>
 * 
 *  <li>Element view:
 * 
 *      <pre>data-e="" id=UID
 *   [ + data-be=UID ]
 *       + CHILDREN
 *   [ + data-ae=UID ]</pre>
 *
 *      <p><strong>Also</strong> when styled:
 *
 *      <pre>data-re="" id=UID
 *   [ + data-be=UID ]
 *       + REPLACED_CONTENT
 *   [ + data-ae=UID ]</pre>
 *
 *      <pre>data-we=UID
 *   [ + data-be=UID ]
 *   + data-e="" id=UID
 *      + CHILDREN
 *   [ + data-ae=UID ]</pre>
 *
 *      <pre>data-we=UID
 *   [ + data-be=UID ]
 *   + data-re="" id=UID
 *      + REPLACED_CONTENT
 *   [ + data-ae=UID ]</pre>
 *         
 *   <li>Table view specificities: see description in 
 *   <code>com.xmlmind.xmleditsrv.webview.TableRendering.java</code>.
 * 
 *   <li>The root view must have an "<code>xxe-re</code>" class if editable 
 *   and "<code>xxe-ro</code>" otherwise.
 * 
 *   <li>Collapsers must have a <code>collapsed</code> attribute which is
 *   added/removed dynamically.
 * 
 *       <p>In the case of the tree view, 
 *       the collapser is <code>&lt;xxe-collapser&gt;</code>.
 *       More expectations in <code>TreeCollapser.js</code>.
 * 
 *       <p>In the case of the styled view, the collapser is 
 *       <code>&lt;xxe-collapser2&gt;</code>. 
 *       See <code>StyledCollapser.js</code>.
 * 
 *       <p><code>&lt;xxe-collapser2&gt;</code> for a collapsible 
 *       styled element view may be found not only 
 *       in the content before/content after of this 
 *       element view, but also inside its <strong>descendant</strong>
 *       element views (e.g. content before in the caption of
 *       a collapsible table or the title of a collapsible section).
 * 
 *       <p>A collapsible styled element has a 
 *       <code>data-collapsible="collapsed;notCollapsibleHead;notCollapsibleFoot"</code>
 *       attribute.
 * </ul>
 */
export class NodeView {
    // -----------------------------------------------------------------------
    // Access by uid
    // -----------------------------------------------------------------------
    
    static content(root, uid, reportError=true) {
        let content = root.getElementById(uid);
        if (content === null && reportError) {
            console.error(`NodeView.content: INTERNAL ERROR: \
cannot find node view "${uid}"`);
        }

        return content;
    }
    
    static textualContent(root, uid, reportError=true) {
        let content = NodeView.content(root, uid, reportError);
        if (content !== null &&
            (content.hasAttribute("data-t") || 
             content.hasAttribute("data-c") || 
             content.hasAttribute("data-p"))) {
            return content;
        }
        
        return null;
    }

    static view(root, uid, reportError=true) {
        let content = NodeView.content(root, uid, reportError);
        if (content !== null) {
            return NodeView.contentToView(content, uid);
        }
        
        return null;
    }

    static contentToView(content, uid) {
        assertOrError(NodeView.isContent(content));
        
        let parent = content.parentElement;
        if (parent !== null &&
            (parent.getAttribute("data-we") === uid ||
             parent.getAttribute("data-wp") === uid ||
             parent.getAttribute("data-wc") === uid)) {
            content = parent;
        }
        // Otherwise the view is equal to its content.
        
        return content;
    }

    // -----------------------------------------------------------------------
    // Categorization of view parts
    // -----------------------------------------------------------------------

    static isContent(node) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                node.id && /*accelerator*/
                (node.hasAttribute("data-t") || 
                 node.hasAttribute("data-e") || 
                 node.hasAttribute("data-c") || 
                 node.hasAttribute("data-p") || 
                 node.hasAttribute("data-re") || 
                 node.hasAttribute("data-rc") || 
                 node.hasAttribute("data-rp")));
    }
    
    static isTextualContent(node) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                node.id && /*accelerator*/
                (node.hasAttribute("data-t") || 
                 node.hasAttribute("data-c") || 
                 node.hasAttribute("data-p")));
    }
    
    static isReplacedContent(node) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                node.id && /*accelerator*/
                (node.hasAttribute("data-re") || 
                 node.hasAttribute("data-rc") || 
                 node.hasAttribute("data-rp")));
    }
    
    static isContentBefore(node, uid) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                (node.getAttribute("data-be") === uid ||
                 node.getAttribute("data-bp") === uid ||
                 node.getAttribute("data-bc") === uid));
    }
    
    static isContentAfter(node, uid) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                (node.getAttribute("data-ae") === uid ||
                 node.getAttribute("data-ap") === uid ||
                 node.getAttribute("data-ac") === uid));
    }

    static isContentWrapper(node) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                (node.hasAttribute("data-we") || 
                 node.hasAttribute("data-wp") || 
                 node.hasAttribute("data-wc")));
    }
    
    static isGeneratedContent(node) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                (node.hasAttribute("data-be") ||
                 node.hasAttribute("data-bp") ||
                 node.hasAttribute("data-bc") ||
                 node.hasAttribute("data-ae") ||
                 node.hasAttribute("data-ap") ||
                 node.hasAttribute("data-ac") ||
                 node.hasAttribute("data-re") ||
                 node.hasAttribute("data-rp") ||
                 node.hasAttribute("data-rc")));
    }
    
    static isActualContent(node) {
        return (node !== null &&
                node.nodeType === Node.ELEMENT_NODE &&
                node.id && /*accelerator*/
                (node.hasAttribute("data-t") || 
                 node.hasAttribute("data-e") || 
                 node.hasAttribute("data-p") || 
                 node.hasAttribute("data-c")));
    }
    
    // -----------------------------------------------------------------------
    // View functions
    // -----------------------------------------------------------------------

    static getUID(view) {
        if (view !== null &&
            view.nodeType === Node.ELEMENT_NODE) {
            // Do not assume that it's a content just because it has an ID.
            if (view.id && NodeView.isContent(view)) {
                // Here we assume that specified view is also the content.
                return view.id;
            } else {
                // A wrapper, when it exists, is always the view.
                for (const attrName of ["data-we", "data-wp", "data-wc"]) {
                    let uid = view.getAttribute(attrName);
                    if (uid !== null) {
                        return uid;
                    }
                }
            }
        }

        return null;
    }
    
    static getContent(view) {
        if (view !== null &&
            view.nodeType === Node.ELEMENT_NODE) {
            if (view.id && NodeView.isContent(view)) {
                return view;
            } else {
                for (const attrName of ["data-we", "data-wp", "data-wc"]) {
                    let uid = view.getAttribute(attrName);
                    if (uid !== null) {
                        let child = view.firstChild;
                        while (child !== null) {
                            if (child.id === uid) {
                                return child;
                            }
                            
                            child = child.nextSibling;
                        }
                    }
                }
            }
        }

        return null;
    }

    static getTextualContent(view) {
        let content = NodeView.getContent(view);
        if (content !== null &&
            (content.hasAttribute("data-t") || 
             content.hasAttribute("data-c") || 
             content.hasAttribute("data-p"))) {
            return content;
        }

        return null;
    }

    static getReplacedContent(view) {
        let content = NodeView.getContent(view);
        if (content !== null &&
            (content.hasAttribute("data-re") || 
             content.hasAttribute("data-rc") || 
             content.hasAttribute("data-rp"))) {
            return content;
        }

        return null;
    }

    static isView(node) {
        if (node !== null &&
            node.nodeType === Node.ELEMENT_NODE) {
            if (NodeView.isContentWrapper(node)) {
                // A wrapper, when it exists, is always the view.
                return true;
            } else {
                let uid = node.id;
                if (uid && NodeView.isContent(node)) {
                    // Content. Is it also the view?
                    return (NodeView.contentToView(node, uid) === node);
                }
            }
        }

        return false;
    }

    static uidIfElementView(node) {
        if (node !== null &&
            node.nodeType === Node.ELEMENT_NODE) {
            let uid = node.getAttribute("data-we");
            if (uid !== null) {
                // An element wrapper, when it exists, is always the element
                // view.
                return uid;
            }
            
            uid = node.id;
            if (uid &&
                (node.hasAttribute("data-e") || node.hasAttribute("data-re"))) {
                // Element content.
                let parent = node.parentElement;
                if (parent === null ||
                    parent.getAttribute("data-we") !== uid) {
                    // No wrapper, hence the element content is also the
                    // element view.
                    return uid;
                }
            }
        }

        return null;
    }
    
    static getNextViewSibling(view) {
        assertOrError(NodeView.isView(view));

        let node = view.nextSibling;
        while (node !== null) {
            if (NodeView.isView(node)) {
                return node;
            }
            
            node = node.nextSibling;
        }

        return null;
    }
    
    static getPreviousViewSibling(view) {
        assertOrError(NodeView.isView(view));
        
        let node = view.previousSibling;
        while (node !== null) {
            if (NodeView.isView(node)) {
                return node;
            }
            
            node = node.previousSibling;
        }

        return null;
    }
    
    static getParentView(view) {
        assertOrError(NodeView.isView(view));
        
        return NodeView.lookupView(view.parentElement);
    }

    // -----------------------------------------------------------------------
    // Lookup (HTML ancestors) functions
    // -----------------------------------------------------------------------
    
    // ----------
    // lookupView
    // ----------
    
    static lookupView(node) {
        let elem = node;
        if (node !== null &&
            node.nodeType !== Node.ELEMENT_NODE) {
            elem = node.parentElement;
        }

        while (elem !== null) {
            let uid = elem.id;
            if (uid) {
                if (NodeView.isContent(elem)) {
                    // Content. Is it also the view?
                    return NodeView.contentToView(elem, uid);
                }
            } else {
                if (NodeView.isContentWrapper(elem)) {
                    // A wrapper, when it exists, is always the view.
                    return elem;
                }
            }
            
            elem = elem.parentElement;
        }

        return null;
    }
    
    // --------------
    // lookupViewPart
    // --------------
    
    static lookupViewPart(node) {
        while (node !== null) {
            if (node.nodeType === Node.ELEMENT_NODE &&
                (NodeView.isContentWrapper(node) ||
                 NodeView.isGeneratedContent(node) ||
                 NodeView.isActualContent(node))) {
                return node;
            }
            
            node = node.parentElement;
        }
        
        return null;
    }
    
    // --------------------
    // lookupTextualContent
    // --------------------
    
    static lookupTextualContent(node) {
        while (node !== null) {
            if (NodeView.isTextualContent(node)) {
                return node;
            }
            
            node = node.parentElement;
        }
        
        return null;
    }
    
    // -----------------------------------------------------------------------
    // Find (inside an HTML node tree) functions
    // -----------------------------------------------------------------------
    
    // ------------------
    // findTextualContent
    // ------------------
    
    static findTextualContent(tree) {
        if (NodeView.isTextualContent(tree)) {
            return tree;
        }

        let child = tree.firstChild;
        while (child !== null) {
            if (child.nodeType === Node.ELEMENT_NODE &&
                !NodeView.isGeneratedContent(child)) {
                let found = NodeView.findTextualContent(child);
                if (found !== null) {
                    return found;
                }
            }
            
            child = child.nextSibling;
        }

        return null;
    }
    
    // -------------------------
    // pickNearestTextualContent
    // -------------------------
    
    static pickNearestTextualContent(tree, clientX, clientY) {
        let found = [null, Number.MAX_SAFE_INTEGER];
        NodeView.doPickNearestTextualContent(tree, clientX, clientY, found);
        return (found[0] === null)? null : found[0];
    }
    
    static doPickNearestTextualContent(tree, clientX, clientY, found) {
        if (NodeView.isTextualContent(tree)) {
            const rects = tree.getClientRects();
            const rectCount = rects.length;
            for (let i = 0; i < rectCount; ++i) {
                const rect = rects[i];
                
                if (rect !== null &&
                    rect.height > 0 && // rect.width=0 OK.
                    clientY >= rect.top && clientY < rect.bottom) {
                    let distance = 0;
                    if (clientX < rect.left) {
                        distance = rect.left - clientX;
                    } else if (clientX >= rect.right) {
                        distance = clientX - rect.right;
                    }

                    if (distance < found[1]) {
                        found[0] = tree;
                        found[1] = distance;
                    }
                }
            }
        }
        
        let child = tree.firstChild;
        while (child !== null) {
            if (child.nodeType === Node.ELEMENT_NODE &&
                !NodeView.isGeneratedContent(child)) {
                NodeView.doPickNearestTextualContent(child, clientX, clientY,
                                                     found);
            }
            
            child = child.nextSibling;
        }
    }
    
    // ---------------------
    // collectTextualContent
    // ---------------------
    
    static collectTextualContent(view1, offset1, view2, offset2, collected) {
        collected.length = 0;

        if (view1 === view2) {
            let textContent1 = NodeView.getTextualContent(view1);
            if (textContent1 !== null) {
                collected.push(textContent1);
            }
            return (offset1 > offset2); 
        }

        // ---
        
        let reversed = false;
        if (view1.compareDocumentPosition(view2) &
                Node.DOCUMENT_POSITION_PRECEDING) {
            // view2 before view1.
            reversed = true;
        }
        if (reversed) {
            let tmp = view1; view1 = view2; view2 = tmp;
            // offset1, offset2 not used below.
        }

        let start = NodeView.getTextualContent(view1);
        if (start === null) {
            // For example a TextNode's view having replaced content.
            start = view1;
        }
        let end = NodeView.getTextualContent(view2);
        if (end === null) {
            end = view2;
        }

        let range = new Range();
        range.setStart(start, 0);
        range.setEnd(end, 0);
        NodeView.doCollectTextualContent(range.commonAncestorContainer,
                                         start, end, [false], collected);
        
        return reversed;
    }
    
    static doCollectTextualContent(tree, start, end, collecting, collected) {
        if (collecting[0]) {
            if (NodeView.isTextualContent(tree)) {
                collected.push(tree);
            }
            
            if (tree === end) {
                return true; // Done.
            }
        } else {
            if (tree === start) {
                collecting[0] = true;
                
                if (NodeView.isTextualContent(tree)) {
                    collected.push(tree);
                }
                
                if (tree === end) {
                    return true; // Done.
                }
            }
        }

        if (!NodeView.isGeneratedContent(tree)) {
            let child = tree.firstChild;
            while (child !== null) {
                if (child.nodeType === Node.ELEMENT_NODE &&
                    NodeView.doCollectTextualContent(child, start, end,
                                                     collecting, collected)) {
                    return true;
                }

                child = child.nextSibling;
            }
        }

        return false;
    }
    
    // -----------------------------------------------------------------------
    // A text node view (having no replaced content; contenteditable=true)
    // is supposed to contain a single HTML text node.
    //
    // This is not always the case. Why?
    //
    // 1) Contenteditable tend to "damage" its text node.
    //    Ideally, you should find a single text node. In practice
    //    we may end up with none, several or even inserted elements
    //    like <br />.
    //
    // 2) CaretPlaceholder.js inserts <a class="xxe-caret"/> caret placeholder
    //    when the document view looses the keyboard focus.
    //
    // 3) TextHighlight.js renders the text selection by inserting
    //    <mark class="xxe-text-sel">SELECTED TEXT</mark>.
    // -----------------------------------------------------------------------

    // --------------------
    // normalizeTextContent
    // --------------------
    
    static normalizeTextualContent(textContent) {
        assertOrError(NodeView.isTextualContent(textContent));

        let child = textContent.firstChild;
        if (child === null) {
            child = document.createTextNode("");
            textContent.appendChild(child);
            // Done.
            return;
        }

        if (child.nextSibling === null &&
            child.nodeType === Node.TEXT_NODE) {
            // Nothing to do.
            return;
        }

        let needNormalize = 0;
        while (child !== null) {
            let next = child.nextSibling;
            
            switch (child.nodeType) {
            case Node.TEXT_NODE:
                ++needNormalize;
                break;
            case Node.ELEMENT_NODE:
                {
                    let inserted = null;

                    let node = child.firstChild;
                    if (node !== null) {
                        if (node.nextSibling === null &&
                            node.nodeType === Node.TEXT_NODE) {
                            // Contains a single text node. Use it as is.
                            child.removeChild(node);
                            inserted = node;
                        } else {
                            let text = child.textContent;
                            if (text !== null && text.length > 0) {
                                // Contains some text. Create new text node.
                                inserted = document.createTextNode(text);
                            }
                        }
                    }
                    
                    if (inserted !== null) {
                        textContent.insertBefore(inserted, /*before*/ child);
                        ++needNormalize;
                    }
                    textContent.removeChild(child);
                }
                break;
            }
            
            child = next;
        }

        if (needNormalize > 1) {
            textContent.normalize();
        }
    }

    // --------------------------
    // textualContentToCharOffset
    // --------------------------
    
    static textualContentToCharOffset(textContent, offset) {
        assertOrError(NodeView.isTextualContent(textContent));
        
        let result = [null, -1];
        
        let node = textContent.firstChild;
        if (node === null) {
            node = document.createTextNode("");
            textContent.appendChild(node);
            result[0] = node;
            result[1] = 0;
        } else {
            result[1] = 0;
            if (!NodeView.findCharOffset(textContent, offset, result)) {
                if (result[0] !== null && result[1] === offset) {
                    // Offset is just after result[0], the last text node
                    // of textContent.

                    // Convert to relative offset.
                    result[1] = result[0].length;
                } else {
                    // Not found.
                    result[0] = null;
                    result[1] = -1;
                }
            }
        }

        return result;
    }

    static findCharOffset(tree, offset, result) {
        let node = tree.firstChild;
        while (node !== null) {
            switch (node.nodeType) {
            case Node.TEXT_NODE:
                {
                    result[0] = node;
                    let start = result[1];
                    result[1] += node.length;
                    
                    if (offset < result[1]) {
                        // Convert to relative offset.
                        result[1] = offset - start;
                        return true;
                    }
                }
                break;
            case Node.ELEMENT_NODE:
                if (NodeView.findCharOffset(node, offset, result)) {
                    return true;
                }
                break;
            }
            
            node = node.nextSibling;
        }

        return false;
    }
    
    // --------------------------
    // charToTextualContentOffset
    // --------------------------
    
    static charToTextualContentOffset(pos) {
        let done = false;
        
        const node = pos[0];
        const offset = pos[1];
        pos[0] = null;
        pos[1] = -1;

        let textContent = NodeView.lookupTextualContent(node);
        if (textContent !== null) {
            let child = textContent.firstChild;
            if (child === null) {
                // Empty text node view.
                pos[0] = textContent;
                pos[1] = 0;
                done = true;
            } else {
                if (child.nextSibling === null &&
                    child.nodeType === Node.TEXT_NODE) {
                    // textContent just contains a single text node.
                    pos[0] = textContent;
                    pos[1] = offset;
                    done = true;
                } else {
                    let startOffset = [0];
                    if (NodeView.findNodeStartOffset(textContent, node,
                                                     startOffset)) {
                        pos[0] = textContent;
                        pos[1] = startOffset[0] + offset;
                        done = true;
                    }
                }
            }
        }

        return done;
    }

    static findNodeStartOffset(tree, searchedNode, result) {
        let node = tree.firstChild;
        while (node !== null) {
            if (node === searchedNode) {
                return true;
            }
            
            switch (node.nodeType) {
            case Node.TEXT_NODE:
                result[0] += node.length;
                break;
            case Node.ELEMENT_NODE:
                if (NodeView.findNodeStartOffset(node, searchedNode, result)) {
                    return true;
                }
                break;
            }
            
            node = node.nextSibling;
        }

        return false;
    }
    
    // -----------------------------------------------------------------------
    // Expand/collapse views
    // -----------------------------------------------------------------------
    
    // ----------------
    // expandViewBranch
    // ----------------
    
    static expandViewBranch(node, docView) {
        let elem = node;
        if (node !== null &&
            node.nodeType !== Node.ELEMENT_NODE) {
            elem = node.parentElement;
        }

        while (elem !== null && elem !== docView) {
            // <xxe-collapser> are found only inside the content before or
            // content after of the element view.
            
            let uid = elem.id;
            if (uid && NodeView.isContent(elem)) {
                // Replaced or actual content.
                
                NodeView.expandCollapsers(elem, uid);
            } else {
                // Wrapper?

                uid = null;
                for (const attrName of ["data-we", "data-wp", "data-wc"]) {
                    uid = elem.getAttribute(attrName);
                    if (uid !== null) {
                        NodeView.expandCollapsers(elem, uid);
                        break;
                    }
                }
            }

            // <xxe-collapser2> are found inside the content before or
            // content after of the element view AND ITS DESCENDANT VIEWS.
            
            if (uid && elem.hasAttribute("data-collapsible")) {
                // A collapsible styled element view.
                NodeView.expandCollapsers2(elem, uid);
            }
            
            elem = elem.parentElement;
        }
    }

    static expandCollapsers(elem, uid) {
        let child = elem.firstChild;
        while (child !== null) {
            if (child.nodeType === Node.ELEMENT_NODE &&
                (NodeView.isContentBefore(child, uid) ||
                 NodeView.isContentAfter(child, uid))) {
                NodeView.doExpandCollapsers(child);
            }

            child = child.nextSibling;
        }
    }

    static doExpandCollapsers(elem) {
        let child = elem.firstChild;
        while (child !== null) {
            if (child.nodeType === Node.ELEMENT_NODE) {
                if ("xxe-collapser" === child.localName) {
                    if (child.collapsed) {
                        child.collapsed = false;
                    }
                } else {
                    NodeView.doExpandCollapsers(child);
                }
            }
            
            child = child.nextSibling;
        }
    }

    static expandCollapsers2(elem, uid) {
        let collapsers = elem.querySelectorAll(`xxe-collapser2[for="${uid}"]`);
        const count = collapsers.length;
        if (count > 0) {
            for (let i = 0; i < count; ++i) {
                let collapser = collapsers[i];
                if (collapser.collapsed) {
                    collapser.collapsed = false;
                }
            }
        }
    }
    
    // -----------------
    // lookupVisibleView
    // -----------------
    
    static lookupVisibleView(node, docView) {
        let elem = node;
        if (node !== null &&
            node.nodeType !== Node.ELEMENT_NODE) {
            elem = node.parentElement;
        }

        let visibleView = null;
        while (elem !== null && elem !== docView) {
            let view = null;

            let uid = elem.id;
            if (uid) {
                if (NodeView.isContent(elem)) {
                    // Content. Is it also the view?
                    view = NodeView.contentToView(elem, uid);
                }
            } else {
                if (NodeView.isContentWrapper(elem)) {
                    // A wrapper, when it exists, is always the view.
                    view = elem;
                }
            }

            if (view !== null) {
                let display =
                    window.getComputedStyle(view).getPropertyValue("display");
                if (display) {
                    if (display === "none") {
                        // May be an ancestor view will be visible?
                        visibleView = null;
                    } else {
                        if (visibleView === null) {
                            // Remember "deepest" visible view.
                            visibleView = view;
                        }
                    }
                }
            }
            
            elem = elem.parentElement;
        }

        return visibleView;
    }
}