Bug 1504343 - Convert xbl-marquee to UA Widget r=bgrins,bzbarsky
authorTimothy Guan-tin Chien <timdream@gmail.com>
Sun, 18 Nov 2018 01:23:52 +0000
changeset 446926 b2248edb85bb91fb05603e8d0b8626150723051f
parent 446925 75ca66f490c31648aa80c228e043806bdee327d0
child 446927 1bfd0fb3b0d740be0ea913164dce90263aa822d1
push id35058
push usercbrindusan@mozilla.com
push dateSun, 18 Nov 2018 11:14:44 +0000
treeherdermozilla-central@e432617f7098 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, bzbarsky
bugs1504343, 840098
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1504343 - Convert xbl-marquee to UA Widget r=bgrins,bzbarsky This patch moves the marquee bindings from xbl-marquee.xml to marquee.js and converts them to a UA Widget. The contenteditable bindings are dropped, replaced with a styling rule that will fix the position of the scrolling text. Inline styles have been moved to the stylesheet so usage of display: -moz-box can continue to be parsed. test_bug840098.html is deleted because it is only valid under the context of in-content XBL bindings. Differential Revision: https://phabricator.services.mozilla.com/D10385
dom/base/test/mochitest.ini
dom/base/test/test_bug840098.html
dom/html/HTMLMarqueeElement.cpp
dom/html/HTMLMarqueeElement.h
layout/style/contenteditable.css
layout/style/res/html.css
toolkit/actors/UAWidgetsChild.jsm
toolkit/content/jar.mn
toolkit/content/widgets/docs/ua_widget.rst
toolkit/content/widgets/marquee.css
toolkit/content/widgets/marquee.js
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -548,17 +548,16 @@ skip-if = toolkit == 'android' || (verif
 [test_bug809003.html]
 [test_bug810494.html]
 [test_bug811701.html]
 [test_bug811701.xhtml]
 [test_bug813919.html]
 [test_bug814576.html]
 [test_bug819051.html]
 [test_bug820909.html]
-[test_bug840098.html]
 [test_bug864595.html]
 [test_bug868999.html]
 [test_bug869000.html]
 [test_bug869002.html]
 [test_bug869006.html]
 [test_bug876282.html]
 [test_bug890580.html]
 [test_bug891952.html]
deleted file mode 100644
--- a/dom/base/test/test_bug840098.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=840098
--->
-<head>
-  <meta charset="utf-8">
-  <title>Test for Bug 840098</title>
-  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
-</head>
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=840098">Mozilla Bug 840098</a>
-<p id="display"></p>
-<div id="content" style="display: none">
-  <div id="foo"></div>
-</div>
-<marquee id="m">Hello</marquee>
-<pre id="test">
-<script type="application/javascript">
-
-/** Test for Bug 840098 **/
-
-var mar = document.getElementById("m");
-var anonymousNode = SpecialPowers.wrap(document).getAnonymousNodes(mar)[0];
-try {
-  SpecialPowers.wrap(document).implementation.createDocument("", "", null).adoptNode(anonymousNode);
-  ok(false, "shouldn't be able to adopt the root of an anonymous subtree");
-} catch (e) {
-  is(e.name, "NotSupportedError", "threw the correct type of error");
-}
-
-</script>
-</pre>
-</body>
-</html>
--- a/dom/html/HTMLMarqueeElement.cpp
+++ b/dom/html/HTMLMarqueeElement.cpp
@@ -3,20 +3,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/HTMLMarqueeElement.h"
 #include "nsGenericHTMLElement.h"
 #include "nsStyleConsts.h"
 #include "nsMappedAttributes.h"
+#include "mozilla/AsyncEventDispatcher.h"
 #include "mozilla/dom/HTMLMarqueeElementBinding.h"
 #include "mozilla/dom/CustomEvent.h"
 // This is to pick up the definition of FunctionStringCallback:
 #include "mozilla/dom/DataTransferItemBinding.h"
+#include "mozilla/dom/ShadowRoot.h"
 
 NS_IMPL_NS_NEW_HTML_ELEMENT(Marquee)
 
 namespace mozilla {
 namespace dom {
 
 HTMLMarqueeElement::~HTMLMarqueeElement()
 {
@@ -60,16 +62,53 @@ HTMLMarqueeElement::IsEventAttributeName
 }
 
 JSObject*
 HTMLMarqueeElement::WrapNode(JSContext *aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return dom::HTMLMarqueeElement_Binding::Wrap(aCx, this, aGivenProto);
 }
 
+nsresult
+HTMLMarqueeElement::BindToTree(nsIDocument* aDocument, nsIContent* aParent,
+                              nsIContent* aBindingParent)
+{
+
+  nsresult rv = nsGenericHTMLElement::BindToTree(aDocument, aParent,
+                                                 aBindingParent);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (nsContentUtils::IsUAWidgetEnabled() && IsInComposedDoc()) {
+    AttachAndSetUAShadowRoot();
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(this,
+                               NS_LITERAL_STRING("UAWidgetBindToTree"),
+                               CanBubble::eYes,
+                               ChromeOnlyDispatch::eYes);
+    dispatcher->RunDOMEventWhenSafe();
+  }
+
+  return rv;
+}
+
+void
+HTMLMarqueeElement::UnbindFromTree(bool aDeep, bool aNullParent)
+{
+  if (GetShadowRoot() && IsInComposedDoc()) {
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(this,
+                               NS_LITERAL_STRING("UAWidgetUnbindFromTree"),
+                               CanBubble::eYes,
+                               ChromeOnlyDispatch::eYes);
+    dispatcher->RunDOMEventWhenSafe();
+  }
+
+  nsGenericHTMLElement::UnbindFromTree(aDeep, aNullParent);
+}
+
 void
 HTMLMarqueeElement::SetStartStopCallback(FunctionStringCallback* aCallback)
 {
   mStartStopCallback = aCallback;
 }
 
 void
 HTMLMarqueeElement::GetBehavior(nsAString& aValue)
@@ -118,16 +157,39 @@ HTMLMarqueeElement::ParseAttribute(int32
       return aResult.ParseNonNegativeIntValue(aValue);
     }
   }
 
   return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue,
                                               aMaybeScriptedPrincipal, aResult);
 }
 
