Project

General

Profile

Download (17.9 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_class_attr = 'fa fa-spinner fa-pulse';
168
      // used to preserve the class attributes of the icon
169
      this.icon_class_attr = null;
170

    
171
      this.container.append(this.children);
172

    
173

    
174
    },
175

    
176
    // Bind events that trigger methods
177
    bindEvents: function() {
178
      var plugin = this;
179

    
180
      /*
181
       Bind event(s) to handlers that trigger other functions, ie:
182
       "plugin.$element.on('click', function() {});". Note the use of
183
       the cached variable we created in the buildCache method.
184

    
185
       All events are namespaced, ie:
186
       ".on('click'+'.'+this._name', function() {});".
187
       This allows us to unbind plugin-specific events using the
188
       unbindEvents method below.
189

    
190
       this works at earliest with v1.7, with 1.4.4 we need to use bind:
191
       */
192
      plugin.$element.bind('mouseenter', function() { // 'mouseenter' or 'click' are appropriate candidates
193
        plugin.showChildren.call(plugin);
194
       });
195

    
196
      plugin.$element.bind('click', function (event){
197
        if(event.target == this){
198
          // prevents eg from executing clicks if the
199
          // trigger element is an <a href=""> element
200
          event.preventDefault();
201
        }
202
        event.stopPropagation();
203
        plugin.showChildren.call(plugin);
204
      });
205

    
206
      plugin.container.mouseleave(function (){
207
        plugin.hideChildren.call(plugin);
208
      });
209

    
210
      $(document).click(function (){
211
        plugin.hideChildren.call(plugin);
212
      });
213

    
214
      /*
215
      plugin.$element.children('i.fa').hover(
216
        function(){
217
          this.addClass(this.options.hoverClass);
218
        },
219
        function(){
220
          this.removeClass(this.options.hoverClass);
221
        }
222
      );
223
      */
224
    },
225

    
226
    // Unbind events that trigger methods
227
    unbindEvents: function() {
228
      /*
229
       Unbind all events in our plugin's namespace that are attached
230
       to "this.$element".
231

    
232
       this works at earliest with v1.7, with 1.4.4 we need to unbind without
233
       namespace specificity
234
       */
235
      this.$element.unbind('click');
236
      // TODO complete this ...
237
    },
238

    
239
    log: function (msg) {
240
      console.log('[' + this._name + '] ' + msg);
241
    },
242

    
243
    showChildren: function(){
244

    
245
      var plugin = this;
246

    
247
      var trigger_position =  this.$element.position();
248

    
249
      this.log('trigger_position: ' + trigger_position.top + ', ' + trigger_position.left);
250

    
251
      // Unused; TODO when re-enabling this needs to be fixed
252
      //         when using rotate, in IE and edge the child element are also rotated, need to reset child elements.
253
      // this.$element.addClass(this.options.activeClass);
254

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

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

    
264
      this.log('baseHeight: ' + this.baseHeight);
265
      this.log('lineHeight: ' + this.lineHeight);
266

    
267
      this.offset_container_top = this.lineHeight - trigger_position.top  + 1;
268

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

    
277
      if(!this.isDataLoaded){
278
        this.icon_class_attr = this.$element.prev('i').attr('class'),
279
        this.$element.prev('i').attr('class', this.loading_class_attr);
280
        $.get(this.requestURI(undefined, undefined), function(html){
281
          plugin.handleDataLoaded(html);
282
        });
283
      } else {
284
        if(this.container.find('ul').length > 0) {
285
          this.container.show();
286
          this.adjustHeightAndMaxWidth();
287
          this.scrollToSelected();
288
        }
289
      }
290
    },
291

    
292
    hideChildren: function(){
293
      //return; // uncomment for debugging
294
      this.container.slideUp();
295
      //this.container.detach();
296
    },
