Bug 1243846 - Implement Intersection Observer API. r=mrbkap, r=mstange
☠☠ backed out by a1832a34a55f ☠ ☠
authorTobias Schneider <schneider@jancona.com>
Wed, 12 Oct 2016 20:15:16 -0700
changeset 362465 c55a20f1422bc61b290cbbc2f1630b08d437875c
parent 362464 0dba8df812863e399430411aab12869bef5ce993
child 362466 1e11858040882eaeb1ceeb3d89e67f50d7982ea2
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-beta@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap, mstange
bugs1243846
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1243846 - Implement Intersection Observer API. r=mrbkap, r=mstange
dom/base/DOMIntersectionObserver.cpp
dom/base/DOMIntersectionObserver.h
dom/base/Element.cpp
dom/base/Element.h
dom/base/FragmentOrElement.h
dom/base/moz.build
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsIDocument.h
dom/base/test/intersectionobserver_subframe.html
dom/base/test/mochitest.ini
dom/base/test/test_intersectionobservers.html
dom/bindings/Bindings.conf
dom/bindings/Errors.msg
dom/webidl/DOMRect.webidl
dom/webidl/IntersectionObserver.webidl
dom/webidl/moz.build
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/style/nsCSSParser.cpp
layout/style/nsCSSParser.h
layout/style/nsCSSPropertyID.h
layout/style/nsCSSValue.cpp
layout/style/nsCSSValue.h
new file mode 100644
--- /dev/null
+++ b/dom/base/DOMIntersectionObserver.cpp
@@ -0,0 +1,448 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DOMIntersectionObserver.h"
+#include "nsCSSParser.h"
+#include "nsCSSPropertyID.h"
+#include "nsIFrame.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserverEntry)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserverEntry)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserverEntry)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DOMIntersectionObserverEntry, mOwner,
+                                      mRootBounds, mBoundingClientRect,
+                                      mIntersectionRect, mTarget)
+
+double
+DOMIntersectionObserverEntry::IntersectionRatio()
+{
+  double targetArea = mBoundingClientRect->Width() * mBoundingClientRect->Height();
+  double intersectionArea = mIntersectionRect->Width() * mIntersectionRect->Height();
+  double intersectionRatio = targetArea > 0.0 ? intersectionArea / targetArea : 0.0;
+  return intersectionRatio;
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMIntersectionObserver)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+  NS_INTERFACE_MAP_ENTRY(DOMIntersectionObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMIntersectionObserver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(DOMIntersectionObserver)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DOMIntersectionObserver)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mRoot)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DOMIntersectionObserver)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRoot)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+already_AddRefed<DOMIntersectionObserver>
+DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
+                                     mozilla::dom::IntersectionCallback& aCb,
+                                     mozilla::ErrorResult& aRv)
+{
+  return Constructor(aGlobal, aCb, IntersectionObserverInit(), aRv);
+}
+
+already_AddRefed<DOMIntersectionObserver>
+DOMIntersectionObserver::Constructor(const mozilla::dom::GlobalObject& aGlobal,
+                                     mozilla::dom::IntersectionCallback& aCb,
+                                     const mozilla::dom::IntersectionObserverInit& aOptions,
+                                     mozilla::ErrorResult& aRv)
+{
+  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
+  if (!window) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+  RefPtr<DOMIntersectionObserver> observer =
+    new DOMIntersectionObserver(window.forget(), aCb);
+
+  observer->mRoot = aOptions.mRoot;
+
+  if (!observer->SetRootMargin(aOptions.mRootMargin)) {
+    aRv.ThrowDOMException(NS_ERROR_DOM_SYNTAX_ERR,
+      NS_LITERAL_CSTRING("rootMargin must be specified in pixels or percent."));
+    return nullptr;
+  }
+
+  if (aOptions.mThreshold.IsDoubleSequence()) {
+    const mozilla::dom::Sequence<double>& thresholds = aOptions.mThreshold.GetAsDoubleSequence();
+    observer->mThresholds.SetCapacity(thresholds.Length());
+    for (const auto& thresh : thresholds) {
+      if (thresh < 0.0 || thresh > 1.0) {
+        aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
+        return nullptr;
+      }
+      observer->mThresholds.AppendElement(thresh);
+    }
+    observer->mThresholds.Sort();
+  } else {
+    double thresh = aOptions.mThreshold.GetAsDouble();
+    if (thresh < 0.0 || thresh > 1.0) {
+      aRv.ThrowTypeError<dom::MSG_THRESHOLD_RANGE_ERROR>();
+      return nullptr;
+    }
+    observer->mThresholds.AppendElement(thresh);
+  }
+
+  return observer.forget();
+}
+
+bool
+DOMIntersectionObserver::SetRootMargin(const nsAString& aString)
+{
+  // By not passing a CSS Loader object we make sure we don't parse in quirks
+  // mode so that pixel/percent and unit-less values will be differentiated.
+  nsCSSParser parser(nullptr);
+  nsCSSValue value;
+  if (!parser.ParseMarginString(aString, nullptr, 0, value, true)) {
+    return false;
+  }
+
+  mRootMargin = value.GetRectValue();
+
+  for (uint32_t i = 0; i < ArrayLength(nsCSSRect::sides); ++i) {
+    nsCSSValue value = mRootMargin.*nsCSSRect::sides[i];
+    if (!(value.IsPixelLengthUnit() || value.IsPercentLengthUnit())) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+void
+DOMIntersectionObserver::GetRootMargin(mozilla::dom::DOMString& aRetVal)
+{
+  mRootMargin.AppendToString(eCSSProperty_DOM, aRetVal, nsCSSValue::eNormalized);
+}
+
+void
+DOMIntersectionObserver::GetThresholds(nsTArray<double>& aRetVal)
+{
+  aRetVal = mThresholds;
+}
+
+void
+DOMIntersectionObserver::Observe(Element& aTarget)
+{
+  if (mObservationTargets.Contains(&aTarget)) {
+    return;
+  }
+  aTarget.RegisterIntersectionObserver(this);
+  mObservationTargets.PutEntry(&aTarget);
+  Connect();
+}
+
+void
+DOMIntersectionObserver::Unobserve(Element& aTarget)
+{
+  if (!mObservationTargets.Contains(&aTarget)) {
+    return;
+  }
+  if (mObservationTargets.Count() == 1) {
+    Disconnect();
+    return;
+  }
+  aTarget.UnregisterIntersectionObserver(this);
+  mObservationTargets.RemoveEntry(&aTarget);
+}
+
+void
+DOMIntersectionObserver::Connect()
+{
+  if (mConnected) {
+    return;
+  }
+  nsIDocument* document = mOwner->GetExtantDoc();
+  document->AddIntersectionObserver(this);
+  mConnected = true;
+}
+
+void
+DOMIntersectionObserver::Disconnect()
+{
+  if (!mConnected) {
+    return;
+  }
+  for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
+    Element* target = iter.Get()->GetKey();
+    target->UnregisterIntersectionObserver(this);
+  }
+  mObservationTargets.Clear();
+  nsIDocument* document = mOwner->GetExtantDoc();
+  document->RemoveIntersectionObserver(this);
+  mConnected = false;
+}
+
+void
+DOMIntersectionObserver::TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal)
+{
+  aRetVal.SwapElements(mQueuedEntries);
+  mQueuedEntries.Clear();
+}
+
+static bool
+CheckSimilarOrigin(nsINode* aNode1, nsINode* aNode2)
+{
+  nsIPrincipal* principal1 = aNode1->NodePrincipal();
+  nsIPrincipal* principal2 = aNode2->NodePrincipal();
+  nsAutoCString baseDomain1;
+  nsAutoCString baseDomain2;
+
+  nsresult rv = principal1->GetBaseDomain(baseDomain1);
+  if (NS_FAILED(rv)) {
+    return principal1 == principal2;
+  }
+
+  rv = principal2->GetBaseDomain(baseDomain2);
+  if (NS_FAILED(rv)) {
+    return principal1 == principal2;
+  }
+
+  return baseDomain1 == baseDomain2;
+}
+
+static Maybe<nsRect>
+EdgeInclusiveIntersection(const nsRect& aRect, const nsRect& aOtherRect)
+{
+  nscoord left = std::max(aRect.x, aOtherRect.x);
+  nscoord top = std::max(aRect.y, aOtherRect.y);
+  nscoord right = std::min(aRect.XMost(), aOtherRect.XMost());
+  nscoord bottom = std::min(aRect.YMost(), aOtherRect.YMost());
+  if (left > right || top > bottom) {
+    return Nothing();
+  }
+  return Some(nsRect(left, top, right - left, bottom - top));
+}
+
+void
+DOMIntersectionObserver::Update(nsIDocument* aDocument, DOMHighResTimeStamp time)
+{
+  Element* root;
+  nsIFrame* rootFrame;
+  nsRect rootRect;
+
+  if (mRoot) {
+    root = mRoot;
+    rootFrame = root->GetPrimaryFrame();
+    if (rootFrame) {
+      if (rootFrame->GetType() == nsGkAtoms::scrollFrame) {
+        nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
+        rootRect = nsLayoutUtils::TransformFrameRectToAncestor(
+          rootFrame,
+          rootFrame->GetContentRectRelativeToSelf(),
+          scrollFrame->GetScrolledFrame());
+      } else {
+        rootRect = nsLayoutUtils::GetAllInFlowRectsUnion(rootFrame,
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
+          nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS);
+      }
+    }
+  } else {
+    nsCOMPtr<nsIPresShell> presShell = aDocument->GetShell();
+    if (presShell) {
+      rootFrame = presShell->GetRootScrollFrame();
+      nsPresContext* presContext = rootFrame->PresContext();
+      while (!presContext->IsRootContentDocument()) {
+        presContext = rootFrame->PresContext()->GetParentPresContext();
+        rootFrame = presContext->PresShell()->GetRootScrollFrame();
+      }
+      root = rootFrame->GetContent()->AsElement();
+      nsIScrollableFrame* scrollFrame = do_QueryFrame(rootFrame);
+      rootRect = scrollFrame->GetScrollPortRect();
+    }
+  }
+
+  nsMargin rootMargin;
+  NS_FOR_CSS_SIDES(side) {
+    nscoord basis = side == NS_SIDE_TOP || side == NS_SIDE_BOTTOM ?
+      rootRect.height : rootRect.width;
+    nsCSSValue value = mRootMargin.*nsCSSRect::sides[side];
+    nsStyleCoord coord;
+    if (value.IsPixelLengthUnit()) {
+      coord.SetCoordValue(value.GetPixelLength());
+    } else if (value.IsPercentLengthUnit()) {
+      coord.SetPercentValue(value.GetPercentValue());
+    } else {
+      MOZ_ASSERT_UNREACHABLE("invalid length unit");
+    }
+    rootMargin.Side(side) = nsLayoutUtils::ComputeCBDependentValue(basis, coord);
+  }
+
+  for (auto iter = mObservationTargets.Iter(); !iter.Done(); iter.Next()) {
+    Element* target = iter.Get()->GetKey();
+    nsIFrame* targetFrame = target->GetPrimaryFrame();
+    nsRect targetRect;
+    Maybe<nsRect> intersectionRect;
+
+    if (rootFrame && targetFrame &&
+       (!mRoot || nsLayoutUtils::IsAncestorFrameCrossDoc(rootFrame, targetFrame))) {
+      targetRect = nsLayoutUtils::GetAllInFlowRectsUnion(
+        targetFrame,
+        nsLayoutUtils::GetContainingBlockForClientRect(targetFrame),
+        nsLayoutUtils::RECTS_ACCOUNT_FOR_TRANSFORMS
+      );
+      intersectionRect = Some(targetFrame->GetVisualOverflowRect());
+
+      nsIFrame* containerFrame = nsLayoutUtils::GetCrossDocParentFrame(targetFrame);
+      while (containerFrame && containerFrame != rootFrame) {
+        if (containerFrame->GetType() == nsGkAtoms::scrollFrame) {
+          nsIScrollableFrame* scrollFrame = do_QueryFrame(containerFrame);
+          nsRect subFrameRect = scrollFrame->GetScrollPortRect();
+          nsRect intersectionRectRelativeToContainer =
+            nsLayoutUtils::TransformFrameRectToAncestor(targetFrame,
+                                                        intersectionRect.value(),
+                                                        containerFrame);
+          intersectionRect = EdgeInclusiveIntersection(intersectionRectRelativeToContainer,
+                                                       subFrameRect);
+          if (!intersectionRect) {
+            break;
+          }
+          targetFrame = containerFrame;
+        }
+
+        // TODO: Apply clip-path.
+
+        containerFrame = nsLayoutUtils::GetCrossDocParentFrame(containerFrame);
+      }
+    }
+
+    nsRect rootIntersectionRect = rootRect;
+    bool isInSimilarOriginBrowsingContext = CheckSimilarOrigin(root, target);
+
+    if (isInSimilarOriginBrowsingContext) {
+      rootIntersectionRect.Inflate(rootMargin);
+    }
+
+    if (intersectionRect.isSome()) {
+      nsRect intersectionRectRelativeToRoot =
+        nsLayoutUtils::TransformFrameRectToAncestor(
+          targetFrame,
+          intersectionRect.value(),
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame)
+      );
+      intersectionRect = EdgeInclusiveIntersection(
+        intersectionRectRelativeToRoot,
+        rootIntersectionRect
+      );
+      if (intersectionRect.isSome()) {
+        intersectionRect = Some(nsLayoutUtils::TransformFrameRectToAncestor(
+          nsLayoutUtils::GetContainingBlockForClientRect(rootFrame),
+          intersectionRect.value(),
+          targetFrame->PresContext()->PresShell()->GetRootScrollFrame()
+        ));
+      }
+    }
+
+    double targetArea = targetRect.width * targetRect.height;
+    double intersectionArea = !intersectionRect ?
+      0 : intersectionRect->width * intersectionRect->height;
+    double intersectionRatio = targetArea > 0.0 ? intersectionArea / targetArea : 0.0;
+
+    size_t threshold = -1;
+    if (intersectionRatio > 0.0) {
+      if (intersectionRatio == 1.0) {
+        threshold = mThresholds.Length();
+      } else {
+        for (size_t k = 0; k < mThresholds.Length(); ++k) {
+          if (mThresholds[k] <= intersectionRatio) {
+            threshold = k + 1;
+          } else {
+            break;
+          }
+        }
+      }
+    } else if (intersectionRect.isSome()) {
+      threshold = 0;
+    }
+
+    if (target->UpdateIntersectionObservation(this, threshold)) {
+      QueueIntersectionObserverEntry(
+        target, time,
+        isInSimilarOriginBrowsingContext ? Some(rootIntersectionRect) : Nothing(),
+        targetRect, intersectionRect
+      );
+    }
+  }
+}
+
+void
+DOMIntersectionObserver::QueueIntersectionObserverEntry(Element* aTarget,
+                                                        DOMHighResTimeStamp time,
+                                                        const Maybe<nsRect>& aRootRect,
+                                                        const nsRect& aTargetRect,
+                                                        const Maybe<nsRect>& aIntersectionRect)
+{
+  RefPtr<DOMRect> rootBounds;
+  if (aRootRect.isSome()) {
+    rootBounds = new DOMRect(this);
+    rootBounds->SetLayoutRect(aRootRect.value());
+  }
+  RefPtr<DOMRect> boundingClientRect = new DOMRect(this);
+  boundingClientRect->SetLayoutRect(aTargetRect);
+  RefPtr<DOMRect> intersectionRect = new DOMRect(this);
+  if (aIntersectionRect.isSome()) {
+    intersectionRect->SetLayoutRect(aIntersectionRect.value());
+  }
+  RefPtr<DOMIntersectionObserverEntry> entry = new DOMIntersectionObserverEntry(
+    this,
+    time,
+    rootBounds.forget(),
+    boundingClientRect.forget(),
+    intersectionRect.forget(),
+    aTarget);
+  mQueuedEntries.AppendElement(entry.forget());
+}
+
+void
+DOMIntersectionObserver::Notify()
+{
+  if (!mQueuedEntries.Length()) {
+    return;
+  }
+  mozilla::dom::Sequence<mozilla::OwningNonNull<DOMIntersectionObserverEntry>> entries;
+  if (entries.SetCapacity(mQueuedEntries.Length(), mozilla::fallible)) {
+    for (uint32_t i = 0; i < mQueuedEntries.Length(); ++i) {
+      RefPtr<DOMIntersectionObserverEntry> next = mQueuedEntries[i];
+      *entries.AppendElement(mozilla::fallible) = next;
+    }
+  }
+  mQueuedEntries.Clear();
+  mCallback->Call(this, entries, *this);
+}
+
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/base/DOMIntersectionObserver.h
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOMIntersectionObserver_h
+#define DOMIntersectionObserver_h
+
+#include "mozilla/dom/IntersectionObserverBinding.h"
+#include "nsCSSValue.h"
+#include "nsTArray.h"
+
+using mozilla::dom::DOMRect;
+using mozilla::dom::Element;
+
+namespace mozilla {
+namespace dom {
+
+class DOMIntersectionObserver;
+
+class DOMIntersectionObserverEntry final : public nsISupports,
+                                           public nsWrapperCache
+{
+  ~DOMIntersectionObserverEntry() {}
+
+public:
+  DOMIntersectionObserverEntry(nsISupports* aOwner,
+                               DOMHighResTimeStamp aTime,
+                               RefPtr<DOMRect> aRootBounds,
+                               RefPtr<DOMRect> aBoundingClientRect,
+                               RefPtr<DOMRect> aIntersectionRect,
+                               Element* aTarget)
+  : mOwner(aOwner),
+    mTime(aTime),
+    mRootBounds(aRootBounds),
+    mBoundingClientRect(aBoundingClientRect),
+    mIntersectionRect(aIntersectionRect),
+    mTarget(aTarget)
+  {
+  }
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DOMIntersectionObserverEntry)
+
+  nsISupports* GetParentObject() const
+  {
+    return mOwner;
+  }
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return mozilla::dom::IntersectionObserverEntryBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  DOMHighResTimeStamp Time()
+  {
+    return mTime;
+  }
+
+  DOMRect* GetRootBounds()
+  {
+    return mRootBounds;
+  }
+
+  DOMRect* BoundingClientRect()
+  {
+    return mBoundingClientRect;
+  }
+
+  DOMRect* IntersectionRect()
+  {
+    return mIntersectionRect;
+  }
+
+  double IntersectionRatio();
+
+  Element* Target()
+  {
+    return mTarget;
+  }
+
+protected:
+  nsCOMPtr<nsISupports> mOwner;
+  DOMHighResTimeStamp   mTime;
+  RefPtr<DOMRect>       mRootBounds;
+  RefPtr<DOMRect>       mBoundingClientRect;
+  RefPtr<DOMRect>       mIntersectionRect;
+  RefPtr<Element>       mTarget;
+};
+
+#define NS_DOM_INTERSECTION_OBSERVER_IID \
+{ 0x8570a575, 0xe303, 0x4d18, \
+  { 0xb6, 0xb1, 0x4d, 0x2b, 0x49, 0xd8, 0xef, 0x94 } }
+
+class DOMIntersectionObserver final : public nsISupports,
+                                      public nsWrapperCache
+{
+  virtual ~DOMIntersectionObserver() { }
+
+public:
+  DOMIntersectionObserver(already_AddRefed<nsPIDOMWindowInner>&& aOwner,
+                          mozilla::dom::IntersectionCallback& aCb)
+  : mOwner(aOwner), mCallback(&aCb), mConnected(false)
+  {
+  }
+  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(DOMIntersectionObserver)
+  NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOM_INTERSECTION_OBSERVER_IID)
+
+  static already_AddRefed<DOMIntersectionObserver>
+  Constructor(const mozilla::dom::GlobalObject& aGlobal,
+              mozilla::dom::IntersectionCallback& aCb,
+              mozilla::ErrorResult& aRv);
+  static already_AddRefed<DOMIntersectionObserver>
+  Constructor(const mozilla::dom::GlobalObject& aGlobal,
+              mozilla::dom::IntersectionCallback& aCb,
+              const mozilla::dom::IntersectionObserverInit& aOptions,
+              mozilla::ErrorResult& aRv);
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return mozilla::dom::IntersectionObserverBinding::Wrap(aCx, this, aGivenProto);
+  }
+
+  nsISupports* GetParentObject() const
+  {
+    return mOwner;
+  }
+
+  Element* GetRoot() const {
+    return mRoot;
+  }
+
+  void GetRootMargin(mozilla::dom::DOMString& aRetVal);
+  void GetThresholds(nsTArray<double>& aRetVal);
+  void Observe(Element& aTarget);
+  void Unobserve(Element& aTarget);
+
+  void Disconnect();
+  void TakeRecords(nsTArray<RefPtr<DOMIntersectionObserverEntry>>& aRetVal);
+
+  mozilla::dom::IntersectionCallback* IntersectionCallback() { return mCallback; }
+
+  bool SetRootMargin(const nsAString& aString);
+
+  void Update(nsIDocument* aDocument, DOMHighResTimeStamp time);
+  void Notify();
+
+protected:
+  void Connect();
+  void QueueIntersectionObserverEntry(Element* aTarget,
+                                      DOMHighResTimeStamp time,
+                                      const Maybe<nsRect>& aRootRect,
+                                      const nsRect& aTargetRect,
+                                      const Maybe<nsRect>& aIntersectionRect);
+
+  nsCOMPtr<nsPIDOMWindowInner>                    mOwner;
+  RefPtr<mozilla::dom::IntersectionCallback>      mCallback;
+  RefPtr<Element>                                 mRoot;
+  nsCSSRect                                       mRootMargin;
+  nsTArray<double>                                mThresholds;
+  nsTHashtable<nsPtrHashKey<Element>>             mObservationTargets;
+  nsTArray<RefPtr<DOMIntersectionObserverEntry>>  mQueuedEntries;
+  bool                                            mConnected;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(DOMIntersectionObserver, NS_DOM_INTERSECTION_OBSERVER_IID)
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -144,16 +144,17 @@
 #include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/Preferences.h"
 #include "nsComputedDOMStyle.h"
 #include "nsDOMStringMap.h"