+nsresult
+HTMLMarqueeElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+                                 const nsAttrValue* aValue,
+                                 const nsAttrValue* aOldValue,
+                                 nsIPrincipal* aMaybeScriptedPrincipal,
+                                 bool aNotify)
+{
+  if (nsContentUtils::IsUAWidgetEnabled() &&
+      IsInComposedDoc() &&
+      aNameSpaceID == kNameSpaceID_None &&
+      aName == nsGkAtoms::direction) {
+    AsyncEventDispatcher* dispatcher =
+      new AsyncEventDispatcher(this,
+                               NS_LITERAL_STRING("UAWidgetAttributeChanged"),
+                               CanBubble::eYes,
+                               ChromeOnlyDispatch::eYes);
+    dispatcher->RunDOMEventWhenSafe();
+  }
+  return nsGenericHTMLElement::AfterSetAttr(
+    aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify);
+}
+
+
 void
 HTMLMarqueeElement::MapAttributesIntoRule(const nsMappedAttributes* aAttributes, MappedDeclarations& aDecls)
 {
   nsGenericHTMLElement::MapImageMarginAttributeInto(aAttributes, aDecls);
   nsGenericHTMLElement::MapImageSizeAttributesInto(aAttributes, aDecls);
   nsGenericHTMLElement::MapCommonAttributesInto(aAttributes, aDecls);
   nsGenericHTMLElement::MapBGColorInto(aAttributes, aDecls);
 }
