Project

General

Profile

Download (16.6 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.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
      // Unused; TODO when re-enabling this needs to be fixed
250
      //         when using rotate, in IE and edge the child element are also rotated, need to reset child elements.
251
      // this.$element.addClass(this.options.activeClass);
252

    
253
      this.container.hide();
254
      if(!this.container.parent() || this.container.parent().length == 0){
255
        // first time this container is used
256
        this.$element.append(this.container);
257
      }
258

    
259
      this.baseHeight = this.$element.parent().height();
260
      this.lineHeight = this.$element.parent().css('line-height').replace('px', ''); // TODO use regex fur replace
261

    
262
      this.log('baseHeight: ' + this.baseHeight);
263
      this.log('lineHeight: ' + this.lineHeight);
264

    
265
      this.offset_container_top = this.lineHeight - trigger_position.top  + 1;
266

    
267
      this.container
268
        .css('top', - this.offset_container_top + 'px')
269
        .css('left', (trigger_position.left - this.alignOffset.left) + 'px')
270
        .css('padding-left', this.alignOffset.padding + 'px')
271
        .css('padding-right', this.alignOffset.padding + 'px')
272
        .css('z-index', 10)
273
        .show();
274

    
275
      if(!this.isDataLoaded){
276
        $.get(this.requestURI(undefined, undefined), function(html){
277
          plugin.handleDataLoaded(html);
278
        });
279
      } else {
280
        this.container.slideDown();
281
        this.adjustHeight();
282
        this.scrollToSelected();
283
      }
284
    },
285

    
286
    hideChildren: function(){
287
      //return; // uncomment for debugging
288
      this.container.slideUp();
289
      //this.container.detach();
290
    },
291

    
292
    handleDataLoaded: function(html){
293

    
294
      this.loading.hide();
295
      this.isDataLoaded = true;
296
      var listContainer = $(html);
297
      if(listContainer[0].tagName != 'UL'){
298
        // unwrap from potential enclosing div, this is
299
        // necessary in case of compose_classification_selector
300
        listContainer = listContainer.children('ul');
301
      }
302
      this.container.hide();
303
      this.children.append(listContainer);
304
      this.itemsCount = listContainer.children().length;
305

    
306
      this.container.show();
307
      this.adjustHeight();
308
      this.scrollToSelected();
309
    },
310

    
311
    calculateViewPortRows: function() {
312

    
313
      var max;
314
      if(this.options.viewPortRows.max) {
315
        max = this.options.viewPortRows.max;
316
      } else {
317
        // no absolute maximum defined: calculate the current max based on the window viewport
318
        max = Math.floor( ($(window).height() - this.element.getBoundingClientRect().top) / this.lineHeight) - 2;
319
        this.log('max: ' + max);
320
      }
321
      var rows = Math.max(this.itemsCount, this.options.viewPortRows.min);
322
      rows = Math.min(rows, max);
323
      this.log('rows: ' + max);
324
      return rows;
325
    },
326

    
327
    adjustHeight: function(){
328

    
329
      var viewPortRows = this.calculateViewPortRows(this.itemsCount); //(itemsCount > this.options.viewPortRows.min ? this.options.viewPortRows.max : this.options.viewPortRows.min);
330
      this.log('itemsCount: ' + this.itemsCount + ' => viewPortRows: ' + viewPortRows);
331

    
332
      this.container.css('height', viewPortRows * this.lineHeight + 'px');
333
      this.children
334
        .css('padding-top', this.lineHeight + 'px') // one row above current
335
        .css('padding-bottom', (viewPortRows - 2) * this.lineHeight + 'px'); // subtract 2 lines (current + one above)
336
    },
337

    
338
    scrollToSelected: function () {
339

    
340
      // first reset the scroll position to 0 so that all calculation are using the same reference position
341
      this.container.scrollTop(0);
342
      var scrollTarget = this.children.find(".focused");
343
      if(scrollTarget && scrollTarget.length > 0){
344
        var position = scrollTarget.position();
345
        if(position == undefined){
346
          // fix for IE >= 9 and Edge
347
          position = scrollTarget.offset();
348
        }
349
        var scroll_target_offset_top = position.top;
350
        this.log("scroll_target_offset_top: " + scroll_target_offset_top + ", offset_container_top: " + this.offset_container_top);
351
        this.container.scrollTop(scroll_target_offset_top - this.lineHeight + 1); // +1 yields a better result
352
      }
353
    },
