From e015f17ac18d4361e04a2ff11ee580e186537a78 Mon Sep 17 00:00:00 2001 From: diosmosis Date: Sat, 29 Jul 2017 19:54:52 -0700 Subject: Extract series picker to new angular component (w/ less manual element positioning). --- plugins/CoreVisualizations/CoreVisualizations.php | 5 + .../series-picker/series-picker.component.html | 49 +++ .../series-picker/series-picker.component.js | 144 +++++++++ .../series-picker/series-picker.component.less | 24 ++ .../CoreVisualizations/javascripts/seriesPicker.js | 327 ++++----------------- plugins/CoreVisualizations/stylesheets/jqplot.css | 3 +- 6 files changed, 277 insertions(+), 275 deletions(-) create mode 100644 plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.html create mode 100644 plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js create mode 100644 plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less (limited to 'plugins/CoreVisualizations') 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 @@ +
+ + + + +
+

{{ ($ctrl.multiselect ? 'General_MetricsToPlot' : 'General_MetricToPlot') | translate }}

+

+ + +

+

+ {{ 'General_RecordsToPlot' | translate }} +

+

+ + +

+
+
\ 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: + * + */ +(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 = ''; - // 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($('