xref: /webtrees/resources/js/treeview.js (revision 2145828ba5a1541af8ce5bc9c55659f079a30406)
1/**
2 * webtrees: online genealogy
3 * Copyright (C) 2019 webtrees development team
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 * You should have received a copy of the GNU General Public License
13 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 */
15
16function TreeViewHandler (treeview_instance, ged) {
17  var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
18
19  this.treeview = $('#' + treeview_instance + '_in');
20  this.loadingImage = $('#' + treeview_instance + '_loading');
21  this.toolbox = $('#tv_tools');
22  this.buttons = $('.tv_button:first', this.toolbox);
23  this.zoom = 100; // in percent
24  this.boxWidth = 180; // default family box width
25  this.boxExpandedWidth = 250; // default expanded family box width
26  this.cookieDays = 3; // lifetime of preferences memory, in days
27  this.ajaxDetails = document.getElementById(treeview_instance + '_out').dataset.urlDetails + '&instance=' + encodeURIComponent(treeview_instance);
28  this.ajaxPersons = document.getElementById(treeview_instance + '_out').dataset.urlIndividuals + '&instance=' + encodeURIComponent(treeview_instance);
29
30  this.container = this.treeview.parent(); // Store the container element ("#" + treeview_instance + "_out")
31  this.auto_box_width = false;
32  this.updating = false;
33
34  // Restore user preferences
35  if (readCookie('compact') === 'true') {
36    tv.compact();
37  }
38
39  /// ////////////////////////////////////////////
40  // Based on https://codepen.io/chriscoyier/pen/zdsty
41  (function ($) {
42    $.fn.drags = function (opt) {
43      var $el = this;
44
45      return $el.css('cursor', 'move').on('mousedown', function (e) {
46        var $drag = $(this);
47        var drg_h = $drag.outerHeight();
48        var drg_w = $drag.outerWidth();
49        var pos_y = $drag.offset().top + drg_h - e.pageY;
50        var pos_x = $drag.offset().left + drg_w - e.pageX;
51
52        $drag.addClass('draggable');
53
54        $(document)
55          .on('mousemove', function (e) {
56            $('.draggable').offset({
57              top: e.pageY + pos_y - drg_h,
58              left: e.pageX + pos_x - drg_w
59            }).on('mouseup', function () {
60              $drag.removeClass('draggable');
61            });
62          }).on('mouseup', function () {
63            $drag.removeClass('draggable');
64            tv.updateTree();
65          });
66        e.preventDefault();
67      });
68    };
69  })(jQuery);
70
71  tv.treeview.drags();
72  /// ////////////////////////////////////////////
73
74  // Add click handlers to buttons
75  tv.toolbox.find('#tvbCompact').each(function (index, tvCompact) {
76    tvCompact.onclick = function () {
77      tv.compact();
78    };
79  });
80  // If we click the "hide/show all partners" button, toggle the setting before reloading the page
81  tv.toolbox.find('#tvbAllPartners').each(function (index, tvAllPartners) {
82    tvAllPartners.onclick = function () {
83      createCookie('allPartners', readCookie('allPartners') === 'true' ? 'false' : 'true', tv.cookieDays);
84      document.location = document.location;
85    };
86  });
87  tv.toolbox.find('#tvbOpen').each(function (index, tvbOpen) {
88    var b = $(tvbOpen, tv.toolbox);
89    tvbOpen.onclick = function () {
90      b.addClass('tvPressed');
91      tv.setLoading();
92      var e = jQuery.Event('click');
93      tv.treeview.find('.tv_box:not(.boxExpanded)').each(function (index, box) {
94        var pos = $(box, tv.treeview).offset();
95        if (pos.left >= tv.leftMin && pos.left <= tv.leftMax && pos.top >= tv.topMin && pos.top <= tv.topMax) {
96          tv.expandBox(box, e);
97        }
98      });
99      b.removeClass('tvPressed');
100      tv.setComplete();
101    };
102  });
103  tv.toolbox.find('#tvbClose').each(function (index, tvbClose) {
104    var b = $(tvbClose, tv.toolbox);
105    tvbClose.onclick = function () {
106      b.addClass('tvPressed');
107      tv.setLoading();
108      tv.treeview.find('.tv_box.boxExpanded').each(function (index, box) {
109        $(box).css('display', 'none').removeClass('boxExpanded').parent().find('.tv_box.collapsedContent').css('display', 'block');
110      });
111      b.removeClass('tvPressed');
112      tv.setComplete();
113    };
114  });
115
116  tv.centerOnRoot(); // fire ajax update if needed, which call setComplete() when all is loaded
117}
118/**
119 * Class TreeView setLoading method
120 */
121TreeViewHandler.prototype.setLoading = function () {
122  this.treeview.css('cursor', 'wait');
123  this.loadingImage.css('display', 'block');
124};
125/**
126 * Class TreeView setComplete  method
127 */
128TreeViewHandler.prototype.setComplete = function () {
129  this.treeview.css('cursor', 'move');
130  this.loadingImage.css('display', 'none');
131};
132
133/**
134 * Class TreeView getSize  method
135 * Store the viewport current size
136 */
137TreeViewHandler.prototype.getSize = function () {
138  var tv = this;
139  // retrieve the current container bounding box
140  var container = tv.container.parent();
141  var offset = container.offset();
142  tv.leftMin = offset.left;
143  tv.leftMax = tv.leftMin + container.innerWidth();
144  tv.topMin = offset.top;
145  tv.topMax = tv.topMin + container.innerHeight();
146  /*
147	 var frm = $("#tvTreeBorder");
148	 tv.treeview.css("width", frm.width());
149	 tv.treeview.css("height", frm.height()); */
150};
151
152/**
153 * Class TreeView updateTree  method
154 * Perform ajax requests to complete the tree after drag
155 * param boolean @center center on root person when done
156 */
157TreeViewHandler.prototype.updateTree = function (center, button) {
158  var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
159  var to_load = [];
160  var elts = [];
161  this.getSize();
162
163  // check which td with datafld attribute are within the container bounding box
164  // and therefore need to be dynamically loaded
165  tv.treeview.find('td[abbr]').each(function (index, el) {
166    el = $(el, tv.treeview);
167    var pos = el.offset();
168    if (pos.left >= tv.leftMin && pos.left <= tv.leftMax && pos.top >= tv.topMin && pos.top <= tv.topMax) {
169      to_load.push(el.attr('abbr'));
170      elts.push(el);
171    }
172  });
173  // if some boxes need update, we perform an ajax request
174  if (to_load.length > 0) {
175    tv.updating = true;
176    tv.setLoading();
177    jQuery.ajax({
178      url: tv.ajaxPersons,
179      dataType: 'json',
180      data: 'q=' + to_load.join(';'),
181      success: function (ret) {
182        var nb = elts.length;
183        var root_element = $('.rootPerson', this.treeview);
184        var l = root_element.offset().left;
185        for (var i = 0; i < nb; i++) {
186          elts[i].removeAttr('abbr').html(ret[i]);
187        }
188        // we now ajust the draggable treeview size to its content size
189        tv.getSize();
190      },
191      complete: function () {
192        if (tv.treeview.find('td[abbr]').length) {
193          tv.updateTree(center, button); // recursive call
194        }
195        // the added boxes need that in mode compact boxes
196        if (tv.auto_box_width) {
197          tv.treeview.find('.tv_box').css('width', 'auto');
198        }
199        tv.updating = true; // avoid an unuseful recursive call when all requested persons are loaded
200        if (center) {
201          tv.centerOnRoot();
202        }
203        if (button) {
204          button.removeClass('tvPressed');
205        }
206        tv.setComplete();
207        tv.updating = false;
208      },
209      timeout: function () {
210        if (button) {
211          button.removeClass('tvPressed');
212        }
213        tv.updating = false;
214        tv.setComplete();
215      }
216    });
217  } else {
218    if (button) {
219      button.removeClass('tvPressed');
220    }
221    tv.setComplete();
222  }
223  return false;
224};
225
226/**
227 * Class TreeView compact method
228 */
229TreeViewHandler.prototype.compact = function () {
230  var tv = this;
231  var b = $('#tvbCompact', tv.toolbox);
232  tv.setLoading();
233  if (tv.auto_box_width) {
234    var w = tv.boxWidth * (tv.zoom / 100) + 'px';
235    var ew = tv.boxExpandedWidth * (tv.zoom / 100) + 'px';
236    tv.treeview.find('.tv_box:not(boxExpanded)', tv.treeview).css('width', w);
237    tv.treeview.find('.boxExpanded', tv.treeview).css('width', ew);
238    tv.auto_box_width = false;
239    if (readCookie('compact')) {
240      createCookie('compact', false, tv.cookieDays);
241    }
242    b.removeClass('tvPressed');
243  } else {
244    tv.treeview.find('.tv_box').css('width', 'auto');
245    tv.auto_box_width = true;
246    if (!readCookie('compact')) {
247      createCookie('compact', true, tv.cookieDays);
248    }
249    if (!tv.updating) {
250      tv.updateTree();
251    }
252    b.addClass('tvPressed');
253  }
254  tv.setComplete();
255  return false;
256};
257
258/**
259 * Class TreeView centerOnRoot method
260 */
261TreeViewHandler.prototype.centerOnRoot = function () {
262  this.loadingImage.css('display', 'block');
263  var tv = this;
264  var tvc = this.container;
265  var tvc_width = tvc.innerWidth() / 2;
266  if (Number.isNaN(tvc_width)) {
267    return false;
268  }
269  var tvc_height = tvc.innerHeight() / 2;
270  var root_person = $('.rootPerson', this.treeview);
271
272  if (!this.updating) {
273    tv.setComplete();
274  }
275  return false;
276};
277
278/**
279 * Class TreeView expandBox method
280 * Called ONLY for elements which have NOT the class tv_link to avoid un-useful requests to the server
281 * @param {string} box   - the person box element
282 * @param {string} event - the call event
283 */
284TreeViewHandler.prototype.expandBox = function (box, event) {
285  var t = $(event.target);
286  if (t.hasClass('tv_link')) {
287    return false;
288  }
289
290  var box = $(box, this.treeview);
291  var bc = box.parent(); // bc is Box Container
292  var pid = box.attr('abbr');
293  var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
294  var expanded;
295  var collapsed;
296
297  if (bc.hasClass('detailsLoaded')) {
298    collapsed = bc.find('.collapsedContent');
299    expanded = bc.find('.tv_box:not(.collapsedContent)');
300  } else {
301    // Cache the box content as an hidden person's box in the box's parent element
302    expanded = box;
303    collapsed = box.clone();
304    bc.append(collapsed.addClass('collapsedContent').css('display', 'none'));
305    // we add a waiting image at the right side of the box
306    var loading_image = this.loadingImage.find('img').clone().addClass('tv_box_loading').css('display', 'block');
307    box.prepend(loading_image);
308    tv.updating = true;
309    tv.setLoading();
310    // perform the Ajax request and load the result in the box
311    box.load(tv.ajaxDetails + '&pid=' + encodeURIComponent(pid), function () {
312      // If Lightbox module is active, we reinitialize it for the new links
313      if (typeof CB_Init === 'function') {
314        CB_Init();
315      }
316      box.css('width', tv.boxExpandedWidth * (tv.zoom / 100) + 'px');
317      loading_image.remove();
318      bc.addClass('detailsLoaded');
319      tv.setComplete();
320      tv.updating = false;
321    });
322  }
323  if (box.hasClass('boxExpanded')) {
324    expanded.css('display', 'none');
325    collapsed.css('display', 'block');
326    box.removeClass('boxExpanded');
327  } else {
328    expanded.css('display', 'block');
329    collapsed.css('display', 'none');
330    expanded.addClass('boxExpanded');
331  }
332  // we must ajust the draggable treeview size to its content size
333  this.getSize();
334  return false;
335};
336
337/**
338 * @param {string} name
339 * @param {string} value
340 * @param {number} days
341 */
342function createCookie (name, value, days) {
343  if (days) {
344    var date = new Date();
345    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
346    document.cookie = name + '=' + value + '; expires=' + date.toGMTString() + '; path=/';
347  } else {
348    document.cookie = name + '=' + value + '; path=/';
349  }
350}
351
352/**
353 * @param   {string} name
354 * @returns {string|null}
355 */
356function readCookie (name) {
357  var name_equals = name + '=';
358  var ca = document.cookie.split(';');
359  for (var i = 0; i < ca.length; i++) {
360    var c = ca[i];
361    while (c.charAt(0) === ' ') {
362      c = c.substring(1, c.length);
363    }
364    if (c.indexOf(name_equals) === 0) {
365      return c.substring(name_equals.length, c.length);
366    }
367  }
368  return null;
369}
370