408 lines
12 KiB
HTML
408 lines
12 KiB
HTML
<!--
|
|
@license
|
|
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
|
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
|
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
|
Code distributed by Google as part of the polymer project is also
|
|
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
|
-->
|
|
|
|
|
|
<link rel="import" href="../polymer/polymer.html">
|
|
<link rel="import" href="../neon-animation/neon-animation-runner-behavior.html">
|
|
<link rel="import" href="../neon-animation/animations/fade-in-animation.html">
|
|
<link rel="import" href="../neon-animation/animations/fade-out-animation.html">
|
|
|
|
<!--
|
|
Material design: [Tooltips](https://www.google.com/design/spec/components/tooltips.html)
|
|
|
|
`<paper-tooltip>` is a label that appears on hover and focus when the user
|
|
hovers over an element with the cursor or with the keyboard. It will be centered
|
|
to an anchor element specified in the `for` attribute, or, if that doesn't exist,
|
|
centered to the parent node containing it.
|
|
|
|
Example:
|
|
|
|
<div style="display:inline-block">
|
|
<button>Click me!</button>
|
|
<paper-tooltip>Tooltip text</paper-tooltip>
|
|
</div>
|
|
|
|
<div>
|
|
<button id="btn">Click me!</button>
|
|
<paper-tooltip for="btn">Tooltip text</paper-tooltip>
|
|
</div>
|
|
|
|
The tooltip can be positioned on the top|bottom|left|right of the anchor using
|
|
the `position` attribute. The default position is bottom.
|
|
|
|
<paper-tooltip for="btn" position="left">Tooltip text</paper-tooltip>
|
|
<paper-tooltip for="btn" position="top">Tooltip text</paper-tooltip>
|
|
|
|
### Styling
|
|
|
|
The following custom properties and mixins are available for styling:
|
|
|
|
Custom property | Description | Default
|
|
----------------|-------------|----------
|
|
`--paper-tooltip-background` | The background color of the tooltip | `#616161`
|
|
`--paper-tooltip-opacity` | The opacity of the tooltip | `0.9`
|
|
`--paper-tooltip-text-color` | The text color of the tooltip | `white`
|
|
`--paper-tooltip` | Mixin applied to the tooltip | `{}`
|
|
|
|
@group Paper Elements
|
|
@element paper-tooltip
|
|
@demo demo/index.html
|
|
-->
|
|
|
|
<dom-module id="paper-tooltip">
|
|
<template>
|
|
<style>
|
|
:host {
|
|
display: block;
|
|
position: absolute;
|
|
outline: none;
|
|
z-index: 1002;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
cursor: default;
|
|
}
|
|
|
|
#tooltip {
|
|
display: block;
|
|
outline: none;
|
|
@apply(--paper-font-common-base);
|
|
font-size: 10px;
|
|
line-height: 1;
|
|
|
|
background-color: var(--paper-tooltip-background, #616161);
|
|
opacity: var(--paper-tooltip-opacity, 0.9);
|
|
color: var(--paper-tooltip-text-color, white);
|
|
|
|
padding: 8px;
|
|
border-radius: 2px;
|
|
|
|
@apply(--paper-tooltip);
|
|
}
|
|
|
|
/* Thanks IE 10. */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
</style>
|
|
|
|
<div id="tooltip" class="hidden">
|
|
<content></content>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
Polymer({
|
|
is: 'paper-tooltip',
|
|
|
|
hostAttributes: {
|
|
role: 'tooltip',
|
|
tabindex: -1
|
|
},
|
|
|
|
behaviors: [
|
|
Polymer.NeonAnimationRunnerBehavior
|
|
],
|
|
|
|
properties: {
|
|
/**
|
|
* The id of the element that the tooltip is anchored to. This element
|
|
* must be a sibling of the tooltip.
|
|
*/
|
|
for: {
|
|
type: String,
|
|
observer: '_findTarget'
|
|
},
|
|
|
|
/**
|
|
* Set this to true if you want to manually control when the tooltip
|
|
* is shown or hidden.
|
|
*/
|
|
manualMode: {
|
|
type: Boolean,
|
|
value: false,
|
|
observer: '_manualModeChanged'
|
|
},
|
|
|
|
/**
|
|
* Positions the tooltip to the top, right, bottom, left of its content.
|
|
*/
|
|
position: {
|
|
type: String,
|
|
value: 'bottom'
|
|
},
|
|
|
|
/**
|
|
* If true, no parts of the tooltip will ever be shown offscreen.
|
|
*/
|
|
fitToVisibleBounds: {
|
|
type: Boolean,
|
|
value: false
|
|
},
|
|
|
|
/**
|
|
* The spacing between the top of the tooltip and the element it is
|
|
* anchored to.
|
|
*/
|
|
offset: {
|
|
type: Number,
|
|
value: 14
|
|
},
|
|
|
|
/**
|
|
* This property is deprecated, but left over so that it doesn't
|
|
* break exiting code. Please use `offset` instead. If both `offset` and
|
|
* `marginTop` are provided, `marginTop` will be ignored.
|
|
* @deprecated since version 1.0.3
|
|
*/
|
|
marginTop: {
|
|
type: Number,
|
|
value: 14
|
|
},
|
|
|
|
/**
|
|
* The delay that will be applied before the `entry` animation is
|
|
* played when showing the tooltip.
|
|
*/
|
|
animationDelay: {
|
|
type: Number,
|
|
value: 500
|
|
},
|
|
|
|
/**
|
|
* The entry and exit animations that will be played when showing and
|
|
* hiding the tooltip. If you want to override this, you must ensure
|
|
* that your animationConfig has the exact format below.
|
|
*/
|
|
animationConfig: {
|
|
type: Object,
|
|
value: function() {
|
|
return {
|
|
'entry': [{
|
|
name: 'fade-in-animation',
|
|
node: this,
|
|
timing: {delay: 0}
|
|
}],
|
|
'exit': [{
|
|
name: 'fade-out-animation',
|
|
node: this
|
|
}]
|
|
}
|
|
}
|
|
},
|
|
|
|
_showing: {
|
|
type: Boolean,
|
|
value: false
|
|
}
|
|
},
|
|
|
|
listeners: {
|
|
'neon-animation-finish': '_onAnimationFinish',
|
|
},
|
|
|
|
/**
|
|
* Returns the target element that this tooltip is anchored to. It is
|
|
* either the element given by the `for` attribute, or the immediate parent
|
|
* of the tooltip.
|
|
*/
|
|
get target () {
|
|
var parentNode = Polymer.dom(this).parentNode;
|
|
// If the parentNode is a document fragment, then we need to use the host.
|
|
var ownerRoot = Polymer.dom(this).getOwnerRoot();
|
|
|
|
var target;
|
|
if (this.for) {
|
|
target = Polymer.dom(ownerRoot).querySelector('#' + this.for);
|
|
} else {
|
|
target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
|
|
ownerRoot.host : parentNode;
|
|
}
|
|
|
|
return target;
|
|
},
|
|
|
|
attached: function() {
|
|
this._findTarget();
|
|
},
|
|
|
|
detached: function() {
|
|
if (!this.manualMode)
|
|
this._removeListeners();
|
|
},
|
|
|
|
show: function() {
|
|
// If the tooltip is already showing, there's nothing to do.
|
|
if (this._showing)
|
|
return;
|
|
|
|
if (Polymer.dom(this).textContent.trim() === ''){
|
|
// Check if effective children are also empty
|
|
var allChildrenEmpty = true;
|
|
var effectiveChildren = Polymer.dom(this).getEffectiveChildNodes();
|
|
for (var i = 0; i < effectiveChildren.length; i++) {
|
|
if (effectiveChildren[i].textContent.trim() !== '') {
|
|
allChildrenEmpty = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allChildrenEmpty) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
this.cancelAnimation();
|
|
this._showing = true;
|
|
this.toggleClass('hidden', false, this.$.tooltip);
|
|
this.updatePosition();
|
|
|
|
this.animationConfig.entry[0].timing = this.animationConfig.entry[0].timing || {};
|
|
this.animationConfig.entry[0].timing.delay = this.animationDelay;
|
|
this._animationPlaying = true;
|
|
this.playAnimation('entry');
|
|
},
|
|
|
|
hide: function() {
|
|
// If the tooltip is already hidden, there's nothing to do.
|
|
if (!this._showing) {
|
|
return;
|
|
}
|
|
|
|
// If the entry animation is still playing, don't try to play the exit
|
|
// animation since this will reset the opacity to 1. Just end the animation.
|
|
if (this._animationPlaying) {
|
|
this.cancelAnimation();
|
|
this._showing = false;
|
|
this._onAnimationFinish();
|
|
return;
|
|
}
|
|
|
|
this._showing = false;
|
|
this._animationPlaying = true;
|
|
this.playAnimation('exit');
|
|
},
|
|
|
|
updatePosition: function() {
|
|
if (!this._target || !this.offsetParent)
|
|
return;
|
|
|
|
var offset = this.offset;
|
|
// If a marginTop has been provided by the user (pre 1.0.3), use it.
|
|
if (this.marginTop != 14 && this.offset == 14)
|
|
offset = this.marginTop;
|
|
|
|
var parentRect = this.offsetParent.getBoundingClientRect();
|
|
var targetRect = this._target.getBoundingClientRect();
|
|
var thisRect = this.getBoundingClientRect();
|
|
|
|
var horizontalCenterOffset = (targetRect.width - thisRect.width) / 2;
|
|
var verticalCenterOffset = (targetRect.height - thisRect.height) / 2;
|
|
|
|
var targetLeft = targetRect.left - parentRect.left;
|
|
var targetTop = targetRect.top - parentRect.top;
|
|
|
|
var tooltipLeft, tooltipTop;
|
|
|
|
switch (this.position) {
|
|
case 'top':
|
|
tooltipLeft = targetLeft + horizontalCenterOffset;
|
|
tooltipTop = targetTop - thisRect.height - offset;
|
|
break;
|
|
case 'bottom':
|
|
tooltipLeft = targetLeft + horizontalCenterOffset;
|
|
tooltipTop = targetTop + targetRect.height + offset;
|
|
break;
|
|
case 'left':
|
|
tooltipLeft = targetLeft - thisRect.width - offset;
|
|
tooltipTop = targetTop + verticalCenterOffset;
|
|
break;
|
|
case 'right':
|
|
tooltipLeft = targetLeft + targetRect.width + offset;
|
|
tooltipTop = targetTop + verticalCenterOffset;
|
|
break;
|
|
}
|
|
|
|
// TODO(noms): This should use IronFitBehavior if possible.
|
|
if (this.fitToVisibleBounds) {
|
|
// Clip the left/right side
|
|
if (parentRect.left + tooltipLeft + thisRect.width > window.innerWidth) {
|
|
this.style.right = '0px';
|
|
this.style.left = 'auto';
|
|
} else {
|
|
this.style.left = Math.max(0, tooltipLeft) + 'px';
|
|
this.style.right = 'auto';
|
|
}
|
|
|
|
// Clip the top/bottom side.
|
|
if (parentRect.top + tooltipTop + thisRect.height > window.innerHeight) {
|
|
this.style.bottom = parentRect.height + 'px';
|
|
this.style.top = 'auto';
|
|
} else {
|
|
this.style.top = Math.max(-parentRect.top, tooltipTop) + 'px';
|
|
this.style.bottom = 'auto';
|
|
}
|
|
} else {
|
|
this.style.left = tooltipLeft + 'px';
|
|
this.style.top = tooltipTop + 'px';
|
|
}
|
|
|
|
},
|
|
|
|
_addListeners: function() {
|
|
if (this._target) {
|
|
this.listen(this._target, 'mouseenter', 'show');
|
|
this.listen(this._target, 'focus', 'show');
|
|
this.listen(this._target, 'mouseleave', 'hide');
|
|
this.listen(this._target, 'blur', 'hide');
|
|
this.listen(this._target, 'tap', 'hide');
|
|
}
|
|
this.listen(this, 'mouseenter', 'hide');
|
|
},
|
|
|
|
_findTarget: function() {
|
|
if (!this.manualMode)
|
|
this._removeListeners();
|
|
|
|
this._target = this.target;
|
|
|
|
if (!this.manualMode)
|
|
this._addListeners();
|
|
},
|
|
|
|
_manualModeChanged: function() {
|
|
if (this.manualMode)
|
|
this._removeListeners();
|
|
else
|
|
this._addListeners();
|
|
},
|
|
|
|
_onAnimationFinish: function() {
|
|
this._animationPlaying = false;
|
|
if (!this._showing) {
|
|
this.toggleClass('hidden', true, this.$.tooltip);
|
|
}
|
|
},
|
|
|
|
_removeListeners: function() {
|
|
if (this._target) {
|
|
this.unlisten(this._target, 'mouseenter', 'show');
|
|
this.unlisten(this._target, 'focus', 'show');
|
|
this.unlisten(this._target, 'mouseleave', 'hide');
|
|
this.unlisten(this._target, 'blur', 'hide');
|
|
this.unlisten(this._target, 'tap', 'hide');
|
|
}
|
|
this.unlisten(this, 'mouseenter', 'hide');
|
|
}
|
|
});
|
|
</script>
|
|
</dom-module>
|