+#include "DOMIntersectionObserver.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 nsIAtom*
 nsIContent::DoGetID() const
 {
   MOZ_ASSERT(HasID(), "Unexpected call");
@@ -3881,8 +3882,49 @@ Element::ClearDataset()
 {
   nsDOMSlots *slots = GetExistingDOMSlots();
 
   MOZ_ASSERT(slots && slots->mDataset,
              "Slots should exist and dataset should not be null.");
   slots->mDataset = nullptr;
 }
 
+nsTArray<Element::nsDOMSlots::IntersectionObserverRegistration>*
+Element::RegisteredIntersectionObservers()
+{
+  nsDOMSlots* slots = DOMSlots();
+  return &slots->mRegisteredIntersectionObservers;
+}
+
+void
+Element::RegisterIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  RegisteredIntersectionObservers()->AppendElement(
+    nsDOMSlots::IntersectionObserverRegistration { aObserver, -1 });
+}
+
+void
+Element::UnregisterIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* observers =
+    RegisteredIntersectionObservers();
+  for (uint32_t i = 0; i < observers->Length(); ++i) {
+    nsDOMSlots::IntersectionObserverRegistration reg = observers->ElementAt(i);
+    if (reg.observer == aObserver) {
+      observers->RemoveElementAt(i);
+      break;
+    }
+  }
+}
+
+bool
+Element::UpdateIntersectionObservation(DOMIntersectionObserver* aObserver, int32_t aThreshold)
+{
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* observers =
+    RegisteredIntersectionObservers();
+  for (auto& reg : *observers) {
+    if (reg.observer == aObserver && reg.previousThreshold != aThreshold) {
+      reg.previousThreshold = aThreshold;
+      return true;
+    }
+  }
+  return false;
+}
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -34,16 +34,17 @@
 #include "nsAttrValue.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/dom/DOMTokenListSupportedTokens.h"
 #include "mozilla/dom/WindowBinding.h"
 #include "mozilla/dom/ElementBinding.h"
 #include "mozilla/dom/Nullable.h"
 #include "Units.h"
