1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
|
/**
* Registry for row actions
*
* Plugins can call DataTable_RowActions_Registry.register() from their JS
* files in order to add new actions to arbitrary data tables. The register()
* method takes an object containing:
* - name: string identifying the action. must be short, no spaces.
* - dataTableIcon: path to the icon for the action
* - createInstance: a factory method to create an instance of the appropriate
* subclass of DataTable_RowAction
* - isAvailable: a method to determine whether the action is available in a
* given row of a data table
*/
var DataTable_RowActions_Registry = {
registry: [],
register: function (action) {
var createInstance = action.createInstance;
action.createInstance = function (dataTable, param) {
var instance = createInstance(dataTable, param);
instance.actionName = action.name;
return instance;
};
this.registry.push(action);
},
getAvailableActionsForReport: function (dataTableParams, tr) {
if (dataTableParams.disable_row_actions == '1') {
return [];
}
var available = [];
for (var i = 0; i < this.registry.length; i++) {
if (this.registry[i].isAvailableOnReport(dataTableParams, tr)) {
available.push(this.registry[i]);
}
}
available.sort(function (a, b) {
return b.order - a.order;
});
return available;
},
getActionByName: function (name) {
for (var i = 0; i < this.registry.length; i++) {
if (this.registry[i].name == name) {
return this.registry[i];
}
}
return false;
}
};
// Register Row Evolution (also servers as example)
DataTable_RowActions_Registry.register({
name: 'RowEvolution',
dataTableIcon: 'icon-evolution',
order: 50,
dataTableIconTooltip: [
_pk_translate('General_RowEvolutionRowActionTooltipTitle'),
_pk_translate('General_RowEvolutionRowActionTooltip')
],
createInstance: function (dataTable, param) {
if (dataTable !== null && typeof dataTable.rowEvolutionActionInstance != 'undefined') {
return dataTable.rowEvolutionActionInstance;
}
if (dataTable === null && param) {
// when row evolution is triggered from the url (not a click on the data table)
// we look for the data table instance in the dom
// This actually doesn't work very good, as opening a row evolution using url params
// directly also triggers loading the report datatable, which might not yet be finished at
// this state, so the datatable might not yet be available
// When migrating/refactoring this it might be good to use promises in some way, so it would
// be possible to actually trigger the row evolution popover once the origin report was loaded.
var report = param.split(':')[0];
var div = $(require('piwik/UI').DataTable.getDataTableByReport(report));
if (div.length && div.data('uiControlObject')) {
dataTable = div.data('uiControlObject');
if (typeof dataTable.rowEvolutionActionInstance != 'undefined') {
return dataTable.rowEvolutionActionInstance;
}
}
}
var instance = new DataTable_RowActions_RowEvolution(dataTable);
if (dataTable !== null) {
dataTable.rowEvolutionActionInstance = instance;
}
return instance;
},
isAvailableOnReport: function (dataTableParams) {
return (
typeof dataTableParams.disable_row_evolution == 'undefined'
|| dataTableParams.disable_row_evolution == "0"
);
},
isAvailableOnRow: function (dataTableParams, tr) {
return !tr.hasClass('totalsRow');
}
});
/**
* DataTable Row Actions
*
* The lifecycle of an action is as follows:
* - for each data table, a new instance of the action is created using the factory
* - when the table is loaded, initTr is called for each tr
* - when the action icon is clicked, trigger is called
* - the label is put together and performAction is called
* - performAction must call openPopover on the base class
* - openPopover calls back doOpenPopover after doing general stuff
*
* The two template methods are performAction and doOpenPopover
*/
//
// BASE CLASS
//
function DataTable_RowAction(dataTable) {
this.dataTable = dataTable;
// has to be overridden in subclasses
this.trEventName = 'piwikTriggerRowAction';
// set in registry
this.actionName = 'RowAction';
}
/** Initialize a row when the table is loaded */
DataTable_RowAction.prototype.initTr = function (tr) {
var self = this;
// For subtables, we need to make sure that the actions are always triggered on the
// action instance connected to the root table. Otherwise sharing data (e.g. for
// for multi-row evolution) wouldn't be possible. Also, sub-tables might have different
// API actions. For the label filter to work, we need to use the parent action.
// We use jQuery events to let subtables access their parents.
tr.unbind(self.trEventName).bind(self.trEventName, function (e, params) {
self.trigger($(this), params.originalEvent, params.label, params.originalRow);
});
};
/**
* This method is called from the click event and the tr event (see this.trEventName).
* It derives the label and calls performAction.
*/
DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel, originalRow) {
var label = this.getLabelFromTr(tr);
// if we have received the event from the sub table, add the label
if (subTableLabel) {
var separator = ' > '; // LabelFilter::SEPARATOR_RECURSIVE_LABEL
label += separator + subTableLabel;
}
// handle sub tables in nested reports: forward to parent
var subtable = tr.closest('table');
if (subtable.is('.subDataTable')) {
subtable.closest('tr').prev().trigger(this.trEventName, {
label: label,
originalEvent: e,
originalRow: tr
});
return;
}
// ascend in action reports
if (subtable.closest('div.dataTableActions').length) {
var allClasses = tr.attr('class');
var matches = allClasses.match(/level[0-9]+/);
var level = parseInt(matches[0].substring(5, matches[0].length), 10);
if (level > 0) {
// .prev(.levelX) does not work for some reason => do it "by hand"
var findLevel = 'level' + (level - 1);
var ptr = tr;
while ((ptr = ptr.prev()).length) {
if (!ptr.hasClass(findLevel) || ptr.hasClass('nodata')) {
continue;
}
ptr.trigger(this.trEventName, {
label: label,
originalEvent: e,
originalRow: tr
});
return;
}
}
}
this.performAction(label, tr, e, originalRow);
};
/** Get the label string from a tr dom element */
DataTable_RowAction.prototype.getLabelFromTr = function (tr) {
if (tr.data('label')) {
return tr.data('label');
}
var rowMetadata = this.getRowMetadata(tr);
if (rowMetadata.combinedLabel) {
return '@' + rowMetadata.combinedLabel;
}
var label = tr.find('span.label');
// handle truncation
var value = label.data('originalText');
if (!value) {
value = label.text();
}
value = value.trim();
value = encodeURIComponent(value);
// if tr is a terminal node, we use the @ operator to distinguish it from branch nodes w/ the same name
if (!tr.hasClass('subDataTable')) {
value = '@' + value;
}
return value;
};
/** Get row metadata object */
DataTable_RowAction.prototype.getRowMetadata = function (tr) {
return tr.data('row-metadata') || {};
};
/**
* Base method for opening popovers.
* This method will remember the parameter in the url and call doOpenPopover().
*/
DataTable_RowAction.prototype.openPopover = function (parameter) {
broadcast.propagateNewPopoverParameter('RowAction', this.actionName + ':' + parameter);
};
broadcast.addPopoverHandler('RowAction', function (param) {
var paramParts = param.split(':');
var rowActionName = paramParts[0];
paramParts.shift();
param = paramParts.join(':');
var rowAction = DataTable_RowActions_Registry.getActionByName(rowActionName);
if (rowAction) {
rowAction.createInstance(null, param).doOpenPopover(param);
}
});
/** To be overridden */
DataTable_RowAction.prototype.performAction = function (label, tr, e) {
};
DataTable_RowAction.prototype.doOpenPopover = function (parameter) {
};
//
// ROW EVOLUTION
//
function DataTable_RowActions_RowEvolution(dataTable) {
this.dataTable = dataTable;
this.trEventName = 'piwikTriggerRowEvolution';
/** The rows to be compared in multi row evolution */
this.multiEvolutionRows = [];
this.multiEvolutionRowsSeries = [];
}
/** Static helper method to launch row evolution from anywhere */
DataTable_RowActions_RowEvolution.launch = function (apiMethod, label) {
var param = 'RowEvolution:' + apiMethod + ':0:' + label;
broadcast.propagateNewPopoverParameter('RowAction', param);
};
DataTable_RowActions_RowEvolution.prototype = new DataTable_RowAction;
DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr, e, originalRow) {
if (e.shiftKey) {
// only mark for multi row evolution if shift key is pressed
this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series'));
return;
}
this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series'));
// check whether we have rows marked for multi row evolution
var extraParams = $.extend({}, $(originalRow || tr).data('param-override'));
if (typeof extraParams !== 'object') {
extraParams = {};
}
if (this.multiEvolutionRows.length > 1) {
extraParams.action = 'getMultiRowEvolutionPopover';
label = this.multiEvolutionRows.join(',');
if (this.multiEvolutionRowsSeries.length > 1) { // when comparison is active
var piwikUrl = piwikHelper.getAngularDependency('piwikUrl');
extraParams.compareDates = piwikUrl.getSearchParam('compareDates');
extraParams.comparePeriods = piwikUrl.getSearchParam('comparePeriods');
extraParams.compareSegments = piwikUrl.getSearchParam('compareSegments');
extraParams.labelSeries = this.multiEvolutionRowsSeries.join(',');
// remove override period/date/segment since we are sending compare params so we can have the whole set of comparison
// serieses for LabelFilter
delete extraParams.period;
delete extraParams.date;
delete extraParams.segment;
}
}
$.each(this.dataTable.param, function (index, value) {
// we automatically add fields like idDimension, idGoal etc.
if (index !== 'idSite' && index.indexOf('id') === 0 && ($.isNumeric(value) || value.indexOf('ecommerce') === 0)) {
extraParams[index] = value;
}
});
if (this.dataTable && (this.dataTable.jsViewDataTable === 'tableGoals' || this.dataTable.jsViewDataTable === 'tableGoalsEntryPages'
|| this.dataTable.jsViewDataTable === 'tableGoalsPages')) {
// When there is a idGoal parameter available, the user is currently viewing a Goal or Ecommerce page
// In this case we want to show the specific goal metrics in the row evolution
if (extraParams['idGoal']) {
extraParams['showGoalMetricsForGoal'] = extraParams['idGoal'];
delete(extraParams['idGoal']);
}
// If no idGoal is available it is a random report switched to goal visualization
// we then ensure the row evolution will show the goal overview metrics
else {
extraParams['showGoalMetricsForGoal'] = -1;
}
}
// check if abandonedCarts is in the dataTable params and if so, propagate to row evolution request
if (this.dataTable.param.abandonedCarts !== undefined) {
extraParams['abandonedCarts'] = this.dataTable.param.abandonedCarts;
}
if (this.dataTable.param.flat !== undefined) {
extraParams['flat'] = this.dataTable.param.flat;
}
var apiMethod = this.dataTable.param.module + '.' + this.dataTable.param.action;
this.openPopover(apiMethod, extraParams, label);
};
DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow = function (label, seriesIndex) {
if (typeof seriesIndex !== 'undefined') {
var self = this;
var found = false;
this.multiEvolutionRows.forEach(function (rowLabel, index) {
var rowSeriesIndex = self.multiEvolutionRowsSeries[index];
if (label === rowLabel && seriesIndex === rowSeriesIndex) {
found = true;
return false;
}
});
if (!found) {
this.multiEvolutionRows.push(label);
this.multiEvolutionRowsSeries.push(seriesIndex);
}
} else if ($.inArray(label, this.multiEvolutionRows) === -1) {
this.multiEvolutionRows.push(label);
this.multiEvolutionRowsSeries = []; // for safety, make sure state is consistent
}
};
DataTable_RowActions_RowEvolution.prototype.openPopover = function (apiMethod, extraParams, label) {
var urlParam = apiMethod + ':' + encodeURIComponent(JSON.stringify(extraParams)) + ':' + label;
DataTable_RowAction.prototype.openPopover.apply(this, [urlParam]);
};
DataTable_RowActions_RowEvolution.prototype.doOpenPopover = function (urlParam) {
var urlParamParts = urlParam.split(':');
var apiMethod = urlParamParts.shift();
var extraParamsString = urlParamParts.shift(),
extraParams = {}; // 0/1 or "0"/"1"
try {
extraParams = JSON.parse(decodeURIComponent(extraParamsString));
} catch (e) {
// assume the parameter is an int/string describing whether to use multi row evolution
if (extraParamsString == '1') {
extraParams.action = 'getMultiRowEvolutionPopover';
} else if (extraParamsString != '0') {
extraParams.action = 'getMultiRowEvolutionPopover';
extraParams.column = extraParamsString;
}
}
var label = urlParamParts.join(':');
this.showRowEvolution(apiMethod, label, extraParams);
};
/** Open the row evolution popover */
DataTable_RowActions_RowEvolution.prototype.showRowEvolution = function (apiMethod, label, extraParams) {
var self = this;
// open the popover
var box = Piwik_Popover.showLoading('Row Evolution');
box.addClass('rowEvolutionPopover');
// prepare loading the popover contents
var requestParams = {
apiMethod: apiMethod,
label: label,
disableLink: 1
};
var callback = function (html) {
Piwik_Popover.setContent(html);
// use the popover title returned from the server
var title = box.find('div.popover-title');
if (title.length) {
Piwik_Popover.setTitle(title.html());
title.remove();
}
Piwik_Popover.onClose(function () {
// reset rows marked for multi row evolution on close
self.multiEvolutionRows = [];
self.multiEvolutionRowsSeries = [];
});
if (self.dataTable !== null) {
// remember label for multi row evolution
box.find('.rowevolution-startmulti').click(function () {
Piwik_Popover.onClose(false); // unbind listener that resets multiEvolutionRows
broadcast.propagateNewPopoverParameter(false);
return false;
});
} else {
// when the popover is launched by copy&pasting a url, we don't have the data table.
// in this case, we can't remember the row marked for multi row evolution so
// we disable the picker.
box.find('.compare-container, .rowevolution-startmulti').remove();
}
// switch metric in multi row evolution
box.find('select.multirowevoltion-metric').change(function () {
var metric = $(this).val();
Piwik_Popover.onClose(false); // unbind listener that resets multiEvolutionRows
extraParams.column = metric;
self.openPopover(apiMethod, extraParams, label);
return true;
});
};
requestParams.module = 'CoreHome';
requestParams.action = 'getRowEvolutionPopover';
requestParams.colors = JSON.stringify(piwik.getSparklineColors());
var idDimension;
if (broadcast.getValueFromUrl('module') === 'Widgetize') {
idDimension = broadcast.getValueFromUrl('subcategory');
} else {
idDimension = broadcast.getValueFromHash('subcategory');
}
if (idDimension && ('' + idDimension).indexOf('customdimension') === 0) {
idDimension = ('' + idDimension).replace('customdimension', '');
idDimension = parseInt(idDimension, 10);
if (idDimension > 0) {
requestParams.idDimension = idDimension;
}
}
$.extend(requestParams, extraParams);
var ajaxRequest = new ajaxHelper();
ajaxRequest.addParams(requestParams, 'get');
ajaxRequest.withTokenInUrl();
ajaxRequest.setCallback(callback);
ajaxRequest.setFormat('html');
ajaxRequest.send();
};
|