| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "core/dom/IntersectionObserver.h" | 5 #include "core/dom/IntersectionObserver.h" |
| 6 | 6 |
| 7 #include "bindings/core/v8/ExceptionState.h" | 7 #include "bindings/core/v8/ExceptionState.h" |
| 8 #include "core/css/parser/CSSParserTokenRange.h" | 8 #include "core/css/parser/CSSParserTokenRange.h" |
| 9 #include "core/css/parser/CSSTokenizer.h" | 9 #include "core/css/parser/CSSTokenizer.h" |
| 10 #include "core/dom/Element.h" | 10 #include "core/dom/Element.h" |
| 11 #include "core/dom/ElementIntersectionObserverData.h" |
| 11 #include "core/dom/ExceptionCode.h" | 12 #include "core/dom/ExceptionCode.h" |
| 12 #include "core/dom/ExecutionContext.h" | 13 #include "core/dom/ExecutionContext.h" |
| 13 #include "core/dom/IntersectionObserverCallback.h" | 14 #include "core/dom/IntersectionObserverCallback.h" |
| 14 #include "core/dom/IntersectionObserverController.h" | 15 #include "core/dom/IntersectionObserverController.h" |
| 15 #include "core/dom/IntersectionObserverEntry.h" | 16 #include "core/dom/IntersectionObserverEntry.h" |
| 16 #include "core/dom/IntersectionObserverInit.h" | 17 #include "core/dom/IntersectionObserverInit.h" |
| 17 #include "core/dom/NodeIntersectionObserverData.h" | |
| 18 #include "core/frame/FrameView.h" | 18 #include "core/frame/FrameView.h" |
| 19 #include "core/frame/LocalDOMWindow.h" | 19 #include "core/frame/LocalDOMWindow.h" |
| 20 #include "core/frame/LocalFrame.h" | 20 #include "core/frame/LocalFrame.h" |
| 21 #include "core/html/HTMLFrameOwnerElement.h" | |
| 22 #include "core/inspector/ConsoleMessage.h" | 21 #include "core/inspector/ConsoleMessage.h" |
| 22 #include "core/layout/LayoutView.h" |
| 23 #include "core/timing/DOMWindowPerformance.h" | 23 #include "core/timing/DOMWindowPerformance.h" |
| 24 #include "core/timing/Performance.h" | 24 #include "core/timing/Performance.h" |
| 25 #include "platform/Timer.h" | 25 #include "platform/Timer.h" |
| 26 #include <algorithm> | 26 #include <algorithm> |
| 27 | 27 |
| 28 namespace blink { | 28 namespace blink { |
| 29 | 29 |
| 30 namespace { | 30 namespace { |
| 31 | 31 |
| 32 // Internal implementation of IntersectionObserverCallback when using | 32 // Internal implementation of IntersectionObserverCallback when using |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 121 thresholdValue > 1.0) { | 121 thresholdValue > 1.0) { |
| 122 exceptionState.throwRangeError( | 122 exceptionState.throwRangeError( |
| 123 "Threshold values must be numbers between 0 and 1"); | 123 "Threshold values must be numbers between 0 and 1"); |
| 124 break; | 124 break; |
| 125 } | 125 } |
| 126 } | 126 } |
| 127 | 127 |
| 128 std::sort(thresholds.begin(), thresholds.end()); | 128 std::sort(thresholds.begin(), thresholds.end()); |
| 129 } | 129 } |
| 130 | 130 |
| 131 // Returns the root Node of a given Document to use as the IntersectionObserver | |
| 132 // root when no root is given. | |
| 133 // TODO(szager): it doesn't support RemoteFrames, see https://crbug.com/615156 | |
| 134 Node* getRootNode(Document* document) { | |
| 135 Frame* mainFrame = document->frame()->tree().top(); | |
| 136 if (mainFrame && mainFrame->isLocalFrame()) | |
| 137 return toLocalFrame(mainFrame)->document(); | |
| 138 return nullptr; | |
| 139 } | |
| 140 | |
| 141 } // anonymous namespace | 131 } // anonymous namespace |
| 142 | 132 |
| 143 IntersectionObserver* IntersectionObserver::create( | 133 IntersectionObserver* IntersectionObserver::create( |
| 144 const IntersectionObserverInit& observerInit, | 134 const IntersectionObserverInit& observerInit, |
| 145 IntersectionObserverCallback& callback, | 135 IntersectionObserverCallback& callback, |
| 146 ExceptionState& exceptionState) { | 136 ExceptionState& exceptionState) { |
| 147 Node* root = observerInit.root(); | 137 Element* root = observerInit.root(); |
| 148 if (!root) { | |
| 149 ExecutionContext* context = callback.getExecutionContext(); | |
| 150 DCHECK(context->isDocument()); | |
| 151 root = getRootNode(toDocument(context)); | |
| 152 } | |
| 153 if (!root) { | |
| 154 exceptionState.throwDOMException( | |
| 155 HierarchyRequestError, | |
| 156 "Unable to get root node in main frame to track."); | |
| 157 return nullptr; | |
| 158 } | |
| 159 | 138 |
| 160 Vector<Length> rootMargin; | 139 Vector<Length> rootMargin; |
| 161 parseRootMargin(observerInit.rootMargin(), rootMargin, exceptionState); | 140 parseRootMargin(observerInit.rootMargin(), rootMargin, exceptionState); |
| 162 if (exceptionState.hadException()) | 141 if (exceptionState.hadException()) |
| 163 return nullptr; | 142 return nullptr; |
| 164 | 143 |
| 165 Vector<float> thresholds; | 144 Vector<float> thresholds; |
| 166 parseThresholds(observerInit.threshold(), thresholds, exceptionState); | 145 parseThresholds(observerInit.threshold(), thresholds, exceptionState); |
| 167 if (exceptionState.hadException()) | 146 if (exceptionState.hadException()) |
| 168 return nullptr; | 147 return nullptr; |
| 169 | 148 |
| 170 return new IntersectionObserver(callback, *root, rootMargin, thresholds); | 149 return new IntersectionObserver(callback, root, rootMargin, thresholds); |
| 171 } | 150 } |
| 172 | 151 |
| 173 IntersectionObserver* IntersectionObserver::create( | 152 IntersectionObserver* IntersectionObserver::create( |
| 174 const Vector<Length>& rootMargin, | 153 const Vector<Length>& rootMargin, |
| 175 const Vector<float>& thresholds, | 154 const Vector<float>& thresholds, |
| 176 Document* document, | 155 Document* document, |
| 177 std::unique_ptr<EventCallback> callback, | 156 std::unique_ptr<EventCallback> callback, |
| 178 ExceptionState& exceptionState) { | 157 ExceptionState& exceptionState) { |
| 179 Node* root = getRootNode(document); | |
| 180 if (!root) { | |
| 181 exceptionState.throwDOMException( | |
| 182 HierarchyRequestError, | |
| 183 "Unable to get root node in main frame to track."); | |
| 184 return nullptr; | |
| 185 } | |
| 186 | |
| 187 IntersectionObserverCallbackImpl* intersectionObserverCallback = | 158 IntersectionObserverCallbackImpl* intersectionObserverCallback = |
| 188 new IntersectionObserverCallbackImpl(document, std::move(callback)); | 159 new IntersectionObserverCallbackImpl(document, std::move(callback)); |
| 189 return new IntersectionObserver(*intersectionObserverCallback, *root, | 160 return new IntersectionObserver(*intersectionObserverCallback, nullptr, |
| 190 rootMargin, thresholds); | 161 rootMargin, thresholds); |
| 191 } | 162 } |
| 192 | 163 |
| 193 IntersectionObserver::IntersectionObserver( | 164 IntersectionObserver::IntersectionObserver( |
| 194 IntersectionObserverCallback& callback, | 165 IntersectionObserverCallback& callback, |
| 195 Node& root, | 166 Element* root, |
| 196 const Vector<Length>& rootMargin, | 167 const Vector<Length>& rootMargin, |
| 197 const Vector<float>& thresholds) | 168 const Vector<float>& thresholds) |
| 198 : m_callback(&callback), | 169 : m_callback(&callback), |
| 199 m_root(&root), | 170 m_root(root), |
| 200 m_thresholds(thresholds), | 171 m_thresholds(thresholds), |
| 201 m_topMargin(Fixed), | 172 m_topMargin(Fixed), |
| 202 m_rightMargin(Fixed), | 173 m_rightMargin(Fixed), |
| 203 m_bottomMargin(Fixed), | 174 m_bottomMargin(Fixed), |
| 204 m_leftMargin(Fixed), | 175 m_leftMargin(Fixed), |
| 176 m_rootIsImplicit(root ? 0 : 1), |
| 205 m_initialState(InitialState::kHidden) { | 177 m_initialState(InitialState::kHidden) { |
| 206 switch (rootMargin.size()) { | 178 switch (rootMargin.size()) { |
| 207 case 0: | 179 case 0: |
| 208 break; | 180 break; |
| 209 case 1: | 181 case 1: |
| 210 m_topMargin = m_rightMargin = m_bottomMargin = m_leftMargin = | 182 m_topMargin = m_rightMargin = m_bottomMargin = m_leftMargin = |
| 211 rootMargin[0]; | 183 rootMargin[0]; |
| 212 break; | 184 break; |
| 213 case 2: | 185 case 2: |
| 214 m_topMargin = m_bottomMargin = rootMargin[0]; | 186 m_topMargin = m_bottomMargin = rootMargin[0]; |
| 215 m_rightMargin = m_leftMargin = rootMargin[1]; | 187 m_rightMargin = m_leftMargin = rootMargin[1]; |
| 216 break; | 188 break; |
| 217 case 3: | 189 case 3: |
| 218 m_topMargin = rootMargin[0]; | 190 m_topMargin = rootMargin[0]; |
| 219 m_rightMargin = m_leftMargin = rootMargin[1]; | 191 m_rightMargin = m_leftMargin = rootMargin[1]; |
| 220 m_bottomMargin = rootMargin[2]; | 192 m_bottomMargin = rootMargin[2]; |
| 221 break; | 193 break; |
| 222 case 4: | 194 case 4: |
| 223 m_topMargin = rootMargin[0]; | 195 m_topMargin = rootMargin[0]; |
| 224 m_rightMargin = rootMargin[1]; | 196 m_rightMargin = rootMargin[1]; |
| 225 m_bottomMargin = rootMargin[2]; | 197 m_bottomMargin = rootMargin[2]; |
| 226 m_leftMargin = rootMargin[3]; | 198 m_leftMargin = rootMargin[3]; |
| 227 break; | 199 break; |
| 228 default: | 200 default: |
| 229 NOTREACHED(); | 201 NOTREACHED(); |
| 230 break; | 202 break; |
| 231 } | 203 } |
| 232 root.document().ensureIntersectionObserverController().addTrackedObserver( | 204 if (root) |
| 205 root->ensureIntersectionObserverData().addObserver(*this); |
| 206 trackingDocument().ensureIntersectionObserverController().addTrackedObserver( |
| 233 *this); | 207 *this); |
| 234 } | 208 } |
| 235 | 209 |
| 236 void IntersectionObserver::clearWeakMembers(Visitor* visitor) { | 210 void IntersectionObserver::clearWeakMembers(Visitor* visitor) { |
| 237 if (ThreadHeap::isHeapObjectAlive(m_root)) | 211 if (ThreadHeap::isHeapObjectAlive(root())) |
| 238 return; | 212 return; |
| 239 IgnorableExceptionState exceptionState; | 213 IgnorableExceptionState exceptionState; |
| 240 disconnect(exceptionState); | 214 disconnect(exceptionState); |
| 241 m_root = nullptr; | 215 m_root = nullptr; |
| 242 } | 216 } |
| 243 | 217 |
| 218 bool IntersectionObserver::rootIsValid() const { |
| 219 return rootIsImplicit() || root(); |
| 220 } |
| 221 |
| 222 Document& IntersectionObserver::trackingDocument() const { |
| 223 Document* document = nullptr; |
| 224 if (rootIsImplicit()) { |
| 225 DCHECK(m_callback->getExecutionContext()); |
| 226 document = toDocument(m_callback->getExecutionContext()); |
| 227 } else { |
| 228 DCHECK(root()); |
| 229 document = &root()->document(); |
| 230 } |
| 231 DCHECK(document); |
| 232 DCHECK(document->frame()); |
| 233 return *document->frame()->localFrameRoot()->document(); |
| 234 } |
| 235 |
| 236 LayoutObject* IntersectionObserver::rootLayoutObject() const { |
| 237 if (rootIsImplicit()) |
| 238 return trackingDocument().layoutView(); |
| 239 return root() ? root()->layoutObject() : nullptr; |
| 240 } |
| 241 |
| 244 void IntersectionObserver::observe(Element* target, | 242 void IntersectionObserver::observe(Element* target, |
| 245 ExceptionState& exceptionState) { | 243 ExceptionState& exceptionState) { |
| 246 if (!m_root) { | 244 if (!rootIsValid()) { |
| 247 exceptionState.throwDOMException( | 245 exceptionState.throwDOMException( |
| 248 InvalidStateError, | 246 InvalidStateError, |
| 249 "observe() called on an IntersectionObserver with an invalid root."); | 247 "observe() called on an IntersectionObserver with an invalid root."); |
| 250 return; | 248 return; |
| 251 } | 249 } |
| 252 | 250 |
| 253 if (!target || m_root.get() == target) | 251 if (!target || root() == target) |
| 252 return; |
| 253 |
| 254 LocalFrame* targetFrame = target->document().frame(); |
| 255 if (!targetFrame) |
| 254 return; | 256 return; |
| 255 | 257 |
| 256 if (target->ensureIntersectionObserverData().getObservationFor(*this)) | 258 if (target->ensureIntersectionObserverData().getObservationFor(*this)) |
| 257 return; | 259 return; |
| 260 |
| 261 bool isDOMDescendant = true; |
| 258 bool shouldReportRootBounds = false; | 262 bool shouldReportRootBounds = false; |
| 259 bool isDOMDescendant = false; | 263 if (rootIsImplicit()) { |
| 260 LocalFrame* targetFrame = target->document().frame(); | 264 Frame* rootFrame = targetFrame->tree().top(); |
| 261 LocalFrame* rootFrame = m_root->document().frame(); | 265 DCHECK(rootFrame); |
| 262 | 266 if (rootFrame == targetFrame) { |
| 263 if (target->document() == rootNode()->document()) { | 267 shouldReportRootBounds = true; |
| 268 } else { |
| 269 shouldReportRootBounds = |
| 270 targetFrame->securityContext()->getSecurityOrigin()->canAccess( |
| 271 rootFrame->securityContext()->getSecurityOrigin()); |
| 272 } |
| 273 } else { |
| 264 shouldReportRootBounds = true; | 274 shouldReportRootBounds = true; |
| 265 isDOMDescendant = rootNode()->isShadowIncludingInclusiveAncestorOf(target); | 275 isDOMDescendant = root()->isShadowIncludingInclusiveAncestorOf(target); |
| 266 } else if (targetFrame && rootFrame) { | |
| 267 shouldReportRootBounds = | |
| 268 targetFrame->securityContext()->getSecurityOrigin()->canAccess( | |
| 269 rootFrame->securityContext()->getSecurityOrigin()); | |
| 270 isDOMDescendant = (targetFrame->tree().top() == rootFrame); | |
| 271 } | 276 } |
| 272 | 277 |
| 273 IntersectionObservation* observation = | 278 IntersectionObservation* observation = |
| 274 new IntersectionObservation(*this, *target, shouldReportRootBounds); | 279 new IntersectionObservation(*this, *target, shouldReportRootBounds); |
| 275 target->ensureIntersectionObserverData().addObservation(*observation); | 280 target->ensureIntersectionObserverData().addObservation(*observation); |
| 276 m_observations.add(observation); | 281 m_observations.add(observation); |
| 277 | 282 |
| 278 if (!isDOMDescendant) { | 283 if (!isDOMDescendant) { |
| 279 m_root->document().addConsoleMessage( | 284 root()->document().addConsoleMessage( |
| 280 ConsoleMessage::create(JSMessageSource, WarningMessageLevel, | 285 ConsoleMessage::create(JSMessageSource, WarningMessageLevel, |
| 281 "IntersectionObserver.observe(target): target " | 286 "IntersectionObserver.observe(target): target " |
| 282 "element is not a descendant of root.")); | 287 "element is not a descendant of root.")); |
| 283 return; | |
| 284 } | 288 } |
| 285 | 289 |
| 286 if (m_initialState == InitialState::kAuto) { | 290 if (m_initialState == InitialState::kAuto) { |
| 287 for (auto& observation : m_observations) | 291 for (auto& observation : m_observations) |
| 288 observation->setLastThresholdIndex(std::numeric_limits<unsigned>::max()); | 292 observation->setLastThresholdIndex(std::numeric_limits<unsigned>::max()); |
| 289 } | 293 } |
| 290 | 294 |
| 291 if (!rootFrame) | 295 if (FrameView* frameView = targetFrame->view()) |
| 292 return; | 296 frameView->scheduleAnimation(); |
| 293 if (FrameView* rootFrameView = rootFrame->view()) | |
| 294 rootFrameView->scheduleAnimation(); | |
| 295 } | 297 } |
| 296 | 298 |
| 297 void IntersectionObserver::unobserve(Element* target, | 299 void IntersectionObserver::unobserve(Element* target, |
| 298 ExceptionState& exceptionState) { | 300 ExceptionState& exceptionState) { |
| 299 if (!m_root) { | 301 if (!rootIsValid()) { |
| 300 exceptionState.throwDOMException( | 302 exceptionState.throwDOMException( |
| 301 InvalidStateError, | 303 InvalidStateError, |
| 302 "unobserve() called on an IntersectionObserver with an invalid root."); | 304 "unobserve() called on an IntersectionObserver with an invalid root."); |
| 303 return; | 305 return; |
| 304 } | 306 } |
| 305 | 307 |
| 306 if (!target || !target->intersectionObserverData()) | 308 if (!target || !target->intersectionObserverData()) |
| 307 return; | 309 return; |
| 308 // TODO(szager): unobserve callback | 310 |
| 309 if (IntersectionObservation* observation = | 311 if (IntersectionObservation* observation = |
| 310 target->intersectionObserverData()->getObservationFor(*this)) | 312 target->intersectionObserverData()->getObservationFor(*this)) |
| 311 observation->disconnect(); | 313 observation->disconnect(); |
| 312 } | 314 } |
| 313 | 315 |
| 314 void IntersectionObserver::computeIntersectionObservations() { | 316 void IntersectionObserver::computeIntersectionObservations() { |
| 317 if (!rootIsValid()) |
| 318 return; |
| 315 Document* callbackDocument = toDocument(m_callback->getExecutionContext()); | 319 Document* callbackDocument = toDocument(m_callback->getExecutionContext()); |
| 316 if (!callbackDocument) | 320 if (!callbackDocument) |
| 317 return; | 321 return; |
| 318 LocalDOMWindow* callbackDOMWindow = callbackDocument->domWindow(); | 322 LocalDOMWindow* callbackDOMWindow = callbackDocument->domWindow(); |
| 319 if (!callbackDOMWindow) | 323 if (!callbackDOMWindow) |
| 320 return; | 324 return; |
| 321 DOMHighResTimeStamp timestamp = | 325 DOMHighResTimeStamp timestamp = |
| 322 DOMWindowPerformance::performance(*callbackDOMWindow)->now(); | 326 DOMWindowPerformance::performance(*callbackDOMWindow)->now(); |
| 323 for (auto& observation : m_observations) | 327 for (auto& observation : m_observations) |
| 324 observation->computeIntersectionObservations(timestamp); | 328 observation->computeIntersectionObservations(timestamp); |
| 325 } | 329 } |
| 326 | 330 |
| 327 void IntersectionObserver::disconnect(ExceptionState& exceptionState) { | 331 void IntersectionObserver::disconnect(ExceptionState& exceptionState) { |
| 328 if (!m_root) { | 332 if (!rootIsValid()) { |
| 329 exceptionState.throwDOMException( | 333 exceptionState.throwDOMException( |
| 330 InvalidStateError, | 334 InvalidStateError, |
| 331 "disconnect() called on an IntersectionObserver with an invalid root."); | 335 "disconnect() called on an IntersectionObserver with an invalid root."); |
| 332 return; | 336 return; |
| 333 } | 337 } |
| 334 | 338 |
| 335 for (auto& observation : m_observations) | 339 for (auto& observation : m_observations) |
| 336 observation->clearRootAndRemoveFromTarget(); | 340 observation->clearRootAndRemoveFromTarget(); |
| 337 m_observations.clear(); | 341 m_observations.clear(); |
| 338 } | 342 } |
| 339 | 343 |
| 340 void IntersectionObserver::removeObservation( | 344 void IntersectionObserver::removeObservation( |
| 341 IntersectionObservation& observation) { | 345 IntersectionObservation& observation) { |
| 342 m_observations.remove(&observation); | 346 m_observations.remove(&observation); |
| 343 } | 347 } |
| 344 | 348 |
| 345 void IntersectionObserver::setInitialState(InitialState initialState) { | 349 void IntersectionObserver::setInitialState(InitialState initialState) { |
| 346 DCHECK(m_observations.isEmpty()); | 350 DCHECK(m_observations.isEmpty()); |
| 347 m_initialState = initialState; | 351 m_initialState = initialState; |
| 348 } | 352 } |
| 349 | 353 |
| 350 HeapVector<Member<IntersectionObserverEntry>> IntersectionObserver::takeRecords( | 354 HeapVector<Member<IntersectionObserverEntry>> IntersectionObserver::takeRecords( |
| 351 ExceptionState& exceptionState) { | 355 ExceptionState& exceptionState) { |
| 352 HeapVector<Member<IntersectionObserverEntry>> entries; | 356 HeapVector<Member<IntersectionObserverEntry>> entries; |
| 353 | 357 |
| 354 if (!m_root) | 358 if (!rootIsValid()) { |
| 355 exceptionState.throwDOMException(InvalidStateError, | 359 exceptionState.throwDOMException(InvalidStateError, |
| 356 "takeRecords() called on an " | 360 "takeRecords() called on an " |
| 357 "IntersectionObserver with an invalid " | 361 "IntersectionObserver with an invalid " |
| 358 "root."); | 362 "root."); |
| 359 else | 363 } else { |
| 360 entries.swap(m_entries); | 364 entries.swap(m_entries); |
| 365 } |
| 361 | 366 |
| 362 return entries; | 367 return entries; |
| 363 } | 368 } |
| 364 | 369 |
| 365 Element* IntersectionObserver::root() const { | |
| 366 Node* node = rootNode(); | |
| 367 if (node && !node->isDocumentNode()) | |
| 368 return toElement(node); | |
| 369 return nullptr; | |
| 370 } | |
| 371 | |
| 372 static void appendLength(StringBuilder& stringBuilder, const Length& length) { | 370 static void appendLength(StringBuilder& stringBuilder, const Length& length) { |
| 373 stringBuilder.appendNumber(length.intValue()); | 371 stringBuilder.appendNumber(length.intValue()); |
| 374 if (length.type() == Percent) | 372 if (length.type() == Percent) |
| 375 stringBuilder.append('%'); | 373 stringBuilder.append('%'); |
| 376 else | 374 else |
| 377 stringBuilder.append("px", 2); | 375 stringBuilder.append("px", 2); |
| 378 } | 376 } |
| 379 | 377 |
| 380 String IntersectionObserver::rootMargin() const { | 378 String IntersectionObserver::rootMargin() const { |
| 381 StringBuilder stringBuilder; | 379 StringBuilder stringBuilder; |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 415 | 413 |
| 416 DEFINE_TRACE(IntersectionObserver) { | 414 DEFINE_TRACE(IntersectionObserver) { |
| 417 visitor->template registerWeakMembers< | 415 visitor->template registerWeakMembers< |
| 418 IntersectionObserver, &IntersectionObserver::clearWeakMembers>(this); | 416 IntersectionObserver, &IntersectionObserver::clearWeakMembers>(this); |
| 419 visitor->trace(m_callback); | 417 visitor->trace(m_callback); |
| 420 visitor->trace(m_observations); | 418 visitor->trace(m_observations); |
| 421 visitor->trace(m_entries); | 419 visitor->trace(m_entries); |
| 422 } | 420 } |
| 423 | 421 |
| 424 } // namespace blink | 422 } // namespace blink |
| OLD | NEW |