+#include "DOMIntersectionObserver.h"
 
 class nsIFrame;
 class nsIDOMMozNamedAttrMap;
 class nsIURI;
 class nsIScrollableFrame;
 class nsAttrValueOrString;
 class nsContentList;
 class nsDOMTokenList;
@@ -56,16 +57,17 @@ class nsDocument;
 class nsDOMStringMap;
 
 namespace mozilla {
 class DeclarationBlock;
 namespace dom {
   struct AnimationFilter;
   struct ScrollIntoViewOptions;
   struct ScrollToOptions;
+  class DOMIntersectionObserver;
   class ElementOrCSSPseudoElement;
   class UnrestrictedDoubleOrKeyframeAnimationOptions;
 } // namespace dom
 } // namespace mozilla
 
 
 already_AddRefed<nsContentList>
 NS_GetContentList(nsINode* aRootNode,
@@ -1144,16 +1146,20 @@ public:
    * sorts of elements expose it to JS as a .dataset property
    */
   // Getter, to be called from bindings.
   already_AddRefed<nsDOMStringMap> Dataset();
   // Callback for destructor of dataset to ensure to null out our weak pointer
   // to it.
   void ClearDataset();
 
+  void RegisterIntersectionObserver(DOMIntersectionObserver* aObserver);
+  void UnregisterIntersectionObserver(DOMIntersectionObserver* aObserver);
+  bool UpdateIntersectionObservation(DOMIntersectionObserver* aObserver, int32_t threshold);
+
 protected:
   /*
    * Named-bools for use with SetAttrAndNotify to make call sites easier to
    * read.
    */
   static const bool kFireMutationEvent           = true;
   static const bool kDontFireMutationEvent       = false;
   static const bool kNotifyDocumentObservers     = true;
@@ -1357,16 +1363,18 @@ protected:
    * the value of xlink:show, converted to a suitably equivalent named target
    * (e.g. _blank).
    */
   virtual void GetLinkTarget(nsAString& aTarget);
 
   nsDOMTokenList* GetTokenList(nsIAtom* aAtom,
                                const DOMTokenListSupportedTokenArray aSupportedTokens = nullptr);
 
+  nsTArray<nsDOMSlots::IntersectionObserverRegistration>* RegisteredIntersectionObservers();
+
 private:
   /**
    * Get this element's client area rect in app units.
    * @return the frame's client area
    */
   nsRect GetClientAreaRect();
 
   nsIScrollableFrame* GetScrollFrame(nsIFrame **aStyledFrame = nullptr,
--- a/dom/base/FragmentOrElement.h
+++ b/dom/base/FragmentOrElement.h
@@ -32,16 +32,17 @@ class nsIDocument;
 class nsDOMStringMap;
 class nsIURI;
 
 namespace mozilla {
 namespace css {
 class Declaration;
 } // namespace css
 namespace dom {
+class DOMIntersectionObserver;
 class Element;
 } // namespace dom
 } // namespace mozilla
 
 /**
  * A class that implements nsIWeakReference
  */
 
@@ -339,16 +340,26 @@ public:
      * XBL binding installed on the lement.
      */
     nsCOMPtr<nsIContent> mXBLInsertionParent;
 
     /**
      * Web components custom element data.
      */
     RefPtr<CustomElementData> mCustomElementData;
+
+    /**
+     * Registered Intersection Observers on the element.
+     */
+    struct IntersectionObserverRegistration {
+      DOMIntersectionObserver* observer;
+      int32_t previousThreshold;
+    };
+
+    nsTArray<IntersectionObserverRegistration> mRegisteredIntersectionObservers;
   };
 
 protected:
   void GetMarkup(bool aIncludeSelf, nsAString& aMarkup);
   void SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError);
 
   // Override from nsINode
   virtual nsINode::nsSlots* CreateSlots() override;
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -160,16 +160,17 @@ EXPORTS.mozilla.dom += [
     'CustomElementRegistry.h',
     'DirectionalityUtils.h',
     'DocumentFragment.h',
     'DocumentType.h',
     'DOMCursor.h',
     'DOMError.h',
     'DOMException.h',
     'DOMImplementation.h',
+    'DOMIntersectionObserver.h',
     'DOMMatrix.h',
     'DOMParser.h',
     'DOMPoint.h',
     'DOMQuad.h',
     'DOMRect.h',
     'DOMRequest.h',
     'DOMStringList.h',
     'DOMTokenListSupportedTokens.h',
@@ -353,16 +354,18 @@ UNIFIED_SOURCES += [
 
 if CONFIG['MOZ_WEBRTC']:
     UNIFIED_SOURCES += [
         'nsDOMDataChannel.cpp',
     ]
 
 # these files couldn't be in UNIFIED_SOURCES for now for reasons given below:
 SOURCES += [
+    # Several conflicts with other bindings.
+    'DOMIntersectionObserver.cpp',
     # Because of OS X headers.
     'nsContentUtils.cpp',
     # this file doesn't like windows.h
     'nsDOMWindowUtils.cpp',
     # Conflicts with windows.h's definition of SendMessage.
     'nsFrameMessageManager.cpp',
     # This file has a #error "Never include windows.h in this file!"
     'nsGlobalWindow.cpp',
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1446,16 +1446,18 @@ nsDocument::~nsDocument()
   mInDestructor = true;
   mInUnlinkOrDeletion = true;
 
   mozilla::DropJSObjects(this);
 
   // Clear mObservers to keep it in sync with the mutationobserver list
   mObservers.Clear();
 
+  mIntersectionObservers.Clear();
+
   if (mStyleSheetSetList) {
     mStyleSheetSetList->Disconnect();
   }
 
   if (mAnimationController) {
     mAnimationController->Disconnect();
   }
 
@@ -1715,16 +1717,18 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildrenCollection)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnonymousContents)
 
   // Traverse all our nsCOMArrays.
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStyleSheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOnDemandBuiltInUASheets)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPreloadingImages)
 
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIntersectionObservers)
+
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSubImportLinks)
 
   for (uint32_t i = 0; i < tmp->mFrameRequestCallbacks.Length(); ++i) {
     NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mFrameRequestCallbacks[i]");
     cb.NoteXPCOMChild(tmp->mFrameRequestCallbacks[i].mCallback);
   }
 
   // Traverse animation components
@@ -1801,16 +1805,18 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mImportManager)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSubImportLinks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFontFaceSet)
 
   tmp->mParentDocument = nullptr;
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPreloadingImages)
 
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mIntersectionObservers)
+
   tmp->ClearAllBoxObjects();
 
   if (tmp->mListenerManager) {
     tmp->mListenerManager->Disconnect();
     tmp->UnsetFlags(NODE_HAS_LISTENERMANAGER);
     tmp->mListenerManager = nullptr;
   }
 
@@ -12301,16 +12307,61 @@ nsDocument::ReportUseCounters()
 
           Telemetry::Accumulate(id, 1);
         }
       }
     }
   }
 }
 
+void
+nsDocument::AddIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  NS_ASSERTION(mIntersectionObservers.IndexOf(aObserver) == nsTArray<int>::NoIndex,
+               "Intersection observer already in the list");
+  mIntersectionObservers.AppendElement(aObserver);
+}
+
+void
+nsDocument::RemoveIntersectionObserver(DOMIntersectionObserver* aObserver)
+{
+  mIntersectionObservers.RemoveElement(aObserver);
+}
+
+void
+nsDocument::UpdateIntersectionObservations()
+{
+  DOMHighResTimeStamp time = 0;
+  if (nsPIDOMWindowInner* window = GetInnerWindow()) {
+    Performance* perf = window->GetPerformance();
+    if (perf) {
+      time = perf->Now();
+    }
+  }
+  for (const auto& observer : mIntersectionObservers) {
+    observer->Update(this, time);
+  }
+}
+
+void
+nsDocument::ScheduleIntersectionObserverNotification()
+{
+  nsCOMPtr<nsIRunnable> notification = NewRunnableMethod(this,
+    &nsDocument::NotifyIntersectionObservers);
+  NS_DispatchToCurrentThread(notification);
+}
+
+void
+nsDocument::NotifyIntersectionObservers()
+{
+  for (const auto& observer : mIntersectionObservers) {
+    observer->Notify();
+  }
+}
+
 XPathEvaluator*
 nsIDocument::XPathEvaluator()
 {
   if (!mXPathEvaluator) {
     mXPathEvaluator = new dom::XPathEvaluator(this);
   }
   return mXPathEvaluator;
 }
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -64,16 +64,17 @@
 #include "nsDataHashtable.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Attributes.h"
 #include "nsIDOMXPathEvaluator.h"
 #include "jsfriendapi.h"
 #include "ImportManager.h"
 #include "mozilla/LinkedList.h"
 #include "CustomElementRegistry.h"
