Merge branch 'feature/svg-chart-tooltips-7024'

This commit is contained in:
Matthias Jentsch 2014-09-03 17:08:56 +02:00
commit 64b9c89692
17 changed files with 803 additions and 33 deletions

View File

@ -238,6 +238,77 @@ the labels to show you can use the 'disableLegend()' call on the GridChart objec
![Various Line Graph Options][graph7]
### Tooltips
It is possible to specify custom tooltip format strings when creating bar charts.
Tooltips provide information about the points of each bar chart column, by aggregating
the values of all data sets with the same x-coordinate.
When no custom format string is given, a sane default format string is used, but its usually
clearer for the user to describe the data of each chart more accurately with a custom one.
**Example #9.1: Bar Charts with custom tooltips**
$this->chart->drawBars(
array(
'label' => 'Hosts critical',
'palette' => Palette::PROBLEM,
'stack' => 'stack1',
'data' => $data2,
'tooltip' => '{title}<br/> {value} of {sum} hosts are ok.'
),
array(
'label' => 'Hosts warning',
'stack' => 'stack1',
'palette' => Palette::WARNING,
'data' => $data,
'tooltip' => '{title}<br/> Oh no, {value} of {sum} hosts are down!'
)
);
As you can see, you can specify a format string for each data set, which allows you to
pass a custom message for all "down" hosts, one custom message for all "Ok" hosts and so on.
In contrast to that, the aggregation of values works on a column basis and will give you the
sum of all y-values with the same x-coordinate and not the aggregation of all values of the data set.
#### Rich Tooltips
It is also possible to use HTML in the tooltip strings to create rich tooltip markups, which can
be useful to provide extended output that spans over multiple lines. Please keep in mind that
users without JavaScript will see the tooltip with all of its html-tags stripped.
![Various Line Graph Options][graph7.1]
#### Available replacements
The available replacements depend on the used chart type, since the tooltip data is
instantiated and populated by the chart. All bar graphs have the following replacements available:
Aggregated values, are calculated from the data points of each column:
- sum: The amount of all Y-values of the current column
- max: The biggest occurring Y-value of the current column
- min: The smallest occurring Y-value of the current column
Column values are also defined by the current column, but are not
the product of any aggregation
- title: The x-value of the current column
Row values are defined by the properties the current data set, and are only useful for rendering the
generic tooltip correctly, since you could also just directly write
those values into your custom tooltip.
- label: The name of the current data set
- color: The color of this data set
## Pie Charts
### The PieChart Object
@ -317,5 +388,6 @@ Rendering is straightforward, assuming $svg is the PieChart/GridChart object, yo
[graph5]: res/GraphExample#5.png
[graph6]: res/GraphExample#6.png
[graph7]: res/GraphExample#7.png
[graph7.1]: res/GraphExample#7.1.png
[graph8]: res/GraphExample#8.png
[graph9]: res/GraphExample#9.png
[graph9]: res/GraphExample#9.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -16,6 +16,13 @@ use Icinga\Chart\Render\RenderContext;
*/
class BarGraph extends Styleable implements Drawable
{
/**
* The dataset order
*
* @var int
*/
private $order = 0;
/**
* The width of the bars.
*
@ -30,14 +37,37 @@ class BarGraph extends Styleable implements Drawable
*/
private $dataSet;
/**
* The tooltips
*
* @var
*/
private $tooltips;
/**
* All graphs
*
* @var
*/
private $graphs;
/**
* Create a new BarGraph with the given dataset
*
* @param array $dataSet An array of datapoints
* @param array $dataSet An array of data points
* @param int $order The graph number displayed by this BarGraph
* @param array $tooltips The tooltips to display for each value
*/
public function __construct(array $dataSet)
{
public function __construct(
array $dataSet,
array &$graphs,
$order,
array $tooltips = null
) {
$this->order = $order;
$this->dataSet = $dataSet;
$this->tooltips = $tooltips;
$this->graphs = $graphs;
}
/**
@ -56,6 +86,30 @@ class BarGraph extends Styleable implements Drawable
}
}
/**
* Draw a single rectangle
*
* @param array $point The
* @param null $index
* @param string $fill The fill color to use
* @param $strokeWidth
*
* @return Rect
*/
private function drawSingleBar($point, $index = null, $fill, $strokeWidth)
{
$rect = new Rect($point[0] - ($this->barWidth / 2), $point[1], $this->barWidth, 100 - $point[1]);
$rect->setFill($fill);
$rect->setStrokeWidth($strokeWidth);
$rect->setStrokeColor('black');
if (isset($index)) {
$rect->setAttribute('data-icinga-graph-index', $index);
}
$rect->setAttribute('data-icinga-graph-type', 'bar');
$rect->setAdditionalStyle('clip-path: url(#clip);');
return $rect;
}
/**
* Render this BarChart
*
@ -68,23 +122,33 @@ class BarGraph extends Styleable implements Drawable
$doc = $ctx->getDocument();
$group = $doc->createElement('g');
$idx = 0;
foreach ($this->dataSet as $point) {
$rect = new Rect($point[0] - 2, $point[1], 4, 100 - $point[1]);
$rect->setFill($this->fill);
$rect->setStrokeWidth($this->strokeWidth);
$rect->setStrokeColor('black');
$rect->setAttribute('data-icinga-graph-index', $idx++);
$rect->setAttribute('data-icinga-graph-type', 'bar');
$rect->setAdditionalStyle('clip-path: url(#clip);');
/*$rect->setAnimation(
new Animation(
'y',
$ctx->yToAbsolute(100),
$ctx->yToAbsolute($point[1]),
rand(1, 1.5)/2
)
);*/
$group->appendChild($rect->toSvg($ctx));
foreach ($this->dataSet as $x => $point) {
// add white background bar, to prevent other bars from altering transparency effects
$bar = $this->drawSingleBar($point, $idx++, 'white', $this->strokeWidth, $idx)->toSvg($ctx);
$group->appendChild($bar);
// draw actual bar
$bar = $this->drawSingleBar($point, null, $this->fill, $this->strokeWidth, $idx)->toSvg($ctx);
$bar->setAttribute('class', 'chart-data');
if (isset($this->tooltips[$x])) {
$data = array(
'label' => isset($this->graphs[$this->order]['label']) ?
strtolower($this->graphs[$this->order]['label']) : '',
'color' => isset($this->graphs[$this->order]['color']) ?
strtolower($this->graphs[$this->order]['color']) : '#fff'
);
$format = isset($this->graphs[$this->order]['tooltip'])
? $this->graphs[$this->order]['tooltip'] : null;
$bar->setAttribute(
'title',
$this->tooltips[$x]->renderNoHtml($this->order, $data, $format)
);
$bar->setAttribute(
'title-rich',
$this->tooltips[$x]->render($this->order, $data, $format)
);
}
$group->appendChild($bar);
}
return $group;
}

View File

@ -41,6 +41,10 @@ class StackedGraph implements Drawable
if (!isset($this->points[$x])) {
$this->points[$x] = 0;
}
// store old y-value for displaying the actual (non-aggregated)
// value in the tooltip
$point[2] = $point[1];
$this->points[$x] += $point[1];
$point[1] = $this->points[$x];
}