@@ -145,25 +207,41 @@ HTMLMarqueeElement::IsAttributeMapped(co
 
 nsMapRuleToAttributesFunc
 HTMLMarqueeElement::GetAttributeMappingFunction() const
 {
   return &MapAttributesIntoRule;
 }
 
 void
+HTMLMarqueeElement::DispatchEventToShadowRoot(const nsAString& aEventTypeArg)
+{
+  // Dispatch the event to the UA Widget Shadow Root, make it inaccessible to document.
+  RefPtr<nsINode> shadow = GetShadowRoot();
+  MOZ_ASSERT(shadow);
+  RefPtr<Event> event = new Event(shadow, nullptr, nullptr);
+  event->InitEvent(aEventTypeArg, false, false);
+  event->SetTrusted(true);
+  shadow->DispatchEvent(*event, IgnoreErrors());
+}
+
+void
 HTMLMarqueeElement::Start()
 {
-  if (mStartStopCallback) {
+  if (GetShadowRoot()) {
+    DispatchEventToShadowRoot(NS_LITERAL_STRING("marquee-start"));
+  } else if (mStartStopCallback) {
     mStartStopCallback->Call(NS_LITERAL_STRING("start"));
   }
 }
 
 void
 HTMLMarqueeElement::Stop()
 {
-  if (mStartStopCallback) {
+  if (GetShadowRoot()) {
+    DispatchEventToShadowRoot(NS_LITERAL_STRING("marquee-stop"));
+  } else if (mStartStopCallback) {
     mStartStopCallback->Call(NS_LITERAL_STRING("stop"));
   }
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/html/HTMLMarqueeElement.h
+++ b/dom/html/HTMLMarqueeElement.h
@@ -22,16 +22,21 @@ public:
   {
   }
 
   // nsISupports
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLMarqueeElement,
                                            nsGenericHTMLElement)
 
+  nsresult BindToTree(nsIDocument* aDocument, nsIContent* aParent,
+                      nsIContent* aBindingParent) override;
+  void UnbindFromTree(bool aDeep = true,
+                      bool aNullParent = true) override;
+
   static const int kDefaultLoop = -1;
   static const int kDefaultScrollAmount = 6;
   static const int kDefaultScrollDelayMS = 85;
 
   bool IsEventAttributeNameInternal(nsAtom *aName) override;
 
   void SetStartStopCallback(FunctionStringCallback* aCallback);
 
@@ -130,28 +135,35 @@ public:
   void Start();
   void Stop();
 
   bool ParseAttribute(int32_t aNamespaceID,
                               nsAtom* aAttribute,
                               const nsAString& aValue,
                               nsIPrincipal* aMaybeScriptedPrincipal,
                               nsAttrValue& aResult) override;
+  virtual nsresult AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
+                                const nsAttrValue* aValue,
+                                const nsAttrValue* aOldValue,
+                                nsIPrincipal* aMaybeScriptedPrincipal,
+                                bool aNotify) override;
   NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override;
   nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override;
 
   nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override;
 
 protected:
   virtual ~HTMLMarqueeElement();
 
   JSObject* WrapNode(JSContext *aCx, JS::Handle<JSObject*> aGivenProto) override;
 
 private:
   RefPtr<FunctionStringCallback> mStartStopCallback;
   static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes,
                                     MappedDeclarations&);
+
+  void DispatchEventToShadowRoot(const nsAString& aEventTypeArg);
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif /* HTMLMarqueeElement_h___ */
--- a/layout/style/contenteditable.css
+++ b/layout/style/contenteditable.css
@@ -75,22 +75,27 @@ input[contenteditable="true"][type="chec
 input[contenteditable="true"][type="radio"],
 input[contenteditable="true"][type="file"] {
   -moz-user-select: all;
   -moz-user-input: none !important;
   -moz-user-focus: none !important;
 }
 
 /* emulation of non-standard HTML <marquee> tag */
-marquee:-moz-read-write {
-  -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-horizontal-editable');
-}
+
+@supports not -moz-bool-pref("dom.ua_widget.enabled") {
 
-marquee[direction="up"]:-moz-read-write, marquee[direction="down"]:-moz-read-write {
-  -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-vertical-editable');
+  marquee:-moz-read-write {
+    -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-horizontal-editable');
+  }
+
+  marquee[direction="up"]:-moz-read-write, marquee[direction="down"]:-moz-read-write {
+    -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-vertical-editable');
+  }
+
 }
 
 *|*:-moz-read-write > input[type="hidden"],
 input[contenteditable="true"][type="hidden"] {
   border: 1px solid black !important;
   visibility: visible !important;
 }
 
--- a/layout/style/res/html.css
+++ b/layout/style/res/html.css
@@ -837,24 +837,34 @@ dialog:not([open]) {
 }
 
 /* emulation of non-standard HTML <marquee> tag */
 marquee {
   inline-size: -moz-available;
   display: inline-block;
   vertical-align: text-bottom;
   text-align: start;
-  -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-horizontal');
 }
 
 marquee[direction="up"], marquee[direction="down"] {
-  -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-vertical');
   block-size: 200px;
 }
 
+@supports not -moz-bool-pref("dom.ua_widget.enabled") {
+
+  marquee {
+    -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-horizontal');
+  }
+
+  marquee[direction="up"], marquee[direction="down"] {
+    -moz-binding: url('chrome://xbl-marquee/content/xbl-marquee.xml#marquee-vertical');
+  }
+
+}
+
 /* PRINT ONLY rules follow */
 @media print {
 
   marquee { -moz-binding: none; }
 
 }
 
 /* Ruby */
--- a/toolkit/actors/UAWidgetsChild.jsm
+++ b/toolkit/actors/UAWidgetsChild.jsm
@@ -56,16 +56,20 @@ class UAWidgetsChild extends ActorChild 
         uri = "chrome://global/content/elements/datetimebox.js";
         widgetName = "DateTimeBoxWidget";
         break;
       case "applet":
       case "embed":
       case "object":
         // TODO (pluginProblems)
         break;
+      case "marquee":
+        uri = "chrome://global/content/elements/marquee.js";
+        widgetName = "MarqueeWidget";
+        break;
     }
 
     if (!uri || !widgetName) {
       return;
     }
 
     let shadowRoot = aElement.openOrClosedShadowRoot;
     let sandbox = aElement.nodePrincipal.isSystemPrincipal ?
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -94,16 +94,18 @@ toolkit.jar:
 *  content/global/bindings/wizard.xml          (widgets/wizard.xml)
    content/global/elements/datetimebox.js      (widgets/datetimebox.js)
    content/global/elements/findbar.js          (widgets/findbar.js)
    content/global/elements/editor.js          (widgets/editor.js)
    content/global/elements/general.js          (widgets/general.js)
    content/global/elements/notificationbox.js  (widgets/notificationbox.js)
    content/global/elements/progressmeter.js    (widgets/progressmeter.js)
    content/global/elements/radio.js            (widgets/radio.js)
+   content/global/elements/marquee.css         (widgets/marquee.css)
+   content/global/elements/marquee.js          (widgets/marquee.js)
    content/global/elements/stringbundle.js     (widgets/stringbundle.js)
    content/global/elements/tabbox.js           (widgets/tabbox.js)
    content/global/elements/textbox.js          (widgets/textbox.js)
    content/global/elements/videocontrols.js    (widgets/videocontrols.js)
    content/global/elements/tree.js             (widgets/tree.js)
 #ifdef XP_MACOSX
    content/global/macWindowMenu.js
 #endif
--- a/toolkit/content/widgets/docs/ua_widget.rst
+++ b/toolkit/content/widgets/docs/ua_widget.rst
@@ -43,8 +43,10 @@ To avoid creating reflectors before DOM 
 
 Other things to watch out for
 -----------------------------
 
 As part of the implementation of the Web Platform, it is important to make sure the web-observable characteristics of the widget correctly reflect what the script on the web expects.
 
 * Do not dispatch non-spec compliant events on the UA Widget Shadow Root host element, as event listeners in web content scripts can access them.
 * The layout and the dimensions of the widget should be ready by the time the constructor returns, since they can be detectable as soon as the content script gets the reference of the host element (i.e. when ``appendChild()`` returns). In order to make this easier we load ``<link>`` elements load chrome stylesheets synchronously when inside a UA Widget Shadow DOM.
+* There shouldn't be any white-spaces nodes in the Shadow DOM, because UA Widget could be placed inside ``white-space: pre``. See bug 1502205.
+* CSP will block inline styles in the Shadow DOM. ``<link>`` is the only safe way to load styles.
copy from layout/style/xbl-marquee/xbl-marquee.css
copy to toolkit/content/widgets/marquee.css
--- a/layout/style/xbl-marquee/xbl-marquee.css
+++ b/toolkit/content/widgets/marquee.css
@@ -1,12 +1,38 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+.horizontalContainer {
+  display: -moz-box;
+  overflow: hidden;
+  width: -moz-available;
+}
+.horizontalOuterDiv {
+  display: -moz-box;
+}
+
+.horizontalInnerDiv {
+  display: table;
+  border-spacing: 0;
+}
+
+.verticalContainer {
+  overflow: hidden;
+  width: -moz-available;
+}
+
+/* disable scrolling in contenteditable */
+:host(:-moz-read-write) .horizontalOuterDiv,
+:host(:-moz-read-write) .verticalInnerDiv {
+  margin: 0 !important;
+  padding: 0 !important;
+}
+
 /* PRINT ONLY rules */
 @media print {
-
-  marquee > * > * { 
-    margin: 0 !important; 
+  .horizontalOuterDiv,
+  .verticalInnerDiv {
+    margin: 0 !important;
     padding: 0 !important;
-  } /* This hack is needed until bug 119078 gets fixed */
+  }
 }
copy from layout/style/xbl-marquee/xbl-marquee.xml
copy to toolkit/content/widgets/marquee.js
--- a/layout/style/xbl-marquee/xbl-marquee.xml
+++ b/toolkit/content/widgets/marquee.js
@@ -1,389 +1,375 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * This is the class of entry. It will construct the actual implementation
+ * according to the value of the "direction" property.
+ */
+this.MarqueeWidget = class {
+  constructor(shadowRoot) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+
+    this.switchImpl();
+  }
+
+  /*
+   * Callback called by UAWidgetsChild wheen the direction property
+   * changes.
+   */
+  onattributechange() {
+    this.switchImpl();
+  }
 
-<bindings id="marqueeBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:html="http://www.w3.org/1999/xhtml"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
+  switchImpl() {
+    let newImpl;
+    switch (this.element.direction) {
+      case "up":
+      case "down":
+        newImpl = MarqueeVerticalImplWidget;
+        break;
+      case "left":
+      case "right":
+        newImpl = MarqueeHorizontalImplWidget;
+        break;
+    }
 
+    // Skip if we are asked to load the same implementation.
+    // This can happen if the property is set again w/o value change.
+    if (this.impl && this.impl.constructor == newImpl) {
+      return;
+    }
+    this.destructor();
+    if (newImpl) {
+      this.impl = new newImpl(this.shadowRoot);
+    }
+  }
+
+  destructor() {
+    if (!this.impl) {
+      return;
+    }
+    this.impl.destructor();
+    this.shadowRoot.firstChild.remove();
+    delete this.impl;
+  }
+};
 
-  <binding id="marquee" bindToUntrustedContent="true">
+this.MarqueeBaseImplWidget = class {
+  constructor(shadowRoot) {
+    this.shadowRoot = shadowRoot;
+    this.element = shadowRoot.host;
+    this.document = this.element.ownerDocument;
+    this.window = this.document.defaultView;
+
+    this.generateContent();
 
-    <resources>
-      <stylesheet src="chrome://xbl-marquee/content/xbl-marquee.css"/>
-    </resources>
-    <implementation>
+    // Set up state.
+    this._currentDirection = this.element.direction || "left";
+    this._currentLoop = this.element.loop;
+    this.dirsign = 1;
+    this.startAt = 0;
+    this.stopAt = 0;
+    this.newPosition = 0;
+    this.runId = 0;
+    this.originalHeight = 0;
+    this.invalidateCache = true;
+
+    this._mutationObserver = new this.window.MutationObserver(
+      (aMutations) => this._mutationActor(aMutations));
+    this._mutationObserver.observe(this.element, { attributes: true,
+      attributeOldValue: true,
+      attributeFilter: ["loop", "", "behavior",
+        "direction", "width", "height"] });
 
-      <property name="outerDiv"
-        onget="return document.getAnonymousNodes(this)[0]"
-      />
+    // init needs to be run after the page has loaded in order to calculate
+    // the correct height/width
+    if (this.document.readyState == "complete") {
+      this.init();
+    } else {
+      this.window.addEventListener("load", this, { once: true });
+    }
+
+    this.shadowRoot.addEventListener("marquee-start", this);
+    this.shadowRoot.addEventListener("marquee-stop", this);
+  }
+
+  destructor() {
+    this._mutationObserver.disconnect();
+    this.window.clearTimeout(this.runId);
 
-      <property name="innerDiv"
-        onget="return document.getAnonymousElementByAttribute(this, 'class', 'innerDiv');"
-      />
+    this.window.removeEventListener("load", this);
+    this.shadowRoot.removeEventListener("marquee-start", this);
+    this.shadowRoot.removeEventListener("marquee-stop", this);
+  }
+
+  handleEvent(aEvent) {
+    if (!aEvent.isTrusted) {
+      return;
+    }
+
+    switch (aEvent.type) {
+      case "load":
+        this.init();
+        break;
+      case "marquee-start":
+        this.doStart();
+        break;
+      case "marquee-stop":
+        this.doStop();
+        break;
+    }
+  }
 
-      <property name="scrollDelayWithTruespeed">
-        <getter>
-          <![CDATA[
-          if (this.scrollDelay < 60 && !this.trueSpeed) {
-            return 60;
+  get outerDiv() {
+    return this.shadowRoot.firstChild;
+  }
+
+  get innerDiv() {
+    return this.shadowRoot.getElementById("innerDiv");
+  }
+
+  get scrollDelayWithTruespeed() {
+    if (this.element.scrollDelay < 60 && !this.element.trueSpeed) {
+      return 60;
+    }
+    return this.element.scrollDelay;
+  }
+
+  doStart() {
+    if (this.runId == 0) {
+      var lambda = () => this._doMove(false);
+      this.runId = this.window.setTimeout(lambda, this.scrollDelayWithTruespeed - this._deltaStartStop);
+      this._deltaStartStop = 0;
+    }
+  }
+
+  doStop() {
+    if (this.runId != 0) {
+      this._deltaStartStop = Date.now() - this._lastMoveDate;
+      this.window.clearTimeout(this.runId);
+    }
+
+    this.runId = 0;
+  }
+
+  _fireEvent(aName, aBubbles, aCancelable) {
+    var e = this.document.createEvent("Events");
+    e.initEvent(aName, aBubbles, aCancelable);
+    this.element.dispatchEvent(e);
+  }
+
+  _doMove(aResetPosition) {
+    this._lastMoveDate = Date.now();
+
+    // invalidateCache is true at first load and whenever an attribute
+    // is changed
+    if (this.invalidateCache) {
+      this.invalidateCache = false; // we only want this to run once every scroll direction change
+
+      var corrvalue = 0;
+
+      switch (this._currentDirection) {
+        case "up": {
+          let height = this.window.getComputedStyle(this.element).height;
+          this.outerDiv.style.height = height;
+          if (this.originalHeight > this.outerDiv.offsetHeight) {
+              corrvalue = this.originalHeight - this.outerDiv.offsetHeight;
           }
-          return this.scrollDelay;
-          ]]>
-        </getter>
-      </property>
+          this.innerDiv.style.padding = height + " 0";
+          this.dirsign = 1;
+          this.startAt = (this.element.behavior == "alternate") ? (this.originalHeight - corrvalue) : 0;
+          this.stopAt  = (this.element.behavior == "alternate" || this.element.behavior == "slide") ?
+                          (parseInt(height) + corrvalue) : (this.originalHeight + parseInt(height));
+        }
+        break;
 
-      <method name="doStart">
-        <body>
-        <![CDATA[
-          if (this.runId == 0) {
-            var lambda = () => this._doMove(false);
-            this.runId = window.setTimeout(lambda, this.scrollDelayWithTruespeed - this._deltaStartStop);
-            this._deltaStartStop = 0;
+        case "down": {
+          let height = this.window.getComputedStyle(this.element).height;
+          this.outerDiv.style.height = height;
+          if (this.originalHeight > this.outerDiv.offsetHeight) {
+              corrvalue = this.originalHeight - this.outerDiv.offsetHeight;
+          }
+          this.innerDiv.style.padding = height + " 0";
+          this.dirsign = -1;
+          this.startAt  = (this.element.behavior == "alternate") ?
+                          (parseInt(height) + corrvalue) : (this.originalHeight + parseInt(height));
+          this.stopAt = (this.element.behavior == "alternate" || this.element.behavior == "slide") ?
+                        (this.originalHeight - corrvalue) : 0;
+        }
+        break;
+
+        case "right":
+          if (this.innerDiv.offsetWidth > this.outerDiv.offsetWidth) {
+              corrvalue = this.innerDiv.offsetWidth - this.outerDiv.offsetWidth;
+          }
+          this.dirsign = -1;
+          this.stopAt  = (this.element.behavior == "alternate" || this.element.behavior == "slide") ?
+                         (this.innerDiv.offsetWidth - corrvalue) : 0;
+          this.startAt = this.outerDiv.offsetWidth + ((this.element.behavior == "alternate") ?
+                         corrvalue : (this.innerDiv.offsetWidth + this.stopAt));
+        break;
+
+        case "left":
+        default:
+          if (this.innerDiv.offsetWidth > this.outerDiv.offsetWidth) {
+              corrvalue = this.innerDiv.offsetWidth - this.outerDiv.offsetWidth;
           }
-        ]]>
-        </body>
-      </method>
-      <method name="doStop">
-        <body>
-        <![CDATA[
-          if (this.runId != 0) {
-            this._deltaStartStop = Date.now()- this._lastMoveDate;
-            clearTimeout(this.runId);
+          this.dirsign = 1;
+          this.startAt = (this.element.behavior == "alternate") ? (this.innerDiv.offsetWidth - corrvalue) : 0;
+          this.stopAt  = this.outerDiv.offsetWidth +
+                         ((this.element.behavior == "alternate" || this.element.behavior == "slide") ?
+                         corrvalue : (this.innerDiv.offsetWidth + this.startAt));
+      }
+
+      if (aResetPosition) {
+        this.newPosition = this.startAt;
+        this._fireEvent("start", false, false);
+      }
+    } // end if
+
+    this.newPosition = this.newPosition + (this.dirsign * this.element.scrollAmount);
+
+    if ((this.dirsign == 1 && this.newPosition > this.stopAt) ||
+        (this.dirsign == -1 && this.newPosition < this.stopAt)) {
+      switch (this.element.behavior) {
+        case "alternate":
+          // lets start afresh
+          this.invalidateCache = true;
+
+          // swap direction
+          const swap = {left: "right", down: "up", up: "down", right: "left"};
+          this._currentDirection = swap[this._currentDirection] || "left";
+          this.newPosition = this.stopAt;
+
+          if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
+            this.outerDiv.scrollTop = this.newPosition;
+          } else {
+            this.outerDiv.scrollLeft = this.newPosition;
           }
 
-          this.runId = 0;
-        ]]>
-        </body>
-      </method>
-
-      <method name="_fireEvent">
-        <parameter name="aName"/>
-        <parameter name="aBubbles"/>
-        <parameter name="aCancelable"/>
-        <body>
-        <![CDATA[
-          var e = document.createEvent("Events");
-          e.initEvent(aName, aBubbles, aCancelable);
-          this.dispatchEvent(e);
-        ]]>
-        </body>
-      </method>
-
-      <method name="_doMove">
-        <parameter name="aResetPosition"/>
-        <body>
-        <![CDATA[
-          this._lastMoveDate = Date.now();
-
-          // invalidateCache is true at first load and whenever an attribute
-          // is changed
-          if (this.invalidateCache) {
-            this.invalidateCache = false; //we only want this to run once every scroll direction change
-
-            var corrvalue = 0;
-
-            switch (this._currentDirection)
-            {
-              case "up":
-                var height = document.defaultView.getComputedStyle(this).height;
-                this.outerDiv.style.height = height;
-                if (this.originalHeight > this.outerDiv.offsetHeight) {
-                    corrvalue = this.originalHeight - this.outerDiv.offsetHeight;
-                }
-                this.innerDiv.style.padding = height + " 0";
-                this.dirsign = 1;
-                this.startAt = (this.behavior == 'alternate') ? (this.originalHeight - corrvalue) : 0;
-                this.stopAt  = (this.behavior == 'alternate' || this.behavior == 'slide') ? 
-                                (parseInt(height) + corrvalue) : (this.originalHeight + parseInt(height));
-              break;
-
-              case "down":
-                var height = document.defaultView.getComputedStyle(this).height;
-                this.outerDiv.style.height = height;
-                if (this.originalHeight > this.outerDiv.offsetHeight) {
-                    corrvalue = this.originalHeight - this.outerDiv.offsetHeight;
-                }
-                this.innerDiv.style.padding = height + " 0";
-                this.dirsign = -1;
-                this.startAt  = (this.behavior == 'alternate') ?
-                                (parseInt(height) + corrvalue) : (this.originalHeight + parseInt(height));
-                this.stopAt = (this.behavior == 'alternate' || this.behavior == 'slide') ? 
-                              (this.originalHeight - corrvalue) : 0;
-              break;
-
-              case "right":
-                if (this.innerDiv.offsetWidth > this.outerDiv.offsetWidth) {
-                    corrvalue = this.innerDiv.offsetWidth - this.outerDiv.offsetWidth;
-                }
-                this.dirsign = -1;
-                this.stopAt  = (this.behavior == 'alternate' || this.behavior == 'slide') ? 
-                               (this.innerDiv.offsetWidth - corrvalue) : 0;
-                this.startAt = this.outerDiv.offsetWidth + ((this.behavior == 'alternate') ? 
-                               corrvalue : (this.innerDiv.offsetWidth + this.stopAt));   
-              break;
+          if (this._currentLoop != 1) {
+            this._fireEvent("bounce", false, true);
+          }
+        break;
 
-              case "left":
-              default:
-                if (this.innerDiv.offsetWidth > this.outerDiv.offsetWidth) {
-                    corrvalue = this.innerDiv.offsetWidth - this.outerDiv.offsetWidth;
-                }
-                this.dirsign = 1;
-                this.startAt = (this.behavior == 'alternate') ? (this.innerDiv.offsetWidth - corrvalue) : 0;
-                this.stopAt  = this.outerDiv.offsetWidth + 
-                               ((this.behavior == 'alternate' || this.behavior == 'slide') ? 
-                               corrvalue : (this.innerDiv.offsetWidth + this.startAt));
-            }
-
-            if (aResetPosition) {
-              this.newPosition = this.startAt;
-              this._fireEvent("start", false, false);
-            }
-          } //end if
-
-          this.newPosition = this.newPosition + (this.dirsign * this.scrollAmount);
-
-          if ((this.dirsign == 1 && this.newPosition > this.stopAt) ||
-              (this.dirsign == -1 && this.newPosition < this.stopAt))
-          {
-            switch (this.behavior) 
-            {
-              case 'alternate':
-                // lets start afresh
-                this.invalidateCache = true;
-
-                // swap direction
-                const swap = {left: "right", down: "up", up: "down", right: "left"};
-                this._currentDirection = swap[this._currentDirection] || "left";
-                this.newPosition = this.stopAt;
-
-                if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
-                  this.outerDiv.scrollTop = this.newPosition;
-                } else {
-                  this.outerDiv.scrollLeft = this.newPosition;
-                }
+        case "slide":
+          if (this._currentLoop > 1) {
+            this.newPosition = this.startAt;
+          }
+        break;
 
-                if (this._currentLoop != 1) {
-                  this._fireEvent("bounce", false, true);
-                }
-              break;
-
-              case 'slide':
-                if (this._currentLoop > 1) {
-                  this.newPosition = this.startAt;
-                }
-              break;
-
-              default:
-                this.newPosition = this.startAt;
-
-                if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
-                  this.outerDiv.scrollTop = this.newPosition;
-                } else {
-                  this.outerDiv.scrollLeft = this.newPosition;
-                }
+        default:
+          this.newPosition = this.startAt;
 
-                //dispatch start event, even when this._currentLoop == 1, comp. with IE6
-                this._fireEvent("start", false, false);
-            }
-
-            if (this._currentLoop > 1) {
-              this._currentLoop--;
-            } else if (this._currentLoop == 1) {
-              if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
-                this.outerDiv.scrollTop = this.stopAt;
-              } else {
-                this.outerDiv.scrollLeft = this.stopAt;
-              }
-              this.stop();
-              this._fireEvent("finish", false, true);
-              return;
-            }
-          }
-          else {
-            if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
-              this.outerDiv.scrollTop = this.newPosition;
-            } else {
-              this.outerDiv.scrollLeft = this.newPosition;
-            }
+          if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
+            this.outerDiv.scrollTop = this.newPosition;
+          } else {
+            this.outerDiv.scrollLeft = this.newPosition;
           }
 
-          var myThis = this;
-          var lambda = function myTimeOutFunction(){myThis._doMove(false);}
-          this.runId = window.setTimeout(lambda, this.scrollDelayWithTruespeed);
-        ]]>
-        </body>
-      </method>
-
-      <method name="init">
-        <body>
-        <![CDATA[
-          this.stop();
-
-          if ((this._currentDirection != "up") && (this._currentDirection != "down")) {
-            var width = window.getComputedStyle(this).width;
-            this.innerDiv.parentNode.style.margin = '0 ' + width;
+          // dispatch start event, even when this._currentLoop == 1, comp. with IE6
+          this._fireEvent("start", false, false);
+      }
 
-            //XXX Adding the margin sometimes causes the marquee to widen, 
-            // see testcase from bug bug 364434: 
-            // https://bugzilla.mozilla.org/attachment.cgi?id=249233
-            // Just add a fixed width with current marquee's width for now
-            if (width != window.getComputedStyle(this).width) {
-              var width = window.getComputedStyle(this).width;
-              this.outerDiv.style.width = width;
-              this.innerDiv.parentNode.style.margin = '0 ' + width;
-            }
-          }
-          else {
-            // store the original height before we add padding
-            this.innerDiv.style.padding = 0;
-            this.originalHeight = this.innerDiv.offsetHeight;
-          }
-
-          this._doMove(true);
-        ]]>
-        </body>
-      </method>
+      if (this._currentLoop > 1) {
+        this._currentLoop--;
+      } else if (this._currentLoop == 1) {
+        if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
+          this.outerDiv.scrollTop = this.stopAt;
+        } else {
+          this.outerDiv.scrollLeft = this.stopAt;
+        }
+        this.element.stop();
+        this._fireEvent("finish", false, true);
+        return;
+      }
+    } else if ((this._currentDirection == "up") || (this._currentDirection == "down")) {
+        this.outerDiv.scrollTop = this.newPosition;
+      } else {
+        this.outerDiv.scrollLeft = this.newPosition;
+      }
 
