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