297

    
298
    handleDataLoaded: function(html){
299

    
300
      this.isDataLoaded = true;
301
      var listContainer = $(html);
302
      if(listContainer[0].tagName != 'UL') {
303
        // unwrap from potential enclosing div, this is
304
        // necessary in case of compose_classification_selector
305
        listContainer = listContainer.children('ul');
306
      }
307

    
308
      this.container.hide();
309

    
310
      if(listContainer.children().length > 0) {
311
        this.children.append(listContainer);
312
        this.itemsCount = listContainer.children().length;
313

    
314
        this.container.show();
315
        this.adjustHeightAndMaxWidth();
316
        this.scrollToSelected();
317

    
318
        // data loading may take quite long
319
        // need to check asynchronously if the
320
        // mouse still is hovering
321
        var this_plugin = this;
322
        setTimeout(function() {
323
          this_plugin.checkMouseOver();
324
        },
325
        300);
326

    
327
      }
328

    
329
      this.$element.prev('i').attr('class', this.icon_class_attr);
330
    },
331

    
332
    calculateViewPortRows: function() {
333

    
334
      var max;
335
      if(this.options.viewPortRows.max) {
336
        max = this.options.viewPortRows.max;
337
      } else {
338
        // no absolute maximum defined: calculate the current max based on the window viewport
339
        max = Math.floor( ($(window).height() - this.element.getBoundingClientRect().top) / this.lineHeight) - 2;
340
        this.log('max: ' + max);
341
      }
342
      var rows = Math.max(this.itemsCount, this.options.viewPortRows.min);
343
      rows = Math.min(rows, max);
344
      this.log('rows: ' + max);
345
      return rows;
346
    },
347

    
348
    adjustHeightAndMaxWidth: function(){
349

    
350
      // adjustHeightAndMaxWidth
351
      var viewPortRows = this.calculateViewPortRows(this.itemsCount); //(itemsCount > this.options.viewPortRows.min ? this.options.viewPortRows.max : this.options.viewPortRows.min);
352
      this.log('itemsCount: ' + this.itemsCount + ' => viewPortRows: ' + viewPortRows);
353

    
354
      this.container.css('height', viewPortRows * this.lineHeight + 'px');
355
      this.children
356
        .css('padding-top', this.lineHeight + 'px') // one row above current
357
        .css('padding-bottom', (viewPortRows - 2) * this.lineHeight + 'px'); // subtract 2 lines (current + one above)
358

    
359
      // adjust width to avoid the container hang out of the viewport
360
      max_width = Math.floor($(window).width() - this.element.getBoundingClientRect().left - 40);
361
      this.log('max_width: ' + max_width);
362
      this.container.css('max-width', max_width + 'px');
363
    },
364

    
365
    scrollToSelected: function () {
366

    
367
      // first reset the scroll position to 0 so that all calculation are using the same reference position
368
      this.container.scrollTop(0);
369
      var scrollTarget = this.children.find(".focused");
370
      if(scrollTarget && scrollTarget.length > 0){
371
        var position = scrollTarget.position();
372
        if(position == undefined){
373
          // fix for IE >= 9 and Edge
374
          position = scrollTarget.offset();
375
        }
376
        var scroll_target_offset_top = position.top;
377
        this.log("scroll_target_offset_top: " + scroll_target_offset_top + ", offset_container_top: " + this.offset_container_top);
378
        this.container.scrollTop(scroll_target_offset_top - this.lineHeight + 1); // +1 yields a better result
379
      }
380
    },
381

    
382
    requestURI: function(pageIndex, pageSize){
383

    
384
      var contentRequest;
385
      var renderFunction;
386
      var proxyRequestQuery= '';
387

    
388
      // pageIndex, pageSize are not yet used, prepared for future though
389
      if(!pageIndex){
390
        pageIndex = 0;
391
      }
392
      if(!pageSize) {
393
        pageSize = 100;
394
      }
395

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

    
400
      } else {
401
        renderFunction = this.options.renderFunction.taxonNodes;
402
        proxyRequestQuery = '?currentTaxon=' + this.taxonUuid;
403
        if(this.taxonUuid) {
404
          if(this.classificationMode == 'siblings') {
405
            contentRequest =
406
              this.options.cdmWebappBaseUri
407
              + this.options.cdmWebappRequests.taxonSiblings
408
                .replace('{classificationUuid}', this.options.classificationUuid)
409
                .replace('{taxonUuid}', this.taxonUuid);
410
          } else {
411
            // default mode is 'children'
412
            contentRequest =
413
              this.options.cdmWebappBaseUri
414
              + this.options.cdmWebappRequests.taxonChildren
415
                .replace('{classificationUuid}', this.options.classificationUuid)
416
                .replace('{taxonUuid}', this.taxonUuid);
417
          }
418
        } else if(this.rankLimitUuid){
419
          contentRequest =
420
            this.options.cdmWebappBaseUri
421
            + this.options.cdmWebappRequests.childNodesAt
422
              .replace('{classificationUuid}', this.options.classificationUuid)
423
              .replace('{rankUuid}', this.rankLimitUuid);
424
        } else {
425
          contentRequest =
426
            this.options.cdmWebappBaseUri
427
            + this.options.cdmWebappRequests.classificationRoot
428
              .replace('{classificationUuid}', this.options.classificationUuid);
429
        }
430
      }