+#include "mozilla/dom/Performance.h"
 
 #define XML_DECLARATION_BITS_DECLARATION_EXISTS   (1 << 0)
 #define XML_DECLARATION_BITS_ENCODING_EXISTS      (1 << 1)
 #define XML_DECLARATION_BITS_STANDALONE_EXISTS    (1 << 2)
 #define XML_DECLARATION_BITS_STANDALONE_YES       (1 << 3)
 
 
 class nsDOMStyleSheetSetList;
@@ -92,16 +93,18 @@ class nsPIBoxObject;
 
 namespace mozilla {
 class EventChainPreVisitor;
 namespace dom {
 class BoxObject;
 class ImageTracker;
 struct LifecycleCallbacks;
 class CallbackFunction;
+class DOMIntersectionObserver;
+class Performance;
 
 struct FullscreenRequest : public LinkedListElement<FullscreenRequest>
 {
   explicit FullscreenRequest(Element* aElement);
   FullscreenRequest(const FullscreenRequest&) = delete;
   ~FullscreenRequest();
 
   Element* GetElement() const { return mElement; }
@@ -768,16 +771,25 @@ public:
   // for radio group
   nsRadioGroupStruct* GetRadioGroup(const nsAString& aName) const;
   nsRadioGroupStruct* GetOrCreateRadioGroup(const nsAString& aName);
 
   virtual nsViewportInfo GetViewportInfo(const mozilla::ScreenIntSize& aDisplaySize) override;
 
   void ReportUseCounters();
 
+  virtual void AddIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) override;
+  virtual void RemoveIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) override;
+  virtual void UpdateIntersectionObservations() override;
+  virtual void ScheduleIntersectionObserverNotification() override;
+  virtual void NotifyIntersectionObservers() override;
+
+
 private:
   void AddOnDemandBuiltInUASheet(mozilla::StyleSheet* aSheet);
   nsRadioGroupStruct* GetRadioGroupInternal(const nsAString& aName) const;
   void SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages);
 
 public:
   // nsIDOMNode
   NS_FORWARD_NSIDOMNODE_TO_NSINODE_OVERRIDABLE
@@ -1320,16 +1332,19 @@ protected:
 
   nsTArray<RefPtr<mozilla::StyleSheet>> mStyleSheets;
   nsTArray<RefPtr<mozilla::StyleSheet>> mOnDemandBuiltInUASheets;
   nsTArray<RefPtr<mozilla::StyleSheet>> mAdditionalSheets[AdditionalSheetTypeCount];
 
   // Array of observers
   nsTObserverArray<nsIDocumentObserver*> mObservers;
 
+  // Array of intersection observers
+  nsTArray<RefPtr<mozilla::dom::DOMIntersectionObserver>> mIntersectionObservers;
+
   // Tracker for animations that are waiting to start.
   // nullptr until GetOrCreatePendingAnimationTracker is called.
   RefPtr<mozilla::PendingAnimationTracker> mPendingAnimationTracker;
 
   // Weak reference to the scope object (aka the script global object)
   // that, unlike mScriptGlobalObject, is never unset once set. This
   // is a weak reference to avoid leaks due to circular references.
   nsWeakPtr mScopeObject;
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -122,16 +122,17 @@ class Attr;
 class BoxObject;
 class CDATASection;
 class Comment;
 struct CustomElementDefinition;
 class DocumentFragment;
 class DocumentTimeline;
 class DocumentType;
 class DOMImplementation;
+class DOMIntersectionObserver;
 class DOMStringList;
 class Element;
 struct ElementCreationOptions;
 struct ElementRegistrationOptions;
 class Event;
 class EventTarget;
 class FontFaceSet;
 class FrameRequestCallback;
@@ -2839,16 +2840,25 @@ public:
   bool HasScriptsBlockedBySandbox();
 
   void ReportHasScrollLinkedEffect();
   bool HasScrollLinkedEffect() const
   {
     return mHasScrollLinkedEffect;
   }
 
+  virtual void AddIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) = 0;
+  virtual void RemoveIntersectionObserver(
+    mozilla::dom::DOMIntersectionObserver* aObserver) = 0;
+  
+  virtual void UpdateIntersectionObservations() = 0;
+  virtual void ScheduleIntersectionObserverNotification() = 0;
+  virtual void NotifyIntersectionObservers() = 0;
+
 protected:
   bool GetUseCounter(mozilla::UseCounter aUseCounter)
   {
     return mUseCounters[aUseCounter];
   }
 
   void SetChildDocumentUseCounter(mozilla::UseCounter aUseCounter)
   {
new file mode 100644
--- /dev/null
+++ b/dom/base/test/intersectionobserver_subframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<style>
+#target5 {
+        position: absolute;
+        top: 0px;
+        left: 0px;
+        width: 20px;
+        height: 20px;
+        background: #f00;
+}
+</style>
+<body>
+<div id="target5"></div>
+<script>
+        var io = new IntersectionObserver(function (records) {
+                parent.postMessage(records[0].rootBounds == null, 'http://mochi.test:8888');
+        }, {});
+        io.observe(document.getElementById("target5"));
+</script>
+</body>
+</html>
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -231,16 +231,17 @@ support-files =
   websocket_helpers.js
   websocket_tests.js
   !/dom/html/test/form_submit_server.sjs
   !/dom/security/test/cors/file_CrossSiteXHR_server.sjs
   !/image/test/mochitest/blue.png
   !/dom/xhr/tests/file_XHRSendData.sjs
   script_bug1238440.js
   file_blobURL_expiring.html
+  intersectionobserver_subframe.html
 
 [test_anchor_area_referrer.html]
 [test_anchor_area_referrer_changing.html]
 [test_anchor_area_referrer_invalid.html]
 [test_anchor_area_referrer_rel.html]
 [test_anonymousContent_api.html]
 [test_anonymousContent_append_after_reflow.html]
 [test_anonymousContent_canvas.html]
@@ -692,16 +693,17 @@ skip-if = e10s || os != 'linux' || build
 [test_htmlcopyencoder.xhtml]
 [test_iframe_referrer.html]
 [test_iframe_referrer_changing.html]
 [test_iframe_referrer_invalid.html]
 [test_Image_constructor.html]
 [test_img_referrer.html]
 [test_innersize_scrollport.html]
 [test_integer_attr_with_leading_zero.html]
+[test_intersectionobservers.html]
 [test_ipc_messagemanager_blob.html]
 [test_link_prefetch.html]
 skip-if = !e10s # Track Bug 1281415
 [test_link_stylesheet.html]
 [test_messagemanager_targetchain.html]
 [test_meta_viewport0.html]
 skip-if = (os != 'b2g' && os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport1.html]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_intersectionobservers.html
