Project

General

Profile

Download (14.3 KB) Statistics
| Branch: | Tag: | Revision:
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 );
(8-8/15)