From b9bcc2972247636e0d5e0cc41500e127147ae160 Mon Sep 17 00:00:00 2001 From: girst Date: Sat, 24 Aug 2019 02:55:41 +0200 Subject: [PATCH] experimental custom-element support Web Components / HTML5 Custom Elements were previously inaccessible to VimFx. Examples of heavy users of this technology is about:logins (Firefox 70) and bugs.chromium.org. Elements within the shadow DOM were unable to be selected with hints mode. VimFx' non-XUL DOM traversal routine was extended to not only search the (regular) DOM, but also check each element if it contains an open shadow DOM, and traverse it too. Interestingly, getElementsByTagName is not defined by the DocumentOrShadowRoot class, only querySelectorAll. Further, it appears that the native C++ function VimFx uses to send key events to clickable elements is unable to be used inside the shadow DOM, resulting in the error message below. NS_ERROR_UNEXPECTED: Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIDOMWindowUtils.dispatchDOMEventViaPresShell] utils.js:291 NOTE: This regression has made it obvious that VimFx uses isXULDocument to make assumptions about the document it is looking at that no longer hold when WebComponents are involved. More might be unearthed in the future. NOTE: the URL for an entry in about:logins is not clickable, as it is a read-only text input with an onclick handler, instead of a link. That edge case is not handled by utils.isTypingElement(). --- extension/lib/commands-frame.coffee | 5 ++-- extension/lib/events-frame.coffee | 2 +- extension/lib/markable-elements.coffee | 21 ++++++++++++---- extension/lib/utils.coffee | 33 +++++++++++++++++++++----- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/extension/lib/commands-frame.coffee b/extension/lib/commands-frame.coffee index 5ee2fed..18d608a 100644 --- a/extension/lib/commands-frame.coffee +++ b/extension/lib/commands-frame.coffee @@ -400,7 +400,8 @@ commands.click_marker_element = ( if element.target == '_blank' and preventTargetBlank targetReset = element.target element.target = '' - if type == 'clickable-special' + if type == 'clickable-special' or + type in ['clickable', 'link'] and utils.isInShadowRoot(element) element.click() else isXUL = utils.isXULDocument(element.ownerDocument) @@ -465,7 +466,7 @@ commands.element_text_select = ({vim, elementIndex, full, scroll = false}) -> # cause the text input to be re-focused, so make sure to blur the active # element, so that the caret does not end up there. window.focus() - window.document.activeElement?.blur?() + utils.getActiveElement(window)?.blur?() selection.addRange(range) diff --git a/extension/lib/events-frame.coffee b/extension/lib/events-frame.coffee index 1bb43d9..d244120 100644 --- a/extension/lib/events-frame.coffee +++ b/extension/lib/events-frame.coffee @@ -122,7 +122,7 @@ class FrameEventManager @listen('submit', ((event) -> return if event.defaultPrevented target = event.originalTarget - {activeElement} = target.ownerDocument + activeElement = utils.getActiveElement(target.ownerDocument.defaultView) if activeElement?.form == target and utils.isTypingElement(activeElement) activeElement.blur() ), false) diff --git a/extension/lib/markable-elements.coffee b/extension/lib/markable-elements.coffee index c0fa745..21d90e7 100644 --- a/extension/lib/markable-elements.coffee +++ b/extension/lib/markable-elements.coffee @@ -60,9 +60,20 @@ getMarkableElements = ( return +findAllDOMs = (dom) -> + return [dom].concat( + Array.from(dom.querySelectorAll('*')) + .filter((e) -> e.shadowRoot?) + .map((e) -> findAllDOMs(e.shadowRoot))... + ) + getAllElements = (document, selector) -> unless utils.isXULDocument(document) - return document.querySelectorAll(selector) + return [].concat( + findAllDOMs(document).map((d) -> + Array.from(d.querySelectorAll(selector)) + )... + ) # Use a `Set` since this algorithm may find the same element more than once. # Ideally we should find a way to find all elements without duplicates. @@ -308,10 +319,12 @@ tryPoint = (elementData, elementRect, x, dx, y, dy, tryRight = 0) -> # In XUL documents there are “anonymous” elements. These are never returned by # `document.elementFromPoint` but their closest non-anonymous parents are. +# The same is true for Web Components (where their hosts are returned), with +# the further caveat that they might be nested. normalize = (element) -> - normalized = element.ownerDocument.getBindingParent(element) or element - normalized = normalized.parentNode while normalized.prefix? - return normalized + element = elem while (elem = element.ownerDocument.getBindingParent(element))? + element = element.parentNode while element.prefix? + return element # Returns whether `element` corresponds to `elementAtPoint`. This is only # complicated for browser elements in the web page content area. diff --git a/extension/lib/utils.coffee b/extension/lib/utils.coffee index 53f50d9..03951b9 100644 --- a/extension/lib/utils.coffee +++ b/extension/lib/utils.coffee @@ -29,6 +29,14 @@ XULButtonElement = Ci.nsIDOMXULButtonElement XULControlElement = Ci.nsIDOMXULControlElement XULMenuListElement = Ci.nsIDOMXULMenuListElement +# Traverse the DOM upwards until we hit its containing document (most likely an +# HTMLDocument or (<=fx68) XULDocument) or the ShadowRoot. +getDocument = (e) -> if e.parentNode? then arguments.callee(e.parentNode) else e +# for shadow DOM custom elements, as they require special handling. +# (ShadowRoot is only available in mozilla63+) +isInShadowRoot = (element) -> + ShadowRoot? and getDocument(element) instanceof ShadowRoot + isXULDocument = (doc) -> XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul' doc.documentElement.namespaceURI == XUL_NS @@ -194,6 +202,10 @@ blurActiveElement = (window) -> # focus to the `` of its containing frame, while blurring the top-most # frame gives focus to the top-most ``. This allows to blur fancy text # editors which use an `