@@ -0,0 +1,1125 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1243846
+
+Some tests ported from IntersectionObserver/polyfill/intersection-observer-test.html
+
+Original license header:
+
+Copyright 2016 Google Inc. All Rights Reserved.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1243846</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body onload="next()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1243846">Mozilla Bug 1243846</a>
+<p id="display"></p>
+<pre id="test">
+<script type="application/javascript">
+
+  var tests = [];
+  var curDescribeMsg = '';
+  var curItMsg = '';
+
+  function beforeEach_fn() { };
+  function afterEach_fn() { };
+
+  function before(fn) {
+    fn();
+  }
+
+  function beforeEach(fn) {
+    beforeEach_fn = fn;
+  }
+
+  function afterEach(fn) {
+    afterEach_fn = fn;
+  }
+
+  function it(msg, fn) {
+    tests.push({
+      msg: `${msg} [${curDescribeMsg}]`,
+      fn: fn
+    });
+  }
+
+  var callbacks = [];
+  function callDelayed(fn, delay) {
+    callbacks.push({
+      fn: fn,
+      time: +new Date() + delay
+    });
+  }
+
+  requestAnimationFrame(function tick() {
+    var i = callbacks.length;
+    while (i--) {
+      var cb = callbacks[i];
+      if (+new Date() >= cb.time) {
+        cb.fn();
+        callbacks.splice(i, 1);
+      }
+    }
+    requestAnimationFrame(tick);
+  });
+
+  function expect(val) {
+    return {
+      to: {
+        throwException: function (regexp) {
+          try {
+            val();
+            ok(false, `${curItMsg} - an exception should have beeen thrown`);
+          } catch (e) {
+            ok(regexp.test(e), `${curItMsg} - supplied regexp should match thrown exception`);
+          }
+        },
+        get be() {
+          var fn = function (expected) {
+            is(val, expected, curItMsg);
+          };
+          fn.ok = function () {
+            ok(val, curItMsg);
+          };
+          fn.greaterThan = function (other) {
+            ok(val > other, `${curItMsg} - ${val} should be greater than ${other}`);
+          };
+          fn.lessThan = function (other) {
+            ok(val < other, `${curItMsg} - ${val} should be less than ${other}`);
+          };
+          return fn;
+        },
+        eql: function (expected) {
+          if (Array.isArray(expected)) {
+            if (!Array.isArray(val)) {
+              ok(false, curItMsg, `${curItMsg} - should be an array,`);
+              return;
+            }
+            is(val.length, expected.length, curItMsg, `${curItMsg} - arrays should be the same length`);
+            if (expected.length != val.length) {
+              return;
+            }
+            for (var i = 0; i < expected.length; i++) {
+              is(val[i], expected[i], `${curItMsg} - array elements at position ${i} should be equal`);
+              if (expected[i] != val[i]) {
+                return;
+              }
+            }
+            ok(true);
+          }
+        },
+      }
+    }
+  }
+
+  function describe(msg, fn) {
+    curDescribeMsg = msg;
+    fn();
+    curDescribeMsg = '';
+  }
+
+  function next() {
+    var test = tests.shift();
+    if (test) {
+      console.log(test.msg);
+      curItMsg = test.msg;
+      var fn = test.fn;
+      beforeEach_fn();
+      if (fn.length) {
+        fn(function () {
+          afterEach_fn();
+          next();
+        });
+      } else {
+        fn();
+        afterEach_fn();
+        next();
+      }
+    } else {
+      SimpleTest.finish();
+    }
+  }
+
+  var sinon = {
+    spy: function () {
+      var fn = function () {
+        fn.callCount++;
+        fn.lastCall = { args: arguments };
+      };
+      fn.callCount = 0;
+      fn.lastCall = { args: [] };
+      return fn;
+    }
+  };
+
+  var ASYNC_TIMEOUT = 124;
+
+
+  var io;
+  var noop = function() {};
+
+
+  // References to DOM elements, which are accessible to any test
+  // and reset prior to each test so state isn't shared.
+  var rootEl;
+  var grandParentEl;
+  var parentEl;
+  var targetEl1;
+  var targetEl2;
+  var targetEl3;
+  var targetEl4;
+  var targetEl5;
+
+
+  describe('IntersectionObserver', function() {
+
+    before(function() {
+
+    });
+
+
+    beforeEach(function() {
+      addStyles();
+      addFixtures();
+    });
+
+
+    afterEach(function() {
+      if (io && 'disconnect' in io) io.disconnect();
+      io = null;
+
+      removeStyles();
+      removeFixtures();
+    });
+
+
+    describe('constructor', function() {
+
+      it('throws when callback is not a function', function() {
+        expect(function() {
+          io = new IntersectionObserver(null);
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('instantiates root correctly', function() {
+        io = new IntersectionObserver(noop);
+        expect(io.root).to.be(null);
+
+        io = new IntersectionObserver(noop, {root: rootEl});
+        expect(io.root).to.be(rootEl);
+      });
+
+
+      it('throws when root is not an Element', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {root: 'foo'});
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('instantiates rootMargin correctly', function() {
+        io = new IntersectionObserver(noop, {rootMargin: '10px'});
+        expect(io.rootMargin).to.be('10px 10px 10px 10px');
+
+        io = new IntersectionObserver(noop, {rootMargin: '10px -5%'});
+        expect(io.rootMargin).to.be('10px -5% 10px -5%');
+
+        io = new IntersectionObserver(noop, {rootMargin: '10px 20% 0px'});
+        expect(io.rootMargin).to.be('10px 20% 0px 20%');
+
+        io = new IntersectionObserver(noop, {rootMargin: '0px 0px -5% 5px'});
+        expect(io.rootMargin).to.be('0px 0px -5% 5px');
+      });
+
+
+      it('throws when rootMargin is not in pixels or percent', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {rootMargin: 'auto'});
+        }).to.throwException(/pixels.*percent/i);
+      });
+
+
+      it('instantiates thresholds correctly', function() {
+        io = new IntersectionObserver(noop);
+        expect(io.thresholds).to.eql([0]);
+
+        io = new IntersectionObserver(noop, {threshold: 0.5});
+        expect(io.thresholds).to.eql([0.5]);
+
+        io = new IntersectionObserver(noop, {threshold: [0.25, 0.5, 0.75]});
+        expect(io.thresholds).to.eql([0.25, 0.5, 0.75]);
+
+        io = new IntersectionObserver(noop, {threshold: [1, .5, 0]});
+        expect(io.thresholds).to.eql([0, .5, 1]);
+      });
+
+      it('throws when a threshold value is not between 0 and 1', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {threshold: [0, -1]});
+        }).to.throwException(/threshold/i);
+      });
+
+      it('throws when a threshold value is not a number', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop, {threshold: "foo"});
+        }).to.throwException(/.*/i);
+      });
+
+    });
+
+
+    describe('observe', function() {
+
+      it('throws when target is not an Element', function() {
+        expect(function() {
+          io = new IntersectionObserver(noop);
+          io.observe(null);
+        }).to.throwException(/.*/i);
+      });
+
+
+      it('triggers if target intersects when observing begins', function(done) {
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl});
+        io.observe(targetEl1);
+      });
+
+
+      it('triggers with the correct arguments', function(done) {
+        io = new IntersectionObserver(function(records, observer) {
+          expect(records.length).to.be(1);
+          expect(records[0] instanceof IntersectionObserverEntry).to.be.ok();
+          expect(observer).to.be(io);
+          expect(this).to.be(io);
+          done();
+        }, {root: rootEl});
+        io.observe(targetEl1);
+      });
+
+
+      it('does not trigger if target does not intersect when observing begins',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        targetEl2.style.top = '-40px';
+        io.observe(targetEl2);
+        callDelayed(function() {
+          expect(spy.callCount).to.be(0);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+
+      it('handles container elements with non-visible overflow',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-40px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            parentEl.style.overflow = 'visible';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('observes one target at a single threshold correctly', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl, threshold: 0.5});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.left = '-5px';
+            io.observe(targetEl1);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be.greaterThan(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-15px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be.lessThan(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-25px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.left = '-10px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+
+      it('observes multiple targets at multiple thresholds correctly',
+          function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {
+          root: rootEl,
+          threshold: [1, 0.5, 0]
+        });
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-15px';
+            targetEl2.style.top = '-5px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '205px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.25);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.75);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-5px';
+            targetEl2.style.top = '-15px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '195px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.75);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.25);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(0.25);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '5px';
+            targetEl2.style.top = '-25px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '185px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(0.75);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '15px';
+            targetEl2.style.top = '-35px';
+            targetEl2.style.left = '0px';
+            targetEl3.style.top = '0px';
+            targetEl3.style.left = '175px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].target).to.be(targetEl3);
+              expect(records[0].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('handles rootMargin properly', function(done) {
+
+        parentEl.style.overflow = 'visible';
+        targetEl1.style.top = '0px';
+        targetEl1.style.left = '-20px';
+        targetEl2.style.top = '-20px';
+        targetEl2.style.left = '0px';
+        targetEl3.style.top = '0px';
+        targetEl3.style.left = '200px';
+        targetEl4.style.top = '180px';
+        targetEl4.style.left = '180px';
+
+        runSequence([
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(4);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(.5);
+              expect(records[2].target).to.be(targetEl3);
+              expect(records[2].intersectionRatio).to.be(.5);
+              expect(records[3].target).to.be(targetEl4);
+              expect(records[3].intersectionRatio).to.be(1);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '10px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl3);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              expect(records[2].target).to.be(targetEl4);
+              expect(records[2].intersectionRatio).to.be(0.5);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '-10px 10%'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl4);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '-5% -2.5% 0px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          },
+          function(done) {
+            io = new IntersectionObserver(function(records) {
+              records = sortRecords(records);
+              expect(records.length).to.be(3);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(0.5);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(0.5);
+              expect(records[2].target).to.be(targetEl4);
+              expect(records[2].intersectionRatio).to.be(0.25);
+              io.disconnect();
+              done();
+            }, {root: rootEl, rootMargin: '5% -2.5% -10px -190px'});
+
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+          }
+        ], done);
+      });
+
+
+      it('handles targets on the boundary of root', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-21px';
+            targetEl2.style.top = '-20px';
+            targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '0px';
+            targetEl1.style.left = '-20px';
+            targetEl2.style.top = '-21px';
+            targetEl2.style.left = '0px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[1].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl1.style.top = '-20px';
+            targetEl1.style.left = '200px';
+            targetEl2.style.top = '200px';
+            targetEl2.style.left = '200px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl2);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            targetEl3.style.top = '20px';
+            targetEl3.style.left = '-20px';
+            targetEl4.style.top = '-20px';
+            targetEl4.style.left = '20px';
+            io.observe(targetEl3);
+            io.observe(targetEl4);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl3);
+              expect(records[1].intersectionRatio).to.be(0);
+              expect(records[1].target).to.be(targetEl4);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+
+      it('handles zero-size targets within the root coordinate space',
+          function(done) {
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(0);
+          done();
+        }, {root: rootEl});
+
+        targetEl1.style.top = '0px';
+        targetEl1.style.left = '0px';
+        targetEl1.style.width = '0px';
+        targetEl1.style.height = '0px';
+        io.observe(targetEl1);
+      });
+
+
+      it('handles root/target elements not yet in the DOM', function(done) {
+
+        rootEl.parentNode.removeChild(rootEl);
+        targetEl1.parentNode.removeChild(targetEl1);
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            io.observe(targetEl1);
+            callDelayed(done, 0);
+          },
+          function(done) {
+            document.getElementById('fixtures').appendChild(rootEl);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            parentEl.insertBefore(targetEl1, targetEl2);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            grandParentEl.parentNode.removeChild(grandParentEl);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.appendChild(targetEl1);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(3);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            rootEl.parentNode.removeChild(rootEl);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(4);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].intersectionRatio).to.be(0);
+              expect(records[0].target).to.be(targetEl1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+      });
+
+
+      it('handles sub-root element scrolling', function(done) {
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl});
+
+        io.observe(targetEl3);
+        callDelayed(function() {
+          parentEl.scrollLeft = 40;
+        }, 0);
+      });
+
+
+      it('supports CSS transitions and transforms', function(done) {
+
+        targetEl1.style.top = '220px';
+        targetEl1.style.left = '220px';
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].intersectionRatio).to.be(1);
+          done();
+        }, {root: rootEl, threshold: [1]});
+
+        io.observe(targetEl1);
+        callDelayed(function() {
+          targetEl1.style.transform = 'translateX(-40px) translateY(-40px)';
+        }, 0);
+      });
+
+
+      it('uses the viewport when no root is specified', function(done) {
+        io = new IntersectionObserver(function(records) {
+          var viewportWidth =
+              document.documentElement.clientWidth || document.body.clientWidth;
+          var viewportHeight =
+              document.documentElement.clientHeight || document.body.clientHeight;
+
+          expect(records.length).to.be(1);
+          expect(records[0].rootBounds.top).to.be(0);
+          expect(records[0].rootBounds.left).to.be(0);
+          expect(records[0].rootBounds.right).to.be(viewportWidth + 4);
+          expect(records[0].rootBounds.width).to.be(viewportWidth + 4);
+          expect(records[0].rootBounds.bottom).to.be(viewportHeight + 4);
+          expect(records[0].rootBounds.height).to.be(viewportHeight + 4);
+          done();
+        });
+
+        // Ensures targetEl1 is visible in the viewport before observing.
+        window.scrollTo(0, 0);
+        rootEl.style.position = 'absolute';
+        rootEl.style.top = '0px';
+        rootEl.style.left = '0px';
+
+        io.observe(targetEl1);
+      });
+
+    });
+
+    describe('observe subframe', function () {
+      
+      it('boundingClientRect matches target.getBoundingClientRect() for an element inside an iframe',
+          function(done) {
+
+        io = new IntersectionObserver(function(records) {
+          expect(records.length).to.be(1);
+          expect(records[0].boundingClientRect.top, targetEl5.getBoundingClientRect().top);
+          expect(records[0].boundingClientRect.left, targetEl5.getBoundingClientRect().left);
+          expect(records[0].boundingClientRect.width, targetEl5.getBoundingClientRect().width);
+          expect(records[0].boundingClientRect.height, targetEl5.getBoundingClientRect().height);
+          done();
+        }, {root: rootEl, threshold: [1]});
+
+        targetEl4.onload = function () {
+          targetEl5 = targetEl4.contentDocument.getElementById('target5');
+          io.observe(targetEl5);
+        }
+
+        targetEl4.src = "intersectionobserver_subframe.html";
+      });
+
+      it('rootBounds should is set to null for cross-origin observations', function(done) {
+
+        window.onmessage = function (e) {
+          expect(e.data).to.be.ok();
+          done();
+        };
+
+        targetEl4.src = "http://example.org/tests/dom/base/test/intersectionobserver_subframe.html";
+
+      });
+    
+    });
+
+    describe('takeRecords', function() {
+
+      it('supports getting records before the callback is invoked',
+          function(done) {
+
+        var lastestRecords = [];
+        io = new IntersectionObserver(function(records) {
+          lastestRecords = lastestRecords.concat(records);
+        }, {root: rootEl});
+        io.observe(targetEl1);
+
+        window.requestAnimationFrame && requestAnimationFrame(function() {
+          lastestRecords = lastestRecords.concat(io.takeRecords());
+        });
+
+        callDelayed(function() {
+          expect(lastestRecords.length).to.be(1);
+          expect(lastestRecords[0].intersectionRatio).to.be(1);
+          done();
+        }, ASYNC_TIMEOUT);
+      });
+
+    });
+
+
+    describe('unobserve', function() {
+
+      it('removes targets from the internal store', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.unobserve(targetEl1);
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '-40px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(1);
+              expect(records[0].target).to.be(targetEl2);
+              expect(records[0].intersectionRatio).to.be(0);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.unobserve(targetEl2);
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(2);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+    });
+
+    describe('disconnect', function() {
+
+      it('removes all targets and stops listening for changes', function(done) {
+
+        var spy = sinon.spy();
+        io = new IntersectionObserver(spy, {root: rootEl});
+
+        runSequence([
+          function(done) {
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '0px';
+            io.observe(targetEl1);
+            io.observe(targetEl2);
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              var records = sortRecords(spy.lastCall.args[0]);
+              expect(records.length).to.be(2);
+              expect(records[0].target).to.be(targetEl1);
+              expect(records[0].intersectionRatio).to.be(1);
+              expect(records[1].target).to.be(targetEl2);
+              expect(records[1].intersectionRatio).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          },
+          function(done) {
+            io.disconnect();
+            targetEl1.style.top = targetEl2.style.top = '0px';
+            targetEl1.style.left = targetEl2.style.left = '-40px';
+            callDelayed(function() {
+              expect(spy.callCount).to.be(1);
+              done();
+            }, ASYNC_TIMEOUT);
+          }
+        ], done);
+
+      });
+
+    });
+
+  });
+
+
+  /**
+   * Runs a sequence of function and when finished invokes the done callback.
+   * Each function in the sequence is invoked with its own done function and
+   * it should call that function once it's complete.
+   * @param {Array<Function>} functions An array of async functions.
+   * @param {Function} done A final callback to be invoked once all function
+   *     have run.
+   */
+  function runSequence(functions, done) {
+    var next = functions.shift();
+    if (next) {
+      next(function() {
+        runSequence(functions, done);
+      });
+    } else {
+      done && done();
+    }
+  }
+
+
+  /**
+   * Sorts an array of records alphebetically by ascending ID. Since the current
+   * native implementation doesn't sort change entries by `observe` order, we do
+   * that ourselves for the non-polyfill case. Since all tests call observe
+   * on targets in sequential order, this should always match.
+   * https://crbug.com/613679
+   * @param {Array<IntersectionObserverEntry>} entries The entries to sort.
+   * @return {Array<IntersectionObserverEntry>} The sorted array.
+   */
+  function sortRecords(entries) {
+    entries = entries.sort(function(a, b) {
+      return a.target.id < b.target.id ? -1 : 1;
+    });
+    return entries;
+  }
+
+
+  /**
+   * Adds the common styles used by all tests to the page.
+   */
+  function addStyles() {
+    var styles = document.createElement('style');
+    styles.id = 'styles';
+    document.documentElement.appendChild(styles);
+
+    var cssText =
+        '#root {' +
+        '  position: relative;' +
+        '  width: 400px;' +
+        '  height: 200px;' +
+        '  background: #eee' +
+        '}' +
+        '#grand-parent {' +
+        '  position: relative;' +
+        '  width: 200px;' +
+        '  height: 200px;' +
+        '}' +
+        '#parent {' +
+        '  position: absolute;' +
+        '  top: 0px;' +
+        '  left: 200px;' +
+        '  overflow: hidden;' +
+        '  width: 200px;' +
+        '  height: 200px;' +
+        '  background: #ddd;' +
+        '}' +
+        '#target1, #target2, #target3, #target4 {' +
+        '  position: absolute;' +
+        '  top: 0px;' +
+        '  left: 0px;' +
+        '  width: 20px;' +
+        '  height: 20px;' +
+        '  transform: translateX(0px) translateY(0px);' +
+        '  transition: transform .5s;' +
+        '  background: #f00;' +
+        '  border: none;' +
+        '}';
+
+    styles.innerHTML = cssText;
+  }
+
+
+  /**
+   * Adds the DOM fixtures used by all tests to the page and assigns them to
+   * global variables so they can be referenced within the tests.
+   */
+  function addFixtures() {
+    var fixtures = document.createElement('div');
+    fixtures.id = 'fixtures';
+
+    fixtures.innerHTML =
+        '<div id="root">' +
+        '  <div id="grand-parent">' +
+        '    <div id="parent">' +
+        '      <div id="target1"></div>' +
+        '      <div id="target2"></div>' +
+        '      <div id="target3"></div>' +
+        '      <iframe id="target4"></iframe>' +
+        '    </div>' +
+        '  </div>' +
+        '</div>';
+
+    document.body.appendChild(fixtures);
+
+    rootEl = document.getElementById('root');
+    grandParentEl = document.getElementById('grand-parent');
+    parentEl = document.getElementById('parent');
+    targetEl1 = document.getElementById('target1');
+    targetEl2 = document.getElementById('target2');
+    targetEl3 = document.getElementById('target3');
+    targetEl4 = document.getElementById('target4');
+  }
+
+
+  /**
+   * Removes the common styles from the page.
+   */
+  function removeStyles() {
+    var styles = document.getElementById('styles');
+    styles.parentNode.removeChild(styles);
+  }
+
+
+  /**
+   * Removes the DOM fixtures from the page and resets the global references.
+   */
+  function removeFixtures() {
+    var fixtures = document.getElementById('fixtures');
+    fixtures.parentNode.removeChild(fixtures);
+
+    rootEl = null;
+    grandParentEl = null;
+    parentEl = null;
+    targetEl1 = null;
+    targetEl2 = null;
+    targetEl3 = null;
+    targetEl4 = null;
+  }
+
+  SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<div id="log">
+</div>
+</body>
+</html>
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -576,16 +576,25 @@ DOMInterfaces = {
     'wrapperCache': False,
 },
 
 'InputStream': {
     'nativeType': 'nsIInputStream',
     'notflattened': True
 },
 