-      <method name="_mutationActor">
-        <parameter name="aMutations"/>
-        <body>
-        <![CDATA[
-          while (aMutations.length > 0) {
-            var mutation = aMutations.shift();
-            var attrName = mutation.attributeName.toLowerCase();
-            var oldValue = mutation.oldValue;
-            var target = mutation.target;
-            var newValue = target.getAttribute(attrName);
+    var myThis = this;
+    var lambda = function myTimeOutFunction() { myThis._doMove(false); };
+    this.runId = this.window.setTimeout(lambda, this.scrollDelayWithTruespeed);
+  }
+
+  init() {
+    this.element.stop();
+
+    if ((this._currentDirection != "up") && (this._currentDirection != "down")) {
+      var width = this.window.getComputedStyle(this.element).width;
+      this.innerDiv.parentNode.style.margin = "0 " + width;
 
-            if (oldValue != newValue) {
-              target.invalidateCache = true;
-              switch (attrName) {
-                case "loop":
-                  target._currentLoop = target.loop;
-                  break;
-                case "direction":
-                  target._currentDirection = target.direction;
-                  break;
-              }
-            }
-          }
-        ]]>
-        </body>
-      </method>
-
-      <constructor>
-        <![CDATA[
-          this.setStartStopCallback(val => {
-            if (val == "start") {
-              this.doStart();
-            } else if (val == "stop") {
-              this.doStop();
-            } else {
-              throw new Error(`setStartStopCallback passed an invalid value: ${val}`);
-            }
-          });
-          // Set up state.
-          this._currentDirection = this.direction || "left";
-          this._currentLoop = this.loop;
-          this.dirsign = 1;
-          this.startAt = 0;
-          this.stopAt = 0;
-          this.newPosition = 0;
-          this.runId = 0;
-          this.originalHeight = 0;
-          this.invalidateCache = true;
+      // XXX Adding the margin sometimes causes the marquee to widen,
+      // see testcase from bug bug 364434:
+      // https://bugzilla.mozilla.org/attachment.cgi?id=249233
+      // Just add a fixed width with current marquee's width for now
+      if (width != this.window.getComputedStyle(this.element).width) {
+        width = this.window.getComputedStyle(this.element).width;
+        this.outerDiv.style.width = width;
+        this.innerDiv.parentNode.style.margin = "0 " + width;
+      }
+    } else {
+      // store the original height before we add padding
+      this.innerDiv.style.padding = 0;
+      this.originalHeight = this.innerDiv.offsetHeight;
+    }
 
-          // hack needed to fix js error, see bug 386470
-          var myThis = this;
-          var lambda = function myScopeFunction() { if (myThis.init) myThis.init(); }
+    this._doMove(true);
+  }
 
-          this._mutationObserver = new MutationObserver(this._mutationActor);
-          this._mutationObserver.observe(this, { attributes: true,
-            attributeOldValue: true,
-            attributeFilter: ['loop', '', 'behavior',
-              'direction', 'width', 'height'] });
+  _mutationActor(aMutations) {
+    while (aMutations.length > 0) {
+      var mutation = aMutations.shift();
+      var attrName = mutation.attributeName.toLowerCase();
+      var oldValue = mutation.oldValue;
+      var target = mutation.target;
+      var newValue = target.getAttribute(attrName);
 
-          // init needs to be run after the page has loaded in order to calculate
-          // the correct height/width
-          if (document.readyState == "complete") {
-            lambda();
-          } else {
-            window.addEventListener("load", lambda);
-          }
-        ]]>
-      </constructor>
-      <destructor>
-        <![CDATA[
-        this.setStartStopCallback(null);
-        ]]>
-      </destructor>
-    </implementation>
-
-  </binding>
-
-  <binding id="marquee-horizontal" bindToUntrustedContent="true"
-           extends="chrome://xbl-marquee/content/xbl-marquee.xml#marquee"
-           inheritstyle="false">
+      if (oldValue != newValue) {
+        this.invalidateCache = true;
+        switch (attrName) {
+          case "loop":
+            this._currentLoop = target.loop;
+            break;
+          case "direction":
+            this._currentDirection = target.direction;
+            break;
+        }
+      }
+    }
+  }
+};
 