View File

@ -0,0 +1,144 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\Chart\Graph;
/**
* A tooltip that stores and aggregates information about displayed data
* points of a graph and replaces them in a format string to render the description
* for specific data points of the graph.
*
* When render() is called, placeholders for the keys for each data entry will be replaced by
* the current value of this data set and the formatted string will be returned.
* The content of the replaced keys can change for each data set and depends on how the data
* is passed to this class. There are several types of properties:
*
* <ul>
* <li>Global properties</li>: Key-value pairs that stay the same every time render is called, and are
* passed to an instance in the constructor.
* <li>Aggregated properties</li>: Global properties that are created automatically from
* all attached data points.
* <li>Local properties</li>: Key-value pairs that only apply to a single data point and
* are passed to the render-function.
* </ul>
*/
class Tooltip
{
/**
* The default format string used
* when no other format is specified
*
* @var string
*/
private $defaultFormat;
/**
* All aggregated points
*
* @var array
*/
private $points = array();
/**
* Contains all static replacements
*
* @var array
*/
private $data = array(
'sum' => 0
);
/**
* Used to format the displayed tooltip.
*
* @var string
*/
protected $tooltipFormat;
/**
* Create a new tooltip with the specified default format string
*
* Allows you to set the global data for this tooltip, that is displayed every
* time render is called.
*
* @param array $data Map of global properties
* @param string $format The default format string
*/
public function __construct (
$data = array(),
$format = '<b>{title}</b></b><br> {value} of {sum} {label}'
) {
$this->data = array_merge($this->data, $data);
$this->defaultFormat = $format;
}
/**
* Add a single data point to update the aggregated properties for this tooltip
*
* @param $point array Contains the (x,y) values of the data set
*/
public function addDataPoint($point)
{
// set x-value
if (!isset($this->data['title'])) {
$this->data['title'] = $point[0];
}
// aggregate y-values
$y = (int)$point[1];
if (isset($point[2])) {
// load original value in case value already aggregated
$y = (int)$point[2];
}
if (!isset($this->data['min']) || $this->data['min'] > $y) {
$this->data['min'] = $y;
}
if (!isset($this->data['max']) || $this->data['max'] < $y) {
$this->data['max'] = $y;
}
$this->data['sum'] += $y;
$this->points[] = $y;
}
/**
* Format the tooltip for a certain data point
*
* @param array $order Which data set to render
* @param array $data The local data for this tooltip
* @param string $format Use a custom format string for this data set
*
* @return mixed|string The tooltip value
*/
public function render($order, $data = array(), $format = null)
{
if (isset($format)) {
$str = $format;
} else {
$str = $this->defaultFormat;
}
$data['value'] = $this->points[$order];
foreach (array_merge($this->data, $data) as $key => $value) {
$str = str_replace('{' . $key . '}', $value, $str);
}
return $str;
}
/**
* Format the tooltip for a certain data point but remove all
* occurring html tags
*
* This is useful for rendering clean tooltips on client without JavaScript
*
* @param array $order Which data set to render
* @param array $data The local data for this tooltip
* @param string $format Use a custom format string for this data set
*
* @return mixed|string The tooltip value, without any HTML tags
*/
public function renderNoHtml($order, $data, $format)
{
return strip_tags($this->render($order, $data, $format));
}
}

