Project

General

Profile

Download (13.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.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

    
138

    
139
      var nextLiElement = this.$element.parent('li').next();
140
      if(nextLiElement != undefined){
141
        this.subTaxonName = nextLiElement.children('a').text();
142
      }
143

    
144
      // Create new elements
145
      this.container = $('<div class="' + this._name + ' box-shadow-b-5-1"></div>')
146
        .css('background-color', 'rgba(255,255,255,0.7)')
147
        .css('position', 'absolute')
148
        .css('overflow', 'auto');
149
      this.children = $('<div class="children"></div>');
150

    
151
      this.loading = $('<i class="fa-spinner fa-2x" />')
152
        .css('position', 'absolute')
153
        .hide();
154

    
155
      this.container.append(this.children).append(this.loading);
156
    },
157

    
158
    // Bind events that trigger methods
159
    bindEvents: function() {
160
      var plugin = this;
161

    
162
      /*
163
       Bind event(s) to handlers that trigger other functions, ie:
164
       "plugin.$element.on('click', function() {});". Note the use of
165
       the cached variable we created in the buildCache method.
166

    
167
       All events are namespaced, ie:
168
       ".on('click'+'.'+this._name', function() {});".
169
       This allows us to unbind plugin-specific events using the
170
       unbindEvents method below.
171

    
172
       this works at earliest with v1.7, with 1.4.4 we need to use bind:
173
       */
174
      plugin.$element.bind('mouseenter', function() { // 'mouseenter' or 'click' are appropriate candidates
175
        /*
176
         Use the "call" method so that inside of the method being
177
         called, ie: "someOtherFunction", the "this" keyword refers
178
         to the plugin instance, not the event handler.
179

    
180
         More: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
181
         */
182
        plugin.showChildren.call(plugin); // TODO? why can't handleShowChildren(plugin) be used?
183
      });
184

    
185
      plugin.$element.children('i.fa').hover(
186
        function(){
187
          this.addClass(this.options.hoverClass);
188
        },
189
        function(){
190
          this.removeClass(this.options.hoverClass);
191
        }
192
      );
193

    
194
      plugin.container.mouseleave(function (){
195
        plugin.hideChildren.call(plugin);
196
      });
197
    },
198

    
199
    // Unbind events that trigger methods
200
    unbindEvents: function() {
201
      /*
202
       Unbind all events in our plugin's namespace that are attached
203
       to "this.$element".
204

    
205
       this works at earliest with v1.7, with 1.4.4 we need to unbind without
206
       namespace specifity
207
       */
208
      this.$element.unbind('click');
209
    },
210

    
211
    log: function (msg) {
212
      console.log('[' + this._name + '] ' + msg);
213
    },
214

    
215
    showChildren: function(){
216

    
217
      var plugin = this;
218
      var trigger_position =  this.$element.position();
219

    
220
      this.log('trigger_position: ' + trigger_position.top + ', ' + trigger_position.left);
221

    
222
      this.$element.addClass(this.options.activeClass);
223

    
224
      this.$element.append(this.container);
225

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

    
229
      this.log('baseHeight: ' + this.baseHeight);
230
      this.log('lineHeight: ' + this.lineHeight);
231

    
232
      this.offset_container_top = this.lineHeight - trigger_position.top;
233

    
234
      this.container
235
        .css('top', - this.offset_container_top + 'px')
236
        .css('left', trigger_position.left + 'px')
237
        .css('padding-left', this.$element.width() + 'px')
238
        .css('padding-right', this.$element.width() + 'px')
239
        .css('z-index', 10)
240
        .show();
241

    
242
      if(!this.isDataLoaded){
243
        $.get(this.requestURI(undefined, undefined), function(html){
244
          plugin.handleDataLoaded(html);
245
        });
246
      } else {
247
        this.adjustHeight();
248
        this.scrollToSelected();
249
      }
250
    },
251

    
252
    hideChildren: function(){
253
      // return;
254
      this.container
255
        .detach();
256
    },
257

    
258
    handleDataLoaded: function(html){
259

    
260
      this.loading.hide();
261
      this.isDataLoaded = true;
262
      var listContainer = $(html);
263
      this.children.append(listContainer);
264
      this.itemsCount = listContainer.children().length;
265

    
266
      this.adjustHeight();
267
      this.scrollToSelected();
268
    },
269

    
270
    calculateViewPortRows: function() {
271

    
272
      var max;
273
      if(this.options.viewPortRows.max) {
274
        max = this.options.viewPortRows.max;
275
      } else {
276
        // no absolute maximum defined: calculate the current max based on the window viewport
277
        max = Math.floor( ($(window).height() - this.element.getBoundingClientRect().top) / this.lineHeight) - 2;
278
        this.log('max: ' + max);
279
      }
280
      return (this.itemsCount > this.options.viewPortRows.min ? max : this.options.viewPortRows.min);
281
    },
