OLD | NEW |
1 // Copyright (c) 2015 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2015 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 | |
5 /** | 4 /** |
6 * @constructor | 5 * @unrestricted |
7 * @param {!Document} doc | |
8 */ | 6 */ |
9 WebInspector.Tooltip = function(doc) | 7 WebInspector.Tooltip = class { |
10 { | 8 /** |
11 this.element = doc.body.createChild("div"); | 9 * @param {!Document} doc |
12 this._shadowRoot = WebInspector.createShadowRootWithCoreStyles(this.element,
"ui/tooltip.css"); | 10 */ |
13 | 11 constructor(doc) { |
14 this._tooltipElement = this._shadowRoot.createChild("div", "tooltip"); | 12 this.element = doc.body.createChild('div'); |
15 doc.addEventListener("mousemove", this._mouseMove.bind(this), true); | 13 this._shadowRoot = WebInspector.createShadowRootWithCoreStyles(this.element,
'ui/tooltip.css'); |
16 doc.addEventListener("mousedown", this._hide.bind(this, true), true); | 14 |
17 doc.addEventListener("mouseleave", this._hide.bind(this, false), true); | 15 this._tooltipElement = this._shadowRoot.createChild('div', 'tooltip'); |
18 doc.addEventListener("keydown", this._hide.bind(this, true), true); | 16 doc.addEventListener('mousemove', this._mouseMove.bind(this), true); |
| 17 doc.addEventListener('mousedown', this._hide.bind(this, true), true); |
| 18 doc.addEventListener('mouseleave', this._hide.bind(this, false), true); |
| 19 doc.addEventListener('keydown', this._hide.bind(this, true), true); |
19 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.Zo
omChanged, this._reset, this); | 20 WebInspector.zoomManager.addEventListener(WebInspector.ZoomManager.Events.Zo
omChanged, this._reset, this); |
20 doc.defaultView.addEventListener("resize", this._reset.bind(this), false); | 21 doc.defaultView.addEventListener('resize', this._reset.bind(this), false); |
| 22 } |
| 23 |
| 24 /** |
| 25 * @param {!Document} doc |
| 26 */ |
| 27 static installHandler(doc) { |
| 28 new WebInspector.Tooltip(doc); |
| 29 } |
| 30 |
| 31 /** |
| 32 * @param {!Element} element |
| 33 * @param {!Element|string} tooltipContent |
| 34 * @param {string=} actionId |
| 35 * @param {!Object=} options |
| 36 */ |
| 37 static install(element, tooltipContent, actionId, options) { |
| 38 if (typeof tooltipContent === 'string' && tooltipContent === '') { |
| 39 delete element[WebInspector.Tooltip._symbol]; |
| 40 return; |
| 41 } |
| 42 element[WebInspector.Tooltip._symbol] = {content: tooltipContent, actionId:
actionId, options: options || {}}; |
| 43 } |
| 44 |
| 45 /** |
| 46 * @param {!Element} element |
| 47 */ |
| 48 static addNativeOverrideContainer(element) { |
| 49 WebInspector.Tooltip._nativeOverrideContainer.push(element); |
| 50 } |
| 51 |
| 52 /** |
| 53 * @param {!Event} event |
| 54 */ |
| 55 _mouseMove(event) { |
| 56 var mouseEvent = /** @type {!MouseEvent} */ (event); |
| 57 var path = mouseEvent.path; |
| 58 if (!path || mouseEvent.buttons !== 0 || (mouseEvent.movementX === 0 && mous
eEvent.movementY === 0)) |
| 59 return; |
| 60 |
| 61 if (this._anchorElement && path.indexOf(this._anchorElement) === -1) |
| 62 this._hide(false); |
| 63 |
| 64 for (var element of path) { |
| 65 if (element === this._anchorElement) { |
| 66 return; |
| 67 } else if (element[WebInspector.Tooltip._symbol]) { |
| 68 this._show(element, mouseEvent); |
| 69 return; |
| 70 } |
| 71 } |
| 72 } |
| 73 |
| 74 /** |
| 75 * @param {!Element} anchorElement |
| 76 * @param {!Event} event |
| 77 */ |
| 78 _show(anchorElement, event) { |
| 79 var tooltip = anchorElement[WebInspector.Tooltip._symbol]; |
| 80 this._anchorElement = anchorElement; |
| 81 this._tooltipElement.removeChildren(); |
| 82 |
| 83 // Check if native tooltips should be used. |
| 84 for (var element of WebInspector.Tooltip._nativeOverrideContainer) { |
| 85 if (this._anchorElement.isSelfOrDescendant(element)) { |
| 86 Object.defineProperty(this._anchorElement, 'title', WebInspector.Tooltip
._nativeTitle); |
| 87 this._anchorElement.title = tooltip.content; |
| 88 return; |
| 89 } |
| 90 } |
| 91 |
| 92 if (typeof tooltip.content === 'string') |
| 93 this._tooltipElement.setTextContentTruncatedIfNeeded(tooltip.content); |
| 94 else |
| 95 this._tooltipElement.appendChild(tooltip.content); |
| 96 |
| 97 if (tooltip.actionId) { |
| 98 var shortcuts = WebInspector.shortcutRegistry.shortcutDescriptorsForAction
(tooltip.actionId); |
| 99 for (var shortcut of shortcuts) { |
| 100 var shortcutElement = this._tooltipElement.createChild('div', 'tooltip-s
hortcut'); |
| 101 shortcutElement.textContent = shortcut.name; |
| 102 } |
| 103 } |
| 104 |
| 105 this._tooltipElement.classList.add('shown'); |
| 106 // Reposition to ensure text doesn't overflow unnecessarily. |
| 107 this._tooltipElement.positionAt(0, 0); |
| 108 |
| 109 // Show tooltip instantly if a tooltip was shown recently. |
| 110 var now = Date.now(); |
| 111 var instant = |
| 112 (this._tooltipLastClosed && now - this._tooltipLastClosed < WebInspector
.Tooltip.Timing.InstantThreshold); |
| 113 this._tooltipElement.classList.toggle('instant', instant); |
| 114 this._tooltipLastOpened = instant ? now : now + WebInspector.Tooltip.Timing.
OpeningDelay; |
| 115 |
| 116 // Get container element. |
| 117 var container = WebInspector.Dialog.modalHostView().element; |
| 118 if (!anchorElement.isDescendant(container)) |
| 119 container = this.element.parentElement; |
| 120 |
| 121 // Posititon tooltip based on the anchor element. |
| 122 var containerBox = container.boxInWindow(this.element.window()); |
| 123 var anchorBox = this._anchorElement.boxInWindow(this.element.window()); |
| 124 const anchorOffset = 2; |
| 125 const pageMargin = 2; |
| 126 var cursorOffset = 10; |
| 127 this._tooltipElement.classList.toggle('tooltip-breakword', !this._tooltipEle
ment.textContent.match('\\s')); |
| 128 this._tooltipElement.style.maxWidth = (containerBox.width - pageMargin * 2)
+ 'px'; |
| 129 this._tooltipElement.style.maxHeight = ''; |
| 130 var tooltipWidth = this._tooltipElement.offsetWidth; |
| 131 var tooltipHeight = this._tooltipElement.offsetHeight; |
| 132 var anchorTooltipAtElement = this._anchorElement.nodeName === 'BUTTON' || th
is._anchorElement.nodeName === 'LABEL'; |
| 133 var tooltipX = anchorTooltipAtElement ? anchorBox.x : event.x + cursorOffset
; |
| 134 tooltipX = Number.constrain( |
| 135 tooltipX, containerBox.x + pageMargin, containerBox.x + containerBox.wid
th - tooltipWidth - pageMargin); |
| 136 var tooltipY; |
| 137 if (!anchorTooltipAtElement) { |
| 138 tooltipY = event.y + cursorOffset + tooltipHeight < containerBox.y + conta
inerBox.height ? |
| 139 event.y + cursorOffset : |
| 140 event.y - tooltipHeight; |
| 141 } else { |
| 142 var onBottom = |
| 143 anchorBox.y + anchorOffset + anchorBox.height + tooltipHeight < contai
nerBox.y + containerBox.height; |
| 144 tooltipY = onBottom ? anchorBox.y + anchorBox.height + anchorOffset : anch
orBox.y - tooltipHeight - anchorOffset; |
| 145 } |
| 146 this._tooltipElement.positionAt(tooltipX, tooltipY); |
| 147 } |
| 148 |
| 149 /** |
| 150 * @param {boolean} removeInstant |
| 151 */ |
| 152 _hide(removeInstant) { |
| 153 delete this._anchorElement; |
| 154 this._tooltipElement.classList.remove('shown'); |
| 155 if (Date.now() > this._tooltipLastOpened) |
| 156 this._tooltipLastClosed = Date.now(); |
| 157 if (removeInstant) |
| 158 delete this._tooltipLastClosed; |
| 159 } |
| 160 |
| 161 _reset() { |
| 162 this._hide(true); |
| 163 this._tooltipElement.positionAt(0, 0); |
| 164 this._tooltipElement.style.maxWidth = '0'; |
| 165 this._tooltipElement.style.maxHeight = '0'; |
| 166 } |
21 }; | 167 }; |
22 | 168 |
23 WebInspector.Tooltip.Timing = { | 169 WebInspector.Tooltip.Timing = { |
24 // Max time between tooltips showing that no opening delay is required. | 170 // Max time between tooltips showing that no opening delay is required. |
25 "InstantThreshold": 300, | 171 'InstantThreshold': 300, |
26 // Wait time before opening a tooltip. | 172 // Wait time before opening a tooltip. |
27 "OpeningDelay": 600 | 173 'OpeningDelay': 600 |
28 }; | 174 }; |
29 | 175 |
30 WebInspector.Tooltip.prototype = { | 176 WebInspector.Tooltip._symbol = Symbol('Tooltip'); |
31 /** | 177 |
32 * @param {!Event} event | |
33 */ | |
34 _mouseMove: function(event) | |
35 { | |
36 var mouseEvent = /** @type {!MouseEvent} */ (event); | |
37 var path = mouseEvent.path; | |
38 if (!path || mouseEvent.buttons !== 0 || (mouseEvent.movementX === 0 &&
mouseEvent.movementY === 0)) | |
39 return; | |
40 | |
41 if (this._anchorElement && path.indexOf(this._anchorElement) === -1) | |
42 this._hide(false); | |
43 | |
44 for (var element of path) { | |
45 if (element === this._anchorElement) { | |
46 return; | |
47 } else if (element[WebInspector.Tooltip._symbol]) { | |
48 this._show(element, mouseEvent); | |
49 return; | |
50 } | |
51 } | |
52 }, | |
53 | |
54 /** | |
55 * @param {!Element} anchorElement | |
56 * @param {!Event} event | |
57 */ | |
58 _show: function(anchorElement, event) | |
59 { | |
60 var tooltip = anchorElement[WebInspector.Tooltip._symbol]; | |
61 this._anchorElement = anchorElement; | |
62 this._tooltipElement.removeChildren(); | |
63 | |
64 // Check if native tooltips should be used. | |
65 for (var element of WebInspector.Tooltip._nativeOverrideContainer) { | |
66 if (this._anchorElement.isSelfOrDescendant(element)) { | |
67 Object.defineProperty(this._anchorElement, "title", WebInspector
.Tooltip._nativeTitle); | |
68 this._anchorElement.title = tooltip.content; | |
69 return; | |
70 } | |
71 } | |
72 | |
73 if (typeof tooltip.content === "string") | |
74 this._tooltipElement.setTextContentTruncatedIfNeeded(tooltip.content
); | |
75 else | |
76 this._tooltipElement.appendChild(tooltip.content); | |
77 | |
78 if (tooltip.actionId) { | |
79 var shortcuts = WebInspector.shortcutRegistry.shortcutDescriptorsFor
Action(tooltip.actionId); | |
80 for (var shortcut of shortcuts) { | |
81 var shortcutElement = this._tooltipElement.createChild("div", "t
ooltip-shortcut"); | |
82 shortcutElement.textContent = shortcut.name; | |
83 } | |
84 } | |
85 | |
86 this._tooltipElement.classList.add("shown"); | |
87 // Reposition to ensure text doesn't overflow unnecessarily. | |
88 this._tooltipElement.positionAt(0, 0); | |
89 | |
90 // Show tooltip instantly if a tooltip was shown recently. | |
91 var now = Date.now(); | |
92 var instant = (this._tooltipLastClosed && now - this._tooltipLastClosed
< WebInspector.Tooltip.Timing.InstantThreshold); | |
93 this._tooltipElement.classList.toggle("instant", instant); | |
94 this._tooltipLastOpened = instant ? now : now + WebInspector.Tooltip.Tim
ing.OpeningDelay; | |
95 | |
96 // Get container element. | |
97 var container = WebInspector.Dialog.modalHostView().element; | |
98 if (!anchorElement.isDescendant(container)) | |
99 container = this.element.parentElement; | |
100 | |
101 // Posititon tooltip based on the anchor element. | |
102 var containerBox = container.boxInWindow(this.element.window()); | |
103 var anchorBox = this._anchorElement.boxInWindow(this.element.window()); | |
104 const anchorOffset = 2; | |
105 const pageMargin = 2; | |
106 var cursorOffset = 10; | |
107 this._tooltipElement.classList.toggle("tooltip-breakword", !this._toolti
pElement.textContent.match("\\s")); | |
108 this._tooltipElement.style.maxWidth = (containerBox.width - pageMargin *
2) + "px"; | |
109 this._tooltipElement.style.maxHeight = ""; | |
110 var tooltipWidth = this._tooltipElement.offsetWidth; | |
111 var tooltipHeight = this._tooltipElement.offsetHeight; | |
112 var anchorTooltipAtElement = this._anchorElement.nodeName === "BUTTON" |
| this._anchorElement.nodeName === "LABEL"; | |
113 var tooltipX = anchorTooltipAtElement ? anchorBox.x : event.x + cursorOf
fset; | |
114 tooltipX = Number.constrain(tooltipX, | |
115 containerBox.x + pageMargin, | |
116 containerBox.x + containerBox.width - tooltipWidth - pageMargin); | |
117 var tooltipY; | |
118 if (!anchorTooltipAtElement) { | |
119 tooltipY = event.y + cursorOffset + tooltipHeight < containerBox.y +
containerBox.height ? event.y + cursorOffset : event.y - tooltipHeight; | |
120 } else { | |
121 var onBottom = anchorBox.y + anchorOffset + anchorBox.height + toolt
ipHeight < containerBox.y + containerBox.height; | |
122 tooltipY = onBottom ? anchorBox.y + anchorBox.height + anchorOffset
: anchorBox.y - tooltipHeight - anchorOffset; | |
123 } | |
124 this._tooltipElement.positionAt(tooltipX, tooltipY); | |
125 }, | |
126 | |
127 /** | |
128 * @param {boolean} removeInstant | |
129 */ | |
130 _hide: function(removeInstant) | |
131 { | |
132 delete this._anchorElement; | |
133 this._tooltipElement.classList.remove("shown"); | |
134 if (Date.now() > this._tooltipLastOpened) | |
135 this._tooltipLastClosed = Date.now(); | |
136 if (removeInstant) | |
137 delete this._tooltipLastClosed; | |
138 }, | |
139 | |
140 _reset: function() | |
141 { | |
142 this._hide(true); | |
143 this._tooltipElement.positionAt(0, 0); | |
144 this._tooltipElement.style.maxWidth = "0"; | |
145 this._tooltipElement.style.maxHeight = "0"; | |
146 } | |
147 }; | |
148 | |
149 WebInspector.Tooltip._symbol = Symbol("Tooltip"); | |
150 | |
151 /** | |
152 * @param {!Document} doc | |
153 */ | |
154 WebInspector.Tooltip.installHandler = function(doc) | |
155 { | |
156 new WebInspector.Tooltip(doc); | |
157 }; | |
158 | |
159 /** | |
160 * @param {!Element} element | |
161 * @param {!Element|string} tooltipContent | |
162 * @param {string=} actionId | |
163 * @param {!Object=} options | |
164 */ | |
165 WebInspector.Tooltip.install = function(element, tooltipContent, actionId, optio
ns) | |
166 { | |
167 if (typeof tooltipContent === "string" && tooltipContent === "") { | |
168 delete element[WebInspector.Tooltip._symbol]; | |
169 return; | |
170 } | |
171 element[WebInspector.Tooltip._symbol] = { content: tooltipContent, actionId:
actionId, options: options || {} }; | |
172 }; | |
173 | |
174 /** | |
175 * @param {!Element} element | |
176 */ | |
177 WebInspector.Tooltip.addNativeOverrideContainer = function(element) | |
178 { | |
179 WebInspector.Tooltip._nativeOverrideContainer.push(element); | |
180 }; | |
181 | 178 |
182 /** @type {!Array.<!Element>} */ | 179 /** @type {!Array.<!Element>} */ |
183 WebInspector.Tooltip._nativeOverrideContainer = []; | 180 WebInspector.Tooltip._nativeOverrideContainer = []; |
184 WebInspector.Tooltip._nativeTitle = /** @type {!ObjectPropertyDescriptor} */(Obj
ect.getOwnPropertyDescriptor(HTMLElement.prototype, "title")); | 181 WebInspector.Tooltip._nativeTitle = |
185 | 182 /** @type {!ObjectPropertyDescriptor} */ (Object.getOwnPropertyDescriptor(HT
MLElement.prototype, 'title')); |
186 Object.defineProperty(HTMLElement.prototype, "title", { | 183 |
187 /** | 184 Object.defineProperty(HTMLElement.prototype, 'title', { |
188 * @return {!Element|string} | 185 /** |
189 * @this {!Element} | 186 * @return {!Element|string} |
190 */ | 187 * @this {!Element} |
191 get: function() | 188 */ |
192 { | 189 get: function() { |
193 var tooltip = this[WebInspector.Tooltip._symbol]; | 190 var tooltip = this[WebInspector.Tooltip._symbol]; |
194 return tooltip ? tooltip.content : ""; | 191 return tooltip ? tooltip.content : ''; |
195 }, | 192 }, |
196 | 193 |
197 /** | 194 /** |
198 * @param {!Element|string} x | 195 * @param {!Element|string} x |
199 * @this {!Element} | 196 * @this {!Element} |
200 */ | 197 */ |
201 set: function(x) | 198 set: function(x) { |
202 { | 199 WebInspector.Tooltip.install(this, x); |
203 WebInspector.Tooltip.install(this, x); | 200 } |
204 } | |
205 }); | 201 }); |
OLD | NEW |