| 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 |