gecko-dev/toolkit/content/widgets/marquee.js
Timothy Guan-tin Chien 0bdd6f623a Bug 1511130 - Part II, Allow UAWidgetsChild to destruct widgets even if it throws during construction r=bgrins
This patch move the actual widget construction to a onsetup method, allow UAWidgetsChild to hold the reference of the widget instance even if the actual setup (happens in the onsetup call) throws. With the reference of the widget kept, UAWidgetsChild will finally able to call its destructor later on.

Depends on D13607

Differential Revision: https://phabricator.services.mozilla.com/D13608

--HG--
extra : moz-landing-system : lando
2018-12-13 20:59:00 +00:00

383 lines
12 KiB
JavaScript

/* 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;
}
/*
* Callback called by UAWidgets right after constructor.
*/
onsetup() {
this.switchImpl();
}
/*
* Callback called by UAWidgetsChild wheen the direction property
* changes.
*/
onchange() {
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);
this.impl.onsetup();
}
}
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;
}
onsetup() {
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>`;
}
};