Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiosmosis <diosmosis@users.noreply.github.com>2017-07-30 05:54:52 +0300
committerdiosmosis <diosmosis@users.noreply.github.com>2017-10-01 21:53:10 +0300
commite015f17ac18d4361e04a2ff11ee580e186537a78 (patch)
treec395f6fafad9368cc201f2f6008637cc1dfee4c0 /plugins/CoreVisualizations
parent52e9403a82235203c5ee2a49c93d23f99ef46254 (diff)
Extract series picker to new angular component (w/ less manual element positioning).
Diffstat (limited to 'plugins/CoreVisualizations')
-rw-r--r--plugins/CoreVisualizations/CoreVisualizations.php5
-rw-r--r--plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.html49
-rw-r--r--plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js144
-rw-r--r--plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less24
-rw-r--r--plugins/CoreVisualizations/javascripts/seriesPicker.js327
-rw-r--r--plugins/CoreVisualizations/stylesheets/jqplot.css3
6 files changed, 277 insertions, 275 deletions
diff --git a/plugins/CoreVisualizations/CoreVisualizations.php b/plugins/CoreVisualizations/CoreVisualizations.php
index b832903228..5c633706e3 100644
--- a/plugins/CoreVisualizations/CoreVisualizations.php
+++ b/plugins/CoreVisualizations/CoreVisualizations.php
@@ -39,12 +39,17 @@ class CoreVisualizations extends \Piwik\Plugin
public function getStylesheetFiles(&$stylesheets)
{
+ $stylesheets[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less";
+
$stylesheets[] = "plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less";
$stylesheets[] = "plugins/CoreVisualizations/stylesheets/jqplot.css";
}
public function getJsFiles(&$jsFiles)
{
+ $jsFiles[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js";
+ $jsFiles[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js";
+
$jsFiles[] = "plugins/CoreVisualizations/javascripts/seriesPicker.js";
$jsFiles[] = "plugins/CoreVisualizations/javascripts/jqplot.js";
$jsFiles[] = "plugins/CoreVisualizations/javascripts/jqplotBarGraph.js";
diff --git a/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.html b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.html
new file mode 100644
index 0000000000..103df58343
--- /dev/null
+++ b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.html
@@ -0,0 +1,49 @@
+<div
+ class="jqplot-seriespicker"
+ ng-class="{open: $ctrl.isPopupVisible}"
+ ng-mouseenter="$ctrl.isPopupVisible = true"
+ ng-mouseleave="$ctrl.onLeavePopup()"
+>
+ <a
+ href="#"
+ ng-click="$event.preventDefault(); $event.stopPropagation();"
+ >
+ +
+ </a>
+ <div
+ class="jqplot-seriespicker-popover"
+ ng-if="$ctrl.isPopupVisible"
+ >
+ <p class="headline">{{ ($ctrl.multiselect ? 'General_MetricsToPlot' : 'General_MetricToPlot') | translate }}</p>
+ <p
+ ng-repeat="columnConfig in $ctrl.selectableColumns"
+ class="pickColumn"
+ ng-click="$ctrl.optionSelected(columnConfig.column, $ctrl.columnStates)"
+ >
+ <input
+ class="select"
+ ng-checked="$ctrl.columnStates[columnConfig.column]"
+ ng-attr-type="{{ $ctrl.multiselect ? 'checkbox' : 'radio' }}"
+ />
+ <label>{{ columnConfig.translation }}</label>
+ </p>
+ <p
+ ng-if="$ctrl.selectableRows.length"
+ class="headline recordsToPlot"
+ >
+ {{ 'General_RecordsToPlot' | translate }}
+ </p>
+ <p
+ ng-repeat="rowConfig in $ctrl.selectableRows"
+ class="pickRow"
+ ng-click="$ctrl.optionSelected(rowConfig.matcher, $ctrl.rowStates)"
+ >
+ <input
+ class="select"
+ ng-checked="$ctrl.rowStates[rowConfig.matcher]"
+ ng-attr-type="{{ $ctrl.multiselect ? 'checkbox' : 'radio' }}"
+ />
+ <label>{{ rowConfig.label }}</label>
+ </p>
+ </div>
+</div> \ No newline at end of file
diff --git a/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js
new file mode 100644
index 0000000000..b84a190ad3
--- /dev/null
+++ b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js
@@ -0,0 +1,144 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * This series picker component is a popup that displays a list of metrics/row
+ * values that can be selected. It's used by certain datatable visualizations
+ * to allow users to select different data series for display.
+ *
+ * Inputs:
+ * - multiselect: true if the picker should allow selecting multiple items, false
+ * if otherwise.
+ * - selectableColumns: the list of selectable metric values. must be a list of
+ * objects with the following properties:
+ * * column: the ID of the column, eg, nb_visits
+ * * translation: the translated text for the column, eg, Visits
+ * - selectableRows: the list of selectable row values. must be a list of objects
+ * with the following properties:
+ * * matcher: the ID of the row
+ * * label: the display text for the row
+ * - selectedColumns: the list of selected columns. should be a list of strings
+ * that correspond to the 'column' property in selectableColumns.
+ * - selectedRows: the list of selected rows. should be a list of strings that
+ * correspond to the 'matcher' property in selectableRows.
+ * - onSelect: expression invoked when a user makes a new selection. invoked
+ * with the following local variables:
+ * * columns: list of IDs of new selected columns, if any
+ * * rows: list of matchers of new selected rows, if any
+ *
+ * Usage:
+ * <piwik-series-picker />
+ */
+(function () {
+ angular.module('piwikApp').component('piwikSeriesPicker', {
+ templateUrl: 'plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.html?cb=' + piwik.cacheBuster,
+ bindings: {
+ multiselect: '<',
+ selectableColumns: '<',
+ selectableRows: '<',
+ selectedColumns: '<',
+ selectedRows: '<',
+ onSelect: '&'
+ },
+ controller: SeriesPickerController
+ });
+
+ SeriesPickerController.$inject = [];
+
+ function SeriesPickerController() {
+ var vm = this;
+ vm.isPopupVisible = false;
+
+ // note: column & row states are separated since it's technically possible (though
+ // highly improbable) that a row value matcher will be the same as a recognized column.
+ vm.columnStates = {};
+ vm.rowStates = {};
+ vm.optionSelected = optionSelected;
+ vm.onLeavePopup = onLeavePopup;
+ vm.$onInit = $onInit;
+
+ function $onInit() {
+ vm.columnStates = getInitialOptionStates(vm.selectableColumns, vm.selectedColumns);
+ vm.rowStates = getInitialOptionStates(vm.selectableRows, vm.selectedRows);
+ }
+
+ function getInitialOptionStates(allOptions, selectedOptions) {
+ var states = {};
+
+ allOptions.forEach(function (columnConfig) {
+ states[columnConfig.column || columnConfig.matcher] = false;
+ });
+
+ selectedOptions.forEach(function (column) {
+ states[column] = true;
+ });
+
+ return states;
+ }
+
+ function optionSelected(optionValue, optionStates) {
+ if (!vm.multiselect) {
+ unselectOptions(vm.columnStates);
+ unselectOptions(vm.rowStates);
+ }
+
+ optionStates[optionValue] = !optionStates[optionValue];
+
+ if (optionStates[optionValue]) {
+ triggerOnSelectAndClose();
+ }
+ }
+
+ function onLeavePopup() {
+ vm.isPopupVisible = false;
+
+ if (optionsChanged()) {
+ triggerOnSelectAndClose();
+ }
+ }
+
+ function triggerOnSelectAndClose() {
+ if (!vm.onSelect) {
+ return;
+ }
+
+ vm.isPopupVisible = false;
+
+ vm.onSelect({
+ columns: getSelected(vm.columnStates),
+ rows: getSelected(vm.rowStates)
+ });
+ }
+
+ function optionsChanged() {
+ return !arrayEqual(getSelected(vm.columnStates), vm.selectedColumns)
+ || !arrayEqual(getSelected(vm.rowStates), vm.selectedRows);
+ }
+
+ function arrayEqual(lhs, rhs) {
+ if (lhs.length !== rhs.length) {
+ return false;
+ }
+
+ return lhs
+ .filter(function (element) { return rhs.indexOf(element) === -1; })
+ .length === 0;
+ }
+
+ function unselectOptions(optionStates) {
+ Object.keys(optionStates).forEach(function (optionName) {
+ optionStates[optionName] = false;
+ });
+ }
+
+ function getSelected(optionStates) {
+ return Object.keys(optionStates).filter(function (optionName) {
+ return !! optionStates[optionName];
+ });
+ }
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less
new file mode 100644
index 0000000000..e950b1eefb
--- /dev/null
+++ b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less
@@ -0,0 +1,24 @@
+piwik-series-picker {
+ display: inline-block;
+
+ .jqplot-seriespicker {
+ &:not(.open) {
+ opacity: .55;
+ }
+
+ > a {
+ display: inline-block;
+ opacity: 0;
+ position: absolute;
+ }
+
+ position: relative;
+ }
+
+ .jqplot-seriespicker-popover {
+ position: absolute;
+
+ top: -3px;
+ left: -4px;
+ }
+}
diff --git a/plugins/CoreVisualizations/javascripts/seriesPicker.js b/plugins/CoreVisualizations/javascripts/seriesPicker.js
index f1e8c7eb3a..f56499dc9a 100644
--- a/plugins/CoreVisualizations/javascripts/seriesPicker.js
+++ b/plugins/CoreVisualizations/javascripts/seriesPicker.js
@@ -7,7 +7,7 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
-(function ($, doc, require) {
+(function ($, require) {
/**
* This class creates and manages the Series Picker for certain DataTable visualizations.
@@ -37,6 +37,7 @@
*
* @param {dataTable} dataTable The dataTable instance to add a series picker to.
* @constructor
+ * @deprecated use the piwik-series-picker directive instead
*/
var SeriesPicker = function (dataTable) {
this.domElem = null;
@@ -54,17 +55,6 @@
// can multiple rows we selected?
this.multiSelect = !! dataTable.props.allow_multi_select_series_picker;
-
- // language strings
- this.lang =
- {
- metricsToPlot: _pk_translate('General_MetricsToPlot'),
- metricToPlot: _pk_translate('General_MetricToPlot'),
- recordsToPlot: _pk_translate('General_RecordsToPlot')
- };
-
- this._pickerState = null;
- this._pickerPopover = null;
};
SeriesPicker.prototype = {
@@ -80,40 +70,62 @@
var self = this;
+ var selectedColumns = this.selectableColumns
+ .filter(isItemDisplayed)
+ .map(function (columnConfig) {
+ return columnConfig.column;
+ });
+
+ var selectedRows = this.selectableRows
+ .filter(isItemDisplayed)
+ .map(function (rowConfig) {
+ return rowConfig.matcher;
+ });
+
// initialize dom element
- this.domElem = $(doc.createElement('a'))
- .addClass('jqplot-seriespicker')
- .attr('href', '#')
- .html('+')
+ var seriesPicker = '<piwik-series-picker'
+ + ' multiselect="' + (this.multiSelect ? 'true' : 'false') + '"'
+ + ' selectable-columns="selectableColumns"'
+ + ' selectable-rows="selectableRows"'
+ + ' selected-columns="selectedColumns"'
+ + ' selected-rows="selectedRows"'
+ + ' on-select="selectionChanged(columns, rows)"/>';
- // set opacity on 'hide'
- .on('hide', function () {
- $(this).css('opacity', .55);
- })
- .trigger('hide')
+ this.domElem = $(seriesPicker); // TODO: don't know if this will work without a root scope
- // show picker on hover
- .hover(
- function () {
- var $this = $(this);
+ $(this).trigger('placeSeriesPicker');
- $this.css('opacity', 1);
- if (!$this.hasClass('open')) {
- $this.addClass('open');
- self._showPicker();
+ piwikHelper.compileAngularComponents(this.domElem, {
+ scope: {
+ selectableColumns: this.selectableColumns,
+ selectableRows: this.selectableRows,
+ selectedColumns: selectedColumns,
+ selectedRows: selectedRows,
+ selectionChanged: function selectionChanged(columns, rows) {
+ if (columns.length === 0 && rows.length === 0) {
+ return;
}
- },
- function () {
- // do nothing on mouseout because using this event doesn't work properly.
- // instead, the timeout check beneath is used (_bindCheckPickerLeave()).
+
+ $(self).trigger('seriesPicked', [columns, rows]);
+
+ // inform dashboard widget about changed parameters (to be restored on reload)
+ var UI = require('piwik/UI');
+ var params = {
+ columns: columns,
+ columns_to_display: columns,
+ rows: rows,
+ rows_to_display: rows
+ };
+
+ var tableNode = $('#' + this.dataTableId);
+ UI.DataTable.prototype.notifyWidgetParametersChange(tableNode, params);
}
- )
- .click(function (e) {
- e.preventDefault();
- return false;
- });
+ }
+ });
- $(this).trigger('placeSeriesPicker');
+ function isItemDisplayed(columnOrRowConfig) {
+ return columnOrRowConfig.displayed;
+ }
},
/**
@@ -124,247 +136,16 @@
* is returned.
*/
getMetricTranslation: function (metric) {
- for (var i = 0; i != this.selectableColumns.length; ++i) {
- if (this.selectableColumns[i].column == metric) {
+ for (var i = 0; i !== this.selectableColumns.length; ++i) {
+ if (this.selectableColumns[i].column === metric) {
return this.selectableColumns[i].translation;
}
}
return metric;
- },
-
- /**
- * Creates the popover DOM element, binds event handlers to it, and then displays it.
- */
- _showPicker: function () {
- this._pickerState = {manipulated: false};
- this._pickerPopover = this._createPopover();
-
- this._positionPopover();
-
- // hide and replot on mouse leave
- var self = this;
- this._bindCheckPickerLeave(function () {
- var replot = self._pickerState.manipulated;
- self._hidePicker(replot);
- });
- },
-
- /**
- * Creates a checkbox and related elements for a selectable column or selectable row.
- */
- _createPickerPopupItem: function (config, type) {
- var self = this;
-
- if (type == 'column') {
- var columnName = config.column,
- columnLabel = config.translation,
- cssClass = 'pickColumn';
- } else {
- var columnName = config.matcher,
- columnLabel = config.label,
- cssClass = 'pickRow';
- }
-
- var checkbox = $(document.createElement('input')).addClass('select')
- .attr('type', this.multiSelect ? 'checkbox' : 'radio');
-
- if (config.displayed && !(!this.multiSelect && this._pickerState.oneChecked)) {
- checkbox.prop('checked', true);
- this._pickerState.oneChecked = true;
- }
-
- // if we are rendering a column, remember the column name
- // if it's a row, remember the string that can be used to match the row
- checkbox.data('name', columnName);
-
- var el = $(document.createElement('p'))
- .append(checkbox)
- .append($('<label/>').text(columnLabel))
- .addClass(cssClass);
-
- var replot = function () {
- self._unbindPickerLeaveCheck();
- self._hidePicker(true);
- };
-
- var checkBox = function (box) {
- if (!self.multiSelect) {
- self._pickerPopover.find('input.select:not(.current)').prop('checked', false);
- }
- box.prop('checked', true);
- replot();
- };
-
- el.click(function (e) {
- self._pickerState.manipulated = true;
- var box = $(this).find('input.select');
- if (!$(e.target).is('input.select')) {
- if (box.is(':checked')) {
- box.prop('checked', false);
- } else {
- checkBox(box);
- }
- } else {
- if (box.is(':checked')) {
- checkBox(box);
- }
- }
- });
-
- return el;
- },
-
- /**
- * Binds an event to document that checks if the user has left the series picker.
- */
- _bindCheckPickerLeave: function (onLeaveCallback) {
- var offset = this._pickerPopover.offset();
- var minX = offset.left;
- var minY = offset.top;
- var maxX = minX + this._pickerPopover.outerWidth();
- var maxY = minY + this._pickerPopover.outerHeight();
-
- var self = this;
- this._onMouseMove = function (e) {
- var currentX = e.pageX, currentY = e.pageY;
- if (currentX < minX || currentX > maxX
- || currentY < minY || currentY > maxY
- ) {
- self._unbindPickerLeaveCheck();
- onLeaveCallback();
- }
- };
-
- $(doc).mousemove(this._onMouseMove);
- },
-
- /**
- * Unbinds the callback that was bound in _bindCheckPickerLeave.
- */
- _unbindPickerLeaveCheck: function () {
- $(doc).unbind('mousemove', this._onMouseMove);
- },
-
- /**
- * Removes and destroys the popover dom element. If any columns/rows were selected, the
- * 'seriesPicked' event is triggered.
- */
- _hidePicker: function (replot) {
- // hide picker
- this._pickerPopover.hide();
- this.domElem.trigger('hide').removeClass('open');
-
- // replot
- if (replot) {
- var columns = [];
- var rows = [];
- this._pickerPopover.find('input:checked').each(function () {
- if ($(this).closest('p').hasClass('pickRow')) {
- rows.push($(this).data('name'));
- } else {
- columns.push($(this).data('name'));
- }
- });
-
- var noRowSelected = this._pickerPopover.find('.pickRow').length > 0
- && this._pickerPopover.find('.pickRow input:checked').length === 0;
- if (columns.length > 0 && !noRowSelected) {
- $(this).trigger('seriesPicked', [columns, rows]);
-
- // inform dashboard widget about changed parameters (to be restored on reload)
- var UI = require('piwik/UI')
- var params = {columns: columns, columns_to_display: columns,
- rows: rows, rows_to_display: rows};
- var tableNode = $('#' + this.dataTableId);
- UI.DataTable.prototype.notifyWidgetParametersChange(tableNode, params);
- }
- }
-
- this._pickerPopover.remove();
- },
-
- /**
- * Creates and returns the popover element. This element shows a list of checkboxes, one
- * for each selectable column/row.
- */
- _createPopover: function () {
- var hasColumns = $.isArray(this.selectableColumns) && this.selectableColumns.length;
- var hasRows = $.isArray(this.selectableRows) && this.selectableRows.length;
-
- var popover = $('<div/>')
- .addClass('jqplot-seriespicker-popover');
-
- // create headline element
- var title = this.multiSelect ? this.lang.metricsToPlot : this.lang.metricToPlot;
- popover.append($('<p/>').addClass('headline').html(title));
-
- // create selectable columns list
- if (hasColumns) {
- for (var i = 0; i < this.selectableColumns.length; i++) {
- var column = this.selectableColumns[i];
- popover.append(this._createPickerPopupItem(column, 'column'));
- }
- }
-
- // create selectable rows list
- if (hasRows) {
- // "records to plot" subheadline
- var header = $('<p/>').addClass('headline').addClass('recordsToPlot').html(this.lang.recordsToPlot);
- popover.append(header);
-
- // render the selectable rows
- for (var i = 0; i < this.selectableRows.length; i++) {
- var row = this.selectableRows[i];
- popover.append(this._createPickerPopupItem(row, 'row'));
- }
- }
-
- popover.hide();
-
- return popover;
- },
-
- /**
- * Positions the popover element.
- */
- _positionPopover: function () {
- var $body = $('body'),
- popover = this._pickerPopover,
- pickerLink = this.domElem,
- pickerLinkLeft = pickerLink.offset().left,
- bodyRight = $body.offset().left + $body.width()
- ;
-
- $body.prepend(popover);
-
- var neededSpace = popover.outerWidth() + 10;
-
- var linkOffset = pickerLink.offset();
- if (navigator.appVersion.indexOf("MSIE 7.") != -1) {
- linkOffset.left -= 10;
- }
-
- // try to display popover to the right
- var margin = parseInt(pickerLink.css('margin-left')) - 4;
-
- var popoverRight = pickerLinkLeft + margin + neededSpace;
- if (popoverRight < bodyRight
- // make sure it's not too far to the left
- || popoverRight < 0
- ) {
- popover.css('margin-left', (linkOffset.left - 4) + 'px').show();
- } else {
- // display to the left
- popover.addClass('alignright')
- .css('margin-left', (linkOffset.left - neededSpace + 38) + 'px')
- .css('background-position', (popover.outerWidth() - 25) + 'px 4px')
- .show();
- }
- popover.css('margin-top', (linkOffset.top - 5) + 'px').show();
}
};
var exports = require('piwik/DataTableVisualizations/Widgets');
exports.SeriesPicker = SeriesPicker;
-})(jQuery, document, require);
+})(jQuery, require);
diff --git a/plugins/CoreVisualizations/stylesheets/jqplot.css b/plugins/CoreVisualizations/stylesheets/jqplot.css
index ef08f1292f..05a92a8e2f 100644
--- a/plugins/CoreVisualizations/stylesheets/jqplot.css
+++ b/plugins/CoreVisualizations/stylesheets/jqplot.css
@@ -211,8 +211,6 @@ a.rowevolution-startmulti {
height: 16px;
margin-top: 3px;
background: url(../../Morpheus/images/chart_line_edit.png) no-repeat center center;
- overflow: hidden;
- text-indent: -999px;
}
.jqplot-seriespicker-popover {
@@ -236,6 +234,7 @@ a.rowevolution-startmulti {
padding: 0 4px 0 0;
line-height: 15px;
vertical-align: middle;
+ white-space: nowrap;
}
.jqplot-seriespicker-popover p.headline {