1
|
/*********************************************************
|
2
|
* taxonomic_children plugin
|
3
|
*********************************************************
|
4
|
*
|
5
|
* Expected dom structure:
|
6
|
* <span data-cdm-taxon-uuid="{taxon-uuid}"> ... </span>
|
7
|
*
|
8
|
* based on https://github.com/johndugan/jquery-plugin-boilerplate
|
9
|
*/
|
10
|
|
11
|
/*
|
12
|
The semi-colon before the function invocation is a safety net against
|
13
|
concatenated scripts and/or other plugins which may not be closed properly.
|
14
|
|
15
|
"undefined" is used because the undefined global variable in ECMAScript 3
|
16
|
is mutable (ie. it can be changed by someone else). Because we don't pass a
|
17
|
value to undefined when the anonymyous function is invoked, we ensure that
|
18
|
undefined is truly undefined. Note, in ECMAScript 5 undefined can no
|
19
|
longer be modified.
|
20
|
|
21
|
"window" and "document" are passed as local variables rather than global.
|
22
|
This (slightly) quickens the resolution process.
|
23
|
*/
|
24
|
|
25
|
;(function ( $, window, document, undefined ) {
|
26
|
|
27
|
/*
|
28
|
Store the name of the plugin in the "pluginName" variable. This
|
29
|
variable is used in the "Plugin" constructor below, as well as the
|
30
|
plugin wrapper to construct the key for the "$.data" method.
|
31
|
|
32
|
More: http://api.jquery.com/jquery.data/
|
33
|
*/
|
34
|
var pluginName = 'taxonomic_children';
|
35
|
|
36
|
/*
|
37
|
The "Plugin" constructor, builds a new instance of the plugin for the
|
38
|
DOM node(s) that the plugin is called on. For example,
|
39
|
"$('h1').pluginName();" creates a new instance of pluginName for
|
40
|
all h1's.
|
41
|
*/
|
42
|
// Create the plugin constructor
|
43
|
function Plugin ( element, options ) {
|
44
|
/*
|
45
|
Provide local access to the DOM node(s) that called the plugin,
|
46
|
as well local access to the plugin name and default options.
|
47
|
*/
|
48
|
this.element = element;
|
49
|
this._name = pluginName;
|
50
|
this._defaults = $.fn[pluginName].defaults;
|
51
|
/*
|
52
|
The "$.extend" method merges the contents of two or more objects,
|
53
|
and stores the result in the first object. The first object is
|
54
|
empty so that we don't alter the default options for future
|
55
|
instances of the plugin.
|
56
|
|
57
|
More: http://api.jquery.com/jquery.extend/
|
58
|
*/
|
59
|
this.options = $.extend( {}, this._defaults, options );
|
60
|
|
61
|
// firebug console stub (avoids errors if firebug is not active)
|
62
|
if (typeof console === "undefined") {
|
63
|
console = {
|
64
|
log: function () {
|
65
|
}
|
66
|
};
|
67
|
}
|
68
|
|
69
|
/*
|
70
|
The "init" method is the starting point for all plugin logic.
|
71
|
Calling the init method here in the "Plugin" constructor function
|
72
|
allows us to store all methods (including the init method) in the
|
73
|
plugin's prototype. Storing methods required by the plugin in its
|
74
|
prototype lowers the memory footprint, as each instance of the
|
75
|
plugin does not need to duplicate all of the same methods. Rather,
|
76
|
each instance can inherit the methods from the constructor
|
77
|
function's prototype.
|
78
|
*/
|
79
|
this.init();
|
80
|
}
|
81
|
|
82
|
// Avoid Plugin.prototype conflicts
|
83
|
$.extend(Plugin.prototype, {
|
84
|
|
85
|
// Initialization logic
|
86
|
init: function () {
|
87
|
/*
|
88
|
Create additional methods below and call them via
|
89
|
"this.myFunction(arg1, arg2)", ie: "this.buildCache();".
|
90
|
|
91
|
Note, you can access the DOM node(s), plugin name, default
|
92
|
plugin options and custom plugin options for a each instance
|
93
|
of the plugin by using the variables "this.element",
|
94
|
"this._name", "this._defaults" and "this.options" created in
|
95
|
the "Plugin" constructor function (as shown in the buildCache
|
96
|
method below).
|
97
|
*/
|
98
|
this.isDataLoaded = false;
|
99
|
this.buildCache();
|
100
|
this.bindEvents();
|
101
|
},
|
102
|
|
103
|
// Remove plugin instance completely
|
104
|
destroy: function() {
|
105
|
/*
|
106
|
The destroy method unbinds all events for the specific instance
|
107
|
of the plugin, then removes all plugin data that was stored in
|
108
|
the plugin instance using jQuery's .removeData method.
|
109
|
|
110
|
Since we store data for each instance of the plugin in its
|
111
|
instantiating element using the $.data method (as explained
|
112
|
in the plugin wrapper below), we can call methods directly on
|
113
|
the instance outside of the plugin initalization, ie:
|
114
|
$('selector').data('plugin_myPluginName').someOtherFunction();
|
115
|
|
116
|
Consequently, the destroy method can be called using:
|
117
|
$('selector').data('plugin_myPluginName').destroy();
|
118
|
*/
|
119
|
this.unbindEvents();
|
120
|
this.$element.removeData();
|
121
|
},
|
122
|
|
123
|
// Cache DOM nodes for performance
|
124
|
buildCache: function () {
|
125
|
/*
|
126
|
Create variable(s) that can be accessed by other plugin
|
127
|
functions. For example, "this.$element = $(this.element);"
|
128
|
will cache a jQuery reference to the elementthat initialized
|
129
|
the plugin. Cached variables can then be used in other methods.
|
130
|
*/
|
131
|
|
132
|
this.$element = $(this.element);
|
133
|
|
134
|
this.taxonUuid = this.$element.attr('data-cdm-taxon-uuid');
|
135
|
this.rankLimitUuid = this.$element.attr('data-rank-limit-uuid');
|
136
|
if(this.rankLimitUuid == '0'){
|
137
|
// '0' is used in the cdm_dataportal settings as value for 'no rank limit'
|
138
|
this.rankLimitUuid = undefined;
|
139
|
}
|
140
|
if(this.$element.hasClass('classification-chooser')){
|
141
|
this.classificationChooser = true;
|
142
|
}
|
143
|
this.destinationUri = this.$element.attr('data-destination-uri');
|
144
|
|
145
|
this.classificationMode = this.$element.attr('data-cdm-classification-mode');
|
146
|
|
147
|
if (this.$element.attr('data-cdm-align-with') == 'prev') {
|
148
|
var prev = this.$element.prev();
|
149
|
this.alignOffset = {
|
150
|
'padding': prev.width(),
|
151
|
'left' : prev.width()
|
152
|
}
|
153
|
} else {
|
154
|
this.alignOffset = {
|
155
|
'padding': this.$element.width(),
|
156
|
'left' : '0'
|
157
|
}
|
158
|
}
|
159
|
|
160
|
// Create new elements
|
161
|
this.container = $('<div class="' + this._name + ' box-shadow-b-5-1"></div>')
|
162
|
.css('background-color', 'rgba(255,255,255,0.7)')
|
163
|
.css('position', 'absolute')
|
164
|
.css('overflow', 'auto');
|
165
|
this.children = $('<div class="children"></div>');
|
166
|
|
167
|
this.loading = $('<i class="fa-spinner fa-2x" />')
|
168
|
.css('position', 'absolute')
|
169
|
.hide();
|
170
|
|
171
|
this.container.append(this.children).append(this.loading);
|
172
|
},
|
173
|
|
174
|
// Bind events that trigger methods
|
175
|
bindEvents: function() {
|
176
|
var plugin = this;
|
177
|
|
178
|
/*
|
179
|
Bind event(s) to handlers that trigger other functions, ie:
|
180
|
"plugin.$element.on('click', function() {});". Note the use of
|
181
|
the cached variable we created in the buildCache method.
|
182
|
|
183
|
All events are namespaced, ie:
|
184
|
".on('click'+'.'+this._name', function() {});".
|
185
|
This allows us to unbind plugin-specific events using the
|
186
|
unbindEvents method below.
|
187
|
|
188
|
this works at earliest with v1.7, with 1.4.4 we need to use bind:
|
189
|
*/
|
190
|
plugin.$element.bind('mouseenter', function() { // 'mouseenter' or 'click' are appropriate candidates
|
191
|
plugin.showChildren.call(plugin);
|
192
|
});
|
193
|
|
194
|
plugin.$element.bind('click', function (event){
|
195
|
if(event.target == this){
|
196
|
// prevents eg from executing clicks if the
|
197
|
// trigger element is an <a href=""> element
|
198
|
event.preventDefault();
|
199
|
}
|
200
|
event.stopPropagation();
|
201
|
plugin.showChildren.call(plugin);
|
202
|
});
|
203
|
|
204
|
plugin.container.mouseleave(function (){
|
205
|
plugin.hideChildren.call(plugin);
|
206
|
});
|
207
|
|
208
|
$(document).click(function (){
|
209
|
plugin.hideChildren.call(plugin);
|
210
|
});
|
211
|
|
212
|
/*
|
213
|
plugin.$element.children('i.fa').hover(
|
214
|
function(){
|
215
|
this.addClass(this.options.hoverClass);
|
216
|
},
|
217
|
function(){
|
218
|
this.removeClass(this.options.hoverClass);
|
219
|
}
|
220
|
);
|
221
|
*/
|
222
|
},
|
223
|
|
224
|
// Unbind events that trigger methods
|
225
|
unbindEvents: function() {
|
226
|
/*
|
227
|
Unbind all events in our plugin's namespace that are attached
|
228
|
to "this.$element".
|
229
|
|
230
|
this works at earliest with v1.7, with 1.4.4 we need to unbind without
|
231
|
namespace specificity
|
232
|
*/
|
233
|
this.$element.unbind('click');
|
234
|
// TODO complete this ...
|
235
|
},
|
236
|
|
237
|
log: function (msg) {
|
238
|
console.log('[' + this._name + '] ' + msg);
|
239
|
},
|
240
|
|
241
|
showChildren: function(){
|
242
|
|
243
|
var plugin = this;
|
244
|
|
245
|
var trigger_position = this.$element.position();
|
246
|
|
247
|
this.log('trigger_position: ' + trigger_position.top + ', ' + trigger_position.left);
|
248
|
|
249
|
this.$element.addClass(this.options.activeClass);
|
250
|
|
251
|
this.$element.append(this.container);
|
252
|
|
253
|
this.baseHeight = this.$element.parent().height();
|
254
|
this.lineHeight = this.$element.parent().css('line-height').replace('px', ''); // TODO use regex fur replace
|
255
|
|
256
|
this.log('baseHeight: ' + this.baseHeight);
|
257
|
this.log('lineHeight: ' + this.lineHeight);
|
258
|
|
259
|
this.offset_container_top = this.lineHeight - trigger_position.top + 1;
|
260
|
|
261
|
this.container
|
262
|
.css('top', - this.offset_container_top + 'px')
|
263
|
.css('left', (trigger_position.left - this.alignOffset.left) + 'px')
|
264
|
.css('padding-left', this.alignOffset.padding + 'px')
|
265
|
.css('padding-right', this.alignOffset.padding + 'px')
|
266
|
.css('z-index', 10)
|
267
|
.show();
|
268
|
|
269
|
if(!this.isDataLoaded){
|
270
|
$.get(this.requestURI(undefined, undefined), function(html){
|
271
|
plugin.handleDataLoaded(html);
|
272
|
});
|
273
|
} else {
|
274
|
this.adjustHeight();
|
275
|
this.scrollToSelected();
|
276
|
}
|
277
|
},
|
278
|
|
279
|
hideChildren: function(){
|
280
|
//return; // uncomment for debugging
|
281
|
this.container
|
282
|
.detach();
|
283
|
},
|
284
|
|
285
|
handleDataLoaded: function(html){
|
286
|
|
287
|
this.loading.hide();
|
288
|
this.isDataLoaded = true;
|
289
|
var listContainer = $(html);
|
290
|
if(listContainer[0].tagName != 'UL'){
|
291
|
// unwrap from potential enclosing div, this is
|
292
|
// necessary in case of compose_classification_selector
|
293
|
listContainer = listContainer.children('ul');
|
294
|
}
|
295
|
this.children.append(listContainer);
|
296
|
this.itemsCount = listContainer.children().length;
|
297
|
|
298
|
this.adjustHeight();
|
299
|
this.scrollToSelected();
|
300
|
},
|
301
|
|
302
|
calculateViewPortRows: function() {
|
303
|
|
304
|
var max;
|
305
|
if(this.options.viewPortRows.max) {
|
306
|
max = this.options.viewPortRows.max;
|
307
|
} else {
|
308
|
// no absolute maximum defined: calculate the current max based on the window viewport
|
309
|
max = Math.floor( ($(window).height() - this.element.getBoundingClientRect().top) / this.lineHeight) - 2;
|
310
|
this.log('max: ' + max);
|
311
|
}
|
312
|
var rows = Math.max(this.itemsCount, this.options.viewPortRows.min);
|
313
|
rows = Math.min(rows, max);
|
314
|
this.log('rows: ' + max);
|
315
|
return rows;
|
316
|
},
|
317
|
|
318
|
adjustHeight: function(){
|
319
|
|
320
|
var viewPortRows = this.calculateViewPortRows(this.itemsCount); //(itemsCount > this.options.viewPortRows.min ? this.options.viewPortRows.max : this.options.viewPortRows.min);
|
321
|
this.log('itemsCount: ' + this.itemsCount + ' => viewPortRows: ' + viewPortRows);
|
322
|
|
323
|
this.container.css('height', viewPortRows * this.lineHeight + 'px');
|
324
|
this.children
|
325
|
.css('padding-top', this.lineHeight + 'px') // one row above current
|
326
|
.css('padding-bottom', (viewPortRows - 2) * this.lineHeight + 'px'); // subtract 2 lines (current + one above)
|
327
|
},
|
328
|
|
329
|
scrollToSelected: function () {
|
330
|
|
331
|
var scrollTarget = this.children.find(".focused");
|
332
|
if(scrollTarget){
|
333
|
var scroll_target_offset_top = scrollTarget.position().top;
|
334
|
this.log("scroll_target_offset_top: " + scroll_target_offset_top + ", offset_container_top: " + this.offset_container_top);
|
335
|
this.container.scrollTop(scroll_target_offset_top - this.lineHeight + 1); // +1 yields a better result
|
336
|
}
|
337
|
},
|
338
|
|
339
|
requestURI: function(pageIndex, pageSize){
|
340
|
|
341
|
var contentRequest;
|
342
|
var renderFunction;
|
343
|
var proxyRequestQuery= '';
|
344
|
|
345
|
// pageIndex, pageSize are not yet used, prepared for future though
|
346
|
if(!pageIndex){
|
347
|
pageIndex = 0;
|
348
|
}
|
349
|
if(!pageSize) {
|
350
|
pageSize = 100;
|
351
|
}
|
352
|
|
353
|
if(this.classificationChooser){
|
354
|
renderFunction = this.options.renderFunction.classifications + '?destination=' + this.destinationUri;
|
355
|
contentRequest = 'NULL'; // using the plain compose function which does not require any data to be passes as parameter
|
356
|
|
357
|
} else {
|
358
|
renderFunction = this.options.renderFunction.taxonNodes;
|
359
|
proxyRequestQuery = '?currentTaxon=' + this.taxonUuid;
|
360
|
if(this.taxonUuid) {
|
361
|
if(this.classificationMode == 'siblings') {
|
362
|
contentRequest =
|
363
|
this.options.cdmWebappBaseUri
|
364
|
+ this.options.cdmWebappRequests.taxonSiblings
|
365
|
.replace('{classificationUuid}', this.options.classificationUuid)
|
366
|
.replace('{taxonUuid}', this.taxonUuid);
|
367
|
} else {
|
368
|
// default mode is 'children'
|
369
|
contentRequest =
|
370
|
this.options.cdmWebappBaseUri
|
371
|
+ this.options.cdmWebappRequests.taxonChildren
|
372
|
.replace('{classificationUuid}', this.options.classificationUuid)
|
373
|
.replace('{taxonUuid}', this.taxonUuid);
|
374
|
}
|
375
|
} else if(this.rankLimitUuid){
|
376
|
contentRequest =
|
377
|
this.options.cdmWebappBaseUri
|
378
|
+ this.options.cdmWebappRequests.childNodesAt
|
379
|
.replace('{classificationUuid}', this.options.classificationUuid)
|
380
|
.replace('{rankUuid}', this.rankLimitUuid);
|
381
|
} else {
|
382
|
contentRequest =
|
383
|
this.options.cdmWebappBaseUri
|
384
|
+ this.options.cdmWebappRequests.classificationRoot
|
385
|
.replace('{classificationUuid}', this.options.classificationUuid);
|
386
|
}
|
387
|
}
|
388
|
|
389
|
|
390
|
|
391
|
this.log("contentRequest: " + contentRequest);
|
392
|
|
393
|
var proxyRequest = this.options.proxyRequest
|
394
|
.replace('{contentRequest}', encodeURIComponent(encodeURIComponent(contentRequest)))
|
395
|
.replace('{renderFunction}', renderFunction);
|
396
|
|
397
|
var request = this.options.proxyBaseUri + '/' + proxyRequest + proxyRequestQuery;
|
398
|
this.log("finalRequest: " + request);
|
399
|
|
400
|
return request;
|
401
|
}
|
402
|
|
403
|
});
|
404
|
|
405
|
/*
|
406
|
Create a lightweight plugin wrapper around the "Plugin" constructor,
|
407
|
preventing against multiple instantiations.
|
408
|
|
409
|
More: http://learn.jquery.com/plugins/basic-plugin-creation/
|
410
|
*/
|
411
|
$.fn[pluginName] = function ( options ) {
|
412
|
this.each(function() {
|
413
|
if ( !$.data( this, "plugin_" + pluginName ) ) {
|
414
|
/*
|
415
|
Use "$.data" to save each instance of the plugin in case
|
416
|
the user wants to modify it. Using "$.data" in this way
|
417
|
ensures the data is removed when the DOM element(s) are
|
418
|
removed via jQuery methods, as well as when the userleaves
|
419
|
the page. It's a smart way to prevent memory leaks.
|
420
|
|
421
|
More: http://api.jquery.com/jquery.data/
|
422
|
*/
|
423
|
$.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
|
424
|
}
|
425
|
});
|
426
|
/*
|
427
|
"return this;" returns the original jQuery object. This allows
|
428
|
additional jQuery methods to be chained.
|
429
|
*/
|
430
|
return this;
|
431
|
};
|
432
|
|
433
|
|
434
|
$.fn[pluginName].defaults = {
|
435
|
hoverClass: undefined,
|
436
|
activeClass: undefined,
|
437
|
/**
|
438
|
* uuid of the current classification - required
|
439
|
*/
|
440
|
classificationUuid: undefined,
|
441
|
/**
|
442
|
* uuid of the current taxon - required
|
443
|
*/
|
444
|
taxonUuid: undefined,
|
445
|
cdmWebappBaseUri: undefined,
|
446
|
proxyBaseUri: undefined,
|
447
|
cdmWebappRequests: {
|
448
|
taxonChildren: "portal/classification/{classificationUuid}/childNodesOf/{taxonUuid}",
|
449
|
taxonSiblings: "portal/classification/{classificationUuid}/siblingsOf/{taxonUuid}",
|
450
|
childNodesAt: "portal/classification/{classificationUuid}/childNodesAt/{rankUuid}.json",
|
451
|
classificationRoot: "portal/classification/{classificationUuid}/childNodes.json"
|
452
|
},
|
453
|
proxyRequest: "cdm_api/proxy/{contentRequest}/{renderFunction}",
|
454
|
renderFunction: {
|
455
|
taxonNodes: "cdm_taxontree",
|
456
|
classifications: "classification_selector"
|
457
|
},
|
458
|
// viewPortRows: if max is 'undefined' the height will be adapted to the window viewport
|
459
|
viewPortRows: {min: 3, max: undefined}
|
460
|
};
|
461
|
|
462
|
})( jQuery, window, document );
|