+'IntersectionObserver': {
+    'nativeType': 'mozilla::dom::DOMIntersectionObserver',
+},
+
+'IntersectionObserverEntry': {
+    'nativeType': 'mozilla::dom::DOMIntersectionObserverEntry',
+    'headerFile': 'DOMIntersectionObserver.h',
+},
+
 'KeyEvent': {
     'concrete': False
 },
 
 'KeyframeEffect': {
     'implicitJSContext': { 'setterOnly': [ 'spacing' ] }
 },
 
--- a/dom/bindings/Errors.msg
+++ b/dom/bindings/Errors.msg
@@ -98,8 +98,9 @@ MSG_DEF(MSG_SW_UPDATE_BAD_REGISTRATION, 
 MSG_DEF(MSG_INVALID_DURATION_ERROR, 1, JSEXN_TYPEERR, "Invalid duration '{0}'.")
 MSG_DEF(MSG_INVALID_EASING_ERROR, 1, JSEXN_TYPEERR, "Invalid easing '{0}'.")
 MSG_DEF(MSG_INVALID_SPACING_MODE_ERROR, 1, JSEXN_TYPEERR, "Invalid spacing '{0}'.")
 MSG_DEF(MSG_USELESS_SETTIMEOUT, 1, JSEXN_TYPEERR, "Useless {0} call (missing quotes around argument?)")
 MSG_DEF(MSG_TOKENLIST_NO_SUPPORTED_TOKENS, 2, JSEXN_TYPEERR, "{0} attribute of <{1}> does not define any supported tokens")
 MSG_DEF(MSG_CACHE_STREAM_CLOSED, 0, JSEXN_TYPEERR, "Response body is a cache file stream that has already been closed.")
 MSG_DEF(MSG_TIME_VALUE_OUT_OF_RANGE, 1, JSEXN_TYPEERR, "{0} is outside the supported range for time values.")
 MSG_DEF(MSG_ONLY_IF_CACHED_WITHOUT_SAME_ORIGIN, 1, JSEXN_TYPEERR, "Request mode '{0}' was used, but request cache mode 'only-if-cached' can only be used with request mode 'same-origin'.")
+MSG_DEF(MSG_THRESHOLD_RANGE_ERROR, 0, JSEXN_RANGEERR, "Threshold values must all be in the range [0, 1].")
--- a/dom/webidl/DOMRect.webidl
+++ b/dom/webidl/DOMRect.webidl
@@ -24,9 +24,16 @@ interface DOMRectReadOnly {
     readonly attribute unrestricted double x;
     readonly attribute unrestricted double y;
     readonly attribute unrestricted double width;
     readonly attribute unrestricted double height;
     readonly attribute unrestricted double top;
     readonly attribute unrestricted double right;
     readonly attribute unrestricted double bottom;
     readonly attribute unrestricted double left;
-};
\ No newline at end of file
+};
+
+dictionary DOMRectInit {
+    unrestricted double x = 0;
+    unrestricted double y = 0;
+    unrestricted double width = 0;
+    unrestricted double height = 0;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/IntersectionObserver.webidl
@@ -0,0 +1,59 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://wicg.github.io/IntersectionObserver/
+ */
+
+[ProbablyShortLivingObject]
+interface IntersectionObserverEntry {
+  [Constant]
+  readonly attribute DOMHighResTimeStamp time;
+  [Constant]
+  readonly attribute DOMRectReadOnly? rootBounds;
+  [Constant]
+  readonly attribute DOMRectReadOnly boundingClientRect;
+  [Constant]
+  readonly attribute DOMRectReadOnly intersectionRect;
+  [Constant]
+  readonly attribute double intersectionRatio;
+  [Constant]
+  readonly attribute Element target;
+};
+
+[Constructor(IntersectionCallback intersectionCallback,
+             optional IntersectionObserverInit options)]
+interface IntersectionObserver {
+  [Constant]
+  readonly attribute Element? root;
+  [Constant]
+  readonly attribute DOMString rootMargin;
+  [Constant,Cached]
+  readonly attribute sequence<double> thresholds;
+  void observe(Element target);
+  void unobserve(Element target);
+  void disconnect();
+  sequence<IntersectionObserverEntry> takeRecords();
+
+  [ChromeOnly]
+  readonly attribute IntersectionCallback intersectionCallback;
+};
+
+callback IntersectionCallback =
+  void (sequence<IntersectionObserverEntry> entries, IntersectionObserver observer);
+
+dictionary IntersectionObserverEntryInit {
+  required DOMHighResTimeStamp time;
+  required DOMRectInit rootBounds;
+  required DOMRectInit boundingClientRect;
+  required DOMRectInit intersectionRect;
+  required Element target;
+};
+
+dictionary IntersectionObserverInit {
+  Element?  root = null;
+  DOMString rootMargin = "0px";
+  (double or sequence<double>) threshold = 0;
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -276,16 +276,17 @@ WEBIDL_FILES = [
     'ImageCapture.webidl',
     'ImageData.webidl',
     'ImageDocument.webidl',
     'InputEvent.webidl',
     'InputMethod.webidl',
     'InputPort.webidl',
     'InputPortManager.webidl',
     'InspectorUtils.webidl',
+    'IntersectionObserver.webidl',
     'IterableIterator.webidl',
     'KeyAlgorithm.webidl',
     'KeyboardEvent.webidl',
     'KeyEvent.webidl',
     'KeyframeAnimationOptions.webidl',
     'KeyframeEffect.webidl',
     'KeyIdsInitData.webidl',
     'LegacyQueryInterface.webidl',
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -75,16 +75,17 @@ using namespace mozilla::widget;
 using namespace mozilla::ipc;
 using namespace mozilla::layout;
 
 static mozilla::LazyLogModule sRefreshDriverLog("nsRefreshDriver");
 #define LOG(...) MOZ_LOG(sRefreshDriverLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
 
 #define DEFAULT_THROTTLED_FRAME_RATE 1
 #define DEFAULT_RECOMPUTE_VISIBILITY_INTERVAL_MS 1000
+#define DEFAULT_NOTIFY_INTERSECTION_OBSERVERS_INTERVAL_MS 100
 // after 10 minutes, stop firing off inactive timers
 #define DEFAULT_INACTIVE_TIMER_DISABLE_SECONDS 600
 
 // The number of seconds spent skipping frames because we are waiting for the compositor
 // before logging.
 #if defined(MOZ_ASAN)
 # define REFRESH_WAIT_WARNING 5
 #elif defined(DEBUG) && !defined(MOZ_VALGRIND)
@@ -971,16 +972,27 @@ nsRefreshDriver::GetMinRecomputeVisibili
   int32_t interval =
     Preferences::GetInt("layout.visibility.min-recompute-interval-ms", -1);
   if (interval <= 0) {
     interval = DEFAULT_RECOMPUTE_VISIBILITY_INTERVAL_MS;
   }
   return TimeDuration::FromMilliseconds(interval);
 }
 
+/* static */ mozilla::TimeDuration
+nsRefreshDriver::GetMinNotifyIntersectionObserversInterval()
+{
+  int32_t interval =
+    Preferences::GetInt("layout.visibility.min-notify-intersection-observers-interval-ms", -1);
+  if (interval <= 0) {
+    interval = DEFAULT_NOTIFY_INTERSECTION_OBSERVERS_INTERVAL_MS;
+  }
+  return TimeDuration::FromMilliseconds(interval);
+}
+
 double
 nsRefreshDriver::GetRefreshTimerInterval() const
 {
   return mThrottled ? GetThrottledTimerInterval() : GetRegularTimerInterval();
 }
 
 RefreshDriverTimer*
 nsRefreshDriver::ChooseTimer() const
@@ -1013,16 +1025,18 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
     mPresContext(aPresContext),
     mRootRefresh(nullptr),
     mPendingTransaction(0),
     mCompletedTransaction(0),
     mFreezeCount(0),
     mThrottledFrameRequestInterval(TimeDuration::FromMilliseconds(
                                      GetThrottledTimerInterval())),
     mMinRecomputeVisibilityInterval(GetMinRecomputeVisibilityInterval()),
+    mMinNotifyIntersectionObserversInterval(
+      GetMinNotifyIntersectionObserversInterval()),
     mThrottled(false),
     mNeedToRecomputeVisibility(false),
     mTestControllingRefreshes(false),
     mViewManagerFlushIsPending(false),
     mRequestedHighPrecision(false),
     mInRefresh(false),
     mWaitingForTransaction(false),
     mSkippedPaints(false),
@@ -1033,16 +1047,17 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
   MOZ_ASSERT(mPresContext,
              "Need a pres context to tell us to call Disconnect() later "
              "and decrement sRefreshDriverCount.");
   mMostRecentRefreshEpochTime = JS_Now();
   mMostRecentRefresh = TimeStamp::Now();
   mMostRecentTick = mMostRecentRefresh;
   mNextThrottledFrameRequestTick = mMostRecentTick;
   mNextRecomputeVisibilityTick = mMostRecentTick;
+  mNextNotifyIntersectionObserversTick = mMostRecentTick;
 
   ++sRefreshDriverCount;
 }
 
 nsRefreshDriver::~nsRefreshDriver()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(ObserverCount() == 0,
@@ -1823,16 +1838,32 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
       aNowTime >= mNextRecomputeVisibilityTick &&
       !presShell->IsPaintingSuppressed()) {
     mNextRecomputeVisibilityTick = aNowTime + mMinRecomputeVisibilityInterval;
     mNeedToRecomputeVisibility = false;
 
     presShell->ScheduleApproximateFrameVisibilityUpdateNow();
   }
 
+  bool notifyIntersectionObservers = false;
+  if (aNowTime >= mNextNotifyIntersectionObserversTick) {
+    mNextNotifyIntersectionObserversTick =
+      aNowTime + mMinNotifyIntersectionObserversInterval;
+    notifyIntersectionObservers = true;
+  }
+  nsCOMArray<nsIDocument> documents;
+  CollectDocuments(mPresContext->Document(), &documents);
+  for (int32_t i = 0; i < documents.Count(); ++i) {
+    nsIDocument* doc = documents[i];
+    doc->UpdateIntersectionObservations();
+    if (notifyIntersectionObservers) {
+      doc->ScheduleIntersectionObserverNotification();
+    }
+  }
+
   /*
    * Perform notification to imgIRequests subscribed to listen
    * for refresh events.
    */
 
   for (auto iter = mStartTable.Iter(); !iter.Done(); iter.Next()) {
     const uint32_t& delay = iter.Key();
     ImageStartData* data = iter.UserData();
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -363,16 +363,17 @@ private:
   // Trigger a refresh immediately, if haven't been disconnected or frozen.
   void DoRefresh();
 
   double GetRefreshTimerInterval() const;
   double GetRegularTimerInterval(bool *outIsDefault = nullptr) const;
   static double GetThrottledTimerInterval();
 
   static mozilla::TimeDuration GetMinRecomputeVisibilityInterval();
+  static mozilla::TimeDuration GetMinNotifyIntersectionObserversInterval();
 
   bool HaveFrameRequestCallbacks() const {
     return mFrameRequestCallbackDocs.Length() != 0;
   }
 
   void FinishedWaitingForTransaction();
 
   mozilla::RefreshDriverTimer* ChooseTimer() const;
@@ -398,16 +399,18 @@ private:
   const mozilla::TimeDuration mThrottledFrameRequestInterval;
 
   // How long we wait, at a minimum, before recomputing approximate frame
   // visibility information. This is a minimum because, regardless of this
   // interval, we only recompute visibility when we've seen a layout or style
   // flush since the last time we did it.
   const mozilla::TimeDuration mMinRecomputeVisibilityInterval;
 
+  const mozilla::TimeDuration mMinNotifyIntersectionObserversInterval;
+
   bool mThrottled;
   bool mNeedToRecomputeVisibility;
   bool mTestControllingRefreshes;
   bool mViewManagerFlushIsPending;
   bool mRequestedHighPrecision;
   bool mInRefresh;
 
   // True if the refresh driver is suspended waiting for transaction
@@ -428,16 +431,17 @@ private:
   // transaction to be completed before we append a note to the gfx critical log.
   // The number is doubled every time the threshold is hit.
   uint64_t mWarningThreshold;
   mozilla::TimeStamp mMostRecentRefresh;
   mozilla::TimeStamp mMostRecentTick;
   mozilla::TimeStamp mTickStart;
   mozilla::TimeStamp mNextThrottledFrameRequestTick;
   mozilla::TimeStamp mNextRecomputeVisibilityTick;
+  mozilla::TimeStamp mNextNotifyIntersectionObserversTick;
 
   // separate arrays for each flush type we support
   ObserverArray mObservers[3];
   RequestTable mRequests;
   ImageStartTable mStartTable;
 
   struct PendingEvent {
     nsCOMPtr<nsINode> mTarget;
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -227,16 +227,22 @@ public:
                                  nsCSSValue& aValue);
 
   bool ParseColorString(const nsSubstring& aBuffer,
                         nsIURI* aURL, // for error reporting
                         uint32_t aLineNumber, // for error reporting
                         nsCSSValue& aValue,
                         bool aSuppressErrors /* false */);
 
+  bool ParseMarginString(const nsSubstring& aBuffer,
+                         nsIURI* aURL, // for error reporting
+                         uint32_t aLineNumber, // for error reporting
+                         nsCSSValue& aValue,
+                         bool aSuppressErrors /* false */);
+
   nsresult ParseSelectorString(const nsSubstring& aSelectorString,
                                nsIURI* aURL, // for error reporting
                                uint32_t aLineNumber, // for error reporting
                                nsCSSSelectorList **aSelectorList);
 
   already_AddRefed<nsCSSKeyframeRule>
   ParseKeyframeRule(const nsSubstring& aBuffer,
                     nsIURI*            aURL,
@@ -1153,17 +1159,18 @@ protected:
    * Calls AppendImpliedEOFCharacters on mScanner.
    */
   void AppendImpliedEOFCharacters(nsAString& aResult);
 
   // Reused utility parsing routines
   void AppendValue(nsCSSPropertyID aPropID, const nsCSSValue& aValue);
   bool ParseBoxProperties(const nsCSSPropertyID aPropIDs[]);
   bool ParseGroupedBoxProperty(int32_t aVariantMask,
-                               nsCSSValue& aValue);
+                               nsCSSValue& aValue,
+                               uint32_t aRestrictions);
   bool ParseBoxCornerRadius(const nsCSSPropertyID aPropID);
   bool ParseBoxCornerRadiiInternals(nsCSSValue array[]);
   bool ParseBoxCornerRadii(const nsCSSPropertyID aPropIDs[]);
 
   int32_t ParseChoice(nsCSSValue aValues[],
                       const nsCSSPropertyID aPropIDs[], int32_t aNumIDs);
 
   CSSParseResult ParseColor(nsCSSValue& aValue);
@@ -2268,16 +2275,42 @@ CSSParserImpl::ParseColorString(const ns
     OUTPUT_ERROR();
   }
 
   ReleaseScanner();
   return colorParsed;
 }
 
 bool
+CSSParserImpl::ParseMarginString(const nsSubstring& aBuffer,
+                                 nsIURI* aURI, // for error reporting
+                                 uint32_t aLineNumber, // for error reporting
+                                 nsCSSValue& aValue,
+                                 bool aSuppressErrors /* false */)
+{
+  nsCSSScanner scanner(aBuffer, aLineNumber);
+  css::ErrorReporter reporter(scanner, mSheet, mChildLoader, aURI);
+  InitScanner(scanner, reporter, aURI, aURI, nullptr);
+
+  nsAutoSuppressErrors suppressErrors(this, aSuppressErrors);
+
+  // Parse a margin, and check that there's nothing else after it.
+  bool marginParsed = ParseGroupedBoxProperty(VARIANT_LP, aValue, 0) && !GetToken(true);
+
+  if (aSuppressErrors) {
+    CLEAR_ERROR();
+  } else {
+    OUTPUT_ERROR();
+  }
+
+  ReleaseScanner();
+  return marginParsed;
+}
+
+bool
 CSSParserImpl::ParseFontFamilyListString(const nsSubstring& aBuffer,
                                          nsIURI* aURI, // for error reporting
                                          uint32_t aLineNumber, // for error reporting
                                          nsCSSValue& aValue)
 {
   nsCSSScanner scanner(aBuffer, aLineNumber);
   css::ErrorReporter reporter(scanner, mSheet, mChildLoader, aURI);
   InitScanner(scanner, reporter, aURI, aURI, nullptr);
@@ -11179,29 +11212,30 @@ CSSParserImpl::ParseBoxProperties(const 
 
   NS_FOR_CSS_SIDES (index) {
     AppendValue(aPropIDs[index], result.*(nsCSSRect::sides[index]));
   }
   return true;
 }
 
 // Similar to ParseBoxProperties, except there is only one property
-// with the result as its value, not four. Requires values be nonnegative.
+// with the result as its value, not four.
 bool
 CSSParserImpl::ParseGroupedBoxProperty(int32_t aVariantMask,
-                                       /** outparam */ nsCSSValue& aValue)
+                                       /** outparam */ nsCSSValue& aValue,
+                                       uint32_t aRestrictions)
 {
   nsCSSRect& result = aValue.SetRectValue();
 
   int32_t count = 0;
   NS_FOR_CSS_SIDES (index) {
     CSSParseResult parseResult =
       ParseVariantWithRestrictions(result.*(nsCSSRect::sides[index]),
                                    aVariantMask, nullptr,
-                                   CSS_PROPERTY_VALUE_NONNEGATIVE);
+                                   aRestrictions);
     if (parseResult == CSSParseResult::NotFound) {
       break;
     }
     if (parseResult == CSSParseResult::Error) {
       return false;
     }
     count++;
   }
@@ -13205,17 +13239,18 @@ CSSParserImpl::ParseBorderImageSlice(boo
 
   // Try parsing "fill" value.
   nsCSSValue imageSliceFillValue;
   bool hasFill = ParseEnum(imageSliceFillValue,
                            nsCSSProps::kBorderImageSliceKTable);
 
   // Parse the box dimensions.
   nsCSSValue imageSliceBoxValue;
-  if (!ParseGroupedBoxProperty(VARIANT_PN, imageSliceBoxValue)) {
+  if (!ParseGroupedBoxProperty(VARIANT_PN, imageSliceBoxValue,
+                               CSS_PROPERTY_VALUE_NONNEGATIVE)) {
     if (!hasFill && aConsumedTokens) {
       *aConsumedTokens = false;
     }
 
     return false;
   }
 
   // Try parsing "fill" keyword again if the first time failed because keyword
@@ -13249,17 +13284,17 @@ CSSParserImpl::ParseBorderImageWidth(boo
       ParseSingleTokenVariant(value, VARIANT_INHERIT, nullptr)) {
     // Keywords "inherit", "initial" and "unset" can not be mixed, so we
     // are done.
     AppendValue(eCSSProperty_border_image_width, value);
     return true;
   }
 
   // Parse the box dimensions.
-  if (!ParseGroupedBoxProperty(VARIANT_ALPN, value)) {
+  if (!ParseGroupedBoxProperty(VARIANT_ALPN, value, CSS_PROPERTY_VALUE_NONNEGATIVE)) {
     return false;
   }
 
   AppendValue(eCSSProperty_border_image_width, value);
   return true;
 }
 
 bool
@@ -13272,17 +13307,17 @@ CSSParserImpl::ParseBorderImageOutset(bo
       ParseSingleTokenVariant(value, VARIANT_INHERIT, nullptr)) {
     // Keywords "inherit", "initial" and "unset" can not be mixed, so we
     // are done.
     AppendValue(eCSSProperty_border_image_outset, value);
     return true;
   }
 
   // Parse the box dimensions.
-  if (!ParseGroupedBoxProperty(VARIANT_LN, value)) {
+  if (!ParseGroupedBoxProperty(VARIANT_LN, value, CSS_PROPERTY_VALUE_NONNEGATIVE)) {
     return false;
   }
 
   AppendValue(eCSSProperty_border_image_outset, value);
   return true;
 }
 
 bool
@@ -17989,16 +18024,27 @@ nsCSSParser::ParseColorString(const nsSu
                               uint32_t           aLineNumber,
                               nsCSSValue&        aValue,
                               bool               aSuppressErrors /* false */)
 {
   return static_cast<CSSParserImpl*>(mImpl)->
     ParseColorString(aBuffer, aURI, aLineNumber, aValue, aSuppressErrors);
 }
 
+bool
+nsCSSParser::ParseMarginString(const nsSubstring& aBuffer,
+                               nsIURI*            aURI,
+                               uint32_t           aLineNumber,
+                               nsCSSValue&        aValue,
+                               bool               aSuppressErrors /* false */)
+{
+  return static_cast<CSSParserImpl*>(mImpl)->
+    ParseMarginString(aBuffer, aURI, aLineNumber, aValue, aSuppressErrors);
+}
+
 nsresult
 nsCSSParser::ParseSelectorString(const nsSubstring&  aSelectorString,
                                  nsIURI*             aURI,
                                  uint32_t            aLineNumber,
                                  nsCSSSelectorList** aSelectorList)
 {
   return static_cast<CSSParserImpl*>(mImpl)->
     ParseSelectorString(aSelectorString, aURI, aLineNumber, aSelectorList);
--- a/layout/style/nsCSSParser.h
+++ b/layout/style/nsCSSParser.h
@@ -201,16 +201,28 @@ public:
    */
   bool ParseColorString(const nsSubstring& aBuffer,
                         nsIURI*            aURL,
                         uint32_t           aLineNumber,
                         nsCSSValue&        aValue,
                         bool               aSuppressErrors = false);
 
   /**
+   * Parse aBuffer into a nsCSSValue |aValue|. Will return false
+   * if aBuffer is not a valid CSS margin specification.
+   * One can use nsRuleNode::GetRectValue to compute an nsCSSRect from
+   * the returned nsCSSValue.
+   */
+  bool ParseMarginString(const nsSubstring& aBuffer,
+                         nsIURI*            aURL,
+                         uint32_t           aLineNumber,
+                         nsCSSValue&        aValue,
+                         bool               aSuppressErrors = false);
+
+  /**
    * Parse aBuffer into a selector list.  On success, caller must
    * delete *aSelectorList when done with it.
    */
   nsresult ParseSelectorString(const nsSubstring&  aSelectorString,
                                nsIURI*             aURL,
                                uint32_t            aLineNumber,
                                nsCSSSelectorList** aSelectorList);
 
--- a/layout/style/nsCSSPropertyID.h
+++ b/layout/style/nsCSSPropertyID.h
@@ -58,17 +58,20 @@ enum nsCSSPropertyID {
   eCSSPropertyExtra_no_properties,
   eCSSPropertyExtra_all_properties,
 
   // Extra dummy values for nsCSSParser internal use.
   eCSSPropertyExtra_x_none_value,
   eCSSPropertyExtra_x_auto_value,
 
   // Extra value to represent custom properties (--*).
-  eCSSPropertyExtra_variable
+  eCSSPropertyExtra_variable,
+
+  // Extra value for use in the DOM API's
+  eCSSProperty_DOM
 };
 
 namespace mozilla {
 
 template<>
 inline PLDHashNumber
 Hash<nsCSSPropertyID>(const nsCSSPropertyID& aValue)
 {
--- a/layout/style/nsCSSValue.cpp
+++ b/layout/style/nsCSSValue.cpp
@@ -1213,17 +1213,18 @@ nsCSSValue::AppendAlignJustifyValueToStr
 
 void
 nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult,
                            Serialization aSerialization) const
 {
   // eCSSProperty_UNKNOWN gets used for some recursive calls below.
   MOZ_ASSERT((0 <= aProperty &&
               aProperty <= eCSSProperty_COUNT_no_shorthands) ||
-             aProperty == eCSSProperty_UNKNOWN,
+             aProperty == eCSSProperty_UNKNOWN ||
+             aProperty == eCSSProperty_DOM,
              "property ID out of range");
 
   nsCSSUnit unit = GetUnit();
   if (unit == eCSSUnit_Null) {
     return;
   }
 
   if (eCSSUnit_String <= unit && unit <= eCSSUnit_Attr) {
@@ -2424,17 +2425,18 @@ nsCSSRect::AppendToString(nsCSSPropertyI
   MOZ_ASSERT(mTop.GetUnit() != eCSSUnit_Null &&
              mTop.GetUnit() != eCSSUnit_Inherit &&
              mTop.GetUnit() != eCSSUnit_Initial &&
              mTop.GetUnit() != eCSSUnit_Unset,
              "parser should have used a bare value");
 
   if (eCSSProperty_border_image_slice == aProperty ||
       eCSSProperty_border_image_width == aProperty ||
-      eCSSProperty_border_image_outset == aProperty) {
+      eCSSProperty_border_image_outset == aProperty ||
+      eCSSProperty_DOM == aProperty) {
     NS_NAMED_LITERAL_STRING(space, " ");
 
     mTop.AppendToString(aProperty, aResult, aSerialization);
     aResult.Append(space);
     mRight.AppendToString(aProperty, aResult, aSerialization);
     aResult.Append(space);
     mBottom.AppendToString(aProperty, aResult, aSerialization);
     aResult.Append(space);
--- a/layout/style/nsCSSValue.h
+++ b/layout/style/nsCSSValue.h
@@ -660,16 +660,20 @@ public:
     { return eCSSUnit_EM <= mUnit && mUnit <= eCSSUnit_RootEM; }
   /**
    * A "pixel" length unit is a some multiple of CSS pixels.
    */
   static bool IsPixelLengthUnit(nsCSSUnit aUnit)
     { return eCSSUnit_Point <= aUnit && aUnit <= eCSSUnit_Pixel; }
   bool      IsPixelLengthUnit() const
     { return IsPixelLengthUnit(mUnit); }
+  static bool IsPercentLengthUnit(nsCSSUnit aUnit)
+    { return aUnit == eCSSUnit_Percent; }
+  bool      IsPercentLengthUnit()
+    { return IsPercentLengthUnit(mUnit); }
   static bool IsFloatUnit(nsCSSUnit aUnit)
     { return eCSSUnit_Number <= aUnit; }
   bool      IsAngularUnit() const  
     { return eCSSUnit_Degree <= mUnit && mUnit <= eCSSUnit_Turn; }
   bool      IsFrequencyUnit() const  
     { return eCSSUnit_Hertz <= mUnit && mUnit <= eCSSUnit_Kilohertz; }
   bool      IsTimeUnit() const  
     { return eCSSUnit_Seconds <= mUnit && mUnit <= eCSSUnit_Milliseconds; }