View File

@ -10,6 +10,7 @@ use Icinga\Chart\Axis;
use Icinga\Chart\Graph\BarGraph;
use Icinga\Chart\Graph\LineGraph;
use Icinga\Chart\Graph\StackedGraph;
use Icinga\Chart\Graph\Tooltip;
use Icinga\Chart\Primitive\Canvas;
use Icinga\Chart\Primitive\Rect;
use Icinga\Chart\Primitive\Path;
@ -74,6 +75,16 @@ class GridChart extends Chart
*/
private $stacks = array();
/**
* An associative array containing all Tooltips used to render the titles
*
* Each tooltip represents the summary for all y-values of a certain x-value
* in the grid chart
*
* @var Tooltip
*/
private $tooltips = array();
/**
* Check if the current dataset has the proper structure for this chart.
*
@ -169,6 +180,26 @@ class GridChart extends Chart
$this->legend->addDataset($graph);
}
}
$this->initTooltips($data);
}
private function initTooltips($data)
{
foreach ($data as &$graph) {
foreach ($graph['data'] as $x => $point) {
if (!array_key_exists($x, $this->tooltips)) {
$this->tooltips[$x] = new Tooltip(
array(
'color' => $graph['color'],
)
);
}
$this->tooltips[$x]->addDataPoint($point);
}
}
}
/**
@ -353,11 +384,16 @@ class GridChart extends Chart
foreach ($this->graphs as $axisName => $graphs) {
$axis = $this->axis[$axisName];
$graphObj = null;
foreach ($graphs as $graph) {
foreach ($graphs as $dataset => $graph) {
// determine the type and create a graph object for it
switch ($graph['graphType']) {
case self::TYPE_BAR:
$graphObj = new BarGraph($axis->transform($graph['data']));
$graphObj = new BarGraph(
$axis->transform($graph['data']),
$graphs,
$dataset,
$this->tooltips
);
break;
case self::TYPE_LINE:
$graphObj = new LineGraph($axis->transform($graph['data']));

View File

@ -25,12 +25,14 @@ class JavaScript
protected static $vendorFiles = array(
'js/vendor/jquery-2.1.0',
'js/vendor/jquery.sparkline'
'js/vendor/jquery.sparkline',
'js/vendor/jquery.tipsy'
);
protected static $ie8VendorFiles = array(
'js/vendor/jquery-1.11.0',
'js/vendor/jquery.sparkline'
'js/vendor/jquery.sparkline',
'js/vendor/jquery.tipsy'
);
public static function listModuleFiles()

View File

@ -24,6 +24,8 @@ class StyleSheet
'css/icinga/monitoring-colors.less',
'css/icinga/selection-toolbar.less',
'css/icinga/login.less',
'css/icinga/charts.less',
'css/vendor/tipsy.css'
);
public static function compileForPdf()

View File

@ -154,30 +154,35 @@ class Monitoring_ChartController extends Controller
->setXAxis(new \Icinga\Chart\Unit\StaticAxis())
->setAxisMin(null, 0);
$tooltip = t('<b>{title}:</b><br />{value} of {sum} services are {label}');
$this->view->chart->drawBars(
array(
'label' => t('Ok'),
'color' => '#44bb77',
'stack' => 'stack1',
'data' => $okBars
'data' => $okBars,
'tooltip' => $tooltip
),
array(
'label' => t('Warning'),
'color' => '#ffaa44',
'stack' => 'stack1',
'data' => $warningBars
'data' => $warningBars,
'tooltip' => $tooltip
),
array(
'label' => t('Critical'),
'color' => '#ff5566',
'stack' => 'stack1',
'data' => $critBars
'data' => $critBars,
'tooltip' => $tooltip
),
array(
'label' => t('Unknown'),
'color' => '#dd66ff',
'stack' => 'stack1',
'data' => $unknownBars
'data' => $unknownBars,
'tooltip' => $tooltip
)
);
}
@ -201,6 +206,7 @@ class Monitoring_ChartController extends Controller
$hostgroup->hosts_unreachable_unhandled
);
}
$tooltip = t('<b>{title}:</b><br /> {value} of {sum} hosts are {label}');
$this->view->chart = new GridChart();
$this->view->chart->alignTopLeft();
$this->view->chart->setAxisLabel('', t('Hosts'))
@ -211,19 +217,22 @@ class Monitoring_ChartController extends Controller
'label' => t('Up'),
'color' => '#44bb77',
'stack' => 'stack1',
'data' => $upBars
'data' => $upBars,
'tooltip' => $tooltip
),
array(
'label' => t('Down'),
'color' => '#ff5566',
'stack' => 'stack1',
'data' => $downBars
'data' => $downBars,
'tooltip' => $tooltip
),
array(
'label' => t('Unreachable'),
'color' => '#dd66ff',
'stack' => 'stack1',
'data' => $unreachableBars
'data' => $unreachableBars,
'tooltip' => $tooltip
)
);
}

View File

@ -0,0 +1,11 @@
// {{{ICINGA_LICENSE_HEADER}}}
// {{{ICINGA_LICENSE_HEADER}}}
/* Add hover effects to chart data */
.chart-data:hover {
opacity: 0.85;
}
.pie-data:hover {
opacity: 0.6;
}

