1/*
2 * Copyright (C) 2016 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27
28#if ENABLE(INTERSECTION_OBSERVER)
29#include "IntersectionObserver.h"
30
31#include "CSSParserTokenRange.h"
32#include "CSSPropertyParserHelpers.h"
33#include "CSSTokenizer.h"
34#include "DOMWindow.h"
35#include "Element.h"
36#include "InspectorInstrumentation.h"
37#include "IntersectionObserverCallback.h"
38#include "IntersectionObserverEntry.h"
39#include "Performance.h"
40#include <wtf/Vector.h>
41
42namespace WebCore {
43
44static ExceptionOr<LengthBox> parseRootMargin(String& rootMargin)
45{
46 CSSTokenizer tokenizer(rootMargin);
47 auto tokenRange = tokenizer.tokenRange();
48 Vector<Length, 4> margins;
49 while (!tokenRange.atEnd()) {
50 if (margins.size() == 4)
51 return Exception { SyntaxError, "Failed to construct 'IntersectionObserver': Extra text found at the end of rootMargin." };
52 RefPtr<CSSPrimitiveValue> parsedValue = CSSPropertyParserHelpers::consumeLengthOrPercent(tokenRange, HTMLStandardMode, ValueRangeAll);
53 if (!parsedValue || parsedValue->isCalculated())
54 return Exception { SyntaxError, "Failed to construct 'IntersectionObserver': rootMargin must be specified in pixels or percent." };
55 if (parsedValue->isPercentage())
56 margins.append(Length(parsedValue->doubleValue(), Percent));
57 else if (parsedValue->isPx())
58 margins.append(Length(parsedValue->intValue(), Fixed));
59 else
60 return Exception { SyntaxError, "Failed to construct 'IntersectionObserver': rootMargin must be specified in pixels or percent." };
61 }
62 switch (margins.size()) {
63 case 0:
64 for (unsigned i = 0; i < 4; ++i)
65 margins.append(Length(0, Fixed));
66 break;
67 case 1:
68 for (unsigned i = 0; i < 3; ++i)
69 margins.append(margins[0]);
70 break;
71 case 2:
72 margins.append(margins[0]);
73 margins.append(margins[1]);
74 break;
75 case 3:
76 margins.append(margins[1]);
77 break;
78 case 4:
79 break;
80 default:
81 ASSERT_NOT_REACHED();
82 }
83
84 return LengthBox(WTFMove(margins[0]), WTFMove(margins[1]), WTFMove(margins[2]), WTFMove(margins[3]));
85}
86
87ExceptionOr<Ref<IntersectionObserver>> IntersectionObserver::create(Document& document, Ref<IntersectionObserverCallback>&& callback, IntersectionObserver::Init&& init)
88{
89 auto rootMarginOrException = parseRootMargin(init.rootMargin);
90 if (rootMarginOrException.hasException())
91 return rootMarginOrException.releaseException();
92
93 Vector<double> thresholds;
94 WTF::switchOn(init.threshold, [&thresholds] (double initThreshold) {
95 thresholds.reserveInitialCapacity(1);
96 thresholds.uncheckedAppend(initThreshold);
97 }, [&thresholds] (Vector<double>& initThresholds) {
98 thresholds = WTFMove(initThresholds);
99 });
100
101 for (auto threshold : thresholds) {
102 if (!(threshold >= 0 && threshold <= 1))
103 return Exception { RangeError, "Failed to construct 'IntersectionObserver': all thresholds must lie in the range [0.0, 1.0]." };
104 }
105
106 return adoptRef(*new IntersectionObserver(document, WTFMove(callback), init.root, rootMarginOrException.releaseReturnValue(), WTFMove(thresholds)));
107}
108
109IntersectionObserver::IntersectionObserver(Document& document, Ref<IntersectionObserverCallback>&& callback, Element* root, LengthBox&& parsedRootMargin, Vector<double>&& thresholds)
110 : ActiveDOMObject(callback->scriptExecutionContext())
111 , m_root(root)
112 , m_rootMargin(WTFMove(parsedRootMargin))
113 , m_thresholds(WTFMove(thresholds))
114 , m_callback(WTFMove(callback))
115{
116 if (m_root) {
117 auto& observerData = m_root->ensureIntersectionObserverData();
118 observerData.observers.append(makeWeakPtr(this));
119 } else if (auto* frame = document.frame())
120 m_implicitRootDocument = makeWeakPtr(frame->mainFrame().document());
121
122 std::sort(m_thresholds.begin(), m_thresholds.end());
123 suspendIfNeeded();
124}
125
126IntersectionObserver::~IntersectionObserver()
127{
128 if (m_root)
129 m_root->intersectionObserverData()->observers.removeFirst(this);
130 disconnect();
131}
132
133String IntersectionObserver::rootMargin() const
134{
135 StringBuilder stringBuilder;
136 PhysicalBoxSide sides[4] = { PhysicalBoxSide::Top, PhysicalBoxSide::Right, PhysicalBoxSide::Bottom, PhysicalBoxSide::Left };
137 for (auto side : sides) {
138 auto& length = m_rootMargin.at(side);
139 stringBuilder.appendNumber(length.intValue());
140 if (length.type() == Percent)
141 stringBuilder.append('%');
142 else
143 stringBuilder.appendLiteral("px");
144 if (side != PhysicalBoxSide::Left)
145 stringBuilder.append(' ');
146 }
147 return stringBuilder.toString();
148}
149
150void IntersectionObserver::observe(Element& target)
151{
152 if (!trackingDocument() || !m_callback || m_observationTargets.contains(&target))
153 return;
154
155 target.ensureIntersectionObserverData().registrations.append({ makeWeakPtr(this), WTF::nullopt });
156 bool hadObservationTargets = hasObservationTargets();
157 m_observationTargets.append(&target);
158 auto* document = trackingDocument();
159 if (!hadObservationTargets)
160 document->addIntersectionObserver(*this);
161 document->scheduleInitialIntersectionObservationUpdate();
162}
163
164void IntersectionObserver::unobserve(Element& target)
165{
166 if (!removeTargetRegistration(target))
167 return;
168
169 bool removed = m_observationTargets.removeFirst(&target);
170 ASSERT_UNUSED(removed, removed);
171
172 if (!hasObservationTargets()) {
173 if (auto* document = trackingDocument())
174 document->removeIntersectionObserver(*this);
175 }
176}
177
178void IntersectionObserver::disconnect()
179{
180 if (!hasObservationTargets())
181 return;
182
183 removeAllTargets();
184 if (auto* document = trackingDocument())
185 document->removeIntersectionObserver(*this);
186}
187
188auto IntersectionObserver::takeRecords() -> TakenRecords
189{
190 return { WTFMove(m_queuedEntries), WTFMove(m_pendingTargets) };
191}
192
193void IntersectionObserver::targetDestroyed(Element& target)
194{
195 m_observationTargets.removeFirst(&target);
196 if (!hasObservationTargets()) {
197 if (auto* document = trackingDocument())
198 document->removeIntersectionObserver(*this);
199 }
200}
201
202bool IntersectionObserver::removeTargetRegistration(Element& target)
203{
204 auto* observerData = target.intersectionObserverData();
205 if (!observerData)
206 return false;
207
208 auto& registrations = observerData->registrations;
209 return registrations.removeFirstMatching([this](auto& registration) {
210 return registration.observer.get() == this;
211 });
212}
213
214void IntersectionObserver::removeAllTargets()
215{
216 for (auto* target : m_observationTargets) {
217 bool removed = removeTargetRegistration(*target);
218 ASSERT_UNUSED(removed, removed);
219 }
220 m_observationTargets.clear();
221}
222
223void IntersectionObserver::rootDestroyed()
224{
225 ASSERT(m_root);
226 disconnect();
227 m_root = nullptr;
228}
229
230bool IntersectionObserver::createTimestamp(DOMHighResTimeStamp& timestamp) const
231{
232 if (!m_callback)
233 return false;
234
235 auto* context = m_callback->scriptExecutionContext();
236 if (!context)
237 return false;
238 ASSERT(context->isDocument());
239 auto& document = downcast<Document>(*context);
240 if (auto* window = document.domWindow()) {
241 timestamp = window->performance().now();
242 return true;
243 }
244 return false;
245}
246
247void IntersectionObserver::appendQueuedEntry(Ref<IntersectionObserverEntry>&& entry)
248{
249 ASSERT(entry->target());
250 m_pendingTargets.append(*entry->target());
251 m_queuedEntries.append(WTFMove(entry));
252}
253
254void IntersectionObserver::notify()
255{
256 if (m_queuedEntries.isEmpty()) {
257 ASSERT(m_pendingTargets.isEmpty());
258 return;
259 }
260
261 auto* context = m_callback->scriptExecutionContext();
262 if (!context)
263 return;
264
265 InspectorInstrumentationCookie cookie = InspectorInstrumentation::willFireObserverCallback(*context, "IntersectionObserver"_s);
266
267 auto takenRecords = takeRecords();
268 m_callback->handleEvent(WTFMove(takenRecords.records), *this);
269
270 InspectorInstrumentation::didFireObserverCallback(cookie);
271}
272
273bool IntersectionObserver::hasPendingActivity() const
274{
275 return (hasObservationTargets() && trackingDocument()) || !m_queuedEntries.isEmpty();
276}
277
278const char* IntersectionObserver::activeDOMObjectName() const
279{
280 return "IntersectionObserver";
281}
282
283bool IntersectionObserver::canSuspendForDocumentSuspension() const
284{
285 return true;
286}
287
288void IntersectionObserver::stop()
289{
290 disconnect();
291 m_callback = nullptr;
292 m_queuedEntries.clear();
293 m_pendingTargets.clear();
294}
295
296} // namespace WebCore
297
298#endif // ENABLE(INTERSECTION_OBSERVER)
299