431

    
432
      this.log("contentRequest: " + contentRequest);
433

    
434
      var proxyRequest = this.options.proxyRequest
435
        .replace('{contentRequest}', encodeURIComponent(encodeURIComponent(contentRequest)))
436
        .replace('{renderFunction}', renderFunction);
437

    
438
      var request = this.options.proxyBaseUri + '/' + proxyRequest + proxyRequestQuery;
439
      this.log("finalRequest: " + request);
440

    
441
      return request;
442
    },
443

    
444
    checkMouseOver: function(){
445
      // see http://stackoverflow.com/questions/6035137/jquery-check-hover-status-before-start-trigger/6035278#6035278
446
      //
447
      // this.container.find(':hover').length == 0
448
      // is(':hover')
449
      //this.log('>>>> hover: ' + this.container.find(':hover').length + ' | ' +  this.container.is(':hover') );
450
      if(!this.container.is(':hover')){
451
        this.hideChildren();
452
      }
453
    }
454

    
455
  });
456

    
457
  /*
458
   Create a lightweight plugin wrapper around the "Plugin" constructor,
459
   preventing against multiple instantiations.
460

    
461
   More: http://learn.jquery.com/plugins/basic-plugin-creation/
462
   */
463
  $.fn[pluginName] = function ( options ) {
464
    this.each(function() {
465
      if ( !$.data( this, "plugin_" + pluginName ) ) {
466
        /*
467
         Use "$.data" to save each instance of the plugin in case
468
         the user wants to modify it. Using "$.data" in this way
469
         ensures the data is removed when the DOM element(s) are
470
         removed via jQuery methods, as well as when the userleaves
471
         the page. It's a smart way to prevent memory leaks.
472

    
473
         More: http://api.jquery.com/jquery.data/
474
         */
475
        $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
476
      }
477
    });
478
    /*
479
     "return this;" returns the original jQuery object. This allows
480
     additional jQuery methods to be chained.
481
     */
482
    return this;
483
  };
484

    
485

    
486
  $.fn[pluginName].defaults = {
487
    hoverClass: undefined, // unused
488
    activeClass: undefined, // unused
489
    /**
490
     * uuid of the current classification - required
491
     */
492
    classificationUuid: undefined,
493
    /**
494
     * uuid of the current taxon - required
495
     */
496
    taxonUuid: undefined,
497
    cdmWebappBaseUri: undefined,
498
    proxyBaseUri: undefined,
499
    cdmWebappRequests: {
500
      taxonChildren: "portal/classification/{classificationUuid}/childNodesOf/{taxonUuid}",
501
      taxonSiblings: "portal/classification/{classificationUuid}/siblingsOf/{taxonUuid}",
502
      childNodesAt: "portal/classification/{classificationUuid}/childNodesAt/{rankUuid}.json",
503
      classificationRoot: "portal/classification/{classificationUuid}/childNodes.json"
504
    },
505
    proxyRequest: "cdm_api/proxy/{contentRequest}/{renderFunction}",
506
    renderFunction: {
507
      taxonNodes: "cdm_taxontree",
508
      classifications: "classification_selector"
509
    },
510
    // viewPortRows: if max is 'undefined' the height will be adapted to the window viewport
511
    viewPortRows: {min: 3, max: undefined}
512
  };
513

    
514
})( jQuery, window, document );
(9-9/16)