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.subTaxonName = undefined;
|
100
|
this.buildCache();
|
101
|
this.bindEvents();
|
102
|
},
|
103
|
|
104
|
// Remove plugin instance completely
|
105
|
destroy: function() {
|
106
|
/*
|
107
|
The destroy method unbinds all events for the specific instance
|
108
|
of the plugin, then removes all plugin data that was stored in
|
109
|
the plugin instance using jQuery's .removeData method.
|
110
|
|
111
|
Since we store data for each instance of the plugin in its
|
112
|
instantiating element using the $.data method (as explained
|
113
|
in the plugin wrapper below), we can call methods directly on
|
114
|
the instance outside of the plugin initalization, ie:
|
115
|
$('selector').data('plugin_myPluginName').someOtherFunction();
|
116
|
|
117
|
Consequently, the destroy method can be called using:
|
118
|
$('selector').data('plugin_myPluginName').destroy();
|
119
|
*/
|
120
|
this.unbindEvents();
|
121
|
this.$element.removeData();
|
122
|
},
|
123
|
|
124
|
// Cache DOM nodes for performance
|
125
|
buildCache: function () {
|
126
|
/*
|
127
|
Create variable(s) that can be accessed by other plugin
|
128
|
functions. For example, "this.$element = $(this.element);"
|
129
|
will cache a jQuery reference to the elementthat initialized
|
130
|
the plugin. Cached variables can then be used in other methods.
|
131
|
*/
|
132
|
|
133
|
this.$element = $(this.element);
|
134
|
|
135
|
this.taxonUuid = this.$element.attr('data-cdm-taxon-uuid');
|
136
|
this.rankLimitUuid = this.$element.attr('data-rank-limit-uuid');
|
137
|
if(this.rankLimitUuid == '0'){
|
138
|
// '0' is used in the cdm_dataportal settings as value for 'no rank limit'
|
139
|
this.rankLimitUuid = undefined;
|
140
|
}
|
141
|
|
142
|
|
143
|
var nextLiElement = this.$element.parent('li').next();
|
144
|
if(nextLiElement != undefined){
|
145
|
this.subTaxonName = nextLiElement.children('a').text();
|
146
|
}
|
147
|
|
148
|
// Create new elements
|
149
|
this.container = $('<div class="' + this._name + ' box-shadow-b-5-1"></div>')
|
150
|
.css('background-color', 'rgba(255,255,255,0.7)')
|
151
|
.css('position', 'absolute')
|
152
|
.css('overflow', 'auto');
|
153
|
this.children = $('<div class="children"></div>');
|
154
|
|
155
|
this.loading = $('<i class="fa-spinner fa-2x" />')
|
156
|
.css('position', 'absolute')
|
157
|
.hide();
|
158
|
|
159
|
this.container.append(this.children).append(this.loading);
|
160
|
},
|
161
|
|
162
|
// Bind events that trigger methods
|
163
|
bindEvents: function() {
|
164
|
var plugin = this;
|
165
|
|
166
|
/*
|
167
|
Bind event(s) to handlers that trigger other functions, ie:
|
168
|
"plugin.$element.on('click', function() {});". Note the use of
|
169
|
the cached variable we created in the buildCache method.
|
170
|
|
171
|
All events are namespaced, ie:
|
172
|
".on('click'+'.'+this._name', function() {});".
|
173
|
This allows us to unbind plugin-specific events using the
|
174
|
unbindEvents method below.
|
175
|
|
176
|
this works at earliest with v1.7, with 1.4.4 we need to use bind:
|
177
|
*/
|
178
|
plugin.$element.bind('mouseenter', function() { // 'mouseenter' or 'click' are appropriate candidates
|
179
|
/*
|
180
|
Use the "call" method so that inside of the method being
|
181
|
called, ie: "someOtherFunction", the "this" keyword refers
|
182
|
to the plugin instance, not the event handler.
|
183
|
|
184
|
More: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
|
185
|
*/
|
186
|
plugin.showChildren.call(plugin); // TODO? why can't handleShowChildren(plugin) be used?
|
187
|
});
|
188
|
|
189
|
plugin.$element.children('i.fa').hover(
|
190
|
function(){
|
191
|
this.addClass(this.options.hoverClass);
|
192
|
},
|
193
|
function(){
|
194
|
this.removeClass(this.options.hoverClass);
|
195
|
}
|
196
|
);
|
197
|
|
198
|
plugin.container.mouseleave(function (){
|
199
|
plugin.hideChildren.call(plugin);
|
200
|
});
|
201
|
},
|
202
|
|
203
|
// Unbind events that trigger methods
|
204
|
unbindEvents: function() {
|
205
|
/*
|
206
|
Unbind all events in our plugin's namespace that are attached
|
207
|
to "this.$element".
|
208
|
|
209
|
this works at earliest with v1.7, with 1.4.4 we need to unbind without
|
210
|
namespace specifity
|
211
|
*/
|
212
|
this.$element.unbind('click');
|
213
|
},
|
214
|
|
215
|
log: function (msg) {
|
216
|
console.log('[' + this._name + '] ' + msg);
|
217
|
},
|
218
|
|
219
|
showChildren: function(){
|
220
|
|
221
|
var plugin = this;
|
222
|
var trigger_position = this.$element.position();
|
223
|
|
224
|
this.log('trigger_position: ' + trigger_position.top + ', ' + trigger_position.left);
|
225
|
|
226
|
this.$element.addClass(this.options.activeClass);
|
227
|
|
228
|
this.$element.append(this.container);
|
229
|
|
230
|
this.baseHeight = this.$element.parent().height();
|
231
|
this.lineHeight = this.$element.parent().css('line-height').replace('px', ''); // TODO use regex fur replace
|
232
|
|
233
|
this.log('baseHeight: ' + this.baseHeight);
|
234
|
this.log('lineHeight: ' + this.lineHeight);
|
235
|
|
236
|
this.offset_container_top = this.lineHeight - trigger_position.top;
|
237
|
|
238
|
this.container
|
239
|
.css('top', - this.offset_container_top + 'px')
|
240
|
.css('left', trigger_position.left + 'px')
|
241
|
.css('padding-left', this.$element.width() + 'px')
|
242
|
.css('padding-right', this.$element.width() + 'px')
|
243
|
.css('z-index', 10)
|
244
|
.show();
|
245
|
|
246
|
if(!this.isDataLoaded){
|
247
|
$.get(this.requestURI(undefined, undefined), function(html){
|
248
|
plugin.handleDataLoaded(html);
|
249
|
});
|
250
|
} else {
|
251
|
this.adjustHeight();
|
252
|
this.scrollToSelected();
|
253
|
}
|
254
|
},
|
255
|
|
256
|
hideChildren: function(){
|
257
|
// return;
|
258
|
this.container
|
259
|
.detach();
|
260
|
},
|
261
|
|
262
|
handleDataLoaded: function(html){
|
263
|
|
264
|
this.loading.hide();
|
265
|
this.isDataLoaded = true;
|
266
|
var listContainer = $(html);
|
267
|
this.children.append(listContainer);
|
268
|
this.itemsCount = listContainer.children().length;
|
269
|
|
270
|
this.adjustHeight();
|
271
|
this.scrollToSelected();
|
272
|
},
|
273
|
|
274
|
calculateViewPortRows: function() {
|
275
|
|
276
|
var max;
|
277
|
if(this.options.viewPortRows.max) {
|
278
|
max = this.options.viewPortRows.max;
|
279
|
} else {
|
280
|
// no absolute maximum defined: calculate the current max based on the window viewport
|
281
|
max = Math.floor( ($(window).height() - this.element.getBoundingClientRect().top) / this.lineHeight) - 2;
|
282
|
this.log('max: ' + max);
|
283
|
}
|
284
|
return (this.itemsCount > this.options.viewPortRows.min ? max : this.options.viewPortRows.min);
|
285
|
},
|
286
|
|
287
|
adjustHeight: function(itemsCount){
|
288
|
|
289
|
var viewPortRows = this.calculateViewPortRows(itemsCount); //(itemsCount > this.options.viewPortRows.min ? this.options.viewPortRows.max : this.options.viewPortRows.min);
|
290
|
this.log('itemsCount: ' + itemsCount + ' => viewPortRows: ' + viewPortRows);
|
291
|
|
292
|
this.container.css('height', viewPortRows * this.lineHeight + 'px');
|
293
|
this.children
|
294
|
.css('padding-top', this.lineHeight + 'px') // one row above current
|
295
|
.css('padding-bottom', (viewPortRows - 2) * this.lineHeight + 'px'); // subtract 2 lines (current + one above)
|
296
|
},
|
297
|
|
298
|
scrollToSelected: function () {
|
299
|
|
300
|
if(this.subTaxonName){
|
301
|
var scrollTarget = this.children
|
302
|
.find("a:contains('" + this.subTaxonName + "')")
|
303
|
.css('font-weight', 'bold');
|
304
|
var scroll_target_offset_top = scrollTarget.position().top;
|
305
|
this.log("scroll_target_offset_top: " + scroll_target_offset_top + ", offset_container_top: " + this.offset_container_top);
|
306
|
this.container.scrollTop(scroll_target_offset_top - this.lineHeight);
|
307
|
}
|
308
|
},
|
309
|
|
310
|
requestURI: function(pageIndex, pageSize){
|
311
|
|
312
|
// pageIndex, pageSize are not yet used, prepared for future though
|
313
|
var contentRequest;
|
314
|
|
315
|
if(!pageIndex){
|
316
|
pageIndex = 0;
|
317
|
}
|
318
|
if(!pageSize) {
|
319
|
pageSize = 100;
|
320
|
}
|
321
|
|
322
|
if(this.taxonUuid){
|
323
|
contentRequest =
|
324
|
this.options.cdmWebappBaseUri
|
325
|
+ this.options.cdmWebappTaxonChildrenRequest
|
326
|
.replace('{classificationUuid}', this.options.classificationUuid)
|
327
|
.replace('{taxonUuid}', this.taxonUuid);
|
328
|
|
329
|
} else if(this.rankLimitUuid){
|
330
|
contentRequest =
|
331
|
this.options.cdmWebappBaseUri
|
332
|
+ this.options.cdmWebappClassificationChildnodesAtRequest
|
333
|
.replace('{classificationUuid}', this.options.classificationUuid)
|
334
|
.replace('{rankUuid}', this.rankLimitUuid);
|
335
|
} else {
|
336
|
contentRequest =
|
337
|
this.options.cdmWebappBaseUri
|
338
|
+ this.options.cdmWebappClassificationRootRequest
|
339
|
.replace('{classificationUuid}', this.options.classificationUuid);
|
340
|
}
|
341
|
|
342
|
this.log("contentRequest: " + contentRequest);
|
343
|
|
344
|
var proxyRequest = this.options.proxyRequest
|
345
|
.replace('{contentRequest}', encodeURIComponent(encodeURIComponent(contentRequest)))
|
346
|
.replace('{renderFunction}', this.options.renderFunction);
|
347
|
|
348
|
var request = this.options.proxyBaseUri + '/' + proxyRequest;
|
349
|
this.log("finalRequest: " + request);
|
350
|
|
351
|
return request;
|
352
|
}
|
353
|
|
354
|
});
|
355
|
|
356
|
/*
|
357
|
Create a lightweight plugin wrapper around the "Plugin" constructor,
|
358
|
preventing against multiple instantiations.
|
359
|
|
360
|
More: http://learn.jquery.com/plugins/basic-plugin-creation/
|
361
|
*/
|
362
|
$.fn[pluginName] = function ( options ) {
|
363
|
this.each(function() {
|
364
|
if ( !$.data( this, "plugin_" + pluginName ) ) {
|
365
|
/*
|
366
|
Use "$.data" to save each instance of the plugin in case
|
367
|
the user wants to modify it. Using "$.data" in this way
|
368
|
ensures the data is removed when the DOM element(s) are
|
369
|
removed via jQuery methods, as well as when the userleaves
|
370
|
the page. It's a smart way to prevent memory leaks.
|
371
|
|
372
|
More: http://api.jquery.com/jquery.data/
|
373
|
*/
|
374
|
$.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
|
375
|
}
|
376
|
});
|
377
|
/*
|
378
|
"return this;" returns the original jQuery object. This allows
|
379
|
additional jQuery methods to be chained.
|
380
|
*/
|
381
|
return this;
|
382
|
};
|
383
|
|
384
|
/*
|
385
|
Attach the default plugin options directly to the plugin object. This
|
386
|
allows users to override default plugin options globally, instead of
|
387
|
passing the same option(s) every time the plugin is initialized.
|
388
|
|
389
|
For example, the user could set the "property" value once for all
|
390
|
instances of the plugin with
|
391
|
"$.fn.pluginName.defaults.property = 'myValue';". Then, every time
|
392
|
plugin is initialized, "property" will be set to "myValue".
|
393
|
|
394
|
More: http://learn.jquery.com/plugins/advanced-plugin-concepts/
|
395
|
*/
|
396
|
$.fn[pluginName].defaults = {
|
397
|
hoverClass: undefined,
|
398
|
activeClass: undefined,
|
399
|
classificationUuid: undefined,
|
400
|
cdmWebappBaseUri: undefined,
|
401
|
proxyBaseUri: undefined,
|
402
|
cdmWebappTaxonChildrenRequest: "portal/classification/{classificationUuid}/childNodesOf/{taxonUuid}",
|
403
|
cdmWebappClassificationChildnodesAtRequest: "portal/classification/{classificationUuid}/childNodesAt/{rankUuid}.json",
|
404
|
cdmWebappClassificationRootRequest: "portal/classification/{classificationUuid}/childNodes.json",
|
405
|
proxyRequest: "cdm_api/proxy/{contentRequest}/{renderFunction}",
|
406
|
renderFunction: "cdm_taxontree",
|
407
|
// viewPortRows: if max is 'undefined' the height will be adapted to the window viewport
|
408
|
viewPortRows: {min: 3, max: undefined}
|
409
|
};
|
410
|
|
411
|
})( jQuery, window, document );
|