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