354

    
355
    requestURI: function(pageIndex, pageSize){
356

    
357
      var contentRequest;
358
      var renderFunction;
359
      var proxyRequestQuery= '';
360

    
361
      // pageIndex, pageSize are not yet used, prepared for future though
362
      if(!pageIndex){
363
        pageIndex = 0;
364
      }
365
      if(!pageSize) {
366
        pageSize = 100;
367
      }
368

    
369
      if(this.classificationChooser){
370
        renderFunction = this.options.renderFunction.classifications + '?destination=' + this.destinationUri;
371
        contentRequest = 'NULL'; // using the plain compose function which does not require any data to be passes as parameter
372

    
373
      } else {
374
        renderFunction = this.options.renderFunction.taxonNodes;
375
        proxyRequestQuery = '?currentTaxon=' + this.taxonUuid;
376
        if(this.taxonUuid) {
377
          if(this.classificationMode == 'siblings') {
378
            contentRequest =
379
              this.options.cdmWebappBaseUri
380
              + this.options.cdmWebappRequests.taxonSiblings
381
                .replace('{classificationUuid}', this.options.classificationUuid)
382
                .replace('{taxonUuid}', this.taxonUuid);
383
          } else {
384
            // default mode is 'children'
385
            contentRequest =
386
              this.options.cdmWebappBaseUri
387
              + this.options.cdmWebappRequests.taxonChildren
388
                .replace('{classificationUuid}', this.options.classificationUuid)
389
                .replace('{taxonUuid}', this.taxonUuid);
390
          }
391
        } else if(this.rankLimitUuid){
392
          contentRequest =
393
            this.options.cdmWebappBaseUri
394
            + this.options.cdmWebappRequests.childNodesAt
395
              .replace('{classificationUuid}', this.options.classificationUuid)
396
              .replace('{rankUuid}', this.rankLimitUuid);
397
        } else {
398
          contentRequest =
399
            this.options.cdmWebappBaseUri
400
            + this.options.cdmWebappRequests.classificationRoot
401
              .replace('{classificationUuid}', this.options.classificationUuid);
402
        }
403
      }
404

    
405
      this.log("contentRequest: " + contentRequest);
406

    
407
      var proxyRequest = this.options.proxyRequest
408
        .replace('{contentRequest}', encodeURIComponent(encodeURIComponent(contentRequest)))
409
        .replace('{renderFunction}', renderFunction);
410

    
411
      var request = this.options.proxyBaseUri + '/' + proxyRequest + proxyRequestQuery;
412
      this.log("finalRequest: " + request);
413

    
414
      return request;
415
    }
416

    
417
  });
418

    
419
  /*
420
   Create a lightweight plugin wrapper around the "Plugin" constructor,
421
   preventing against multiple instantiations.
422

    
423
   More: http://learn.jquery.com/plugins/basic-plugin-creation/
424
   */
425
  $.fn[pluginName] = function ( options ) {
426
    this.each(function() {
427
      if ( !$.data( this, "plugin_" + pluginName ) ) {
428
        /*
429
         Use "$.data" to save each instance of the plugin in case
430
         the user wants to modify it. Using "$.data" in this way
431
         ensures the data is removed when the DOM element(s) are
432
         removed via jQuery methods, as well as when the userleaves
433
         the page. It's a smart way to prevent memory leaks.
434

    
435
         More: http://api.jquery.com/jquery.data/
436
         */
437
        $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
438
      }
439
    });
440
    /*
441
     "return this;" returns the original jQuery object. This allows
442
     additional jQuery methods to be chained.
443
     */
444
    return this;
445
  };
446

    
447

    
448
  $.fn[pluginName].defaults = {
449
    hoverClass: undefined, // unused
450
    activeClass: undefined, // unused
451
    /**
452
     * uuid of the current classification - required
453
     */
454
    classificationUuid: undefined,
455
    /**
456
     * uuid of the current taxon - required
457
     */
458
    taxonUuid: undefined,
459
    cdmWebappBaseUri: undefined,
460
    proxyBaseUri: undefined,
461
    cdmWebappRequests: {
462
      taxonChildren: "portal/classification/{classificationUuid}/childNodesOf/{taxonUuid}",
463
      taxonSiblings: "portal/classification/{classificationUuid}/siblingsOf/{taxonUuid}",
464
      childNodesAt: "portal/classification/{classificationUuid}/childNodesAt/{rankUuid}.json",
465
      classificationRoot: "portal/classification/{classificationUuid}/childNodes.json"
466
    },
467
    proxyRequest: "cdm_api/proxy/{contentRequest}/{renderFunction}",
468
    renderFunction: {
469
      taxonNodes: "cdm_taxontree",
470
      classifications: "classification_selector"
471
    },
472
    // viewPortRows: if max is 'undefined' the height will be adapted to the window viewport
473
    viewPortRows: {min: 3, max: undefined}
474
  };
475

    
476
})( jQuery, window, document );
(8-8/15)