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

--HG--
rename : layout/style/xbl-marquee/xbl-marquee.css => toolkit/content/widgets/marquee.css
rename : layout/style/xbl-marquee/xbl-marquee.xml => toolkit/content/widgets/marquee.js
extra : moz-landing-system : lando
This commit is contained in:
Timothy Guan-tin Chien 2018-11-18 01:23:52 +00:00
Родитель 9ca1f4544a
Коммит a685c05ea5
11 изменённых файлов: 535 добавлений и 46 удалений

Просмотреть файл

@ -553,7 +553,6 @@ skip-if = toolkit == 'android' || (verify && !debug && (os == 'linux')) #bug 687
[test_bug814576.html]
[test_bug819051.html]
[test_bug820909.html]
[test_bug840098.html]
[test_bug864595.html]
[test_bug868999.html]
[test_bug869000.html]

Просмотреть файл

@ -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>

Просмотреть файл

@ -8,10 +8,12 @@
#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)
@ -65,6 +67,43 @@ 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)
{
@ -123,6 +162,29 @@ HTMLMarqueeElement::ParseAttribute(int32_t aNamespaceID,
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)
{
@ -149,10 +211,24 @@ 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"));
}
}
@ -160,7 +236,9 @@ HTMLMarqueeElement::Start()
void
HTMLMarqueeElement::Stop()
{
if (mStartStopCallback) {
if (GetShadowRoot()) {
DispatchEventToShadowRoot(NS_LITERAL_STRING("marquee-stop"));
} else if (mStartStopCallback) {
mStartStopCallback->Call(NS_LITERAL_STRING("stop"));
}
}

Просмотреть файл

@ -27,6 +27,11 @@ public:
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;
@ -135,6 +140,11 @@ public:
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;
@ -149,6 +159,8 @@ private:
RefPtr<FunctionStringCallback> mStartStopCallback;
static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes,
MappedDeclarations&);
void DispatchEventToShadowRoot(const nsAString& aEventTypeArg);
};
} // namespace dom

Просмотреть файл

@ -80,12 +80,17 @@ input[contenteditable="true"][type="file"] {
}
/* emulation of non-standard HTML <marquee> tag */
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 {
@supports not -moz-bool-pref("dom.ua_widget.enabled") {
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"],

Просмотреть файл

@ -842,14 +842,24 @@ marquee {
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 {

Просмотреть файл

@ -61,6 +61,10 @@ class UAWidgetsChild extends ActorChild {
case "object":
// TODO (pluginProblems)
break;
case "marquee":
uri = "chrome://global/content/elements/marquee.js";
widgetName = "MarqueeWidget";
break;
}
if (!uri || !widgetName) {

Просмотреть файл

@ -99,6 +99,8 @@ toolkit.jar:
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)

Просмотреть файл

@ -48,3 +48,5 @@ As part of the implementation of the Web Platform, it is important to make sure
* 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.

Просмотреть файл

@ -0,0 +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 {
.horizontalOuterDiv,
.verticalInnerDiv {
margin: 0 !important;
padding: 0 !important;
}
}

Просмотреть файл

@ -0,0 +1,375 @@
/* 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();
}
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;
}
};
this.MarqueeBaseImplWidget = class {
constructor(shadowRoot) {
this.shadowRoot = shadowRoot;
this.element = shadowRoot.host;
this.document = this.element.ownerDocument;
this.window = this.document.defaultView;
this.generateContent();
// 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"] });
// 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);
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;
}
}
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;
}
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;
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;
}
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;
}
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;
}
// 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.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;
}
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;
// 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;
}
this._doMove(true);
}
_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);
if (oldValue != newValue) {
this.invalidateCache = true;
switch (attrName) {
case "loop":
this._currentLoop = target.loop;
break;
case "direction":
this._currentDirection = target.direction;
break;
}
}
}
}
};
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>`;
}
};
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>`;
}
};