282

    
283
    adjustHeight: function(itemsCount){
284

    
285
      var viewPortRows = this.calculateViewPortRows(itemsCount); //(itemsCount > this.options.viewPortRows.min ? this.options.viewPortRows.max : this.options.viewPortRows.min);
286
      this.log('itemsCount: ' + itemsCount + ' => viewPortRows: ' + viewPortRows);
287

    
288
      this.container.css('height', viewPortRows * this.lineHeight + 'px');
289
      this.children
290
        .css('padding-top', this.lineHeight + 'px') // one row above current
291
        .css('padding-bottom', (viewPortRows - 2) * this.lineHeight + 'px'); // subtract 2 lines (current + one above)
292
    },
293

    
294
    scrollToSelected: function () {
295

    
296
      if(this.subTaxonName){
297
        var scrollTarget = this.children
298
          .find("a:contains('" + this.subTaxonName + "')")
299
          .css('font-weight', 'bold');
300
        var scroll_target_offset_top = scrollTarget.position().top;
301
        this.log("scroll_target_offset_top: " + scroll_target_offset_top + ", offset_container_top: " + this.offset_container_top);
302
        this.container.scrollTop(scroll_target_offset_top - this.lineHeight);
303
      }
304
    },
305

    
306
    requestURI: function(pageIndex, pageSize){
307

    
308
      // pageIndex, pageSize are not yet used, prepared for future though
309
      var contentRequest;
310

    
311
      if(!pageIndex){
312
        pageIndex = 0;
313
      }
314
      if(!pageSize) {
315
        pageSize = 100;
316
      }
317

    
318
      if(this.taxonUuid){
319
        contentRequest =
320
          this.options.cdmWebappBaseUri
321
          + this.options.cdmWebappTaxonChildrenRequest
322
            .replace('{classificationUuid}', this.options.classificationUuid)
323
            .replace('{taxonUuid}', this.taxonUuid);
324

    
325
      } else if(this.rankLimitUuid){
326
        contentRequest =
327
          this.options.cdmWebappBaseUri
328
          + this.options.cdmWebappClassificationChildnodesAtRequest
329
            .replace('{classificationUuid}', this.options.classificationUuid)
330
            .replace('{rankUuid}', this.rankLimitUuid);
331
      } else {
332

    
333
      }
334

    
335
      this.log("contentRequest: " + contentRequest);
336

    
337
      var proxyRequest = this.options.proxyRequest
338
        .replace('{contentRequest}', encodeURIComponent(encodeURIComponent(contentRequest)))
339
        .replace('{renderFunction}', this.options.renderFunction);
340

    
341
      var request = this.options.proxyBaseUri + '/' + proxyRequest;
342
      this.log("finalRequest: " + request);
343

    
344
      return request;
345
    }
346

    
347
  });
348

    
349
  /*
350
   Create a lightweight plugin wrapper around the "Plugin" constructor,
351
   preventing against multiple instantiations.
352

    
353
   More: http://learn.jquery.com/plugins/basic-plugin-creation/
354
   */
355
  $.fn[pluginName] = function ( options ) {
356
    this.each(function() {
357
      if ( !$.data( this, "plugin_" + pluginName ) ) {
358
        /*
359
         Use "$.data" to save each instance of the plugin in case
360
         the user wants to modify it. Using "$.data" in this way
361
         ensures the data is removed when the DOM element(s) are
362
         removed via jQuery methods, as well as when the userleaves
363
         the page. It's a smart way to prevent memory leaks.
364

    
365
         More: http://api.jquery.com/jquery.data/
366
         */
367
        $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
368
      }
369
    });
370
    /*
371
     "return this;" returns the original jQuery object. This allows
372
     additional jQuery methods to be chained.
373
     */
374
    return this;
375
  };
376

    
377
  /*
378
   Attach the default plugin options directly to the plugin object. This
379
   allows users to override default plugin options globally, instead of
380
   passing the same option(s) every time the plugin is initialized.
381

    
382
   For example, the user could set the "property" value once for all
383
   instances of the plugin with
384
   "$.fn.pluginName.defaults.property = 'myValue';". Then, every time
385
   plugin is initialized, "property" will be set to "myValue".
386

    
387
   More: http://learn.jquery.com/plugins/advanced-plugin-concepts/
388
   */
389
  $.fn[pluginName].defaults = {
390
    hoverClass: undefined,
391
    activeClass: undefined,
392
    classificationUuid: undefined,
393
    cdmWebappBaseUri: undefined,
394
    proxyBaseUri: undefined,
395
    cdmWebappTaxonChildrenRequest: "portal/classification/{classificationUuid}/childNodesOf/{taxonUuid}",
396
    cdmWebappClassificationChildnodesAtRequest: "portal/classification/{classificationUuid}/childNodesAt/{rankUuid}.json",
397
    cdmWebappClassificationRootRequest: "portal/classification/{classificationUuid}/childNodes.json",
398
    proxyRequest: "cdm_api/proxy/{contentRequest}/{renderFunction}",
399
    renderFunction: "cdm_taxontree",
400
    viewPortRows: {min: 5, max: undefined}
401
  };
402

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