-    <!-- White-space isn't allowed because a marquee could be
-         inside 'white-space: pre' -->
-    <content>
-      <html:div style="display: -moz-box; overflow: hidden; width: -moz-available;"
-        ><html:div style="display: -moz-box;"
-          ><html:div class="innerDiv" style="display: table; border-spacing: 0;"
-            ><html:div
-              ><children
-            /></html:div
-          ></html:div
-        ></html:div
-      ></html:div>
-    </content>
-
-  </binding>
-
-  <binding id="marquee-vertical" bindToUntrustedContent="true"
-           extends="chrome://xbl-marquee/content/xbl-marquee.xml#marquee"
-           inheritstyle="false">
+this.MarqueeHorizontalImplWidget = class extends MarqueeBaseImplWidget {
+  // White-space isn't allowed because a marquee could be
+  // inside 'white-space: pre'
+  generateContent() {
+    this.shadowRoot.innerHTML = `<div class="horizontalContainer"
+        ><link rel="stylesheet" type="text/css" href="chrome://global/content/elements/marquee.css"
+          /><div class="horizontalOuterDiv"
+            ><div id="innerDiv" class="horizontalInnerDiv"
+              ><div
+                ><slot
+              /></div
+            ></div
+          ></div
+      ></div>`;
+  }
+};
 