30
public/css/vendor/tipsy.css vendored Normal file
View File

@ -0,0 +1,30 @@
/* tipsy, facebook style tooltips for jquery
version 1.0.0a
(c) 2008-2010 jason frame [jason@onehackoranother.com]
released under the MIT license */
.tipsy { font-size: 14px; position: absolute; padding: 5px; z-index: 100000; }
.tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; }
/* Rounded corners */
.tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; }
/* Uncomment for shadow */
/*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/
.tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; }
/* Rules to colour arrows */
.tipsy-arrow-n { border-bottom-color: #000; }
.tipsy-arrow-s { border-top-color: #000; }
.tipsy-arrow-e { border-left-color: #000; }
.tipsy-arrow-w { border-right-color: #000; }
.tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;}
.tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;}
.tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; }
.tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; }
.tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; }

View File

@ -109,6 +109,62 @@
if (searchField.length && searchField.val().length) {
this.searchValue = searchField.val();
}
$('[title]').each(function () {
var $el = $(this);
$el.attr('title', $el.attr('title-rich') || $el.attr('title'));
// $el.attr('title', null);
});
$('svg rect.chart-data[title]', el).tipsy({ gravity: 'e', html: true });
$('.historycolorgrid a[title]', el).tipsy({ gravity: 's', offset: 2 });
$('img.icon[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, offset: 2 });
$('[title]', el).tipsy({ gravity: $.fn.tipsy.autoNS, delayIn: 500 });
// Rescue or remove all orphaned tooltips
$('.tipsy').each(function () {
function isElementInDOM(ele) {
while (ele = ele.parentNode) {
if (ele == document) return true;
}
return false;
};
var arrow = $('.tipsy-arrow', this)[0];
if (!icinga.utils.elementsOverlap(arrow, $('#main')[0])) {
$(this).remove();
return;
}
// all tooltips are direct children of the body
// so we need find out whether the tooltip belongs applied area,
// by checking if both areas overlap
if (!icinga.utils.elementsOverlap(arrow, el)) {
// tooltip does not belong to this area
return;
}
var pointee = $.data(this, 'tipsy-pointee');
if (!pointee || !isElementInDOM(pointee)) {
var orphan = this;
var oldTitle = $(this).find('.tipsy-inner').html();
var migrated = false;
// try to find an element with the same title
$('[original-title="' + oldTitle + '"]').each(function() {
// get stored instance of Tipsy from newly created element
// point it to the orphaned tooltip
var tipsy = $.data(this, 'tipsy');
tipsy.$tip = $(orphan);
$.data(this, 'tipsy', tipsy);
// orphaned tooltip has the new element as pointee
$.data(orphan, 'tipsy-pointee', this);
migrated = true;
});
if (!migrated) {
$(orphan).remove();
}
}
});
},
/**
@ -162,6 +218,13 @@
// $(document).on('keyup', 'form.auto input', this.formChangeDelayed);
// $(document).on('change', 'form.auto input', this.formChanged);
// $(document).on('change', 'form.auto select', this.submitForm);
$(document).on('mouseenter', '[title-original]', { gravity: 'n' }, function(event) {
icinga.ui.hoverTooltip (this, event.data);
});
$(document).on('mouseleave', '[title-original]', {}, function() {
icinga.ui.unhoverTooltip (this);
});
},
menuTitleHovered: function (event) {

View File

@ -10,6 +10,9 @@
'use strict';
// The currently hovered tooltip
var tooltip = null;
// Stores the icinga-data-url of the last focused table.
var focusedTableDataUrl = null;
@ -767,7 +770,6 @@
this.debugTimer = null;
this.timeCounterTimer = null;
}
};
}(Icinga, jQuery));

View File

@ -200,6 +200,40 @@
return params;
},
/**
* Check whether two HTMLElements overlap
*
* @param a {HTMLElement}
* @param b {HTMLElement}
*
* @returns {Boolean} whether elements overlap, will return false when one
* element is not in the DOM
*/
elementsOverlap: function(a, b)
{
// a bounds
var aoff = $(a).offset();
if (!aoff) {
return false;
}
var at = aoff.top;
var ah = a.offsetHeight;
var al = aoff.left;
var aw = a.offsetWidth;
// b bounds
var boff = $(b).offset();
if (!boff) {
return false;
}
var bt = boff.top;
var bh = b.offsetHeight;
var bl = boff.left;
var bw = b.offsetWidth;
return !(at > (bt + bh) || bt > (at + ah)) && !(bl > (al + aw) || al > (bl + bw));
},
/**
* Cleanup
*/

