diff options
77 files changed, 2254 insertions, 530 deletions
diff --git a/application/palemoon/app/profile/palemoon.js b/application/palemoon/app/profile/palemoon.js index 20919eca4..f0e860749 100644 --- a/application/palemoon/app/profile/palemoon.js +++ b/application/palemoon/app/profile/palemoon.js @@ -236,6 +236,15 @@ pref("general.useragent.complexOverride.moodle", false); // bug 797703 // At startup, check if we're the default browser and prompt user if not. pref("browser.shell.checkDefaultBrowser", true); pref("browser.shell.shortcutFavicons",true); +pref("browser.shell.mostRecentDateSetAsDefault", ""); +#ifdef RELEASE_OR_BETA +pref("browser.shell.skipDefaultBrowserCheckOnFirstRun", false); +#else +pref("browser.shell.skipDefaultBrowserCheckOnFirstRun", true); +#endif +pref("browser.shell.skipDefaultBrowserCheck", true); +pref("browser.shell.defaultBrowserCheckCount", 0); +pref("browser.defaultbrowser.notificationbar", false); // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore diff --git a/application/palemoon/base/jar.mn b/application/palemoon/base/jar.mn index fd2df79e1..622d8e0da 100644 --- a/application/palemoon/base/jar.mn +++ b/application/palemoon/base/jar.mn @@ -108,7 +108,7 @@ browser.jar: * content/browser/sanitize.xul (content/sanitize.xul) * content/browser/sanitizeDialog.js (content/sanitizeDialog.js) content/browser/sanitizeDialog.css (content/sanitizeDialog.css) -* content/browser/autocomplete.css (content/autocomplete.css) + content/browser/autocomplete.css (content/autocomplete.css) * content/browser/autocomplete.xml (content/autocomplete.xml) content/browser/tabbrowser.css (content/tabbrowser.css) * content/browser/tabbrowser.xml (content/tabbrowser.xml) diff --git a/application/palemoon/components/nsBrowserGlue.js b/application/palemoon/components/nsBrowserGlue.js index 6563df4e6..d1f10111a 100644 --- a/application/palemoon/components/nsBrowserGlue.js +++ b/application/palemoon/components/nsBrowserGlue.js @@ -13,7 +13,11 @@ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -// Define Lazy Module Gitters +// Define Lazy Service Getters +XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", + "@mozilla.org/alerts-service;1", "nsIAlertsService"); + +// Define Lazy Module Getters [ ["AddonManager", "resource://gre/modules/AddonManager.jsm"], ["NetUtil", "resource://gre/modules/NetUtil.jsm"], @@ -37,10 +41,13 @@ Cu.import("resource://gre/modules/Services.jsm"); ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"], ["AutoCompletePopup", "resource:///modules/AutoCompletePopup.jsm"], ["DateTimePickerHelper", "resource://gre/modules/DateTimePickerHelper.jsm"], + ["ShellService", "resource:///modules/ShellService.jsm"], ].forEach(([name, resource]) => XPCOMUtils.defineLazyModuleGetter(this, name, resource)); -XPCOMUtils.defineLazyServiceGetter(this, "AlertsService", - "@mozilla.org/alerts-service;1", "nsIAlertsService"); +// Define Lazy Getters +XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() { + return Services.strings.createBundle('chrome://browser/locale/browser.properties'); +}); const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser"; const PREF_PLUGINS_UPDATEURL = "plugins.update.url"; @@ -562,17 +569,17 @@ BrowserGlue.prototype = { } // Perform default browser checking. - var shell; - try { - shell = Components.classes["@mozilla.org/browser/shell-service;1"] - .getService(Components.interfaces.nsIShellService); - } catch (e) { } - if (shell) { -#ifdef DEBUG - let shouldCheck = false; -#else - let shouldCheck = shell.shouldCheckDefaultBrowser; -#endif + if (ShellService) { + let shouldCheck = ShellService.shouldCheckDefaultBrowser; + + const skipDefaultBrowserCheck = + Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheckOnFirstRun") && + Services.prefs.getBoolPref("browser.shell.skipDefaultBrowserCheck"); + + const usePromptLimit = false; + let promptCount = + usePromptLimit ? Services.prefs.getIntPref("browser.shell.defaultBrowserCheckCount") : 0; + let willRecoverSession = false; try { let ss = Cc["@mozilla.org/browser/sessionstartup;1"]. @@ -582,9 +589,25 @@ BrowserGlue.prototype = { } catch (ex) { /* never mind; suppose SessionStore is broken */ } - let isDefault = shell.isDefaultBrowser(true, false); // startup check, check all assoc + // startup check, check all assoc + let isDefault = false; + let isDefaultError = false; + try { + isDefault = ShellService.isDefaultBrowser(true, false); + } catch (ex) { + isDefaultError = true; + } + + if (isDefault) { + let now = (Math.floor(Date.now() / 1000)).toString(); + Services.prefs.setCharPref("browser.shell.mostRecentDateSetAsDefault", now); + } + + let willPrompt = shouldCheck && !isDefault && !willRecoverSession; - if (shouldCheck && !isDefault && !willRecoverSession) { + // Skip the "Set Default Browser" check during first-run or after the + // browser has been run a few times. + if (willPrompt) { Services.tm.mainThread.dispatch(function() { var win = this.getMostRecentBrowserWindow(); var brandBundle = win.document.getElementById("bundle_brand"); @@ -613,9 +636,9 @@ BrowserGlue.prototype = { claimAllTypes = (parseFloat(version) < 6.2); } catch (ex) { } #endif - shell.setDefaultBrowser(claimAllTypes, false); + ShellService.setDefaultBrowser(claimAllTypes, false); } - shell.shouldCheckDefaultBrowser = checkEveryTime.value; + ShellService.shouldCheckDefaultBrowser = checkEveryTime.value; }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); } } diff --git a/application/palemoon/confvars.sh b/application/palemoon/confvars.sh index 8a1d00c68..5109c0829 100644 --- a/application/palemoon/confvars.sh +++ b/application/palemoon/confvars.sh @@ -79,12 +79,6 @@ MOZ_JSDOWNLOADS=1 # conformant implementations. MOZ_WEBGL_CONFORMANT=1 -# Platform Feature: Windows Maintaince Service -# XXX: This is never used -if test "$OS_ARCH" = "WINNT"; then - MOZ_MAINTENANCE_SERVICE= -fi - # Set the chrome packing format # Possible values are omni, jar, and flat # Currently, only omni and flat are supported @@ -103,3 +97,11 @@ if test "$OS_ARCH" = "WINNT" -o \ "$OS_ARCH" = "Linux"; then MOZ_BUNDLED_FONTS=1 fi + +# Short-circuit a few services to be removed +MOZ_MAINTENANCE_SERVICE= +MOZ_SERVICES_HEALTHREPORT= +MOZ_ADDON_SIGNING=0 +MOZ_REQUIRE_SIGNING=0 +MOZ_PROFILE_MIGRATOR= + diff --git a/browser/themes/osx/shared.inc b/browser/themes/osx/shared.inc index 3076450e2..b3ea4e199 100644 --- a/browser/themes/osx/shared.inc +++ b/browser/themes/osx/shared.inc @@ -1,4 +1,4 @@ -%include ../../../../toolkit/themes/osx/global/shared.inc
+%include ../../../toolkit/themes/osx/global/shared.inc
%include ../shared/browser.inc
%filter substitution
diff --git a/browser/themes/shared/tabs.inc.css b/browser/themes/shared/tabs.inc.css index 632a6e606..c505416e4 100644 --- a/browser/themes/shared/tabs.inc.css +++ b/browser/themes/shared/tabs.inc.css @@ -55,8 +55,9 @@ .tab-background-middle { -moz-box-flex: 1; background-clip: padding-box; - border-left: @tabCurveHalfWidth@ solid transparent; - border-right: @tabCurveHalfWidth@ solid transparent; + /* Deliberately create a 1px overlap left/right to cover rounding gaps */ + border-left: calc(@tabCurveHalfWidth@ - 1px) solid transparent; + border-right: calc(@tabCurveHalfWidth@ - 1px) solid transparent; margin: 0 -@tabCurveHalfWidth@; } diff --git a/devtools/shared/css/generated/properties-db.js b/devtools/shared/css/generated/properties-db.js index 83b3efafc..070167496 100644 --- a/devtools/shared/css/generated/properties-db.js +++ b/devtools/shared/css/generated/properties-db.js @@ -3064,6 +3064,7 @@ exports.CSS_PROPERTIES = { "text-emphasis-style", "-webkit-text-fill-color", "text-indent", + "text-justify", "text-orientation", "text-overflow", "text-rendering", @@ -3240,6 +3241,7 @@ exports.CSS_PROPERTIES = { "dialog", "difference", "disabled", + "distribute", "dotted", "double", "drag", @@ -3299,6 +3301,8 @@ exports.CSS_PROPERTIES = { "inline-table", "inset", "inside", + "inter-character", + "inter-word", "intersect", "isolate", "italic", @@ -8865,6 +8869,23 @@ exports.CSS_PROPERTIES = { "unset" ] }, + "text-justify": { + "isInherited": true, + "subproperties": [ + "text-justify" + ], + "supports": [], + "values": [ + "auto", + "distribute", + "inherit", + "initial", + "inter-character", + "inter-word", + "none", + "unset" + ] + }, "text-orientation": { "isInherited": true, "subproperties": [ diff --git a/dom/animation/Animation.cpp b/dom/animation/Animation.cpp index 6dd583ed1..bd318f79e 100644 --- a/dom/animation/Animation.cpp +++ b/dom/animation/Animation.cpp @@ -230,6 +230,10 @@ Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline) return; } + StickyTimeDuration activeTime = mEffect + ? mEffect->GetComputedTiming().mActiveTime + : StickyTimeDuration(); + RefPtr<AnimationTimeline> oldTimeline = mTimeline; if (oldTimeline) { oldTimeline->RemoveAnimation(this); @@ -240,6 +244,9 @@ Animation::SetTimelineNoUpdate(AnimationTimeline* aTimeline) mHoldTime.SetNull(); } + if (!aTimeline) { + MaybeQueueCancelEvent(activeTime); + } UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); } @@ -722,8 +729,10 @@ TimeStamp Animation::ElapsedTimeToTimeStamp( const StickyTimeDuration& aElapsedTime) const { - return AnimationTimeToTimeStamp(aElapsedTime + - mEffect->SpecifiedTiming().mDelay); + TimeDuration delay = mEffect + ? mEffect->SpecifiedTiming().mDelay + : TimeDuration(); + return AnimationTimeToTimeStamp(aElapsedTime + delay); } @@ -771,14 +780,28 @@ Animation::CancelNoUpdate() DispatchPlaybackEvent(NS_LITERAL_STRING("cancel")); + StickyTimeDuration activeTime = mEffect + ? mEffect->GetComputedTiming().mActiveTime + : StickyTimeDuration(); + mHoldTime.SetNull(); mStartTime.SetNull(); - UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); - if (mTimeline) { mTimeline->RemoveAnimation(this); } + MaybeQueueCancelEvent(activeTime); + + // When an animation is cancelled it no longer needs further ticks from the + // timeline. However, if we queued a cancel event and this was the last + // animation attached to the timeline, the timeline will stop observing the + // refresh driver and there may be no subsequent refresh driver tick for + // dispatching the queued event. + // + // By calling UpdateTiming *after* removing ourselves from our timeline, we + // ensure the timeline will register with the refresh driver for at least one + // more tick. + UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async); } void @@ -819,6 +842,17 @@ Animation::HasLowerCompositeOrderThan(const Animation& aOther) const return thisTransition->HasLowerCompositeOrderThan(*otherTransition); } if (thisTransition || otherTransition) { + // Cancelled transitions no longer have an owning element. To be strictly + // correct we should store a strong reference to the owning element + // so that if we arrive here while sorting cancel events, we can sort + // them in the correct order. + // + // However, given that cancel events are almost always queued + // synchronously in some deterministic manner, we can be fairly sure + // that cancel events will be dispatched in a deterministic order + // (which is our only hard requirement until specs say otherwise). + // Furthermore, we only reach here when we have events with equal + // timestamps so this is an edge case we can probably ignore for now. return thisTransition; } } diff --git a/dom/animation/Animation.h b/dom/animation/Animation.h index c59d7d6ce..3263b30c4 100644 --- a/dom/animation/Animation.h +++ b/dom/animation/Animation.h @@ -326,6 +326,16 @@ public: void NotifyEffectTimingUpdated(); + /** + * Used by subclasses to synchronously queue a cancel event in situations + * where the Animation may have been cancelled. + * + * We need to do this synchronously because after a CSS animation/transition + * is canceled, it will be released by its owning element and may not still + * exist when we would normally go to queue events on the next tick. + */ + virtual void MaybeQueueCancelEvent(StickyTimeDuration aActiveTime) {}; + protected: void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime); void SilentlySetPlaybackRate(double aPlaybackRate); diff --git a/dom/animation/AnimationEffectReadOnly.cpp b/dom/animation/AnimationEffectReadOnly.cpp index aff28a37b..bf2e2197d 100644 --- a/dom/animation/AnimationEffectReadOnly.cpp +++ b/dom/animation/AnimationEffectReadOnly.cpp @@ -127,10 +127,6 @@ AnimationEffectReadOnly::GetComputedTimingAt( } const TimeDuration& localTime = aLocalTime.Value(); - // Calculate the time within the active interval. - // https://w3c.github.io/web-animations/#active-time - StickyTimeDuration activeTime; - StickyTimeDuration beforeActiveBoundary = std::max(std::min(StickyTimeDuration(aTiming.mDelay), result.mEndTime), zeroDuration); @@ -148,7 +144,7 @@ AnimationEffectReadOnly::GetComputedTimingAt( // The animation isn't active or filling at this time. return result; } - activeTime = + result.mActiveTime = std::max(std::min(StickyTimeDuration(localTime - aTiming.mDelay), result.mActiveDuration), zeroDuration); @@ -159,13 +155,14 @@ AnimationEffectReadOnly::GetComputedTimingAt( // The animation isn't active or filling at this time. return result; } - activeTime = std::max(StickyTimeDuration(localTime - aTiming.mDelay), - zeroDuration); + result.mActiveTime + = std::max(StickyTimeDuration(localTime - aTiming.mDelay), + zeroDuration); } else { MOZ_ASSERT(result.mActiveDuration != zeroDuration, "How can we be in the middle of a zero-duration interval?"); result.mPhase = ComputedTiming::AnimationPhase::Active; - activeTime = localTime - aTiming.mDelay; + result.mActiveTime = localTime - aTiming.mDelay; } // Convert active time to a multiple of iterations. @@ -176,7 +173,7 @@ AnimationEffectReadOnly::GetComputedTimingAt( ? 0.0 : result.mIterations; } else { - overallProgress = activeTime / result.mDuration; + overallProgress = result.mActiveTime / result.mDuration; } // Factor in iteration start offset. @@ -208,7 +205,8 @@ AnimationEffectReadOnly::GetComputedTimingAt( if (result.mPhase == ComputedTiming::AnimationPhase::After && progress == 0.0 && result.mIterations != 0.0 && - (activeTime != zeroDuration || result.mDuration == zeroDuration)) { + (result.mActiveTime != zeroDuration || + result.mDuration == zeroDuration)) { // The only way we can be in the after phase with a progress of zero and // a current iteration of zero, is if we have a zero iteration count or // were clipped using a negative end delay--both of which we should have diff --git a/dom/animation/ComputedTiming.h b/dom/animation/ComputedTiming.h index 4a98e3933..b1c6674a5 100644 --- a/dom/animation/ComputedTiming.h +++ b/dom/animation/ComputedTiming.h @@ -29,6 +29,8 @@ struct ComputedTiming // Will equal StickyTimeDuration::Forever() if the animation repeats // indefinitely. StickyTimeDuration mActiveDuration; + // The time within the active interval. + StickyTimeDuration mActiveTime; // The effect end time in local time (i.e. an offset from the effect's // start time). Will equal StickyTimeDuration::Forever() if the animation // plays indefinitely. @@ -62,12 +64,12 @@ struct ComputedTiming } enum class AnimationPhase { - Null, // Not sampled (null sample time) + Idle, // Not sampled (null sample time) Before, // Sampled prior to the start of the active interval Active, // Sampled within the active interval After // Sampled after (or at) the end of the active interval }; - AnimationPhase mPhase = AnimationPhase::Null; + AnimationPhase mPhase = AnimationPhase::Idle; ComputedTimingFunction::BeforeFlag mBeforeFlag = ComputedTimingFunction::BeforeFlag::Unset; diff --git a/dom/animation/test/css-animations/file_event-dispatch.html b/dom/animation/test/css-animations/file_event-dispatch.html new file mode 100644 index 000000000..266205bc3 --- /dev/null +++ b/dom/animation/test/css-animations/file_event-dispatch.html @@ -0,0 +1,252 @@ +<!doctype html> +<meta charset=utf-8> +<title>Tests for CSS animation event dispatch</title> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/> +<script src="../testcommon.js"></script> +<style> + @keyframes anim { + from { margin-left: 0px; } + to { margin-left: 100px; } + } +</style> +<body> +<script> +'use strict'; + +/** + * Helper class to record the elapsedTime member of each event. + * The EventWatcher class in testharness.js allows us to wait on + * multiple events in a certain order but only records the event + * parameters of the most recent event. + */ +function AnimationEventHandler(target) { + this.target = target; + this.target.onanimationstart = function(evt) { + this.animationstart = evt.elapsedTime; + }.bind(this); + this.target.onanimationiteration = function(evt) { + this.animationiteration = evt.elapsedTime; + }.bind(this); + this.target.onanimationend = function(evt) { + this.animationend = evt.elapsedTime; + }.bind(this); +} +AnimationEventHandler.prototype.clear = function() { + this.animationstart = undefined; + this.animationiteration = undefined; + this.animationend = undefined; +} + +function setupAnimation(t, animationStyle) { + var div = addDiv(t, { style: "animation: " + animationStyle }); + var watcher = new EventWatcher(t, div, [ 'animationstart', + 'animationiteration', + 'animationend' ]); + var handler = new AnimationEventHandler(div); + var animation = div.getAnimations()[0]; + + return [animation, watcher, handler, div]; +} + +promise_test(function(t) { + // Add 1ms delay to ensure that the delay is not included in the elapsedTime. + const [animation, watcher] = setupAnimation(t, 'anim 100s 1ms'); + + return watcher.wait_for('animationstart').then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Idle -> Active'); + +promise_test(function(t) { + const [animation, watcher, handler] = setupAnimation(t, 'anim 100s'); + + // Seek to After phase. + animation.finish(); + return watcher.wait_for([ 'animationstart', + 'animationend' ]).then(function() { + assert_equals(handler.animationstart, 0.0); + assert_equals(handler.animationend, 100); + }); +}, 'Idle -> After'); + +promise_test(function(t) { + const [animation, watcher, handler] = + setupAnimation(t, 'anim 100s 100s paused'); + + return animation.ready.then(function() { + // Seek to Active phase. + animation.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for('animationstart'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Before -> Active'); + +promise_test(function(t) { + const [animation, watcher, handler] = + setupAnimation(t, 'anim 100s 100s paused'); + + return animation.ready.then(function() { + // Seek to After phase. + animation.finish(); + return watcher.wait_for([ 'animationstart', 'animationend' ]); + }).then(function(evt) { + assert_equals(handler.animationstart, 0.0); + assert_equals(handler.animationend, 100.0); + }); +}, 'Before -> After'); + +promise_test(function(t) { + const [animation, watcher, handler] = + setupAnimation(t, 'anim 100s 100s paused'); + + // Seek to Active phase. + animation.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for('animationstart').then(function() { + // Seek to Before phase. + animation.currentTime = 0; + return watcher.wait_for('animationend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Before'); + +promise_test(function(t) { + const [animation, watcher, handler] = setupAnimation(t, 'anim 100s paused'); + + return watcher.wait_for('animationstart').then(function(evt) { + // Seek to After phase. + animation.finish(); + return watcher.wait_for('animationend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'Active -> After'); + +promise_test(function(t) { + const [animation, watcher, handler] = + setupAnimation(t, 'anim 100s 100s paused'); + + // Seek to After phase. + animation.finish(); + return watcher.wait_for([ 'animationstart', + 'animationend' ]).then(function() { + // Seek to Before phase. + animation.currentTime = 0; + handler.clear(); + return watcher.wait_for([ 'animationstart', 'animationend' ]); + }).then(function() { + assert_equals(handler.animationstart, 100.0); + assert_equals(handler.animationend, 0.0); + }); +}, 'After -> Before'); + +promise_test(function(t) { + const [animation, watcher, handler] = + setupAnimation(t, 'anim 100s 100s paused'); + + // Seek to After phase. + animation.finish(); + return watcher.wait_for([ 'animationstart', + 'animationend' ]).then(function() { + // Seek to Active phase. + animation.currentTime = 100 * MS_PER_SEC; + handler.clear(); + return watcher.wait_for('animationstart'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'After -> Active'); + +promise_test(function(t) { + const [animation, watcher, handler] + = setupAnimation(t, 'anim 100s 100s 3 paused'); + + return animation.ready.then(function() { + // Seek to iteration 0 (no animationiteration event should be dispatched) + animation.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for('animationstart'); + }).then(function(evt) { + // Seek to iteration 2 + animation.currentTime = 300 * MS_PER_SEC; + handler.clear(); + return watcher.wait_for('animationiteration'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 200); + // Seek to After phase (no animationiteration event should be dispatched) + animation.currentTime = 400 * MS_PER_SEC; + return watcher.wait_for('animationend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 300); + }); +}, 'Active -> Active (forwards)'); + +promise_test(function(t) { + const [animation, watcher, handler] = setupAnimation(t, 'anim 100s 100s 3'); + + // Seek to After phase. + animation.finish(); + return watcher.wait_for([ 'animationstart', + 'animationend' ]).then(function() { + // Seek to iteration 2 (no animationiteration event should be dispatched) + animation.pause(); + animation.currentTime = 300 * MS_PER_SEC; + return watcher.wait_for('animationstart'); + }).then(function() { + // Seek to mid of iteration 0 phase. + animation.currentTime = 200 * MS_PER_SEC; + return watcher.wait_for('animationiteration'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 200.0); + // Seek to before phase (no animationiteration event should be dispatched) + animation.currentTime = 0; + return watcher.wait_for('animationend'); + }); +}, 'Active -> Active (backwards)'); + +promise_test(function(t) { + const [animation, watcher, handler, div] = + setupAnimation(t, 'anim 100s paused'); + return watcher.wait_for('animationstart').then(function(evt) { + // Seek to Idle phase. + div.style.display = 'none'; + flushComputedStyle(div); + + // FIXME: bug 1302648: Add test for animationcancel event here. + + // Restart this animation. + div.style.display = ''; + return watcher.wait_for('animationstart'); + }); +}, 'Active -> Idle -> Active: animationstart is fired by restarting animation'); + +promise_test(function(t) { + const [animation, watcher, handler, div] = + setupAnimation(t, 'anim 100s 100s 2 paused'); + + // Make After. + animation.finish(); + return watcher.wait_for([ 'animationstart', + 'animationend' ]).then(function(evt) { + animation.playbackRate = -1; + return watcher.wait_for('animationstart'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 200); + // Seek to 1st iteration + animation.currentTime = 200 * MS_PER_SEC - 1; + return watcher.wait_for('animationiteration'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100); + // Seek to before + animation.currentTime = 100 * MS_PER_SEC - 1; + return watcher.wait_for('animationend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0); + assert_equals(animation.playState, 'running'); // delay + }); +}, 'Negative playbackRate sanity test(Before -> Active -> Before)'); + +done(); +</script> +</body> +</html> diff --git a/dom/animation/test/css-animations/file_event-order.html b/dom/animation/test/css-animations/file_event-order.html new file mode 100644 index 000000000..da78b6541 --- /dev/null +++ b/dom/animation/test/css-animations/file_event-order.html @@ -0,0 +1,160 @@ +<!doctype html> +<meta charset=utf-8> +<title>Tests for CSS animation event order</title> +<link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/> +<script src="../testcommon.js"></script> +<style> + @keyframes anim { + from { margin-left: 0px; } + to { margin-left: 100px; } + } +</style> +<body> +<script type='text/javascript'> +'use strict'; + +/** + * Asserts that the set of actual and received events match. + * @param actualEvents An array of the received AnimationEvent objects. + * @param expectedEvents A series of array objects representing the expected + * events, each having the form: + * [ event type, target element, elapsed time ] + */ +function checkEvents(actualEvents, ...expectedEvents) { + assert_equals(actualEvents.length, expectedEvents.length, + `Number of actual events (${actualEvents.length}: \ +${actualEvents.map(event => event.type).join(', ')}) should match expected \ +events (${expectedEvents.map(event => event.type).join(', ')})`); + + actualEvents.forEach((actualEvent, i) => { + assert_equals(expectedEvents[i][0], actualEvent.type, + 'Event type should match'); + assert_equals(expectedEvents[i][1], actualEvent.target, + 'Event target should match'); + assert_equals(expectedEvents[i][2], actualEvent.elapsedTime, + 'Event\'s elapsed time should match'); + }); +} + +function setupAnimation(t, animationStyle, receiveEvents) { + const div = addDiv(t, { style: "animation: " + animationStyle }); + const watcher = new EventWatcher(t, div, [ 'animationstart', + 'animationiteration', + 'animationend' ]); + + ['start', 'iteration', 'end'].forEach(name => { + div['onanimation' + name] = function(evt) { + receiveEvents.push({ type: evt.type, + target: evt.target, + elapsedTime: evt.elapsedTime }); + }.bind(this); + }); + + const animation = div.getAnimations()[0]; + + return [animation, watcher, div]; +} + +promise_test(function(t) { + let events = []; + const [animation1, watcher1, div1] = + setupAnimation(t, 'anim 100s 2 paused', events); + const [animation2, watcher2, div2] = + setupAnimation(t, 'anim 100s 2 paused', events); + + return Promise.all([ watcher1.wait_for('animationstart'), + watcher2.wait_for('animationstart') ]).then(function() { + checkEvents(events, ['animationstart', div1, 0], + ['animationstart', div2, 0]); + + events.length = 0; // Clear received event array + + animation1.currentTime = 100 * MS_PER_SEC; + animation2.currentTime = 100 * MS_PER_SEC; + return Promise.all([ watcher1.wait_for('animationiteration'), + watcher2.wait_for('animationiteration') ]); + }).then(function() { + checkEvents(events, ['animationiteration', div1, 100], + ['animationiteration', div2, 100]); + + events.length = 0; // Clear received event array + + animation1.finish(); + animation2.finish(); + + return Promise.all([ watcher1.wait_for('animationend'), + watcher2.wait_for('animationend') ]); + }).then(function() { + checkEvents(events, ['animationend', div1, 200], + ['animationend', div2, 200]); + }); +}, 'Test same events are ordered by elements.'); + +promise_test(function(t) { + let events = []; + const [animation1, watcher1, div1] = + setupAnimation(t, 'anim 200s 400s', events); + const [animation2, watcher2, div2] = + setupAnimation(t, 'anim 300s 2', events); + + return watcher2.wait_for('animationstart').then(function(evt) { + animation1.currentTime = 400 * MS_PER_SEC; + animation2.currentTime = 400 * MS_PER_SEC; + + events.length = 0; // Clear received event array + + return Promise.all([ watcher1.wait_for('animationstart'), + watcher2.wait_for('animationiteration') ]); + }).then(function() { + checkEvents(events, ['animationiteration', div2, 300], + ['animationstart', div1, 0]); + }); +}, 'Test start and iteration events are ordered by time.'); + +promise_test(function(t) { + let events = []; + const [animation1, watcher1, div1] = + setupAnimation(t, 'anim 150s', events); + const [animation2, watcher2, div2] = + setupAnimation(t, 'anim 100s 2', events); + + return Promise.all([ watcher1.wait_for('animationstart'), + watcher2.wait_for('animationstart') ]).then(function() { + animation1.currentTime = 150 * MS_PER_SEC; + animation2.currentTime = 150 * MS_PER_SEC; + + events.length = 0; // Clear received event array + + return Promise.all([ watcher1.wait_for('animationend'), + watcher2.wait_for('animationiteration') ]); + }).then(function() { + checkEvents(events, ['animationiteration', div2, 100], + ['animationend', div1, 150]); + }); +}, 'Test iteration and end events are ordered by time.'); + +promise_test(function(t) { + let events = []; + const [animation1, watcher1, div1] = + setupAnimation(t, 'anim 100s 100s', events); + const [animation2, watcher2, div2] = + setupAnimation(t, 'anim 100s 2', events); + + animation1.finish(); + animation2.finish(); + + return Promise.all([ watcher1.wait_for([ 'animationstart', + 'animationend' ]), + watcher2.wait_for([ 'animationstart', + 'animationend' ]) ]).then(function() { + checkEvents(events, ['animationstart', div2, 0], + ['animationstart', div1, 0], + ['animationend', div1, 100], + ['animationend', div2, 200]); + }); +}, 'Test start and end events are sorted correctly when fired simultaneously'); + +done(); +</script> +</body> +</html> diff --git a/dom/animation/test/css-animations/test_event-dispatch.html b/dom/animation/test/css-animations/test_event-dispatch.html new file mode 100644 index 000000000..de3be0301 --- /dev/null +++ b/dom/animation/test/css-animations/test_event-dispatch.html @@ -0,0 +1,15 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_event-dispatch.html"); + }); +</script> +</html> diff --git a/dom/animation/test/css-transitions/test_csstransition-events.html b/dom/animation/test/css-animations/test_event-order.html index 92559ad67..57f1f3876 100644 --- a/dom/animation/test/css-transitions/test_csstransition-events.html +++ b/dom/animation/test/css-animations/test_event-order.html @@ -9,6 +9,7 @@ setup({explicit_done: true}); SpecialPowers.pushPrefEnv( { "set": [["dom.animations-api.core.enabled", true]]}, function() { - window.open("file_csstransition-events.html"); + window.open("file_event-order.html"); }); </script> +</html> diff --git a/dom/animation/test/css-transitions/file_animation-cancel.html b/dom/animation/test/css-transitions/file_animation-cancel.html index 6094b383f..71f02fb11 100644 --- a/dom/animation/test/css-transitions/file_animation-cancel.html +++ b/dom/animation/test/css-transitions/file_animation-cancel.html @@ -11,13 +11,12 @@ promise_test(function(t) { div.style.transition = 'margin-left 100s'; div.style.marginLeft = '1000px'; - flushComputedStyle(div); - var animation = div.getAnimations()[0]; - return animation.ready.then(waitForFrame).then(function() { + var transition = div.getAnimations()[0]; + return transition.ready.then(waitForFrame).then(function() { assert_not_equals(getComputedStyle(div).marginLeft, '1000px', 'transform style is animated before cancelling'); - animation.cancel(); + transition.cancel(); assert_equals(getComputedStyle(div).marginLeft, div.style.marginLeft, 'transform style is no longer animated after cancelling'); }); @@ -29,45 +28,21 @@ promise_test(function(t) { div.style.transition = 'margin-left 100s'; div.style.marginLeft = '1000px'; - flushComputedStyle(div); - - div.addEventListener('transitionend', function() { - assert_unreached('Got unexpected end event on cancelled transition'); - }); - - var animation = div.getAnimations()[0]; - return animation.ready.then(function() { - // Seek to just before the end then cancel - animation.currentTime = 99.9 * 1000; - animation.cancel(); - // Then wait a couple of frames and check that no event was dispatched - return waitForAnimationFrames(2); - }); -}, 'Cancelled CSS transitions do not dispatch events'); - -promise_test(function(t) { - var div = addDiv(t, { style: 'margin-left: 0px' }); - flushComputedStyle(div); - - div.style.transition = 'margin-left 100s'; - div.style.marginLeft = '1000px'; - flushComputedStyle(div); - - var animation = div.getAnimations()[0]; - return animation.ready.then(function() { - animation.cancel(); + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + transition.cancel(); assert_equals(getComputedStyle(div).marginLeft, '1000px', 'margin-left style is not animated after cancelling'); - animation.play(); + transition.play(); assert_equals(getComputedStyle(div).marginLeft, '0px', 'margin-left style is animated after re-starting transition'); - return animation.ready; + return transition.ready; }).then(function() { - assert_equals(animation.playState, 'running', + assert_equals(transition.playState, 'running', 'Transition succeeds in running after being re-started'); }); -}, 'After cancelling a transition, it can still be re-used'); +}, 'After canceling a transition, it can still be re-used'); promise_test(function(t) { var div = addDiv(t, { style: 'margin-left: 0px' }); @@ -75,20 +50,19 @@ promise_test(function(t) { div.style.transition = 'margin-left 100s'; div.style.marginLeft = '1000px'; - flushComputedStyle(div); - var animation = div.getAnimations()[0]; - return animation.ready.then(function() { - animation.finish(); - animation.cancel(); + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + transition.finish(); + transition.cancel(); assert_equals(getComputedStyle(div).marginLeft, '1000px', 'margin-left style is not animated after cancelling'); - animation.play(); + transition.play(); assert_equals(getComputedStyle(div).marginLeft, '0px', 'margin-left style is animated after re-starting transition'); - return animation.ready; + return transition.ready; }).then(function() { - assert_equals(animation.playState, 'running', + assert_equals(transition.playState, 'running', 'Transition succeeds in running after being re-started'); }); }, 'After cancelling a finished transition, it can still be re-used'); @@ -99,10 +73,9 @@ test(function(t) { div.style.transition = 'margin-left 100s'; div.style.marginLeft = '1000px'; - flushComputedStyle(div); - var animation = div.getAnimations()[0]; - animation.cancel(); + var transition = div.getAnimations()[0]; + transition.cancel(); assert_equals(getComputedStyle(div).marginLeft, '1000px', 'margin-left style is not animated after cancelling'); @@ -113,7 +86,7 @@ test(function(t) { assert_equals(getComputedStyle(div).marginLeft, '1000px', 'margin-left style is still not animated after updating' + ' transition-duration'); - assert_equals(animation.playState, 'idle', + assert_equals(transition.playState, 'idle', 'Transition is still idle after updating transition-duration'); }, 'After cancelling a transition, updating transition properties doesn\'t make' + ' it live again'); @@ -124,15 +97,14 @@ promise_test(function(t) { div.style.transition = 'margin-left 100s'; div.style.marginLeft = '1000px'; - flushComputedStyle(div); - var animation = div.getAnimations()[0]; - return animation.ready.then(function() { - assert_equals(animation.playState, 'running'); + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); div.style.display = 'none'; return waitForFrame(); }).then(function() { - assert_equals(animation.playState, 'idle'); + assert_equals(transition.playState, 'idle'); assert_equals(getComputedStyle(div).marginLeft, '1000px'); }); }, 'Setting display:none on an element cancels its transitions'); @@ -147,19 +119,115 @@ promise_test(function(t) { childDiv.style.transition = 'margin-left 100s'; childDiv.style.marginLeft = '1000px'; - flushComputedStyle(childDiv); - var animation = childDiv.getAnimations()[0]; - return animation.ready.then(function() { - assert_equals(animation.playState, 'running'); + var transition = childDiv.getAnimations()[0]; + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); parentDiv.style.display = 'none'; return waitForFrame(); }).then(function() { - assert_equals(animation.playState, 'idle'); + assert_equals(transition.playState, 'idle'); assert_equals(getComputedStyle(childDiv).marginLeft, '1000px'); }); }, 'Setting display:none cancels transitions on a child element'); +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); + // Set an unrecognized property value + div.style.transitionProperty = 'none'; + flushComputedStyle(div); + return waitForFrame(); + }).then(function() { + assert_equals(transition.playState, 'idle'); + assert_equals(getComputedStyle(div).marginLeft, '1000px'); + }); +}, 'Removing a property from transition-property cancels transitions on that '+ + 'property'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); + div.style.transition = 'margin-top 10s -10s'; // combined duration is zero + flushComputedStyle(div); + return waitForFrame(); + }).then(function() { + assert_equals(transition.playState, 'idle'); + assert_equals(getComputedStyle(div).marginLeft, '1000px'); + }); +}, 'Setting zero combined duration'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); + div.style.marginLeft = '2000px'; + flushComputedStyle(div); + return waitForFrame(); + }).then(function() { + assert_equals(transition.playState, 'idle'); + }); +}, 'Changing style to another interpolable value cancels the original ' + + 'transition'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); + div.style.marginLeft = 'auto'; + flushComputedStyle(div); + return waitForFrame(); + }).then(function() { + assert_equals(div.getAnimations().length, 0, + 'There should be no transitions'); + assert_equals(transition.playState, 'idle'); + }); +}, 'An after-change style value can\'t be interpolated'); + +promise_test(function(t) { + var div = addDiv(t, { style: 'margin-left: 0px' }); + flushComputedStyle(div); + + div.style.transition = 'margin-left 100s'; + div.style.marginLeft = '1000px'; + + var transition = div.getAnimations()[0]; + return transition.ready.then(function() { + assert_equals(transition.playState, 'running'); + div.style.marginLeft = '0px'; + flushComputedStyle(div); + return waitForFrame(); + }).then(function() { + assert_equals(transition.playState, 'idle'); + }); +}, 'Reversing a running transition cancels the original transition'); + done(); </script> </body> diff --git a/dom/animation/test/css-transitions/file_csstransition-events.html b/dom/animation/test/css-transitions/file_csstransition-events.html deleted file mode 100644 index 5011bc130..000000000 --- a/dom/animation/test/css-transitions/file_csstransition-events.html +++ /dev/null @@ -1,223 +0,0 @@ -<!doctype html> -<meta charset=utf-8> -<title>Tests for CSS-Transition events</title> -<link rel="help" href="https://drafts.csswg.org/css-transitions-2/#transition-events"> -<script src="../testcommon.js"></script> -<body> -<script> -'use strict'; - -/** - * Helper class to record the elapsedTime member of each event. - * The EventWatcher class in testharness.js allows us to wait on - * multiple events in a certain order but only records the event - * parameters of the most recent event. - */ -function TransitionEventHandler(target) { - this.target = target; - this.target.ontransitionrun = function(evt) { - this.transitionrun = evt.elapsedTime; - }.bind(this); - this.target.ontransitionstart = function(evt) { - this.transitionstart = evt.elapsedTime; - }.bind(this); - this.target.ontransitionend = function(evt) { - this.transitionend = evt.elapsedTime; - }.bind(this); -} - -TransitionEventHandler.prototype.clear = function() { - this.transitionrun = undefined; - this.transitionstart = undefined; - this.transitionend = undefined; -}; - -function setupTransition(t, transitionStyle) { - var div, watcher, handler, transition; - transitionStyle = transitionStyle || 'transition: margin-left 100s 100s'; - div = addDiv(t, { style: transitionStyle }); - watcher = new EventWatcher(t, div, [ 'transitionrun', - 'transitionstart', - 'transitionend' ]); - handler = new TransitionEventHandler(div); - flushComputedStyle(div); - - div.style.marginLeft = '100px'; - flushComputedStyle(div); - - transition = div.getAnimations()[0]; - - return [transition, watcher, handler]; -} - -// On the next frame (i.e. when events are queued), whether or not the -// transition is still pending depends on the implementation. -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - return watcher.wait_for('transitionrun').then(function(evt) { - assert_equals(evt.elapsedTime, 0.0); - }); -}, 'Idle -> Pending or Before'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - // Force the transition to leave the idle phase - transition.startTime = document.timeline.currentTime; - return watcher.wait_for('transitionrun').then(function(evt) { - assert_equals(evt.elapsedTime, 0.0); - }); -}, 'Idle -> Before'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - // Seek to Active phase. - transition.currentTime = 100 * MS_PER_SEC; - transition.pause(); - return watcher.wait_for([ 'transitionrun', - 'transitionstart' ]).then(function(evt) { - assert_equals(handler.transitionrun, 0.0); - assert_equals(handler.transitionstart, 0.0); - }); -}, 'Idle or Pending -> Active'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - // Seek to After phase. - transition.finish(); - return watcher.wait_for([ 'transitionrun', - 'transitionstart', - 'transitionend' ]).then(function(evt) { - assert_equals(handler.transitionrun, 0.0); - assert_equals(handler.transitionstart, 0.0); - assert_equals(handler.transitionend, 100.0); - }); -}, 'Idle or Pending -> After'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - - return Promise.all([ watcher.wait_for('transitionrun'), - transition.ready ]).then(function() { - transition.currentTime = 100 * MS_PER_SEC; - return watcher.wait_for('transitionstart'); - }).then(function() { - assert_equals(handler.transitionstart, 0.0); - }); -}, 'Before -> Active'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - return Promise.all([ watcher.wait_for('transitionrun'), - transition.ready ]).then(function() { - // Seek to After phase. - transition.currentTime = 200 * MS_PER_SEC; - return watcher.wait_for([ 'transitionstart', 'transitionend' ]); - }).then(function(evt) { - assert_equals(handler.transitionstart, 0.0); - assert_equals(handler.transitionend, 100.0); - }); -}, 'Before -> After'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - // Seek to Active phase. - transition.currentTime = 100 * MS_PER_SEC; - return watcher.wait_for([ 'transitionrun', - 'transitionstart' ]).then(function(evt) { - // Seek to Before phase. - transition.currentTime = 0; - return watcher.wait_for('transitionend'); - }).then(function(evt) { - assert_equals(evt.elapsedTime, 0.0); - }); -}, 'Active -> Before'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - // Seek to Active phase. - transition.currentTime = 100 * MS_PER_SEC; - return watcher.wait_for([ 'transitionrun', - 'transitionstart' ]).then(function(evt) { - // Seek to After phase. - transition.currentTime = 200 * MS_PER_SEC; - return watcher.wait_for('transitionend'); - }).then(function(evt) { - assert_equals(evt.elapsedTime, 100.0); - }); -}, 'Active -> After'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - // Seek to After phase. - transition.finish(); - return watcher.wait_for([ 'transitionrun', - 'transitionstart', - 'transitionend' ]).then(function(evt) { - // Seek to Before phase. - transition.currentTime = 0; - return watcher.wait_for([ 'transitionstart', 'transitionend' ]); - }).then(function(evt) { - assert_equals(handler.transitionstart, 100.0); - assert_equals(handler.transitionend, 0.0); - }); -}, 'After -> Before'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - // Seek to After phase. - transition.finish(); - return watcher.wait_for([ 'transitionrun', - 'transitionstart', - 'transitionend' ]).then(function(evt) { - // Seek to Active phase. - transition.currentTime = 100 * MS_PER_SEC; - return watcher.wait_for('transitionstart'); - }).then(function(evt) { - assert_equals(evt.elapsedTime, 100.0); - }); -}, 'After -> Active'); - -promise_test(function(t) { - var [transition, watcher, handler] = - setupTransition(t, 'transition: margin-left 100s -50s'); - - return watcher.wait_for([ 'transitionrun', - 'transitionstart' ]).then(function() { - assert_equals(handler.transitionrun, 50.0); - assert_equals(handler.transitionstart, 50.0); - transition.finish(); - return watcher.wait_for('transitionend'); - }).then(function(evt) { - assert_equals(evt.elapsedTime, 100.0); - }); -}, 'Calculating the interval start and end time with negative start delay.'); - -promise_test(function(t) { - var [transition, watcher, handler] = setupTransition(t); - - return watcher.wait_for('transitionrun').then(function(evt) { - // We can't set the end delay via generated effect timing. - // Because CSS-Transition use the AnimationEffectTimingReadOnly. - transition.effect = new KeyframeEffect(handler.target, - { marginleft: [ '0px', '100px' ]}, - { duration: 100 * MS_PER_SEC, - endDelay: -50 * MS_PER_SEC }); - // Seek to Before and play. - transition.cancel(); - transition.play(); - return watcher.wait_for('transitionstart'); - }).then(function() { - assert_equals(handler.transitionstart, 0.0); - - // Seek to After phase. - transition.finish(); - return watcher.wait_for('transitionend'); - }).then(function(evt) { - assert_equals(evt.elapsedTime, 50.0); - }); -}, 'Calculating the interval start and end time with negative end delay.'); - -done(); -</script> -</body> -</html> diff --git a/dom/animation/test/css-transitions/file_event-dispatch.html b/dom/animation/test/css-transitions/file_event-dispatch.html new file mode 100644 index 000000000..7140cda36 --- /dev/null +++ b/dom/animation/test/css-transitions/file_event-dispatch.html @@ -0,0 +1,474 @@ +<!doctype html> +<meta charset=utf-8> +<title>Tests for CSS-Transition events</title> +<link rel="help" href="https://drafts.csswg.org/css-transitions-2/#transition-events"> +<script src="../testcommon.js"></script> +<body> +<script> +'use strict'; + +/** + * Helper class to record the elapsedTime member of each event. + * The EventWatcher class in testharness.js allows us to wait on + * multiple events in a certain order but only records the event + * parameters of the most recent event. + */ +function TransitionEventHandler(target) { + this.target = target; + this.target.ontransitionrun = function(evt) { + this.transitionrun = evt.elapsedTime; + }.bind(this); + this.target.ontransitionstart = function(evt) { + this.transitionstart = evt.elapsedTime; + }.bind(this); + this.target.ontransitionend = function(evt) { + this.transitionend = evt.elapsedTime; + }.bind(this); + this.target.ontransitioncancel = function(evt) { + this.transitioncancel = evt.elapsedTime; + }.bind(this); +} + +TransitionEventHandler.prototype.clear = function() { + this.transitionrun = undefined; + this.transitionstart = undefined; + this.transitionend = undefined; + this.transitioncancel = undefined; +}; + +function setupTransition(t, transitionStyle) { + var div = addDiv(t, { style: 'transition: ' + transitionStyle }); + var watcher = new EventWatcher(t, div, [ 'transitionrun', + 'transitionstart', + 'transitionend', + 'transitioncancel' ]); + flushComputedStyle(div); + + div.style.marginLeft = '100px'; + var transition = div.getAnimations()[0]; + + return [transition, watcher, div]; +} + +// On the next frame (i.e. when events are queued), whether or not the +// transition is still pending depends on the implementation. +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + return watcher.wait_for('transitionrun').then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Idle -> Pending or Before'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + // Force the transition to leave the idle phase + transition.startTime = document.timeline.currentTime; + return watcher.wait_for('transitionrun').then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Idle -> Before'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + var handler = new TransitionEventHandler(div); + + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + transition.pause(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + assert_equals(handler.transitionrun, 0.0); + assert_equals(handler.transitionstart, 0.0); + }); +}, 'Idle or Pending -> Active'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + var handler = new TransitionEventHandler(div); + + // Seek to After phase. + transition.finish(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart', + 'transitionend' ]).then(function(evt) { + assert_equals(handler.transitionrun, 0.0); + assert_equals(handler.transitionstart, 0.0); + assert_equals(handler.transitionend, 100.0); + }); +}, 'Idle or Pending -> After'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + + return Promise.all([ watcher.wait_for('transitionrun'), + transition.ready ]).then(function() { + // Make idle + div.style.display = 'none'; + flushComputedStyle(div); + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Before -> Idle (display: none)'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + + return Promise.all([ watcher.wait_for('transitionrun'), + transition.ready ]).then(function() { + // Make idle + transition.timeline = null; + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Before -> Idle (Animation.timeline = null)'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + + return Promise.all([ watcher.wait_for('transitionrun'), + transition.ready ]).then(function() { + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for('transitionstart'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Before -> Active'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + var handler = new TransitionEventHandler(div); + + return Promise.all([ watcher.wait_for('transitionrun'), + transition.ready ]).then(function() { + // Seek to After phase. + transition.currentTime = 200 * MS_PER_SEC; + return watcher.wait_for([ 'transitionstart', 'transitionend' ]); + }).then(function(evt) { + assert_equals(handler.transitionstart, 0.0); + assert_equals(handler.transitionend, 100.0); + }); +}, 'Before -> After'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s'); + + // Seek to Active start position. + transition.pause(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Make idle + div.style.display = 'none'; + flushComputedStyle(div); + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Idle, no delay (display: none)'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s'); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Make idle + transition.currentTime = 0; + transition.timeline = null; + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Idle, no delay (Animation.timeline = null)'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + // Pause so the currentTime is fixed and we can accurately compare the event + // time in transition cancel events. + transition.pause(); + + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Make idle + div.style.display = 'none'; + flushComputedStyle(div); + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Idle, with positive delay (display: none)'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Make idle + transition.currentTime = 100 * MS_PER_SEC; + transition.timeline = null; + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Idle, with positive delay (Animation.timeline = null)'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s -50s'); + + // Pause so the currentTime is fixed and we can accurately compare the event + // time in transition cancel events. + transition.pause(); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Make idle + div.style.display = 'none'; + flushComputedStyle(div); + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 50.0); + }); +}, 'Active -> Idle, with negative delay (display: none)'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s -50s'); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Make idle + transition.currentTime = 50 * MS_PER_SEC; + transition.timeline = null; + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Idle, with negative delay (Animation.timeline = null)'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Seek to Before phase. + transition.currentTime = 0; + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 0.0); + }); +}, 'Active -> Before'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Seek to After phase. + transition.currentTime = 200 * MS_PER_SEC; + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'Active -> After'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + var handler = new TransitionEventHandler(div); + + // Seek to After phase. + transition.finish(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart', + 'transitionend' ]).then(function(evt) { + // Seek to Before phase. + transition.currentTime = 0; + return watcher.wait_for([ 'transitionstart', 'transitionend' ]); + }).then(function(evt) { + assert_equals(handler.transitionstart, 100.0); + assert_equals(handler.transitionend, 0.0); + }); +}, 'After -> Before'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s 100s'); + // Seek to After phase. + transition.finish(); + return watcher.wait_for([ 'transitionrun', + 'transitionstart', + 'transitionend' ]).then(function(evt) { + // Seek to Active phase. + transition.currentTime = 100 * MS_PER_SEC; + return watcher.wait_for('transitionstart'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'After -> Active'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s -50s'); + var handler = new TransitionEventHandler(div); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function() { + assert_equals(handler.transitionrun, 50.0); + assert_equals(handler.transitionstart, 50.0); + transition.finish(); + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 100.0); + }); +}, 'Calculating the interval start and end time with negative start delay.'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + var handler = new TransitionEventHandler(div); + + return watcher.wait_for('transitionrun').then(function(evt) { + // We can't set the end delay via generated effect timing. + // Because CSS-Transition use the AnimationEffectTimingReadOnly. + transition.effect = new KeyframeEffect(div, + { marginleft: [ '0px', '100px' ]}, + { duration: 100 * MS_PER_SEC, + endDelay: -50 * MS_PER_SEC }); + // Seek to Before and play. + transition.cancel(); + transition.play(); + return watcher.wait_for([ 'transitioncancel', + 'transitionrun', + 'transitionstart' ]); + }).then(function() { + assert_equals(handler.transitionstart, 0.0); + + // Seek to After phase. + transition.finish(); + return watcher.wait_for('transitionend'); + }).then(function(evt) { + assert_equals(evt.elapsedTime, 50.0); + }); +}, 'Calculating the interval start and end time with negative end delay.'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + + return watcher.wait_for('transitionrun').then(function() { + // Make idle + div.style.display = 'none'; + flushComputedStyle(div); + return watcher.wait_for('transitioncancel'); + }).then(function() { + transition.cancel(); + // Then wait a couple of frames and check that no event was dispatched + return waitForAnimationFrames(2); + }); +}, 'Call Animation.cancel after cancelling transition.'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + + return watcher.wait_for('transitionrun').then(function(evt) { + // Make idle + div.style.display = 'none'; + flushComputedStyle(div); + transition.play(); + watcher.wait_for([ 'transitioncancel', + 'transitionrun', + 'transitionstart' ]); + }); +}, 'Restart transition after cancelling transition immediately'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s 100s'); + + return watcher.wait_for('transitionrun').then(function(evt) { + // Make idle + div.style.display = 'none'; + flushComputedStyle(div); + transition.play(); + transition.cancel(); + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + // Then wait a couple of frames and check that no event was dispatched + return waitForAnimationFrames(2); + }); +}, 'Call Animation.cancel after restarting transition immediately'); + +promise_test(function(t) { + var [transition, watcher] = + setupTransition(t, 'margin-left 100s'); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + // Make idle + transition.timeline = null; + return watcher.wait_for('transitioncancel'); + }).then(function(evt) { + transition.timeline = document.timeline; + transition.play(); + + return watcher.wait_for(['transitionrun', 'transitionstart']); + }); +}, 'Set timeline and play transition after clear the timeline'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s'); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function() { + transition.cancel(); + return watcher.wait_for('transitioncancel'); + }).then(function() { + // Make After phase + transition.effect = null; + + // Then wait a couple of frames and check that no event was dispatched + return waitForAnimationFrames(2); + }); +}, 'Set null target effect after cancel the transition'); + +promise_test(function(t) { + var [transition, watcher, div] = + setupTransition(t, 'margin-left 100s'); + + return watcher.wait_for([ 'transitionrun', + 'transitionstart' ]).then(function(evt) { + transition.effect = null; + return watcher.wait_for('transitionend'); + }).then(function(evt) { + transition.cancel(); + return watcher.wait_for('transitioncancel'); + }); +}, 'Cancel the transition after clearing the target effect'); + +done(); +</script> +</body> +</html> diff --git a/dom/animation/test/css-transitions/file_setting-effect.html b/dom/animation/test/css-transitions/file_setting-effect.html index c61877194..81279ea55 100644 --- a/dom/animation/test/css-transitions/file_setting-effect.html +++ b/dom/animation/test/css-transitions/file_setting-effect.html @@ -7,6 +7,8 @@ promise_test(function(t) { var div = addDiv(t); + var watcher = new EventWatcher(t, div, [ 'transitionend', + 'transitioncancel' ]); div.style.left = '0px'; div.style.transition = 'left 100s'; @@ -20,11 +22,14 @@ promise_test(function(t) { assert_equals(transition.transitionProperty, 'left'); assert_equals(transition.playState, 'finished'); assert_equals(window.getComputedStyle(div).left, '100px'); + return watcher.wait_for('transitionend'); }); }, 'Test for removing a transition effect'); promise_test(function(t) { var div = addDiv(t); + var watcher = new EventWatcher(t, div, [ 'transitionend', + 'transitioncancel' ]); div.style.left = '0px'; div.style.transition = 'left 100s'; @@ -46,6 +51,8 @@ promise_test(function(t) { promise_test(function(t) { var div = addDiv(t); + var watcher = new EventWatcher(t, div, [ 'transitionend', + 'transitioncancel' ]); div.style.left = '0px'; div.style.width = '0px'; @@ -65,6 +72,8 @@ promise_test(function(t) { promise_test(function(t) { var div = addDiv(t); + var watcher = new EventWatcher(t, div, [ 'transitionend', + 'transitioncancel' ]); div.style.left = '0px'; div.style.width = '0px'; diff --git a/dom/animation/test/css-transitions/test_event-dispatch.html b/dom/animation/test/css-transitions/test_event-dispatch.html new file mode 100644 index 000000000..c90431cd1 --- /dev/null +++ b/dom/animation/test/css-transitions/test_event-dispatch.html @@ -0,0 +1,14 @@ +<!doctype html> +<meta charset=utf-8> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +'use strict'; +setup({explicit_done: true}); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.core.enabled", true]]}, + function() { + window.open("file_event-dispatch.html"); + }); +</script> diff --git a/dom/animation/test/mochitest.ini b/dom/animation/test/mochitest.ini index feb424518..db6dffada 100644 --- a/dom/animation/test/mochitest.ini +++ b/dom/animation/test/mochitest.ini @@ -19,6 +19,8 @@ support-files = css-animations/file_document-get-animations.html css-animations/file_effect-target.html css-animations/file_element-get-animations.html + css-animations/file_event-dispatch.html + css-animations/file_event-order.html css-animations/file_keyframeeffect-getkeyframes.html css-animations/file_pseudoElement-get-animations.html css-transitions/file_animation-cancel.html @@ -32,6 +34,7 @@ support-files = css-transitions/file_document-get-animations.html css-transitions/file_effect-target.html css-transitions/file_element-get-animations.html + css-transitions/file_event-dispatch.html css-transitions/file_keyframeeffect-getkeyframes.html css-transitions/file_pseudoElement-get-animations.html css-transitions/file_setting-effect.html @@ -72,6 +75,8 @@ support-files = [css-animations/test_document-get-animations.html] [css-animations/test_effect-target.html] [css-animations/test_element-get-animations.html] +[css-animations/test_event-dispatch.html] +[css-animations/test_event-order.html] [css-animations/test_keyframeeffect-getkeyframes.html] [css-animations/test_pseudoElement-get-animations.html] [css-transitions/test_animation-cancel.html] @@ -85,6 +90,7 @@ support-files = [css-transitions/test_document-get-animations.html] [css-transitions/test_effect-target.html] [css-transitions/test_element-get-animations.html] +[css-transitions/test_event-dispatch.html] [css-transitions/test_keyframeeffect-getkeyframes.html] [css-transitions/test_pseudoElement-get-animations.html] [css-transitions/test_setting-effect.html] diff --git a/dom/base/nsGkAtomList.h b/dom/base/nsGkAtomList.h index e4ae7ede8..50b4449ec 100644 --- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -694,6 +694,7 @@ GK_ATOM(onadapterremoved, "onadapterremoved") GK_ATOM(onafterprint, "onafterprint") GK_ATOM(onafterscriptexecute, "onafterscriptexecute") GK_ATOM(onalerting, "onalerting") +GK_ATOM(onanimationcancel, "onanimationcancel") GK_ATOM(onanimationend, "onanimationend") GK_ATOM(onanimationiteration, "onanimationiteration") GK_ATOM(onanimationstart, "onanimationstart") @@ -704,6 +705,7 @@ GK_ATOM(onattributechanged, "onattributechanged") GK_ATOM(onattributereadreq, "onattributereadreq") GK_ATOM(onattributewritereq, "onattributewritereq") GK_ATOM(onaudioprocess, "onaudioprocess") +GK_ATOM(onauxclick, "onauxclick") GK_ATOM(onbeforecopy, "onbeforecopy") GK_ATOM(onbeforecut, "onbeforecut") GK_ATOM(onbeforepaste, "onbeforepaste") @@ -941,6 +943,7 @@ GK_ATOM(ontouchstart, "ontouchstart") GK_ATOM(ontouchend, "ontouchend") GK_ATOM(ontouchmove, "ontouchmove") GK_ATOM(ontouchcancel, "ontouchcancel") +GK_ATOM(ontransitioncancel, "ontransitioncancel") GK_ATOM(ontransitionend, "ontransitionend") GK_ATOM(ontransitionrun, "ontransitionrun") GK_ATOM(ontransitionstart, "ontransitionstart") diff --git a/dom/events/EventNameList.h b/dom/events/EventNameList.h index ba2427623..509863e6c 100644 --- a/dom/events/EventNameList.h +++ b/dom/events/EventNameList.h @@ -164,6 +164,10 @@ EVENT(change, eFormChange, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(auxclick, + eMouseAuxClick, + EventNameType_All, + eMouseEventClass) EVENT(click, eMouseClick, EventNameType_All, @@ -1003,6 +1007,10 @@ EVENT(transitionend, eTransitionEnd, EventNameType_All, eTransitionEventClass) +EVENT(transitioncancel, + eTransitionCancel, + EventNameType_All, + eTransitionEventClass) EVENT(animationstart, eAnimationStart, EventNameType_All, @@ -1015,6 +1023,10 @@ EVENT(animationiteration, eAnimationIteration, EventNameType_All, eAnimationEventClass) +EVENT(animationcancel, + eAnimationCancel, + EventNameType_All, + eAnimationEventClass) // Webkit-prefixed versions of Transition & Animation events, for web compat: EVENT(webkitAnimationEnd, diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp index c6b304183..7bbfe21b7 100644 --- a/dom/events/EventStateManager.cpp +++ b/dom/events/EventStateManager.cpp @@ -492,6 +492,7 @@ IsMessageMouseUserActivity(EventMessage aMessage) return aMessage == eMouseMove || aMessage == eMouseUp || aMessage == eMouseDown || + aMessage == eMouseAuxClick || aMessage == eMouseDoubleClick || aMessage == eMouseClick || aMessage == eMouseActivate || @@ -4633,6 +4634,32 @@ EventStateManager::SetClickCount(WidgetMouseEvent* aEvent, } nsresult +EventStateManager::InitAndDispatchClickEvent(WidgetMouseEvent* aEvent, + nsEventStatus* aStatus, + EventMessage aMessage, + nsIPresShell* aPresShell, + nsIContent* aMouseTarget, + nsWeakFrame aCurrentTarget, + bool aNoContentDispatch) +{ + WidgetMouseEvent event(aEvent->IsTrusted(), aMessage, + aEvent->mWidget, WidgetMouseEvent::eReal); + + event.mRefPoint = aEvent->mRefPoint; + event.mClickCount = aEvent->mClickCount; + event.mModifiers = aEvent->mModifiers; + event.buttons = aEvent->buttons; + event.mTime = aEvent->mTime; + event.mTimeStamp = aEvent->mTimeStamp; + event.mFlags.mNoContentDispatch = aNoContentDispatch; + event.button = aEvent->button; + event.inputSource = aEvent->inputSource; + + return aPresShell->HandleEventWithTarget(&event, aCurrentTarget, + aMouseTarget, aStatus); +} + +nsresult EventStateManager::CheckForAndDispatchClick(WidgetMouseEvent* aEvent, nsEventStatus* aStatus) { @@ -4651,17 +4678,7 @@ EventStateManager::CheckForAndDispatchClick(WidgetMouseEvent* aEvent, (aEvent->button == WidgetMouseEvent::eMiddleButton || aEvent->button == WidgetMouseEvent::eRightButton); - WidgetMouseEvent event(aEvent->IsTrusted(), eMouseClick, - aEvent->mWidget, WidgetMouseEvent::eReal); - event.mRefPoint = aEvent->mRefPoint; - event.mClickCount = aEvent->mClickCount; - event.mModifiers = aEvent->mModifiers; - event.buttons = aEvent->buttons; - event.mTime = aEvent->mTime; - event.mTimeStamp = aEvent->mTimeStamp; - event.mFlags.mNoContentDispatch = notDispatchToContents; - event.button = aEvent->button; - event.inputSource = aEvent->inputSource; + bool fireAuxClick = notDispatchToContents; nsCOMPtr<nsIPresShell> presShell = mPresContext->GetPresShell(); if (presShell) { @@ -4680,23 +4697,22 @@ EventStateManager::CheckForAndDispatchClick(WidgetMouseEvent* aEvent, // HandleEvent clears out mCurrentTarget which we might need again nsWeakFrame currentTarget = mCurrentTarget; - ret = presShell->HandleEventWithTarget(&event, currentTarget, - mouseContent, aStatus); + ret = InitAndDispatchClickEvent(aEvent, aStatus, eMouseClick, + presShell, mouseContent, currentTarget, + notDispatchToContents); + if (NS_SUCCEEDED(ret) && aEvent->mClickCount == 2 && mouseContent && mouseContent->IsInComposedDoc()) { //fire double click - WidgetMouseEvent event2(aEvent->IsTrusted(), eMouseDoubleClick, - aEvent->mWidget, WidgetMouseEvent::eReal); - event2.mRefPoint = aEvent->mRefPoint; - event2.mClickCount = aEvent->mClickCount; - event2.mModifiers = aEvent->mModifiers; - event2.buttons = aEvent->buttons; - event2.mFlags.mNoContentDispatch = notDispatchToContents; - event2.button = aEvent->button; - event2.inputSource = aEvent->inputSource; - - ret = presShell->HandleEventWithTarget(&event2, currentTarget, - mouseContent, aStatus); + ret = InitAndDispatchClickEvent(aEvent, aStatus, eMouseDoubleClick, + presShell, mouseContent, currentTarget, + notDispatchToContents); + } + if (NS_SUCCEEDED(ret) && mouseContent && fireAuxClick && + mouseContent->IsInComposedDoc()) { + ret = InitAndDispatchClickEvent(aEvent, aStatus, eMouseAuxClick, + presShell, mouseContent, currentTarget, + false); } } } diff --git a/dom/events/EventStateManager.h b/dom/events/EventStateManager.h index 49ecf0586..d0461e7fa 100644 --- a/dom/events/EventStateManager.h +++ b/dom/events/EventStateManager.h @@ -415,6 +415,13 @@ protected: */ void UpdateDragDataTransfer(WidgetDragEvent* dragEvent); + static nsresult InitAndDispatchClickEvent(WidgetMouseEvent* aEvent, + nsEventStatus* aStatus, + EventMessage aMessage, + nsIPresShell* aPresShell, + nsIContent* aMouseTarget, + nsWeakFrame aCurrentTarget, + bool aNoContentDispatch); nsresult SetClickCount(WidgetMouseEvent* aEvent, nsEventStatus* aStatus); nsresult CheckForAndDispatchClick(WidgetMouseEvent* aEvent, nsEventStatus* aStatus); @@ -1046,6 +1053,7 @@ private: #define NS_EVENT_NEEDS_FRAME(event) \ (!(event)->HasPluginActivationEventMessage() && \ (event)->mMessage != eMouseClick && \ - (event)->mMessage != eMouseDoubleClick) + (event)->mMessage != eMouseDoubleClick && \ + (event)->mMessage != eMouseAuxClick) #endif // mozilla_EventStateManager_h_ diff --git a/dom/events/WheelHandlingHelper.cpp b/dom/events/WheelHandlingHelper.cpp index 7665ee922..81f2b6bfa 100644 --- a/dom/events/WheelHandlingHelper.cpp +++ b/dom/events/WheelHandlingHelper.cpp @@ -257,6 +257,7 @@ WheelTransaction::OnEvent(WidgetEvent* aEvent) case eMouseUp: case eMouseDown: case eMouseDoubleClick: + case eMouseAuxClick: case eMouseClick: case eContextMenu: case eDrop: diff --git a/dom/events/test/mochitest.ini b/dom/events/test/mochitest.ini index e100e60a1..0397487bb 100644 --- a/dom/events/test/mochitest.ini +++ b/dom/events/test/mochitest.ini @@ -184,3 +184,4 @@ skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM [test_wheel_default_action.html] [test_bug687787.html] [test_bug1298970.html] +[test_bug1304044.html] diff --git a/dom/events/test/test_bug1304044.html b/dom/events/test/test_bug1304044.html new file mode 100644 index 000000000..0911dcf73 --- /dev/null +++ b/dom/events/test/test_bug1304044.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1304044 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1304044</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + var eventsFired = []; + var target; + var eventsExpected; + + function GetNodeString(node) { + if (node == window) + return "window"; + if (node == document) + return "document"; + if (node.id) + return node.id; + if (node.nodeName) + return node.nodeName; + return node; + } + + function TargetAndListener(listener, target) { + this.listener = listener; + this.target = target; + } + + TargetAndListener.prototype.toString = function() { + var targetName = GetNodeString(this.target); + var listenerName = GetNodeString(this.listener); + return "(listener: " + listenerName + ", target: " + targetName + ")"; + } + + var tests = [ + TestAuxClickBubblesForEventListener, + TestAuxClickBubblesForOnAuxClick, + ]; + + function CompareEvents(evt, expected) { + return evt && expected && evt.listener == expected.listener && + evt.target == expected.target; + } + + function ResetEventsFired() { + eventsFired = []; + } + + function ClearEventListeners() { + for (i in arguments) { + arguments[i].removeEventListener("auxclick", log_event); + } + } + + function ClickTarget(tgt) { + synthesizeMouseAtCenter(tgt, {type : "mousedown", button: 2}, window); + synthesizeMouseAtCenter(tgt, {type : "mouseup", button: 2}, window); + } + + function log_event(e) { + eventsFired[eventsFired.length] = new TargetAndListener(this, e.target); + } + + function CompareEventsToExpected(expected, actual) { + for (var i = 0; i < expected.length || i < actual.length; i++) { + ok(CompareEvents(actual[i], expected[i]), + "Auxclick receiver's don't match: TargetAndListener " + + i + ": Expected: " + expected[i] + ", Actual: " + actual[i]); + } + } + + function TestAuxClickBubblesForEventListener() { + target.addEventListener("auxclick", log_event); + document.addEventListener("auxclick", log_event); + window.addEventListener("auxclick", log_event); + + ClickTarget(target) + CompareEventsToExpected(eventsExpected, eventsFired); + ResetEventsFired(); + ClearEventListeners(target, document, window); + } + + function TestAuxClickBubblesForOnAuxClick() { + target.onauxclick = log_event; + document.onauxclick = log_event; + window.onauxclick = log_event; + + ClickTarget(target); + CompareEventsToExpected(eventsExpected, eventsFired); + ResetEventsFired(); + } + + function RunTests(){ + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + } + + function Begin() { + target = document.getElementById("target"); + eventsExpected = [ + new TargetAndListener(target, target), + new TargetAndListener(document, target), + new TargetAndListener(window, target), + ]; + RunTests(); + target.remove(); + SimpleTest.finish(); + } + + window.onload = function() { + SimpleTest.waitForExplicitFinish(); + SimpleTest.executeSoon(Begin); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1304044">Mozilla Bug 1304044</a> +<p id="display"> + <div id="target">Target</div> +</p> +<div id="content" style:"display:none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_legacy_event.html b/dom/events/test/test_legacy_event.html index d772be106..b2105a6df 100644 --- a/dom/events/test/test_legacy_event.html +++ b/dom/events/test/test_legacy_event.html @@ -73,22 +73,15 @@ function triggerShortAnimation(node) { node.style.animation = "anim1 1ms linear"; } -// This function triggers a long animation with two iterations, which is -// *nearly* at the end of its first iteration. It will hit the end of that -// iteration (firing an event) almost immediately, 1ms in the future. +// This function triggers a very short (10ms long) animation with many +// iterations, which will cause a start event followed by an iteration event +// on each subsequent tick, to fire. // -// NOTE: It's important that this animation have a *long* duration. If it were -// short (e.g. 1ms duration), then we might jump past all its iterations in -// a single refresh-driver tick. And if that were to happens, we'd *never* fire -// any animationiteration events -- the CSS Animations spec says this event -// must not be fired "...when an animationend event would fire at the same time" -// (which would be the case in this example with a 1ms duration). So, to make -// sure our event does fire, we use a long duration and a nearly-as-long -// negative delay. This ensures we hit the end of the first iteration right -// away, and that we don't risk hitting the end of the second iteration at the -// same time. +// NOTE: We need the many iterations since if an animation frame coincides +// with the animation starting or ending we dispatch only the start or end +// event and not the iteration event. function triggerAnimationIteration(node) { - node.style.animation = "anim1 300s -299.999s linear 2"; + node.style.animation = "anim1 10ms linear 20000"; } // GENERAL UTILITY FUNCTIONS diff --git a/dom/plugins/base/nsPluginInstanceOwner.cpp b/dom/plugins/base/nsPluginInstanceOwner.cpp index b7651be1a..291ae576d 100644 --- a/dom/plugins/base/nsPluginInstanceOwner.cpp +++ b/dom/plugins/base/nsPluginInstanceOwner.cpp @@ -2532,6 +2532,7 @@ nsEventStatus nsPluginInstanceOwner::ProcessEvent(const WidgetGUIEvent& anEvent) NS_ASSERTION(anEvent.mMessage == eMouseDown || anEvent.mMessage == eMouseUp || anEvent.mMessage == eMouseDoubleClick || + anEvent.mMessage == eMouseAuxClick || anEvent.mMessage == eMouseOver || anEvent.mMessage == eMouseOut || anEvent.mMessage == eMouseMove || @@ -2594,6 +2595,7 @@ nsEventStatus nsPluginInstanceOwner::ProcessEvent(const WidgetGUIEvent& anEvent) switch (anEvent.mMessage) { case eMouseClick: case eMouseDoubleClick: + case eMouseAuxClick: // Button up/down events sent instead. return rv; default: @@ -2797,6 +2799,7 @@ nsEventStatus nsPluginInstanceOwner::ProcessEvent(const WidgetGUIEvent& anEvent) switch (anEvent.mMessage) { case eMouseClick: case eMouseDoubleClick: + case eMouseAuxClick: // Button up/down events sent instead. return rv; default: diff --git a/dom/webidl/EventHandler.webidl b/dom/webidl/EventHandler.webidl index e65a75787..1edc45ac9 100644 --- a/dom/webidl/EventHandler.webidl +++ b/dom/webidl/EventHandler.webidl @@ -33,6 +33,7 @@ interface GlobalEventHandlers { // attribute OnErrorEventHandler onerror; attribute EventHandler onfocus; //(Not implemented)attribute EventHandler oncancel; + attribute EventHandler onauxclick; attribute EventHandler oncanplay; attribute EventHandler oncanplaythrough; attribute EventHandler onchange; @@ -128,14 +129,14 @@ interface GlobalEventHandlers { attribute EventHandler onmozpointerlockerror; // CSS-Animation and CSS-Transition handlers. + attribute EventHandler onanimationcancel; attribute EventHandler onanimationend; attribute EventHandler onanimationiteration; attribute EventHandler onanimationstart; + attribute EventHandler ontransitioncancel; attribute EventHandler ontransitionend; - // We will ship transitionrun and transitionstart events - // on Firefox 53. (For detail, see bug 1324985) -// attribute EventHandler ontransitionrun; -// attribute EventHandler ontransitionstart; + attribute EventHandler ontransitionrun; + attribute EventHandler ontransitionstart; // CSS-Animation and CSS-Transition legacy handlers. // This handler isn't standard. diff --git a/gfx/layers/apz/src/APZCTreeManager.cpp b/gfx/layers/apz/src/APZCTreeManager.cpp index 857ae5958..f54326360 100644 --- a/gfx/layers/apz/src/APZCTreeManager.cpp +++ b/gfx/layers/apz/src/APZCTreeManager.cpp @@ -1145,6 +1145,7 @@ APZCTreeManager::UpdateWheelTransaction(LayoutDeviceIntPoint aRefPoint, case eMouseUp: case eMouseDown: case eMouseDoubleClick: + case eMouseAuxClick: case eMouseClick: case eContextMenu: case eDrop: diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index 509168d7a..6391c3e70 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -9,7 +9,8 @@ JSMSG_INVALID_OPTION_VALUE: false, JSMSG_INVALID_DIGITS_VALUE: false, JSMSG_INTL_OBJECT_REINITED: false, JSMSG_INVALID_CURRENCY_CODE: false, JSMSG_UNDEFINED_CURRENCY: false, JSMSG_INVALID_TIME_ZONE: false, - JSMSG_DATE_NOT_FINITE: false, + JSMSG_DATE_NOT_FINITE: false, JSMSG_INVALID_KEYS_TYPE: false, + JSMSG_INVALID_KEY: false, intl_Collator_availableLocales: false, intl_availableCollations: false, intl_CompareStrings: false, @@ -3024,3 +3025,126 @@ function Intl_getCalendarInfo(locales) { return result; } + +/** + * This function is a custom method designed after Intl API, but currently + * not part of the spec or spec proposal. + * We want to use it internally to retrieve translated values from CLDR in + * order to ensure they're aligned with what Intl API returns. + * + * This API may one day be a foundation for an ECMA402 API spec proposal. + * + * The function takes two arguments - locales which is a list of locale strings + * and options which is an object with two optional properties: + * + * keys: + * an Array of string values that are paths to individual terms + * + * style: + * a String with a value "long", "short" or "narrow" + * + * It returns an object with properties: + * + * locale: + * a negotiated locale string + * + * style: + * negotiated style + * + * values: + * A key-value pair list of requested keys and corresponding + * translated values + * + */ +function Intl_getDisplayNames(locales, options) { + // 1. Let requestLocales be ? CanonicalizeLocaleList(locales). + const requestedLocales = CanonicalizeLocaleList(locales); + + // 2. If options is undefined, then + if (options === undefined) + // a. Let options be ObjectCreate(%ObjectPrototype%). + options = {}; + // 3. Else, + else + // a. Let options be ? ToObject(options). + options = ToObject(options); + + const DateTimeFormat = dateTimeFormatInternalProperties; + + // 4. Let localeData be %DateTimeFormat%.[[localeData]]. + const localeData = DateTimeFormat.localeData; + + // 5. Let opt be a new Record. + const localeOpt = new Record(); + // 6. Set localeOpt.[[localeMatcher]] to "best fit". + localeOpt.localeMatcher = "best fit"; + + // 7. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], requestedLocales, localeOpt, + // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). + const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + // 8. Let style be ? GetOption(options, "style", "string", « "long", "short", "narrow" », "long"). + const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); + // 9. Let keys be ? Get(options, "keys"). + let keys = options.keys; + + // 10. If keys is undefined, + if (keys === undefined) { + // a. Let keys be ArrayCreate(0). + keys = []; + } else if (!IsObject(keys)) { + // 11. Else, + // a. If Type(keys) is not Object, throw a TypeError exception. + ThrowTypeError(JSMSG_INVALID_KEYS_TYPE); + } + + // 12. Let processedKeys be ArrayCreate(0). + // (This really should be a List, but we use an Array here in order that + // |intl_ComputeDisplayNames| may infallibly access the list's length via + // |ArrayObject::length|.) + let processedKeys = []; + // 13. Let len be ? ToLength(? Get(keys, "length")). + let len = ToLength(keys.length); + // 14. Let i be 0. + // 15. Repeat, while i < len + for (let i = 0; i < len; i++) { + // a. Let processedKey be ? ToString(? Get(keys, i)). + // b. Perform ? CreateDataPropertyOrThrow(processedKeys, i, processedKey). + callFunction(std_Array_push, processedKeys, ToString(keys[i])); + } + + // 16. Let names be ? ComputeDisplayNames(r.[[locale]], style, processedKeys). + const names = intl_ComputeDisplayNames(r.locale, style, processedKeys); + + // 17. Let values be ObjectCreate(%ObjectPrototype%). + const values = {}; + + // 18. Set i to 0. + // 19. Repeat, while i < len + for (let i = 0; i < len; i++) { + // a. Let key be ? Get(processedKeys, i). + const key = processedKeys[i]; + // b. Let name be ? Get(names, i). + const name = names[i]; + // c. Assert: Type(name) is string. + assert(typeof name === "string", "unexpected non-string value"); + // d. Assert: the length of name is greater than zero. + assert(name.length > 0, "empty string value"); + // e. Perform ? DefinePropertyOrThrow(values, key, name). + _DefineDataProperty(values, key, name); + } + + // 20. Let options be ObjectCreate(%ObjectPrototype%). + // 21. Perform ! DefinePropertyOrThrow(result, "locale", r.[[locale]]). + // 22. Perform ! DefinePropertyOrThrow(result, "style", style). + // 23. Perform ! DefinePropertyOrThrow(result, "values", values). + const result = { locale: r.locale, style, values }; + + // 24. Return result. + return result; + +} diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp index c8c91b251..86874f404 100644 --- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp @@ -7013,7 +7013,8 @@ nsLayoutUtils::GetTextRunFlagsForStyle(nsStyleContext* aStyleContext, nscoord aLetterSpacing) { uint32_t result = 0; - if (aLetterSpacing != 0) { + if (aLetterSpacing != 0 || + aStyleText->mTextJustify == StyleTextJustify::InterCharacter) { result |= gfxTextRunFactory::TEXT_DISABLE_OPTIONAL_LIGATURES; } if (aStyleText->mControlCharacterVisibility == NS_STYLE_CONTROL_CHARACTER_VISIBILITY_HIDDEN) { diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp index 00c0016fd..fa31443fd 100644 --- a/layout/generic/nsTextFrame.cpp +++ b/layout/generic/nsTextFrame.cpp @@ -2936,22 +2936,40 @@ nsTextFrame::GetTrimmedOffsets(const nsTextFragment* aFrag, return offsets; } -static bool IsJustifiableCharacter(const nsTextFragment* aFrag, int32_t aPos, +static bool IsJustifiableCharacter(const nsStyleText* aTextStyle, + const nsTextFragment* aFrag, int32_t aPos, bool aLangIsCJ) { NS_ASSERTION(aPos >= 0, "negative position?!"); + + StyleTextJustify justifyStyle = aTextStyle->mTextJustify; + if (justifyStyle == StyleTextJustify::None) { + return false; + } + char16_t ch = aFrag->CharAt(aPos); - if (ch == '\n' || ch == '\t' || ch == '\r') + if (ch == '\n' || ch == '\t' || ch == '\r') { return true; + } if (ch == ' ' || ch == CH_NBSP) { // Don't justify spaces that are combined with diacriticals - if (!aFrag->Is2b()) + if (!aFrag->Is2b()) { return true; + } return !nsTextFrameUtils::IsSpaceCombiningSequenceTail( - aFrag->Get2b() + aPos + 1, aFrag->GetLength() - (aPos + 1)); + aFrag->Get2b() + aPos + 1, aFrag->GetLength() - (aPos + 1)); } - if (ch < 0x2150u) + + if (justifyStyle == StyleTextJustify::InterCharacter) { + return true; + } else if (justifyStyle == StyleTextJustify::InterWord) { + return false; + } + + // text-justify: auto + if (ch < 0x2150u) { return false; + } if (aLangIsCJ) { if ((0x2150u <= ch && ch <= 0x22ffu) || // Number Forms, Arrows, Mathematical Operators (0x2460u <= ch && ch <= 0x24ffu) || // Enclosed Alphanumerics @@ -3279,7 +3297,7 @@ PropertyProvider::ComputeJustification( gfxSkipCharsIterator iter = run.GetPos(); for (uint32_t i = 0; i < length; ++i) { uint32_t offset = originalOffset + i; - if (!IsJustifiableCharacter(mFrag, offset, isCJ)) { + if (!IsJustifiableCharacter(mTextStyle, mFrag, offset, isCJ)) { continue; } diff --git a/layout/reftests/transform-3d/animate-backface-hidden.html b/layout/reftests/transform-3d/animate-backface-hidden.html index ce957bf73..27b587006 100644 --- a/layout/reftests/transform-3d/animate-backface-hidden.html +++ b/layout/reftests/transform-3d/animate-backface-hidden.html @@ -17,7 +17,7 @@ body { padding: 50px } height: 200px; width: 200px; backface-visibility: hidden; /* use a -99.9s delay to start at 99.9% and then move to 0% */ - animation: flip 100s -99.9s linear 2; + animation: flip 100s -99.9s linear 2 paused; } </style> @@ -27,7 +27,13 @@ body { padding: 50px } <script> -document.getElementById("test").addEventListener("animationiteration", IterationListener, false); +document.getElementById("test").addEventListener("animationstart", StartListener, false); + +function StartListener(event) { + var test = document.getElementById("test"); + test.style.animationPlayState = 'running'; + test.addEventListener("animationiteration", IterationListener, false); +} function IterationListener(event) { setTimeout(RemoveReftestWait, 0); diff --git a/layout/reftests/transform-3d/animate-preserve3d-parent.html b/layout/reftests/transform-3d/animate-preserve3d-parent.html index ae3fec196..d05beac6f 100644 --- a/layout/reftests/transform-3d/animate-preserve3d-parent.html +++ b/layout/reftests/transform-3d/animate-preserve3d-parent.html @@ -18,7 +18,7 @@ body { padding: 50px } border: 1px solid black; transform-style: preserve-3d; /* use a -99.9s delay to start at 99.9% and then move to 0% */ - animation: spin 100s -99.9s linear 2; + animation: spin 100s -99.9s linear 2 paused; } #child { @@ -39,7 +39,13 @@ body { padding: 50px } <script> -document.getElementById("parent").addEventListener("animationiteration", IterationListener, false); +document.getElementById("parent").addEventListener("animationstart", StartListener, false); + +function StartListener(event) { + var test = document.getElementById("parent"); + test.style.animationPlayState = 'running'; + test.addEventListener("animationiteration", IterationListener, false); +} function IterationListener(event) { setTimeout(RemoveReftestWait, 0); diff --git a/layout/reftests/w3c-css/submitted/check-for-references.sh b/layout/reftests/w3c-css/submitted/check-for-references.sh index 977cee3f4..c30e18515 100755 --- a/layout/reftests/w3c-css/submitted/check-for-references.sh +++ b/layout/reftests/w3c-css/submitted/check-for-references.sh @@ -15,7 +15,7 @@ do else echo "Unexpected type $TYPE for $DIRNAME/$TEST" fi - if grep "rel=\"$REFTYPE\"" "$DIRNAME/$TEST" | head -1 | grep -q "href=\"$REF\"" + if grep "rel=\(\"$REFTYPE\"\|'$REFTYPE'\)" "$DIRNAME/$TEST" | head -1 | grep -q "href=\(\"$REF\"\|'$REF'\)" then #echo "Good link for $DIRNAME/$TEST" echo -n diff --git a/layout/reftests/w3c-css/submitted/text3/reftest.list b/layout/reftests/w3c-css/submitted/text3/reftest.list index 2712e4363..a1e3d70d0 100644 --- a/layout/reftests/w3c-css/submitted/text3/reftest.list +++ b/layout/reftests/w3c-css/submitted/text3/reftest.list @@ -5,4 +5,9 @@ == text-align-match-parent-root-ltr.html text-align-match-parent-root-ltr-ref.html == text-align-match-parent-root-rtl.html text-align-match-parent-root-rtl-ref.html +pref(layout.css.text-justify.enabled,true) == text-justify-none-001.html text-justify-none-001-ref.html +pref(layout.css.text-justify.enabled,true) == text-justify-inter-word-001.html text-justify-inter-word-001-ref.html +pref(layout.css.text-justify.enabled,true) == text-justify-inter-character-001.html text-justify-inter-character-001-ref.html +pref(layout.css.text-justify.enabled,true) == text-justify-distribute-001.html text-justify-inter-character-001-ref.html + == text-word-spacing-001.html text-word-spacing-ref.html diff --git a/layout/reftests/w3c-css/submitted/text3/text-justify-distribute-001.html b/layout/reftests/w3c-css/submitted/text3/text-justify-distribute-001.html new file mode 100644 index 000000000..4d29f0fee --- /dev/null +++ b/layout/reftests/w3c-css/submitted/text3/text-justify-distribute-001.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Text 7.4. Justification Method: text-justify: distribute</title> +<link rel="author" title="Chun-Min (Jeremy) Chen" href="mailto:jeremychen@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org"> +<link rel="help" href="https://drafts.csswg.org/css-text-3/#text-justify-property"> +<link rel='match' href='text-justify-inter-character-001-ref.html'> +<meta name="assert" content="text-justify:distribute means justification adjusts spacing between each pair of adjacent typographic character units."> +<style type='text/css'> +p { + font-size: 1.5em; + border: 1px solid black; + padding: 10px; + margin-right: 310px; +} +.test { + text-align-last: justify; + text-justify: distribute; +} +</style> +</head> +<body> +<p lang="en" class="test">XX</p> +<p lang="ja" class="test">文字</p> +</body> +</html> diff --git a/layout/reftests/w3c-css/submitted/text3/text-justify-inter-character-001-ref.html b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-character-001-ref.html new file mode 100644 index 000000000..0a42a5cf8 --- /dev/null +++ b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-character-001-ref.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Text 7.4. Justification Method: text-justify: inter-character</title> +<link rel="author" title="Chun-Min (Jeremy) Chen" href="mailto:jeremychen@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org"> +<style type='text/css'> +p { + font-size: 1.5em; + border: 1px solid black; + padding: 10px; + margin-right: 310px; +} +.right { + float: right; +} +</style> +</head> +<body> +<p lang="en">X<span class="right">X</span></p> +<p lang="ja">文<span class="right">字</span></p> +</body> +</html> diff --git a/layout/reftests/w3c-css/submitted/text3/text-justify-inter-character-001.html b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-character-001.html new file mode 100644 index 000000000..639ab7ecb --- /dev/null +++ b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-character-001.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Text 7.4. Justification Method: text-justify: inter-character</title> +<link rel="author" title="Chun-Min (Jeremy) Chen" href="mailto:jeremychen@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org"> +<link rel="help" href="https://drafts.csswg.org/css-text-3/#text-justify-property"> +<link rel='match' href='text-justify-inter-character-001-ref.html'> +<meta name="assert" content="text-justify:inter-character means justification adjusts spacing between each pair of adjacent typographic character units."> +<style type='text/css'> +p { + font-size: 1.5em; + border: 1px solid black; + padding: 10px; + margin-right: 310px; +} +.test { + text-align-last: justify; + text-justify: inter-character; +} +</style> +</head> +<body> +<p lang="en" class="test">XX</p> +<p lang="ja" class="test">文字</p> +</body> +</html> diff --git a/layout/reftests/w3c-css/submitted/text3/text-justify-inter-word-001-ref.html b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-word-001-ref.html new file mode 100644 index 000000000..687e864e7 --- /dev/null +++ b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-word-001-ref.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Text 7.4. Justification Method: text-justify: inter-word</title> +<link rel="author" title="Chun-Min (Jeremy) Chen" href="mailto:jeremychen@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org"> +<style type='text/css'> +p { + font-size: 1.5em; + border: 1px solid black; + padding: 10px; + margin-right: 310px; +} +.right { + float: right; +} +</style> +</head> +<body> +<p lang="en">Latin<span class="right">text</span></p> +<p lang="ja">日本<span class="right">文字</span></p> +<p lang="th">อักษรไทย<span class="right">อักษรไทย</span></p> +</body> +</html> diff --git a/layout/reftests/w3c-css/submitted/text3/text-justify-inter-word-001.html b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-word-001.html new file mode 100644 index 000000000..75aec2f5f --- /dev/null +++ b/layout/reftests/w3c-css/submitted/text3/text-justify-inter-word-001.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Text 7.4. Justification Method: text-justify: inter-word</title> +<link rel="author" title="Chun-Min (Jeremy) Chen" href="mailto:jeremychen@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org"> +<link rel="help" href="https://drafts.csswg.org/css-text-3/#text-justify-property"> +<link rel='match' href='text-justify-inter-word-001-ref.html'> +<meta name="assert" content="text-justify:inter-word means justification adjusts spacing at word separators only."> +<style type='text/css'> +p { + font-size: 1.5em; + border: 1px solid black; + padding: 10px; + margin-right: 310px; +} +.test { + text-align-last: justify; + text-justify: inter-word; +} +</style> +</head> +<body> +<p lang="en" class="test">Latin text</p> +<p lang="ja" class="test">日本 文字</p> +<p lang="th" class="test">อักษรไทย อักษรไทย</p> +</body> +</html> diff --git a/layout/reftests/w3c-css/submitted/text3/text-justify-none-001-ref.html b/layout/reftests/w3c-css/submitted/text3/text-justify-none-001-ref.html new file mode 100644 index 000000000..c8500ac9f --- /dev/null +++ b/layout/reftests/w3c-css/submitted/text3/text-justify-none-001-ref.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Text 7.4. Justification Method: text-justify: none</title> +<link rel="author" title="Chun-Min (Jeremy) Chen" href="mailto:jeremychen@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org"> +<style type='text/css'> +p { + font-size: 1.5em; + border: 1px solid black; + padding: 10px; + margin-right: 310px; +} +</style> +</head> +<body> +<p lang="en">Latin text</p> +<p lang="ja">日本 文字</p> +<p lang="th">อักษรไทย อักษรไทย</p> +</body> +</html> diff --git a/layout/reftests/w3c-css/submitted/text3/text-justify-none-001.html b/layout/reftests/w3c-css/submitted/text3/text-justify-none-001.html new file mode 100644 index 000000000..2b5511195 --- /dev/null +++ b/layout/reftests/w3c-css/submitted/text3/text-justify-none-001.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Text 7.4. Justification Method: text-justify: none</title> +<link rel="author" title="Chun-Min (Jeremy) Chen" href="mailto:jeremychen@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org"> +<link rel="help" href="https://drafts.csswg.org/css-text-3/#text-justify-property"> +<link rel='match' href='text-justify-none-001-ref.html'> +<meta name="assert" content="text-justify:none means justification is disabled: there are no justification opportunities within the text."> +<style type='text/css'> +p { + font-size: 1.5em; + border: 1px solid black; + padding: 10px; + margin-right: 310px; +} +.test { + text-align-last: justify; + text-justify: none; +} +</style> +</head> +<body> +<p lang="en" class="test">Latin text</p> +<p lang="ja" class="test">日本 文字</p> +<p lang="th" class="test">อักษรไทย อักษรไทย</p> +</body> +</html> diff --git a/layout/style/AnimationCommon.h b/layout/style/AnimationCommon.h index 37030411c..025c034a4 100644 --- a/layout/style/AnimationCommon.h +++ b/layout/style/AnimationCommon.h @@ -251,6 +251,26 @@ ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, aField.Traverse(&aCallback, aName); } +// Return the TransitionPhase or AnimationPhase to use when the animation +// doesn't have a target effect. +template <typename PhaseType> +PhaseType GetAnimationPhaseWithoutEffect(const dom::Animation& aAnimation) +{ + MOZ_ASSERT(!aAnimation.GetEffect(), + "Should only be called when we do not have an effect"); + + Nullable<TimeDuration> currentTime = aAnimation.GetCurrentTime(); + if (currentTime.IsNull()) { + return PhaseType::Idle; + } + + // If we don't have a target effect, the duration will be zero so the phase is + // 'before' if the current time is less than zero. + return currentTime.Value() < TimeDuration() + ? PhaseType::Before + : PhaseType::After; +}; + } // namespace mozilla #endif /* !defined(mozilla_css_AnimationCommon_h) */ diff --git a/layout/style/nsAnimationManager.cpp b/layout/style/nsAnimationManager.cpp index ed2b5afc7..aa1b6fe78 100644 --- a/layout/style/nsAnimationManager.cpp +++ b/layout/style/nsAnimationManager.cpp @@ -33,11 +33,15 @@ using mozilla::dom::AnimationPlayState; using mozilla::dom::KeyframeEffectReadOnly; using mozilla::dom::CSSAnimation; +typedef mozilla::ComputedTiming::AnimationPhase AnimationPhase; + namespace { -// Pair of an event message and elapsed time used when determining the set of -// events to queue. -typedef Pair<EventMessage, StickyTimeDuration> EventPair; +struct AnimationEventParams { + EventMessage mMessage; + StickyTimeDuration mElapsedTime; + TimeStamp mTimeStamp; +}; } // anonymous namespace @@ -154,12 +158,8 @@ CSSAnimation::HasLowerCompositeOrderThan(const CSSAnimation& aOther) const } void -CSSAnimation::QueueEvents() +CSSAnimation::QueueEvents(StickyTimeDuration aActiveTime) { - if (!mEffect) { - return; - } - // If the animation is pending, we ignore animation events until we finish // pending. if (mPendingState != PendingState::NotPending) { @@ -194,77 +194,116 @@ CSSAnimation::QueueEvents() } nsAnimationManager* manager = presContext->AnimationManager(); - ComputedTiming computedTiming = mEffect->GetComputedTiming(); - if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Null) { - return; // do nothing + const StickyTimeDuration zeroDuration; + uint64_t currentIteration = 0; + ComputedTiming::AnimationPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + StickyTimeDuration iterationStartTime; + + if (!mEffect) { + currentPhase = GetAnimationPhaseWithoutEffect + <ComputedTiming::AnimationPhase>(*this); + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + currentPhase = computedTiming.mPhase; + currentIteration = computedTiming.mCurrentIteration; + if (currentPhase == mPreviousPhase && + currentIteration == mPreviousIteration) { + return; + } + intervalStartTime = + std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), + zeroDuration); + intervalEndTime = + std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), + zeroDuration); + + uint64_t iterationBoundary = mPreviousIteration > currentIteration + ? currentIteration + 1 + : currentIteration; + iterationStartTime = + computedTiming.mDuration.MultDouble( + (iterationBoundary - computedTiming.mIterationStart)); } - // Note that script can change the start time, so we have to handle moving - // backwards through the animation as well as forwards. An 'animationstart' - // is dispatched if we enter the active phase (regardless if that is from - // before or after the animation's active phase). An 'animationend' is - // dispatched if we leave the active phase (regardless if that is to before - // or after the animation's active phase). - - bool wasActive = mPreviousPhaseOrIteration != PREVIOUS_PHASE_BEFORE && - mPreviousPhaseOrIteration != PREVIOUS_PHASE_AFTER; - bool isActive = - computedTiming.mPhase == ComputedTiming::AnimationPhase::Active; - bool isSameIteration = - computedTiming.mCurrentIteration == mPreviousPhaseOrIteration; - bool skippedActivePhase = - (mPreviousPhaseOrIteration == PREVIOUS_PHASE_BEFORE && - computedTiming.mPhase == ComputedTiming::AnimationPhase::After) || - (mPreviousPhaseOrIteration == PREVIOUS_PHASE_AFTER && - computedTiming.mPhase == ComputedTiming::AnimationPhase::Before); - bool skippedFirstIteration = - isActive && - mPreviousPhaseOrIteration == PREVIOUS_PHASE_BEFORE && - computedTiming.mCurrentIteration > 0; - - MOZ_ASSERT(!skippedActivePhase || (!isActive && !wasActive), - "skippedActivePhase only makes sense if we were & are inactive"); - - if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Before) { - mPreviousPhaseOrIteration = PREVIOUS_PHASE_BEFORE; - } else if (computedTiming.mPhase == ComputedTiming::AnimationPhase::Active) { - mPreviousPhaseOrIteration = computedTiming.mCurrentIteration; - } else if (computedTiming.mPhase == ComputedTiming::AnimationPhase::After) { - mPreviousPhaseOrIteration = PREVIOUS_PHASE_AFTER; + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime); + + AutoTArray<AnimationEventParams, 2> events; + + // Handle cancel event first + if ((mPreviousPhase != AnimationPhase::Idle && + mPreviousPhase != AnimationPhase::After) && + currentPhase == AnimationPhase::Idle) { + TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(aActiveTime); + events.AppendElement(AnimationEventParams{ eAnimationCancel, + aActiveTime, + activeTimeStamp }); } - AutoTArray<EventPair, 2> events; - StickyTimeDuration initialAdvance = StickyTimeDuration(InitialAdvance()); - StickyTimeDuration iterationStart = computedTiming.mDuration * - computedTiming.mCurrentIteration; - const StickyTimeDuration& activeDuration = computedTiming.mActiveDuration; - - if (skippedFirstIteration) { - // Notify animationstart and animationiteration in same tick. - events.AppendElement(EventPair(eAnimationStart, initialAdvance)); - events.AppendElement(EventPair(eAnimationIteration, - std::max(iterationStart, initialAdvance))); - } else if (!wasActive && isActive) { - events.AppendElement(EventPair(eAnimationStart, initialAdvance)); - } else if (wasActive && !isActive) { - events.AppendElement(EventPair(eAnimationEnd, activeDuration)); - } else if (wasActive && isActive && !isSameIteration) { - events.AppendElement(EventPair(eAnimationIteration, iterationStart)); - } else if (skippedActivePhase) { - events.AppendElement(EventPair(eAnimationStart, - std::min(initialAdvance, activeDuration))); - events.AppendElement(EventPair(eAnimationEnd, activeDuration)); - } else { - return; // No events need to be sent + switch (mPreviousPhase) { + case AnimationPhase::Idle: + case AnimationPhase::Before: + if (currentPhase == AnimationPhase::Active) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == AnimationPhase::After) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalStartTime, + startTimeStamp }); + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalEndTime, + endTimeStamp }); + } + break; + case AnimationPhase::Active: + if (currentPhase == AnimationPhase::Before) { + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == AnimationPhase::Active) { + // The currentIteration must have changed or element we would have + // returned early above. + MOZ_ASSERT(currentIteration != mPreviousIteration); + events.AppendElement(AnimationEventParams{ eAnimationIteration, + iterationStartTime, + iterationTimeStamp }); + } else if (currentPhase == AnimationPhase::After) { + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalEndTime, + endTimeStamp }); + } + break; + case AnimationPhase::After: + if (currentPhase == AnimationPhase::Before) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalEndTime, + startTimeStamp}); + events.AppendElement(AnimationEventParams{ eAnimationEnd, + intervalStartTime, + endTimeStamp }); + } else if (currentPhase == AnimationPhase::Active) { + events.AppendElement(AnimationEventParams{ eAnimationStart, + intervalEndTime, + endTimeStamp }); + } + break; } - for (const EventPair& pair : events){ + mPreviousPhase = currentPhase; + mPreviousIteration = currentIteration; + + for (const AnimationEventParams& event : events){ manager->QueueEvent( AnimationEventInfo(owningElement, owningPseudoType, - pair.first(), mAnimationName, - pair.second(), - ElapsedTimeToTimeStamp(pair.second()), + event.mMessage, mAnimationName, + event.mElapsedTime, event.mTimeStamp, this)); } } diff --git a/layout/style/nsAnimationManager.h b/layout/style/nsAnimationManager.h index abe3aeeb8..d838d090a 100644 --- a/layout/style/nsAnimationManager.h +++ b/layout/style/nsAnimationManager.h @@ -76,7 +76,8 @@ public: , mIsStylePaused(false) , mPauseShouldStick(false) , mNeedsNewAnimationIndexWhenRun(false) - , mPreviousPhaseOrIteration(PREVIOUS_PHASE_BEFORE) + , mPreviousPhase(ComputedTiming::AnimationPhase::Idle) + , mPreviousIteration(0) { // We might need to drop this assertion once we add a script-accessible // constructor but for animations generated from CSS markup the @@ -109,8 +110,6 @@ public: void PauseFromStyle(); void CancelFromStyle() override { - mOwningElement = OwningElementRef(); - // When an animation is disassociated with style it enters an odd state // where its composite order is undefined until it first transitions // out of the idle state. @@ -125,10 +124,15 @@ public: mNeedsNewAnimationIndexWhenRun = true; Animation::CancelFromStyle(); + + // We need to do this *after* calling CancelFromStyle() since + // CancelFromStyle might synchronously trigger a cancel event for which + // we need an owning element to target the event at. + mOwningElement = OwningElementRef(); } void Tick() override; - void QueueEvents(); + void QueueEvents(StickyTimeDuration aActiveTime = StickyTimeDuration()); bool IsStylePaused() const { return mIsStylePaused; } @@ -157,6 +161,10 @@ public: // reflect changes to that markup. bool IsTiedToMarkup() const { return mOwningElement.IsSet(); } + void MaybeQueueCancelEvent(StickyTimeDuration aActiveTime) override { + QueueEvents(aActiveTime); + } + protected: virtual ~CSSAnimation() { @@ -257,13 +265,10 @@ protected: // its animation index should be updated. bool mNeedsNewAnimationIndexWhenRun; - enum { - PREVIOUS_PHASE_BEFORE = uint64_t(-1), - PREVIOUS_PHASE_AFTER = uint64_t(-2) - }; - // One of the PREVIOUS_PHASE_* constants, or an integer for the iteration - // whose start we last notified on. - uint64_t mPreviousPhaseOrIteration; + // Phase and current iteration from the previous time we queued events. + // This is used to determine what new events to dispatch. + ComputedTiming::AnimationPhase mPreviousPhase; + uint64_t mPreviousIteration; }; } /* namespace dom */ diff --git a/layout/style/nsCSSKeywordList.h b/layout/style/nsCSSKeywordList.h index 933ff6e7b..94968faca 100644 --- a/layout/style/nsCSSKeywordList.h +++ b/layout/style/nsCSSKeywordList.h @@ -238,6 +238,7 @@ CSS_KEY(disc, disc) CSS_KEY(disclosure-closed, disclosure_closed) CSS_KEY(disclosure-open, disclosure_open) CSS_KEY(discretionary-ligatures, discretionary_ligatures) +CSS_KEY(distribute, distribute) CSS_KEY(dot, dot) CSS_KEY(dotted, dotted) CSS_KEY(double, double) @@ -333,7 +334,8 @@ CSS_KEY(inline-start, inline_start) CSS_KEY(inline-table, inline_table) CSS_KEY(inset, inset) CSS_KEY(inside, inside) -// CSS_KEY(inter-character, inter_character) // TODO see bug 1055672 +CSS_KEY(inter-character, inter_character) +CSS_KEY(inter-word, inter_word) CSS_KEY(interpolatematrix, interpolatematrix) CSS_KEY(intersect, intersect) CSS_KEY(isolate, isolate) diff --git a/layout/style/nsCSSPropList.h b/layout/style/nsCSSPropList.h index 6931d8c2b..b04921dcb 100644 --- a/layout/style/nsCSSPropList.h +++ b/layout/style/nsCSSPropList.h @@ -4027,6 +4027,17 @@ CSS_PROP_TEXT( nullptr, offsetof(nsStyleText, mTextIndent), eStyleAnimType_Coord) +CSS_PROP_TEXT( + text-justify, + text_justify, + TextJustify, + CSS_PROPERTY_PARSE_VALUE | + CSS_PROPERTY_APPLIES_TO_PLACEHOLDER, + "layout.css.text-justify.enabled", + VARIANT_HK, + kTextJustifyKTable, + CSS_PROP_NO_OFFSET, + eStyleAnimType_Discrete) CSS_PROP_VISIBILITY( text-orientation, text_orientation, diff --git a/layout/style/nsCSSProps.cpp b/layout/style/nsCSSProps.cpp index f3a7f898d..9805eae14 100644 --- a/layout/style/nsCSSProps.cpp +++ b/layout/style/nsCSSProps.cpp @@ -2035,6 +2035,17 @@ KTableEntry nsCSSProps::kTextAlignLastKTable[] = { { eCSSKeyword_UNKNOWN, -1 } }; +const KTableEntry nsCSSProps::kTextJustifyKTable[] = { + { eCSSKeyword_none, StyleTextJustify::None }, + { eCSSKeyword_auto, StyleTextJustify::Auto }, + { eCSSKeyword_inter_word, StyleTextJustify::InterWord }, + { eCSSKeyword_inter_character, StyleTextJustify::InterCharacter }, + // For legacy reasons, UAs must also support the keyword "distribute" with + // the exact same meaning and behavior as "inter-character". + { eCSSKeyword_distribute, StyleTextJustify::InterCharacter }, + { eCSSKeyword_UNKNOWN, -1 } +}; + const KTableEntry nsCSSProps::kTextCombineUprightKTable[] = { { eCSSKeyword_none, NS_STYLE_TEXT_COMBINE_UPRIGHT_NONE }, { eCSSKeyword_all, NS_STYLE_TEXT_COMBINE_UPRIGHT_ALL }, diff --git a/layout/style/nsCSSProps.h b/layout/style/nsCSSProps.h index ab78e6174..dfe35afd8 100644 --- a/layout/style/nsCSSProps.h +++ b/layout/style/nsCSSProps.h @@ -869,6 +869,7 @@ public: static const KTableEntry kTextEmphasisPositionKTable[]; static const KTableEntry kTextEmphasisStyleFillKTable[]; static const KTableEntry kTextEmphasisStyleShapeKTable[]; + static const KTableEntry kTextJustifyKTable[]; static const KTableEntry kTextOrientationKTable[]; static const KTableEntry kTextOverflowKTable[]; static const KTableEntry kTextTransformKTable[]; diff --git a/layout/style/nsComputedDOMStyle.cpp b/layout/style/nsComputedDOMStyle.cpp index 4eb24b76b..4f8d3edf6 100644 --- a/layout/style/nsComputedDOMStyle.cpp +++ b/layout/style/nsComputedDOMStyle.cpp @@ -3875,6 +3875,16 @@ nsComputedDOMStyle::DoGetTextIndent() } already_AddRefed<CSSValue> +nsComputedDOMStyle::DoGetTextJustify() +{ + RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue; + val->SetIdent( + nsCSSProps::ValueToKeywordEnum(StyleText()->mTextJustify, + nsCSSProps::kTextJustifyKTable)); + return val.forget(); +} + +already_AddRefed<CSSValue> nsComputedDOMStyle::DoGetTextOrientation() { RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue; diff --git a/layout/style/nsComputedDOMStyle.h b/layout/style/nsComputedDOMStyle.h index 223b29a14..27e2086e9 100644 --- a/layout/style/nsComputedDOMStyle.h +++ b/layout/style/nsComputedDOMStyle.h @@ -421,6 +421,7 @@ private: already_AddRefed<CSSValue> DoGetTextEmphasisPosition(); already_AddRefed<CSSValue> DoGetTextEmphasisStyle(); already_AddRefed<CSSValue> DoGetTextIndent(); + already_AddRefed<CSSValue> DoGetTextJustify(); already_AddRefed<CSSValue> DoGetTextOrientation(); already_AddRefed<CSSValue> DoGetTextOverflow(); already_AddRefed<CSSValue> DoGetTextTransform(); diff --git a/layout/style/nsComputedDOMStylePropertyList.h b/layout/style/nsComputedDOMStylePropertyList.h index 7c0457e34..1983208ac 100644 --- a/layout/style/nsComputedDOMStylePropertyList.h +++ b/layout/style/nsComputedDOMStylePropertyList.h @@ -239,6 +239,7 @@ COMPUTED_STYLE_PROP(text_emphasis_color, TextEmphasisColor) COMPUTED_STYLE_PROP(text_emphasis_position, TextEmphasisPosition) COMPUTED_STYLE_PROP(text_emphasis_style, TextEmphasisStyle) COMPUTED_STYLE_PROP(text_indent, TextIndent) +COMPUTED_STYLE_PROP(text_justify, TextJustify) COMPUTED_STYLE_PROP(text_orientation, TextOrientation) COMPUTED_STYLE_PROP(text_overflow, TextOverflow) COMPUTED_STYLE_PROP(text_shadow, TextShadow) diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index fa29fe0f1..9b9fc3948 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -1414,6 +1414,7 @@ struct SetEnumValueHelper DEFINE_ENUM_CLASS_SETTER(StyleFillRule, Nonzero, Evenodd) DEFINE_ENUM_CLASS_SETTER(StyleFloat, None, InlineEnd) DEFINE_ENUM_CLASS_SETTER(StyleFloatEdge, ContentBox, MarginBox) + DEFINE_ENUM_CLASS_SETTER(StyleTextJustify, None, InterCharacter) DEFINE_ENUM_CLASS_SETTER(StyleUserFocus, None, SelectMenu) DEFINE_ENUM_CLASS_SETTER(StyleUserSelect, None, MozText) DEFINE_ENUM_CLASS_SETTER(StyleUserInput, None, Auto) @@ -4783,6 +4784,12 @@ nsRuleNode::ComputeTextData(void* aStartStruct, SETCOORD_UNSET_INHERIT, aContext, mPresContext, conditions); + // text-justify: enum, inherit, initial + SetValue(*aRuleData->ValueForTextJustify(), text->mTextJustify, conditions, + SETVAL_ENUMERATED | SETVAL_UNSET_INHERIT, + parentText->mTextJustify, + StyleTextJustify::Auto); + // text-transform: enum, inherit, initial SetValue(*aRuleData->ValueForTextTransform(), text->mTextTransform, conditions, SETVAL_ENUMERATED | SETVAL_UNSET_INHERIT, diff --git a/layout/style/nsStyleConsts.h b/layout/style/nsStyleConsts.h index ee78dcb64..be588113e 100644 --- a/layout/style/nsStyleConsts.h +++ b/layout/style/nsStyleConsts.h @@ -185,6 +185,14 @@ enum class StyleShapeSourceType : uint8_t { Box, }; +// text-justify +enum class StyleTextJustify : uint8_t { + None, + Auto, + InterWord, + InterCharacter, +}; + // user-focus enum class StyleUserFocus : uint8_t { None, diff --git a/layout/style/nsStyleStruct.cpp b/layout/style/nsStyleStruct.cpp index 553239e0e..52491a288 100644 --- a/layout/style/nsStyleStruct.cpp +++ b/layout/style/nsStyleStruct.cpp @@ -3788,6 +3788,7 @@ nsStyleText::nsStyleText(StyleStructContext aContext) , mTextAlignLast(NS_STYLE_TEXT_ALIGN_AUTO) , mTextAlignTrue(false) , mTextAlignLastTrue(false) + , mTextJustify(StyleTextJustify::Auto) , mTextTransform(NS_STYLE_TEXT_TRANSFORM_NONE) , mWhiteSpace(NS_STYLE_WHITESPACE_NORMAL) , mWordBreak(NS_STYLE_WORDBREAK_NORMAL) @@ -3824,6 +3825,7 @@ nsStyleText::nsStyleText(const nsStyleText& aSource) , mTextAlignLast(aSource.mTextAlignLast) , mTextAlignTrue(false) , mTextAlignLastTrue(false) + , mTextJustify(aSource.mTextJustify) , mTextTransform(aSource.mTextTransform) , mWhiteSpace(aSource.mWhiteSpace) , mWordBreak(aSource.mWordBreak) @@ -3885,6 +3887,7 @@ nsStyleText::CalcDifference(const nsStyleText& aNewData) const (mTextSizeAdjust != aNewData.mTextSizeAdjust) || (mLetterSpacing != aNewData.mLetterSpacing) || (mLineHeight != aNewData.mLineHeight) || + (mTextJustify != aNewData.mTextJustify) || (mTextIndent != aNewData.mTextIndent) || (mWordSpacing != aNewData.mWordSpacing) || (mTabSize != aNewData.mTabSize)) { diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h index 05a6be91e..1cadea840 100644 --- a/layout/style/nsStyleStruct.h +++ b/layout/style/nsStyleStruct.h @@ -2071,6 +2071,7 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleText uint8_t mTextAlignLast; // [inherited] see nsStyleConsts.h bool mTextAlignTrue : 1; // [inherited] see nsStyleConsts.h bool mTextAlignLastTrue : 1; // [inherited] see nsStyleConsts.h + mozilla::StyleTextJustify mTextJustify; // [inherited] uint8_t mTextTransform; // [inherited] see nsStyleConsts.h uint8_t mWhiteSpace; // [inherited] see nsStyleConsts.h uint8_t mWordBreak; // [inherited] see nsStyleConsts.h diff --git a/layout/style/nsTransitionManager.cpp b/layout/style/nsTransitionManager.cpp index 4a1a5b7ad..118702e8f 100644 --- a/layout/style/nsTransitionManager.cpp +++ b/layout/style/nsTransitionManager.cpp @@ -46,8 +46,6 @@ using mozilla::dom::KeyframeEffectReadOnly; using namespace mozilla; using namespace mozilla::css; -typedef mozilla::ComputedTiming::AnimationPhase AnimationPhase; - namespace { struct TransitionEventParams { EventMessage mMessage; @@ -180,10 +178,9 @@ CSSTransition::UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) } void -CSSTransition::QueueEvents() +CSSTransition::QueueEvents(StickyTimeDuration aActiveTime) { - if (!mEffect || - !mOwningElement.IsSet()) { + if (!mOwningElement.IsSet()) { return; } @@ -197,69 +194,123 @@ CSSTransition::QueueEvents() return; } - ComputedTiming computedTiming = mEffect->GetComputedTiming(); - const StickyTimeDuration zeroDuration; - StickyTimeDuration intervalStartTime = - std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay), - computedTiming.mActiveDuration), zeroDuration); - StickyTimeDuration intervalEndTime = - std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay), - computedTiming.mActiveDuration), zeroDuration); + const StickyTimeDuration zeroDuration = StickyTimeDuration(); + + TransitionPhase currentPhase; + StickyTimeDuration intervalStartTime; + StickyTimeDuration intervalEndTime; + + if (!mEffect) { + currentPhase = GetAnimationPhaseWithoutEffect<TransitionPhase>(*this); + intervalStartTime = zeroDuration; + intervalEndTime = zeroDuration; + } else { + ComputedTiming computedTiming = mEffect->GetComputedTiming(); + + currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase); + intervalStartTime = + std::max(std::min(StickyTimeDuration(-mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), zeroDuration); + intervalEndTime = + std::max(std::min((EffectEnd() - mEffect->SpecifiedTiming().mDelay), + computedTiming.mActiveDuration), zeroDuration); + } // TimeStamps to use for ordering the events when they are dispatched. We // use a TimeStamp so we can compare events produced by different elements, // perhaps even with different timelines. // The zero timestamp is for transitionrun events where we ignore the delay // for the purpose of ordering events. - TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); - TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); + TimeStamp zeroTimeStamp = AnimationTimeToTimeStamp(zeroDuration); + TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime); + TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime); - TransitionPhase currentPhase; if (mPendingState != PendingState::NotPending && (mPreviousTransitionPhase == TransitionPhase::Idle || mPreviousTransitionPhase == TransitionPhase::Pending)) { currentPhase = TransitionPhase::Pending; - } else { - currentPhase = static_cast<TransitionPhase>(computedTiming.mPhase); } AutoTArray<TransitionEventParams, 3> events; + + // Handle cancel events firts + if (mPreviousTransitionPhase != TransitionPhase::Idle && + currentPhase == TransitionPhase::Idle) { + TimeStamp activeTimeStamp = ElapsedTimeToTimeStamp(aActiveTime); + events.AppendElement(TransitionEventParams{ eTransitionCancel, + aActiveTime, + activeTimeStamp }); + } + + // All other events switch (mPreviousTransitionPhase) { case TransitionPhase::Idle: - if (currentPhase == TransitionPhase::After) { + if (currentPhase == TransitionPhase::Pending || + currentPhase == TransitionPhase::Before) { + events.AppendElement(TransitionEventParams{ eTransitionRun, + intervalStartTime, + zeroTimeStamp }); + } else if (currentPhase == TransitionPhase::Active) { + events.AppendElement(TransitionEventParams{ eTransitionRun, + intervalStartTime, + zeroTimeStamp }); + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == TransitionPhase::After) { + events.AppendElement(TransitionEventParams{ eTransitionRun, + intervalStartTime, + zeroTimeStamp }); + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalEndTime, - endTimeStamp }); + intervalEndTime, + endTimeStamp }); } break; case TransitionPhase::Pending: case TransitionPhase::Before: - if (currentPhase == TransitionPhase::After) { + if (currentPhase == TransitionPhase::Active) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); + } else if (currentPhase == TransitionPhase::After) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalStartTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalEndTime, - endTimeStamp }); + intervalEndTime, + endTimeStamp }); } break; case TransitionPhase::Active: if (currentPhase == TransitionPhase::After) { events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalEndTime, - endTimeStamp }); + intervalEndTime, + endTimeStamp }); } else if (currentPhase == TransitionPhase::Before) { events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalStartTime, - startTimeStamp }); + intervalStartTime, + startTimeStamp }); } break; case TransitionPhase::After: - if (currentPhase == TransitionPhase::Before) { + if (currentPhase == TransitionPhase::Active) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalEndTime, + startTimeStamp }); + } else if (currentPhase == TransitionPhase::Before) { + events.AppendElement(TransitionEventParams{ eTransitionStart, + intervalEndTime, + startTimeStamp }); events.AppendElement(TransitionEventParams{ eTransitionEnd, - intervalStartTime, - endTimeStamp }); + intervalStartTime, + endTimeStamp }); } break; } diff --git a/layout/style/nsTransitionManager.h b/layout/style/nsTransitionManager.h index 56ec61572..1c48cc8cd 100644 --- a/layout/style/nsTransitionManager.h +++ b/layout/style/nsTransitionManager.h @@ -214,6 +214,10 @@ public: const TimeDuration& aStartTime, double aPlaybackRate); + void MaybeQueueCancelEvent(StickyTimeDuration aActiveTime) override { + QueueEvents(aActiveTime); + } + protected: virtual ~CSSTransition() { @@ -225,7 +229,10 @@ protected: void UpdateTiming(SeekFlag aSeekFlag, SyncNotifyFlag aSyncNotifyFlag) override; - void QueueEvents(); + void QueueEvents(StickyTimeDuration activeTime = StickyTimeDuration()); + + + enum class TransitionPhase; // The (pseudo-)element whose computed transition-property refers to this // transition (if any). @@ -250,7 +257,7 @@ protected: // to be queued on this tick. // See: https://drafts.csswg.org/css-transitions-2/#transition-phase enum class TransitionPhase { - Idle = static_cast<int>(ComputedTiming::AnimationPhase::Null), + Idle = static_cast<int>(ComputedTiming::AnimationPhase::Idle), Before = static_cast<int>(ComputedTiming::AnimationPhase::Before), Active = static_cast<int>(ComputedTiming::AnimationPhase::Active), After = static_cast<int>(ComputedTiming::AnimationPhase::After), diff --git a/layout/style/test/property_database.js b/layout/style/test/property_database.js index 62d413d98..272931c15 100644 --- a/layout/style/test/property_database.js +++ b/layout/style/test/property_database.js @@ -5694,6 +5694,17 @@ if (IsCSSPropertyPrefEnabled("layout.css.text-combine-upright.enabled")) { } } +if (IsCSSPropertyPrefEnabled("layout.css.text-justify.enabled")) { + gCSSProperties["text-justify"] = { + domProp: "textJustify", + inherited: true, + type: CSS_TYPE_LONGHAND, + initial_values: [ "auto" ], + other_values: [ "none", "inter-word", "inter-character", "distribute" ], + invalid_values: [] + }; +} + if (IsCSSPropertyPrefEnabled("svg.paint-order.enabled")) { gCSSProperties["paint-order"] = { domProp: "paintOrder", diff --git a/layout/style/test/test_animations.html b/layout/style/test/test_animations.html index eaccba122..4019af77f 100644 --- a/layout/style/test/test_animations.html +++ b/layout/style/test/test_animations.html @@ -1195,9 +1195,6 @@ is_approx(px_to_num(cs.marginRight), 100 * gTF.ease_in(0.4), 0.01, "large negative delay test at 0ms"); check_events([{ type: 'animationstart', target: div, animationName: 'anim2', elapsedTime: 3.6, - pseudoElement: "" }, - { type: 'animationiteration', target: div, - animationName: 'anim2', elapsedTime: 3.6, pseudoElement: "" }], "right after start in large negative delay test"); advance_clock(380); diff --git a/layout/style/test/test_animations_event_handler_attribute.html b/layout/style/test/test_animations_event_handler_attribute.html index e5def2b34..036a77779 100644 --- a/layout/style/test/test_animations_event_handler_attribute.html +++ b/layout/style/test/test_animations_event_handler_attribute.html @@ -88,21 +88,55 @@ checkReceivedEvents("animationend", targets); targets.forEach(div => { div.remove(); }); -// 2. Test CSS Transition event handlers. +// 2a. Test CSS Transition event handlers (without transitioncancel) -var targets = createAndRegisterTargets([ 'ontransitionend' ]); +var targets = createAndRegisterTargets([ 'ontransitionrun', + 'ontransitionstart', + 'ontransitionend', + 'ontransitioncancel' ]); targets.forEach(div => { - div.style.transition = 'margin-left 100ms'; + div.style.transition = 'margin-left 100ms 200ms'; getComputedStyle(div).marginLeft; // flush div.style.marginLeft = "200px"; getComputedStyle(div).marginLeft; // flush }); +advance_clock(0); +checkReceivedEvents("transitionrun", targets); + +advance_clock(200); +checkReceivedEvents("transitionstart", targets); + advance_clock(100); checkReceivedEvents("transitionend", targets); targets.forEach(div => { div.remove(); }); +// 2b. Test CSS Transition cancel event handler. + +var targets = createAndRegisterTargets([ 'ontransitioncancel' ]); +targets.forEach(div => { + div.style.transition = 'margin-left 100ms 200ms'; + getComputedStyle(div).marginLeft; // flush + div.style.marginLeft = "200px"; + getComputedStyle(div).marginLeft; // flush +}); + +advance_clock(200); + +targets.forEach(div => { + div.style.display = "none" +}); +getComputedStyle(targets[0]).display; // flush + +advance_clock(0); +checkReceivedEvents("transitioncancel", targets); + +advance_clock(100); +targets.forEach( div => { is(div.receivedEventType, undefined); }); + +targets.forEach(div => { div.remove(); }); + // 3. Test prefixed CSS Animation event handlers. var targets = createAndRegisterTargets([ 'onwebkitanimationstart', diff --git a/layout/style/test/test_animations_event_order.html b/layout/style/test/test_animations_event_order.html index 5af7639cc..7204934d2 100644 --- a/layout/style/test/test_animations_event_order.html +++ b/layout/style/test/test_animations_event_order.html @@ -46,7 +46,10 @@ var gDisplay = document.getElementById('display'); [ 'animationstart', 'animationiteration', 'animationend', - 'transitionend' ] + 'transitionrun', + 'transitionstart', + 'transitionend', + 'transitioncancel' ] .forEach(event => gDisplay.addEventListener(event, event => gEventsReceived.push(event), @@ -322,9 +325,13 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[1], 'transitionend' ], - 'Simultaneous transitionend on siblings'); + 'Simultaneous transitionrun/start/end on siblings'); divs.forEach(div => div.remove()); divs = []; @@ -360,10 +367,16 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[2], 'transitionrun' ], + [ divs[2], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[2], 'transitionend' ], [ divs[1], 'transitionend' ], - 'Simultaneous transitionend on children'); + 'Simultaneous transitionrun/start/end on children'); divs.forEach(div => div.remove()); divs = []; @@ -408,11 +421,19 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[0], '::before', 'transitionrun' ], + [ divs[0], '::before', 'transitionstart' ], + [ divs[0], '::after', 'transitionrun' ], + [ divs[0], '::after', 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[0], '::before', 'transitionend' ], [ divs[0], '::after', 'transitionend' ], [ divs[1], 'transitionend' ], - 'Simultaneous transitionend on pseudo-elements'); + 'Simultaneous transitionrun/start/end on pseudo-elements'); divs.forEach(div => div.remove()); divs = []; @@ -441,9 +462,13 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[1], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[1], 'transitionend' ], [ divs[0], 'transitionend' ], - 'Sorting of transitionend events by time'); + 'Sorting of transitionrun/start/end events by time'); divs.forEach(div => div.remove()); divs = []; @@ -468,9 +493,13 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10 * 1000); -checkEventOrder([ divs[1], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionend' ], [ divs[0], 'transitionend' ], - 'Sorting of transitionend events by time' + + 'Sorting of transitionrun/start/end events by time' + '(including delay)'); divs.forEach(div => div.remove()); @@ -492,9 +521,14 @@ getComputedStyle(div).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ 'margin-left', 'transitionend' ], +checkEventOrder([ 'margin-left', 'transitionrun' ], + [ 'margin-left', 'transitionstart' ], + [ 'opacity', 'transitionrun' ], + [ 'opacity', 'transitionstart' ], + [ 'margin-left', 'transitionend' ], [ 'opacity', 'transitionend' ], - 'Sorting of transitionend events by transition-property') + 'Sorting of transitionrun/start/end events by ' + + 'transition-property') div.remove(); div = undefined; @@ -519,7 +553,11 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ divs[0], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionend' ], [ divs[1], 'transitionend' ], 'Transition events are sorted by document position first, ' + 'before transition-property'); @@ -543,7 +581,11 @@ getComputedStyle(div).marginLeft; advance_clock(0); advance_clock(10000); -checkEventOrder([ 'opacity', 'transitionend' ], +checkEventOrder([ 'margin-left', 'transitionrun' ], + [ 'margin-left', 'transitionstart' ], + [ 'opacity', 'transitionrun' ], + [ 'opacity', 'transitionstart' ], + [ 'opacity', 'transitionend' ], [ 'margin-left', 'transitionend' ], 'Transition events are sorted by time first, before ' + 'transition-property'); @@ -571,9 +613,50 @@ getComputedStyle(divs[0]).marginLeft; advance_clock(0); advance_clock(15 * 1000); -checkEventOrder([ divs[1], 'transitionend' ], +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitionend' ], [ divs[0], 'transitionend' ], - 'Simultaneous transitionend on siblings'); + 'Simultaneous transitionrun/start/end on siblings'); + +divs.forEach(div => div.remove()); +divs = []; + +// 4j. Test sorting transitions with cancel +// The order of transitioncancel is based on StyleManager. +// So this test looks like wrong result at a glance. However +// the gecko will cancel div1's transition before div2 in this case. + +divs = [ document.createElement('div'), + document.createElement('div') ]; +divs.forEach((div, i) => { + gDisplay.appendChild(div); + div.style.marginLeft = '0px'; + div.setAttribute('id', 'div' + i); +}); + +divs[0].style.transition = 'margin-left 10s 5s'; +divs[1].style.transition = 'margin-left 10s'; + +getComputedStyle(divs[0]).marginLeft; +divs.forEach(div => div.style.marginLeft = '100px'); +getComputedStyle(divs[0]).marginLeft; + +advance_clock(0); +advance_clock(5 * 1000); +divs.forEach(div => div.style.display = 'none' ); +getComputedStyle(divs[0]).display; +advance_clock(10 * 1000); + +checkEventOrder([ divs[0], 'transitionrun' ], + [ divs[1], 'transitionrun' ], + [ divs[1], 'transitionstart' ], + [ divs[0], 'transitionstart' ], + [ divs[1], 'transitioncancel' ], + [ divs[0], 'transitioncancel' ], + 'Simultaneous transitionrun/start/cancel on siblings'); divs.forEach(div => div.remove()); divs = []; diff --git a/layout/style/test/test_animations_omta.html b/layout/style/test/test_animations_omta.html index 4b276c896..0b2a61ecc 100644 --- a/layout/style/test/test_animations_omta.html +++ b/layout/style/test/test_animations_omta.html @@ -1408,9 +1408,6 @@ addAsyncAnimTest(function *() { "large negative delay test at 0ms"); check_events([{ type: 'animationstart', target: gDiv, animationName: 'anim2', elapsedTime: 3.6, - pseudoElement: "" }, - { type: 'animationiteration', target: gDiv, - animationName: 'anim2', elapsedTime: 3.6, pseudoElement: "" }], "right after start in large negative delay test"); advance_clock(380); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 4239c83bd..bf7626391 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -2516,6 +2516,9 @@ pref("layout.css.convertFromNode.enabled", true); // Is support for CSS "text-align: unsafe X" enabled? pref("layout.css.text-align-unsafe-value.enabled", false); +// Is support for CSS text-justify property enabled? +pref("layout.css.text-justify.enabled", true); + // Is support for CSS "float: inline-{start,end}" and // "clear: inline-{start,end}" enabled? #if defined(MOZ_B2G) || !defined(RELEASE_OR_BETA) diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js index ac2c1e077..d30ae89f5 100644 --- a/testing/profiles/prefs_general.js +++ b/testing/profiles/prefs_general.js @@ -182,6 +182,9 @@ user_pref("layout.css.prefixes.device-pixel-ratio-webkit", true); // Enable CSS shape-outside for testing user_pref("layout.css.shape-outside.enabled", true); +// Enable CSS text-justify for testing +user_pref("layout.css.text-justify.enabled", true); + // Disable spammy layout warnings because they pollute test logs user_pref("layout.spammy_warnings.enabled", false); diff --git a/toolkit/content/widgets/datetimepopup.xml b/toolkit/content/widgets/datetimepopup.xml index b4335e1ce..19e7b4f8a 100644 --- a/toolkit/content/widgets/datetimepopup.xml +++ b/toolkit/content/widgets/datetimepopup.xml @@ -26,8 +26,8 @@ this.l10n = {}; const mozIntl = Components.classes["@mozilla.org/mozintl;1"] .getService(Components.interfaces.mozIMozIntl); - mozIntl.addGetCalendarInfo(l10n); - mozIntl.addGetDisplayNames(l10n); + mozIntl.addGetCalendarInfo(this.l10n); + mozIntl.addGetDisplayNames(this.l10n); // Notify DateTimePickerHelper.jsm that binding is ready. this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady")); ]]></constructor> diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm index 939e2e269..8d742ea42 100644 --- a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm +++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm @@ -766,8 +766,17 @@ this.AddonUpdateChecker = { * down in-progress update requests */ checkForUpdates: function AUC_checkForUpdates(aId, aUpdateKey, aUrl, aObserver) { - // Exclude default theme - if (aId != "{972ce4c6-7e08-4474-a285-3208198ce6fd}") + // Define an array of internally used IDs to NOT send to AUS such as the + // Default Theme. Please keep this list in sync with: + // toolkit/mozapps/webextensions/AddonUpdateChecker.jsm + let internalIDS = [ + '{972ce4c6-7e08-4474-a285-3208198ce6fd}', + 'modern@themes.mozilla.org' + ]; + + // If the ID is not in the array then go ahead and query AUS + if (internalIDS.indexOf(aId) == -1) { return new UpdateParser(aId, aUpdateKey, aUrl, aObserver); + } } }; diff --git a/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm index 918ba5328..bdd3a81e7 100644 --- a/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm +++ b/toolkit/mozapps/webextensions/internal/AddonUpdateChecker.jsm @@ -929,8 +929,17 @@ this.AddonUpdateChecker = { * down in-progress update requests */ checkForUpdates: function(aId, aUpdateKey, aUrl, aObserver) { - // Exclude default theme - if (aId != "{972ce4c6-7e08-4474-a285-3208198ce6fd}") + // Define an array of internally used IDs to NOT send to AUS such as the + // Default Theme. Please keep this list in sync with: + // toolkit/mozapps/extensions/AddonUpdateChecker.jsm + let internalIDS = [ + '{972ce4c6-7e08-4474-a285-3208198ce6fd}', + 'modern@themes.mozilla.org' + ]; + + // If the ID is not in the array then go ahead and query AUS + if (internalIDS.indexOf(aId) == -1) { return new UpdateParser(aId, aUpdateKey, aUrl, aObserver); + } } }; diff --git a/widget/BasicEvents.h b/widget/BasicEvents.h index da8d819ef..a6228f179 100644 --- a/widget/BasicEvents.h +++ b/widget/BasicEvents.h @@ -585,6 +585,7 @@ public: case eMouseEventClass: mFlags.mComposed = mMessage == eMouseClick || mMessage == eMouseDoubleClick || + mMessage == eMouseAuxClick || mMessage == eMouseDown || mMessage == eMouseUp || mMessage == eMouseEnter || mMessage == eMouseLeave || mMessage == eMouseOver || mMessage == eMouseOut || diff --git a/widget/EventMessageList.h b/widget/EventMessageList.h index 7fe642637..2eed7e618 100644 --- a/widget/EventMessageList.h +++ b/widget/EventMessageList.h @@ -84,6 +84,7 @@ NS_EVENT_MESSAGE(eMouseEnterIntoWidget) NS_EVENT_MESSAGE(eMouseExitFromWidget) NS_EVENT_MESSAGE(eMouseDoubleClick) NS_EVENT_MESSAGE(eMouseClick) +NS_EVENT_MESSAGE(eMouseAuxClick) // eMouseActivate is fired when the widget is activated by a click. NS_EVENT_MESSAGE(eMouseActivate) NS_EVENT_MESSAGE(eMouseOver) @@ -340,9 +341,11 @@ NS_EVENT_MESSAGE(eScrolledAreaChanged) NS_EVENT_MESSAGE(eTransitionStart) NS_EVENT_MESSAGE(eTransitionRun) NS_EVENT_MESSAGE(eTransitionEnd) +NS_EVENT_MESSAGE(eTransitionCancel) NS_EVENT_MESSAGE(eAnimationStart) NS_EVENT_MESSAGE(eAnimationEnd) NS_EVENT_MESSAGE(eAnimationIteration) +NS_EVENT_MESSAGE(eAnimationCancel) // Webkit-prefixed versions of Transition & Animation events, for web compat: NS_EVENT_MESSAGE(eWebkitTransitionEnd) diff --git a/widget/WidgetEventImpl.cpp b/widget/WidgetEventImpl.cpp index 52e2b9b40..7dd292cb0 100644 --- a/widget/WidgetEventImpl.cpp +++ b/widget/WidgetEventImpl.cpp @@ -236,6 +236,7 @@ WidgetEvent::HasMouseEventMessage() const case eMouseUp: case eMouseClick: case eMouseDoubleClick: + case eMouseAuxClick: case eMouseEnterIntoWidget: case eMouseExitFromWidget: case eMouseActivate: diff --git a/widget/nsBaseWidget.cpp b/widget/nsBaseWidget.cpp index b820fed3c..909660f71 100644 --- a/widget/nsBaseWidget.cpp +++ b/widget/nsBaseWidget.cpp @@ -3086,6 +3086,7 @@ case _value: eventName.AssignLiteral(_name) ; break _ASSIGN_eventName(eMouseDown,"eMouseDown"); _ASSIGN_eventName(eMouseUp,"eMouseUp"); _ASSIGN_eventName(eMouseClick,"eMouseClick"); + _ASSIGN_eventName(eMouseAuxClick,"eMouseAuxClick"); _ASSIGN_eventName(eMouseDoubleClick,"eMouseDoubleClick"); _ASSIGN_eventName(eMouseMove,"eMouseMove"); _ASSIGN_eventName(eLoad,"eLoad"); diff --git a/widget/windows/WinUtils.cpp b/widget/windows/WinUtils.cpp index 0a57ad439..bd42e78f6 100644 --- a/widget/windows/WinUtils.cpp +++ b/widget/windows/WinUtils.cpp @@ -1138,7 +1138,8 @@ WinUtils::GetIsMouseFromTouch(EventMessage aEventMessage) const uint32_t MOZ_T_I_SIGNATURE = TABLET_INK_TOUCH | TABLET_INK_SIGNATURE; const uint32_t MOZ_T_I_CHECK_TCH = TABLET_INK_TOUCH | TABLET_INK_CHECK; return ((aEventMessage == eMouseMove || aEventMessage == eMouseDown || - aEventMessage == eMouseUp || aEventMessage == eMouseDoubleClick) && + aEventMessage == eMouseUp || aEventMessage == eMouseAuxClick || + aEventMessage == eMouseDoubleClick) && (GetMessageExtraInfo() & MOZ_T_I_SIGNATURE) == MOZ_T_I_CHECK_TCH); } |