-    <!-- White-space isn't allowed because a marquee could be
-         inside 'white-space: pre' -->
-    <content>
-      <html:div style="overflow: hidden; width: -moz-available;"
-        ><html:div class="innerDiv"
-          ><children
-        /></html:div
-      ></html:div>
-    </content>
-
-  </binding>
-
-  <binding id="marquee-horizontal-editable" bindToUntrustedContent="true"
-           inheritstyle="false">
-
-    <!-- White-space isn't allowed because a marquee could be 
-         inside 'white-space: pre' -->
-    <content>
-      <html:div style="display: inline-block; overflow: auto; width: -moz-available;"
-        ><children
-      /></html:div>
-    </content>
-
-  </binding>
-
-  <binding id="marquee-vertical-editable" bindToUntrustedContent="true"
-           inheritstyle="false">
-
-    <!-- White-space isn't allowed because a marquee could be 
-         inside 'white-space: pre' -->
-    <content>
-      <html:div style="overflow: auto; height: inherit; width: -moz-available;"
-        ><children/></html:div>
-    </content>
-
-  </binding>
-
-</bindings>
+this.MarqueeVerticalImplWidget = class extends MarqueeBaseImplWidget {
+  // White-space isn't allowed because a marquee could be
+  // inside 'white-space: pre'
+  generateContent() {
+    this.shadowRoot.innerHTML = `<div class="verticalContainer"
+        ><link rel="stylesheet" type="text/css" href="chrome://global/content/elements/marquee.css"
+          /><div id="innerDiv" class="verticalInnerDiv"><slot /></div
+      ></div>`;
+  }
+};