Web Inspector: Search: allow DOM searches to be case sensitive
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Controllers / DOMManager.js
1 /*
2  * Copyright (C) 2009, 2010 Google Inc. All rights reserved.
3  * Copyright (C) 2009 Joseph Pecoraro
4  * Copyright (C) 2013 Apple Inc. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are
8  * met:
9  *
10  *     * Redistributions of source code must retain the above copyright
11  * notice, this list of conditions and the following disclaimer.
12  *     * Redistributions in binary form must reproduce the above
13  * copyright notice, this list of conditions and the following disclaimer
14  * in the documentation and/or other materials provided with the
15  * distribution.
16  *     * Neither the name of Google Inc. nor the names of its
17  * contributors may be used to endorse or promote products derived from
18  * this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32
33 // FIXME: DOMManager lacks advanced multi-target support. (DOMNodes per-target)
34
35 WI.DOMManager = class DOMManager extends WI.Object
36 {
37     constructor()
38     {
39         super();
40
41         this._idToDOMNode = {};
42         this._document = null;
43         this._attributeLoadNodeIds = {};
44         this._restoreSelectedNodeIsAllowed = true;
45         this._loadNodeAttributesTimeout = 0;
46         this._inspectedNode = null;
47
48         this._breakpointsForEventListeners = new Map;
49
50         this._hasRequestedDocument = false;
51         this._pendingDocumentRequestCallbacks = null;
52
53         WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
54     }
55
56     // Target
57
58     initializeTarget(target)
59     {
60         // FIXME: This should be improved when adding better DOM multi-target support since it is really per-target.
61         // This currently uses a setTimeout since it doesn't need to happen immediately, and DOMManager uses the
62         // global DOMAgent to request the document, so we want to make sure we've transitioned the global agents
63         // to this target if necessary.
64         if (target.DOMAgent) {
65             setTimeout(() => {
66                 this.ensureDocument();
67             });
68         }
69     }
70
71     transitionPageTarget()
72     {
73         this._documentUpdated();
74     }
75
76     // Public
77
78     get eventListenerBreakpoints()
79     {
80         return Array.from(this._breakpointsForEventListeners.values());
81     }
82
83     requestDocument(callback)
84     {
85         if (this._document) {
86             callback(this._document);
87             return;
88         }
89
90         if (this._pendingDocumentRequestCallbacks)
91             this._pendingDocumentRequestCallbacks.push(callback);
92         else
93             this._pendingDocumentRequestCallbacks = [callback];
94
95         if (this._hasRequestedDocument)
96             return;
97
98         if (!WI.pageTarget)
99             return;
100
101         if (!WI.pageTarget.DOMAgent)
102             return;
103
104         this._hasRequestedDocument = true;
105
106         WI.pageTarget.DOMAgent.getDocument((error, root) => {
107             if (!error)
108                 this._setDocument(root);
109
110             for (let callback of this._pendingDocumentRequestCallbacks)
111                 callback(this._document);
112
113             this._pendingDocumentRequestCallbacks = null;
114         });
115     }
116
117     ensureDocument()
118     {
119         this.requestDocument(function(){});
120     }
121
122     pushNodeToFrontend(objectId, callback)
123     {
124         this._dispatchWhenDocumentAvailable(DOMAgent.requestNode.bind(DOMAgent, objectId), callback);
125     }
126
127     pushNodeByPathToFrontend(path, callback)
128     {
129         this._dispatchWhenDocumentAvailable(DOMAgent.pushNodeByPathToFrontend.bind(DOMAgent, path), callback);
130     }
131
132     didAddEventListener(nodeId)
133     {
134         // Called from WI.DOMObserver.
135
136         let node = this._idToDOMNode[nodeId];
137         if (!node)
138             return;
139
140         node.dispatchEventToListeners(WI.DOMNode.Event.EventListenersChanged);
141     }
142
143     willRemoveEventListener(nodeId)
144     {
145         // Called from WI.DOMObserver.
146
147         let node = this._idToDOMNode[nodeId];
148         if (!node)
149             return;
150
151         node.dispatchEventToListeners(WI.DOMNode.Event.EventListenersChanged);
152     }
153
154     didFireEvent(nodeId, eventName, timestamp, data)
155     {
156         // Called from WI.DOMObserver.
157
158         let node = this._idToDOMNode[nodeId];
159         if (!node)
160             return;
161
162         node.didFireEvent(eventName, timestamp, data);
163     }
164
165     videoLowPowerChanged(nodeId, timestamp, isLowPower)
166     {
167         // Called from WI.DOMObserver.
168
169         let node = this._idToDOMNode[nodeId];
170         if (!node)
171             return;
172
173         node.videoLowPowerChanged(timestamp, isLowPower);
174     }
175
176     // Private
177
178     _wrapClientCallback(callback)
179     {
180         if (!callback)
181             return null;
182
183         return function(error, result) {
184             if (error)
185                 console.error("Error during DOMAgent operation: " + error);
186             callback(error ? null : result);
187         };
188     }
189
190     _dispatchWhenDocumentAvailable(func, callback)
191     {
192         var callbackWrapper = this._wrapClientCallback(callback);
193
194         function onDocumentAvailable()
195         {
196             if (this._document)
197                 func(callbackWrapper);
198             else {
199                 if (callbackWrapper)
200                     callbackWrapper("No document");
201             }
202         }
203         this.requestDocument(onDocumentAvailable.bind(this));
204     }
205
206     _attributeModified(nodeId, name, value)
207     {
208         var node = this._idToDOMNode[nodeId];
209         if (!node)
210             return;
211
212         node._setAttribute(name, value);
213         this.dispatchEventToListeners(WI.DOMManager.Event.AttributeModified, {node, name});
214         node.dispatchEventToListeners(WI.DOMNode.Event.AttributeModified, {name});
215     }
216
217     _attributeRemoved(nodeId, name)
218     {
219         var node = this._idToDOMNode[nodeId];
220         if (!node)
221             return;
222
223         node._removeAttribute(name);
224         this.dispatchEventToListeners(WI.DOMManager.Event.AttributeRemoved, {node, name});
225         node.dispatchEventToListeners(WI.DOMNode.Event.AttributeRemoved, {name});
226     }
227
228     _inlineStyleInvalidated(nodeIds)
229     {
230         for (var nodeId of nodeIds)
231             this._attributeLoadNodeIds[nodeId] = true;
232         if (this._loadNodeAttributesTimeout)
233             return;
234         this._loadNodeAttributesTimeout = setTimeout(this._loadNodeAttributes.bind(this), 0);
235     }
236
237     _loadNodeAttributes()
238     {
239         function callback(nodeId, error, attributes)
240         {
241             if (error) {
242                 console.error("Error during DOMAgent operation: " + error);
243                 return;
244             }
245             var node = this._idToDOMNode[nodeId];
246             if (node) {
247                 node._setAttributesPayload(attributes);
248                 this.dispatchEventToListeners(WI.DOMManager.Event.AttributeModified, {node, name: "style"});
249                 node.dispatchEventToListeners(WI.DOMNode.Event.AttributeModified, {name: "style"});
250             }
251         }
252
253         this._loadNodeAttributesTimeout = 0;
254
255         for (var nodeId in this._attributeLoadNodeIds) {
256             var nodeIdAsNumber = parseInt(nodeId);
257             DOMAgent.getAttributes(nodeIdAsNumber, callback.bind(this, nodeIdAsNumber));
258         }
259         this._attributeLoadNodeIds = {};
260     }
261
262     _characterDataModified(nodeId, newValue)
263     {
264         var node = this._idToDOMNode[nodeId];
265         node._nodeValue = newValue;
266         this.dispatchEventToListeners(WI.DOMManager.Event.CharacterDataModified, {node});
267     }
268
269     nodeForId(nodeId)
270     {
271         return this._idToDOMNode[nodeId];
272     }
273
274     _documentUpdated()
275     {
276         this._setDocument(null);
277     }
278
279     _setDocument(payload)
280     {
281         this._idToDOMNode = {};
282
283         for (let breakpoint of this._breakpointsForEventListeners.values())
284             WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointRemoved, {breakpoint});
285         this._breakpointsForEventListeners.clear();
286
287         let newDocument = null;
288         if (payload && "nodeId" in payload)
289             newDocument = new WI.DOMNode(this, null, false, payload);
290
291         if (this._document === newDocument)
292             return;
293
294         this._document = newDocument;
295
296         if (!this._document)
297             this._hasRequestedDocument = false;
298
299         this.dispatchEventToListeners(WI.DOMManager.Event.DocumentUpdated, {document: this._document});
300     }
301
302     _setDetachedRoot(payload)
303     {
304         new WI.DOMNode(this, null, false, payload);
305     }
306
307     _setChildNodes(parentId, payloads)
308     {
309         if (!parentId && payloads.length) {
310             this._setDetachedRoot(payloads[0]);
311             return;
312         }
313
314         var parent = this._idToDOMNode[parentId];
315         parent._setChildrenPayload(payloads);
316     }
317
318     _childNodeCountUpdated(nodeId, newValue)
319     {
320         var node = this._idToDOMNode[nodeId];
321         node.childNodeCount = newValue;
322         this.dispatchEventToListeners(WI.DOMManager.Event.ChildNodeCountUpdated, node);
323     }
324
325     _childNodeInserted(parentId, prevId, payload)
326     {
327         var parent = this._idToDOMNode[parentId];
328         var prev = this._idToDOMNode[prevId];
329         var node = parent._insertChild(prev, payload);
330         this._idToDOMNode[node.id] = node;
331         this.dispatchEventToListeners(WI.DOMManager.Event.NodeInserted, {node, parent});
332     }
333
334     _childNodeRemoved(parentId, nodeId)
335     {
336         var parent = this._idToDOMNode[parentId];
337         var node = this._idToDOMNode[nodeId];
338         parent._removeChild(node);
339         this._unbind(node);
340         this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node, parent});
341     }
342
343     _customElementStateChanged(elementId, newState)
344     {
345         const node = this._idToDOMNode[elementId];
346         node._customElementState = newState;
347         this.dispatchEventToListeners(WI.DOMManager.Event.CustomElementStateChanged, {node});
348     }
349
350     _pseudoElementAdded(parentId, pseudoElement)
351     {
352         var parent = this._idToDOMNode[parentId];
353         if (!parent)
354             return;
355
356         var node = new WI.DOMNode(this, parent.ownerDocument, false, pseudoElement);
357         node.parentNode = parent;
358         this._idToDOMNode[node.id] = node;
359         console.assert(!parent.pseudoElements().get(node.pseudoType()));
360         parent.pseudoElements().set(node.pseudoType(), node);
361         this.dispatchEventToListeners(WI.DOMManager.Event.NodeInserted, {node, parent});
362     }
363
364     _pseudoElementRemoved(parentId, pseudoElementId)
365     {
366         var pseudoElement = this._idToDOMNode[pseudoElementId];
367         if (!pseudoElement)
368             return;
369
370         var parent = pseudoElement.parentNode;
371         console.assert(parent);
372         console.assert(parent.id === parentId);
373         if (!parent)
374             return;
375
376         parent._removeChild(pseudoElement);
377         this._unbind(pseudoElement);
378         this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node: pseudoElement, parent});
379     }
380
381     _unbind(node)
382     {
383         delete this._idToDOMNode[node.id];
384
385         for (let i = 0; node.children && i < node.children.length; ++i)
386             this._unbind(node.children[i]);
387
388         let templateContent = node.templateContent();
389         if (templateContent)
390             this._unbind(templateContent);
391
392         for (let pseudoElement of node.pseudoElements().values())
393             this._unbind(pseudoElement);
394
395         // FIXME: Handle shadow roots.
396     }
397
398     get restoreSelectedNodeIsAllowed()
399     {
400         return this._restoreSelectedNodeIsAllowed;
401     }
402
403     inspectElement(nodeId)
404     {
405         var node = this._idToDOMNode[nodeId];
406         if (!node || !node.ownerDocument)
407             return;
408
409         this.dispatchEventToListeners(WI.DOMManager.Event.DOMNodeWasInspected, {node});
410
411         this._inspectModeEnabled = false;
412         this.dispatchEventToListeners(WI.DOMManager.Event.InspectModeStateChanged);
413     }
414
415     inspectNodeObject(remoteObject)
416     {
417         this._restoreSelectedNodeIsAllowed = false;
418
419         function nodeAvailable(nodeId)
420         {
421             remoteObject.release();
422
423             console.assert(nodeId);
424             if (!nodeId)
425                 return;
426
427             this.inspectElement(nodeId);
428
429             // Re-resolve the node in the console's object group when adding to the console.
430             let domNode = this.nodeForId(nodeId);
431             WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup).then((remoteObject) => {
432                 const specialLogStyles = true;
433                 const shouldRevealConsole = false;
434                 WI.consoleLogViewController.appendImmediateExecutionWithResult(WI.UIString("Selected Element"), remoteObject, specialLogStyles, shouldRevealConsole);
435             });
436         }
437
438         remoteObject.pushNodeToFrontend(nodeAvailable.bind(this));
439     }
440
441     querySelector(nodeOrNodeId, selector, callback)
442     {
443         let nodeId = nodeOrNodeId instanceof WI.DOMNode ? nodeOrNodeId.id : nodeOrNodeId;
444         console.assert(typeof nodeId === "number");
445
446         if (typeof callback === "function")
447             DOMAgent.querySelector(nodeId, selector, this._wrapClientCallback(callback));
448         else
449             return DOMAgent.querySelector(nodeId, selector).then(({nodeId}) => nodeId);
450     }
451
452     querySelectorAll(nodeOrNodeId, selector, callback)
453     {
454         let nodeId = nodeOrNodeId instanceof WI.DOMNode ? nodeOrNodeId.id : nodeOrNodeId;
455         console.assert(typeof nodeId === "number");
456
457         if (typeof callback === "function")
458             DOMAgent.querySelectorAll(nodeId, selector, this._wrapClientCallback(callback));
459         else
460             return DOMAgent.querySelectorAll(nodeId, selector).then(({nodeIds}) => nodeIds);
461     }
462
463     highlightDOMNode(nodeId, mode)
464     {
465         if (this._hideDOMNodeHighlightTimeout) {
466             clearTimeout(this._hideDOMNodeHighlightTimeout);
467             this._hideDOMNodeHighlightTimeout = undefined;
468         }
469
470         this._highlightedDOMNodeId = nodeId;
471         if (nodeId)
472             DOMAgent.highlightNode.invoke({nodeId, highlightConfig: this._buildHighlightConfig(mode)});
473         else
474             DOMAgent.hideHighlight();
475     }
476
477     highlightDOMNodeList(nodeIds, mode)
478     {
479         // COMPATIBILITY (iOS 11): DOM.highlightNodeList did not exist.
480         if (!DOMAgent.highlightNodeList)
481             return;
482
483         if (this._hideDOMNodeHighlightTimeout) {
484             clearTimeout(this._hideDOMNodeHighlightTimeout);
485             this._hideDOMNodeHighlightTimeout = undefined;
486         }
487
488         DOMAgent.highlightNodeList(nodeIds, this._buildHighlightConfig(mode));
489     }
490
491     highlightSelector(selectorText, frameId, mode)
492     {
493         // COMPATIBILITY (iOS 8): DOM.highlightSelector did not exist.
494         if (!DOMAgent.highlightSelector)
495             return;
496
497         if (this._hideDOMNodeHighlightTimeout) {
498             clearTimeout(this._hideDOMNodeHighlightTimeout);
499             this._hideDOMNodeHighlightTimeout = undefined;
500         }
501
502         DOMAgent.highlightSelector(this._buildHighlightConfig(mode), selectorText, frameId);
503     }
504
505     highlightRect(rect, usePageCoordinates)
506     {
507         DOMAgent.highlightRect.invoke({
508             x: rect.x,
509             y: rect.y,
510             width: rect.width,
511             height: rect.height,
512             color: {r: 111, g: 168, b: 220, a: 0.66},
513             outlineColor: {r: 255, g: 229, b: 153, a: 0.66},
514             usePageCoordinates
515         });
516     }
517
518     hideDOMNodeHighlight()
519     {
520         this.highlightDOMNode(0);
521     }
522
523     highlightDOMNodeForTwoSeconds(nodeId)
524     {
525         this.highlightDOMNode(nodeId);
526         this._hideDOMNodeHighlightTimeout = setTimeout(this.hideDOMNodeHighlight.bind(this), 2000);
527     }
528
529     get inspectModeEnabled()
530     {
531         return this._inspectModeEnabled;
532     }
533
534     set inspectModeEnabled(enabled)
535     {
536         if (enabled === this._inspectModeEnabled)
537             return;
538
539         DOMAgent.setInspectModeEnabled(enabled, this._buildHighlightConfig(), (error) => {
540             this._inspectModeEnabled = error ? false : enabled;
541             this.dispatchEventToListeners(WI.DOMManager.Event.InspectModeStateChanged);
542         });
543     }
544
545     setInspectedNode(node)
546     {
547         console.assert(node instanceof WI.DOMNode);
548         if (node === this._inspectedNode)
549             return;
550
551         let callback = (error) => {
552             console.assert(!error, error);
553             if (error)
554                 return;
555
556             this._inspectedNode = node;
557         };
558
559         // COMPATIBILITY (iOS 11): DOM.setInspectedNode did not exist.
560         if (!DOMAgent.setInspectedNode) {
561             ConsoleAgent.addInspectedNode(node.id, callback);
562             return;
563         }
564
565         DOMAgent.setInspectedNode(node.id, callback);
566     }
567
568     getSupportedEventNames(callback)
569     {
570         if (!DOMAgent.getSupportedEventNames)
571             return Promise.resolve(new Set);
572
573         if (!this._getSupportedEventNamesPromise) {
574             this._getSupportedEventNamesPromise = DOMAgent.getSupportedEventNames()
575             .then(({eventNames}) => new Set(eventNames));
576         }
577
578         return this._getSupportedEventNamesPromise;
579     }
580
581     setEventListenerDisabled(eventListener, disabled)
582     {
583         DOMAgent.setEventListenerDisabled(eventListener.eventListenerId, disabled, (error) => {
584             if (error)
585                 console.error(error);
586         });
587     }
588
589     setBreakpointForEventListener(eventListener)
590     {
591         let breakpoint = new WI.EventBreakpoint(WI.EventBreakpoint.Type.Listener, eventListener.type, {eventListener});
592         this._breakpointsForEventListeners.set(eventListener.eventListenerId, breakpoint);
593
594         DOMAgent.setBreakpointForEventListener(eventListener.eventListenerId, (error) => {
595             if (error) {
596                 console.error(error);
597                 return;
598             }
599
600             WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointAdded, {breakpoint});
601         });
602     }
603
604     removeBreakpointForEventListener(eventListener)
605     {
606         let breakpoint = this._breakpointsForEventListeners.get(eventListener.eventListenerId);
607         console.assert(breakpoint);
608
609         this._breakpointsForEventListeners.delete(eventListener.eventListenerId);
610
611         DOMAgent.removeBreakpointForEventListener(eventListener.eventListenerId, (error) => {
612             if (error) {
613                 console.error(error);
614                 return;
615             }
616
617             WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointRemoved, {breakpoint});
618         });
619     }
620
621     breakpointForEventListenerId(eventListenerId)
622     {
623         return this._breakpointsForEventListeners.get(eventListenerId) || null;
624     }
625
626     _buildHighlightConfig(mode = "all")
627     {
628         let highlightConfig = {showInfo: mode === "all"};
629
630         if (mode === "all" || mode === "content")
631             highlightConfig.contentColor = {r: 111, g: 168, b: 220, a: 0.66};
632
633         if (mode === "all" || mode === "padding")
634             highlightConfig.paddingColor = {r: 147, g: 196, b: 125, a: 0.66};
635
636         if (mode === "all" || mode === "border")
637             highlightConfig.borderColor = {r: 255, g: 229, b: 153, a: 0.66};
638
639         if (mode === "all" || mode === "margin")
640             highlightConfig.marginColor = {r: 246, g: 178, b: 107, a: 0.66};
641
642         return highlightConfig;
643     }
644
645     // Private
646
647     _mainResourceDidChange(event)
648     {
649         if (!event.target.isMainFrame())
650             return;
651
652         this._restoreSelectedNodeIsAllowed = true;
653
654         this.ensureDocument();
655     }
656 };
657
658 WI.DOMManager.Event = {
659     AttributeModified: "dom-manager-attribute-modified",
660     AttributeRemoved: "dom-manager-attribute-removed",
661     CharacterDataModified: "dom-manager-character-data-modified",
662     NodeInserted: "dom-manager-node-inserted",
663     NodeRemoved: "dom-manager-node-removed",
664     CustomElementStateChanged: "dom-manager-custom-element-state-changed",
665     DocumentUpdated: "dom-manager-document-updated",
666     ChildNodeCountUpdated: "dom-manager-child-node-count-updated",
667     DOMNodeWasInspected: "dom-manager-dom-node-was-inspected",
668     InspectModeStateChanged: "dom-manager-inspect-mode-state-changed",
669 };