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