1<!-- 2@license 3Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9--> 10 11 12<link rel="import" href="../polymer/polymer.html"> 13<link rel="import" href="../neon-animation/neon-animation-runner-behavior.html"> 14<link rel="import" href="../neon-animation/animations/fade-in-animation.html"> 15<link rel="import" href="../neon-animation/animations/fade-out-animation.html"> 16 17<!-- 18Material design: [Tooltips](https://www.google.com/design/spec/components/tooltips.html) 19 20`<paper-tooltip>` is a label that appears on hover and focus when the user 21hovers over an element with the cursor or with the keyboard. It will be centered 22to an anchor element specified in the `for` attribute, or, if that doesn't exist, 23centered to the parent node containing it. 24 25Example: 26 27 <div style="display:inline-block"> 28 <button>Click me!</button> 29 <paper-tooltip>Tooltip text</paper-tooltip> 30 </div> 31 32 <div> 33 <button id="btn">Click me!</button> 34 <paper-tooltip for="btn">Tooltip text</paper-tooltip> 35 </div> 36 37The tooltip can be positioned on the top|bottom|left|right of the anchor using 38the `position` attribute. The default position is bottom. 39 40 <paper-tooltip for="btn" position="left">Tooltip text</paper-tooltip> 41 <paper-tooltip for="btn" position="top">Tooltip text</paper-tooltip> 42 43### Styling 44 45The following custom properties and mixins are available for styling: 46 47Custom property | Description | Default 48----------------|-------------|---------- 49`--paper-tooltip-background` | The background color of the tooltip | `#616161` 50`--paper-tooltip-opacity` | The opacity of the tooltip | `0.9` 51`--paper-tooltip-text-color` | The text color of the tooltip | `white` 52`--paper-tooltip` | Mixin applied to the tooltip | `{}` 53 54@group Paper Elements 55@element paper-tooltip 56@demo demo/index.html 57--> 58 59<dom-module id="paper-tooltip"> 60 <template> 61 <style> 62 :host { 63 display: block; 64 position: absolute; 65 outline: none; 66 z-index: 1002; 67 -moz-user-select: none; 68 -ms-user-select: none; 69 -webkit-user-select: none; 70 user-select: none; 71 cursor: default; 72 } 73 74 #tooltip { 75 display: block; 76 outline: none; 77 @apply(--paper-font-common-base); 78 font-size: 10px; 79 line-height: 1; 80 81 background-color: var(--paper-tooltip-background, #616161); 82 opacity: var(--paper-tooltip-opacity, 0.9); 83 color: var(--paper-tooltip-text-color, white); 84 85 padding: 8px; 86 border-radius: 2px; 87 88 @apply(--paper-tooltip); 89 } 90 91 /* Thanks IE 10. */ 92 .hidden { 93 display: none !important; 94 } 95 </style> 96 97 <div id="tooltip" class="hidden"> 98 <content></content> 99 </div> 100 </template> 101 102 <script> 103 Polymer({ 104 is: 'paper-tooltip', 105 106 hostAttributes: { 107 role: 'tooltip', 108 tabindex: -1 109 }, 110 111 behaviors: [ 112 Polymer.NeonAnimationRunnerBehavior 113 ], 114 115 properties: { 116 /** 117 * The id of the element that the tooltip is anchored to. This element 118 * must be a sibling of the tooltip. 119 */ 120 for: { 121 type: String, 122 observer: '_findTarget' 123 }, 124 125 /** 126 * Set this to true if you want to manually control when the tooltip 127 * is shown or hidden. 128 */ 129 manualMode: { 130 type: Boolean, 131 value: false, 132 observer: '_manualModeChanged' 133 }, 134 135 /** 136 * Positions the tooltip to the top, right, bottom, left of its content. 137 */ 138 position: { 139 type: String, 140 value: 'bottom' 141 }, 142 143 /** 144 * If true, no parts of the tooltip will ever be shown offscreen. 145 */ 146 fitToVisibleBounds: { 147 type: Boolean, 148 value: false 149 }, 150 151 /** 152 * The spacing between the top of the tooltip and the element it is 153 * anchored to. 154 */ 155 offset: { 156 type: Number, 157 value: 14 158 }, 159 160 /** 161 * This property is deprecated, but left over so that it doesn't 162 * break exiting code. Please use `offset` instead. If both `offset` and 163 * `marginTop` are provided, `marginTop` will be ignored. 164 * @deprecated since version 1.0.3 165 */ 166 marginTop: { 167 type: Number, 168 value: 14 169 }, 170 171 /** 172 * The delay that will be applied before the `entry` animation is 173 * played when showing the tooltip. 174 */ 175 animationDelay: { 176 type: Number, 177 value: 500 178 }, 179 180 /** 181 * The entry and exit animations that will be played when showing and 182 * hiding the tooltip. If you want to override this, you must ensure 183 * that your animationConfig has the exact format below. 184 */ 185 animationConfig: { 186 type: Object, 187 value: function() { 188 return { 189 'entry': [{ 190 name: 'fade-in-animation', 191 node: this, 192 timing: {delay: 0} 193 }], 194 'exit': [{ 195 name: 'fade-out-animation', 196 node: this 197 }] 198 } 199 } 200 }, 201 202 _showing: { 203 type: Boolean, 204 value: false 205 } 206 }, 207 208 listeners: { 209 'neon-animation-finish': '_onAnimationFinish', 210 }, 211 212 /** 213 * Returns the target element that this tooltip is anchored to. It is 214 * either the element given by the `for` attribute, or the immediate parent 215 * of the tooltip. 216 */ 217 get target () { 218 var parentNode = Polymer.dom(this).parentNode; 219 // If the parentNode is a document fragment, then we need to use the host. 220 var ownerRoot = Polymer.dom(this).getOwnerRoot(); 221 222 var target; 223 if (this.for) { 224 target = Polymer.dom(ownerRoot).querySelector('#' + this.for); 225 } else { 226 target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? 227 ownerRoot.host : parentNode; 228 } 229 230 return target; 231 }, 232 233 attached: function() { 234 this._findTarget(); 235 }, 236 237 detached: function() { 238 if (!this.manualMode) 239 this._removeListeners(); 240 }, 241 242 show: function() { 243 // If the tooltip is already showing, there's nothing to do. 244 if (this._showing) 245 return; 246 247 if (Polymer.dom(this).textContent.trim() === ''){ 248 // Check if effective children are also empty 249 var allChildrenEmpty = true; 250 var effectiveChildren = Polymer.dom(this).getEffectiveChildNodes(); 251 for (var i = 0; i < effectiveChildren.length; i++) { 252 if (effectiveChildren[i].textContent.trim() !== '') { 253 allChildrenEmpty = false; 254 break; 255 } 256 } 257 if (allChildrenEmpty) { 258 return; 259 } 260 } 261 262 263 this.cancelAnimation(); 264 this._showing = true; 265 this.toggleClass('hidden', false, this.$.tooltip); 266 this.updatePosition(); 267 268 this.animationConfig.entry[0].timing = this.animationConfig.entry[0].timing || {}; 269 this.animationConfig.entry[0].timing.delay = this.animationDelay; 270 this._animationPlaying = true; 271 this.playAnimation('entry'); 272 }, 273 274 hide: function() { 275 // If the tooltip is already hidden, there's nothing to do. 276 if (!this._showing) { 277 return; 278 } 279 280 // If the entry animation is still playing, don't try to play the exit 281 // animation since this will reset the opacity to 1. Just end the animation. 282 if (this._animationPlaying) { 283 this.cancelAnimation(); 284 this._showing = false; 285 this._onAnimationFinish(); 286 return; 287 } 288 289 this._showing = false; 290 this._animationPlaying = true; 291 this.playAnimation('exit'); 292 }, 293 294 updatePosition: function() { 295 if (!this._target || !this.offsetParent) 296 return; 297 298 var offset = this.offset; 299 // If a marginTop has been provided by the user (pre 1.0.3), use it. 300 if (this.marginTop != 14 && this.offset == 14) 301 offset = this.marginTop; 302 303 var parentRect = this.offsetParent.getBoundingClientRect(); 304 var targetRect = this._target.getBoundingClientRect(); 305 var thisRect = this.getBoundingClientRect(); 306 307 var horizontalCenterOffset = (targetRect.width - thisRect.width) / 2; 308 var verticalCenterOffset = (targetRect.height - thisRect.height) / 2; 309 310 var targetLeft = targetRect.left - parentRect.left; 311 var targetTop = targetRect.top - parentRect.top; 312 313 var tooltipLeft, tooltipTop; 314 315 switch (this.position) { 316 case 'top': 317 tooltipLeft = targetLeft + horizontalCenterOffset; 318 tooltipTop = targetTop - thisRect.height - offset; 319 break; 320 case 'bottom': 321 tooltipLeft = targetLeft + horizontalCenterOffset; 322 tooltipTop = targetTop + targetRect.height + offset; 323 break; 324 case 'left': 325 tooltipLeft = targetLeft - thisRect.width - offset; 326 tooltipTop = targetTop + verticalCenterOffset; 327 break; 328 case 'right': 329 tooltipLeft = targetLeft + targetRect.width + offset; 330 tooltipTop = targetTop + verticalCenterOffset; 331 break; 332 } 333 334 // TODO(noms): This should use IronFitBehavior if possible. 335 if (this.fitToVisibleBounds) { 336 // Clip the left/right side 337 if (parentRect.left + tooltipLeft + thisRect.width > window.innerWidth) { 338 this.style.right = '0px'; 339 this.style.left = 'auto'; 340 } else { 341 this.style.left = Math.max(0, tooltipLeft) + 'px'; 342 this.style.right = 'auto'; 343 } 344 345 // Clip the top/bottom side. 346 if (parentRect.top + tooltipTop + thisRect.height > window.innerHeight) { 347 this.style.bottom = parentRect.height + 'px'; 348 this.style.top = 'auto'; 349 } else { 350 this.style.top = Math.max(-parentRect.top, tooltipTop) + 'px'; 351 this.style.bottom = 'auto'; 352 } 353 } else { 354 this.style.left = tooltipLeft + 'px'; 355 this.style.top = tooltipTop + 'px'; 356 } 357 358 }, 359 360 _addListeners: function() { 361 if (this._target) { 362 this.listen(this._target, 'mouseenter', 'show'); 363 this.listen(this._target, 'focus', 'show'); 364 this.listen(this._target, 'mouseleave', 'hide'); 365 this.listen(this._target, 'blur', 'hide'); 366 this.listen(this._target, 'tap', 'hide'); 367 } 368 this.listen(this, 'mouseenter', 'hide'); 369 }, 370 371 _findTarget: function() { 372 if (!this.manualMode) 373 this._removeListeners(); 374 375 this._target = this.target; 376 377 if (!this.manualMode) 378 this._addListeners(); 379 }, 380 381 _manualModeChanged: function() { 382 if (this.manualMode) 383 this._removeListeners(); 384 else 385 this._addListeners(); 386 }, 387 388 _onAnimationFinish: function() { 389 this._animationPlaying = false; 390 if (!this._showing) { 391 this.toggleClass('hidden', true, this.$.tooltip); 392 } 393 }, 394 395 _removeListeners: function() { 396 if (this._target) { 397 this.unlisten(this._target, 'mouseenter', 'show'); 398 this.unlisten(this._target, 'focus', 'show'); 399 this.unlisten(this._target, 'mouseleave', 'hide'); 400 this.unlisten(this._target, 'blur', 'hide'); 401 this.unlisten(this._target, 'tap', 'hide'); 402 } 403 this.unlisten(this, 'mouseenter', 'hide'); 404 } 405 }); 406 </script> 407</dom-module> 408