33
public/js/vendor/SOURCE.jquery.tipsy vendored Normal file
View File

@ -0,0 +1,33 @@
jquery.tipsy.js SOURCE
======================
This file contains information about how to acquire and install the source files for jquery.tipsy
# version
1.0.0a
# license
MIT license
# used files
src/javascript/tipsy.css
src/javascript/jquery.tipsy.js
# source
https://github.com/jaz303/tipsy.git
# installation
mv src/javascript/tipsy.css ICINGAWEB/public/css/vendor/tipsy.css
mv src/javascript/jquery.tipsy.js ICINGAWEB/public/js/vendor/jquery.tipsy.js
uglifyjs src/javascript/jquery.tipsy.js ICINGAWEB/public/js/vendor/jquery.tipsy.min.js

262
public/js/vendor/jquery.tipsy.js vendored Normal file
View File

@ -0,0 +1,262 @@
// tipsy, facebook style tooltips for jquery
// version 1.0.0a
// (c) 2008-2010 jason frame [jason@onehackoranother.com]
// released under the MIT license
(function($) {
function maybeCall(thing, ctx) {
return (typeof thing == 'function') ? (thing.call(ctx)) : thing;
};
function isElementInDOM(ele) {
while (ele = ele.parentNode) {
if (ele == document) return true;
}
return false;
};
function Tipsy(element, options) {
this.$element = $(element);
this.options = options;
this.enabled = true;
this.fixTitle();
};
Tipsy.prototype = {
show: function() {
var title = this.getTitle();
if (title && this.enabled) {
var $tip = this.tip();
$tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title);
$tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity
$tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body);
var pos = $.extend({}, this.$element.offset(), {
width: this.$element[0].offsetWidth,
height: this.$element[0].offsetHeight
});
var actualWidth = $tip[0].offsetWidth,
actualHeight = $tip[0].offsetHeight,
gravity = maybeCall(this.options.gravity, this.$element[0]);
var tp;
switch (gravity.charAt(0)) {
case 'n':
tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 's':
tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2};
break;
case 'e':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset};
break;
case 'w':
tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset};
break;
}
if (gravity.length == 2) {
if (gravity.charAt(1) == 'w') {
tp.left = pos.left + pos.width / 2 - 15;
} else {
tp.left = pos.left + pos.width / 2 - actualWidth + 15;
}
}
$tip.css(tp).addClass('tipsy-' + gravity);
$tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0);
if (this.options.className) {
$tip.addClass(maybeCall(this.options.className, this.$element[0]));
}
if (this.options.fade) {
$tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity});
} else {
$tip.css({visibility: 'visible', opacity: this.options.opacity});
}
}
},
hide: function() {
if (this.options.fade) {
this.tip().stop().fadeOut(function() { $(this).remove(); });
} else {
this.tip().remove();
}
},
fixTitle: function() {
var $e = this.$element;
if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') {
$e.attr('original-title', $e.attr('title') || '').removeAttr('title');
}
},
getTitle: function() {
var title, $e = this.$element, o = this.options;
this.fixTitle();
var title, o = this.options;
if (typeof o.title == 'string') {
title = $e.attr(o.title == 'title' ? 'original-title' : o.title);
} else if (typeof o.title == 'function') {
title = o.title.call($e[0]);
}
title = ('' + title).replace(/(^\s*|\s*$)/, "");
return title || o.fallback;
},
tip: function() {
if (!this.$tip) {
this.$tip = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
this.$tip.data('tipsy-pointee', this.$element[0]);
}
return this.$tip;
},
validate: function() {
if (!this.$element[0].parentNode) {
this.hide();
this.$element = null;
this.options = null;
}
},
enable: function() { this.enabled = true; },
disable: function() { this.enabled = false; },
toggleEnabled: function() { this.enabled = !this.enabled; }
};
$.fn.tipsy = function(options) {
if (options === true) {
return this.data('tipsy');
} else if (typeof options == 'string') {
var tipsy = this.data('tipsy');
if (tipsy) tipsy[options]();
return this;
}
options = $.extend({}, $.fn.tipsy.defaults, options);
function get(ele) {
var tipsy = $.data(ele, 'tipsy');
if (!tipsy) {
tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options));
$.data(ele, 'tipsy', tipsy);
}
return tipsy;
}
function enter() {
var tipsy = get(this);
var ele = this;
tipsy.hoverState = 'in';
if (options.delayIn == 0) {
tipsy.show();
} else {
tipsy.fixTitle();
setTimeout(function() {
if (tipsy.hoverState == 'in' && ele && isElementInDOM(ele))
tipsy.show();
}, options.delayIn);
}
};
function leave() {
var tipsy = get(this);
tipsy.hoverState = 'out';
if (options.delayOut == 0) {
tipsy.hide();
} else {
setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut);
}
};
if (!options.live) this.each(function() { get(this); });
if (options.trigger != 'manual') {
var binder = options.live ? 'live' : 'bind',
eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus',
eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur';
this[binder](eventIn, enter)[binder](eventOut, leave);
}
return this;
};
$.fn.tipsy.defaults = {
className: null,
delayIn: 0,
delayOut: 0,
fade: false,
fallback: '',
gravity: 'n',
html: false,
live: false,
offset: 0,
opacity: 0.8,
title: 'title',
trigger: 'hover'
};
$.fn.tipsy.revalidate = function() {
$('.tipsy').each(function() {
var pointee = $.data(this, 'tipsy-pointee');
if (!pointee || !isElementInDOM(pointee)) {
$(this).remove();
}
});
};
// Overwrite this method to provide options on a per-element basis.
// For example, you could store the gravity in a 'tipsy-gravity' attribute:
// return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' });
// (remember - do not modify 'options' in place!)
$.fn.tipsy.elementOptions = function(ele, options) {
return $.metadata ? $.extend({}, options, $(ele).metadata()) : options;
};
$.fn.tipsy.autoNS = function() {
return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n';
};
$.fn.tipsy.autoWE = function() {
return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w';
};
/**
* yields a closure of the supplied parameters, producing a function that takes
* no arguments and is suitable for use as an autogravity function like so:
*
* @param margin (int) - distance from the viewable region edge that an
* element should be before setting its tooltip's gravity to be away
* from that edge.
* @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer
* if there are no viewable region edges effecting the tooltip's
* gravity. It will try to vary from this minimally, for example,
* if 'sw' is preferred and an element is near the right viewable
* region edge, but not the top edge, it will set the gravity for
* that element's tooltip to be 'se', preserving the southern
* component.
*/
$.fn.tipsy.autoBounds = function(margin, prefer) {
return function() {
var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)},
boundTop = $(document).scrollTop() + margin,
boundLeft = $(document).scrollLeft() + margin,
$this = $(this);
if ($this.offset().top < boundTop) dir.ns = 'n';
if ($this.offset().left < boundLeft) dir.ew = 'w';
if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e';
if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's';
return dir.ns + (dir.ew ? dir.ew : '');
}
};
})(jQuery);

2
public/js/vendor/jquery.tipsy.min.js vendored Normal file

File diff suppressed because one or more lines are too long