resources/js/treeview.js
/**
* webtrees: online genealogy
* Copyright (C) 2023 webtrees development team
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function TreeViewHandler (treeview_instance, ged) {
var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
this.treeview = $('#' + treeview_instance + '_in');
this.loadingImage = $('#' + treeview_instance + '_loading');
this.toolbox = $('#tv_tools');
this.buttons = $('.tv_button:first', this.toolbox);
this.zoom = 100; // in percent
this.boxWidth = 180; // default family box width
this.boxExpandedWidth = 250; // default expanded family box width
this.cookieDays = 3; // lifetime of preferences memory, in days
this.ajaxDetails = document.getElementById(treeview_instance + '_out').dataset.urlDetails + '&instance=' + encodeURIComponent(treeview_instance);
this.ajaxPersons = document.getElementById(treeview_instance + '_out').dataset.urlIndividuals + '&instance=' + encodeURIComponent(treeview_instance);
this.container = this.treeview.parent(); // Store the container element ("#" + treeview_instance + "_out")
this.auto_box_width = false;
this.updating = false;
// Restore user preferences
if (readCookie('compact') === 'true') {
tv.compact();
}
// Drag handlers for the treeview canvas
(function () {
let dragging = false;
let isDown = false;
let drag_start_x;
let drag_start_y;
tv.treeview.on('mousedown touchstart', function (event) {
let pageX = (event.type === 'touchstart') ? event.touches[0].pageX : event.pageX;
let pageY = (event.type === 'touchstart') ? event.touches[0].pageY : event.pageY;
drag_start_x = tv.treeview.offset().left - pageX;
drag_start_y = tv.treeview.offset().top - pageY;
isDown = true;
});
$(document).on('mousemove touchmove', function (event) {
if (isDown) {
event.preventDefault();
dragging = true;
let pageX = (event.type === 'touchmove') ? event.touches[0].pageX : event.pageX;
let pageY = (event.type === 'touchmove') ? event.touches[0].pageY : event.pageY;
tv.treeview.offset({
left: pageX + drag_start_x,
top: pageY + drag_start_y,
});
}
});
$(document).on('mouseup touchend', function (event) {
isDown = false;
if (dragging) {
event.preventDefault();
dragging = false;
tv.updateTree();
}
});
})();
// Add click handlers to buttons
tv.toolbox.find('#tvbCompact').each(function (index, tvCompact) {
tvCompact.onclick = function () {
tv.compact();
};
});
// If we click the "hide/show all partners" button, toggle the setting before reloading the page
tv.toolbox.find('#tvbAllPartners').each(function (index, tvAllPartners) {
tvAllPartners.onclick = function () {
createCookie('allPartners', readCookie('allPartners') === 'true' ? 'false' : 'true', tv.cookieDays);
document.location = document.location;
};
});
tv.toolbox.find('#tvbOpen').each(function (index, tvbOpen) {
var b = $(tvbOpen, tv.toolbox);
tvbOpen.onclick = function () {
b.addClass('tvPressed');
tv.setLoading();
var e = jQuery.Event('click');
tv.treeview.find('.tv_box:not(.boxExpanded)').each(function (index, box) {
var pos = $(box, tv.treeview).offset();
if (pos.left >= tv.leftMin && pos.left <= tv.leftMax && pos.top >= tv.topMin && pos.top <= tv.topMax) {
tv.expandBox(box, e);
}
});
b.removeClass('tvPressed');
tv.setComplete();
};
});
tv.toolbox.find('#tvbClose').each(function (index, tvbClose) {
var b = $(tvbClose, tv.toolbox);
tvbClose.onclick = function () {
b.addClass('tvPressed');
tv.setLoading();
tv.treeview.find('.tv_box.boxExpanded').each(function (index, box) {
$(box).css('display', 'none').removeClass('boxExpanded').parent().find('.tv_box.collapsedContent').css('display', 'block');
});
b.removeClass('tvPressed');
tv.setComplete();
};
});
tv.centerOnRoot(); // fire ajax update if needed, which call setComplete() when all is loaded
}
/**
* Class TreeView setLoading method
*/
TreeViewHandler.prototype.setLoading = function () {
this.treeview.css('cursor', 'wait');
this.loadingImage.css('display', 'block');
};
/**
* Class TreeView setComplete method
*/
TreeViewHandler.prototype.setComplete = function () {
this.treeview.css('cursor', 'move');
this.loadingImage.css('display', 'none');
};
/**
* Class TreeView getSize method
* Store the viewport current size
*/
TreeViewHandler.prototype.getSize = function () {
var tv = this;
// retrieve the current container bounding box
var container = tv.container.parent();
var offset = container.offset();
tv.leftMin = offset.left;
tv.leftMax = tv.leftMin + container.innerWidth();
tv.topMin = offset.top;
tv.topMax = tv.topMin + container.innerHeight();
/*
var frm = $("#tvTreeBorder");
tv.treeview.css("width", frm.width());
tv.treeview.css("height", frm.height()); */
};
/**
* Class TreeView updateTree method
* Perform ajax requests to complete the tree after drag
* param boolean @center center on root person when done
*/
TreeViewHandler.prototype.updateTree = function (center, button) {
var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
var to_load = [];
var elts = [];
this.getSize();
// check which td with datafld attribute are within the container bounding box
// and therefore need to be dynamically loaded
tv.treeview.find('td[abbr]').each(function (index, el) {
el = $(el, tv.treeview);
var pos = el.offset();
if (pos.left >= tv.leftMin && pos.left <= tv.leftMax && pos.top >= tv.topMin && pos.top <= tv.topMax) {
to_load.push(el.attr('abbr'));
elts.push(el);
}
});
// if some boxes need update, we perform an ajax request
if (to_load.length > 0) {
tv.updating = true;
tv.setLoading();
jQuery.ajax({
url: tv.ajaxPersons,
dataType: 'json',
data: 'q=' + to_load.join(';'),
success: function (ret) {
var nb = elts.length;
var root_element = $('.rootPerson', this.treeview);
var l = root_element.offset().left;
for (var i = 0; i < nb; i++) {
elts[i].removeAttr('abbr').html(ret[i]);
}
// we now adjust the draggable treeview size to its content size
tv.getSize();
},
complete: function () {
if (tv.treeview.find('td[abbr]').length) {
tv.updateTree(center, button); // recursive call
}
// the added boxes need that in mode compact boxes
if (tv.auto_box_width) {
tv.treeview.find('.tv_box').css('width', 'auto');
}
tv.updating = true; // avoid an unuseful recursive call when all requested persons are loaded
if (center) {
tv.centerOnRoot();
}
if (button) {
button.removeClass('tvPressed');
}
tv.setComplete();
tv.updating = false;
},
timeout: function () {
if (button) {
button.removeClass('tvPressed');
}
tv.updating = false;
tv.setComplete();
}
});
} else {
if (button) {
button.removeClass('tvPressed');
}
tv.setComplete();
}
return false;
};
/**
* Class TreeView compact method
*/
TreeViewHandler.prototype.compact = function () {
var tv = this;
var b = $('#tvbCompact', tv.toolbox);
tv.setLoading();
if (tv.auto_box_width) {
var w = tv.boxWidth * (tv.zoom / 100) + 'px';
var ew = tv.boxExpandedWidth * (tv.zoom / 100) + 'px';
tv.treeview.find('.tv_box:not(boxExpanded)', tv.treeview).css('width', w);
tv.treeview.find('.boxExpanded', tv.treeview).css('width', ew);
tv.auto_box_width = false;
if (readCookie('compact')) {
createCookie('compact', false, tv.cookieDays);
}
b.removeClass('tvPressed');
} else {
tv.treeview.find('.tv_box').css('width', 'auto');
tv.auto_box_width = true;
if (!readCookie('compact')) {
createCookie('compact', true, tv.cookieDays);
}
if (!tv.updating) {
tv.updateTree();
}
b.addClass('tvPressed');
}
tv.setComplete();
return false;
};
/**
* Class TreeView centerOnRoot method
*/
TreeViewHandler.prototype.centerOnRoot = function () {
this.loadingImage.css('display', 'block');
var tv = this;
var tvc = this.container;
var tvc_width = tvc.innerWidth() / 2;
if (Number.isNaN(tvc_width)) {
return false;
}
var tvc_height = tvc.innerHeight() / 2;
var root_person = $('.rootPerson', this.treeview);
if (!this.updating) {
tv.setComplete();
}
return false;
};
/**
* Class TreeView expandBox method
* Called ONLY for elements which have NOT the class tv_link to avoid un-useful requests to the server
* @param {string} box - the person box element
* @param {string} event - the call event
*/
TreeViewHandler.prototype.expandBox = function (box, event) {
var t = $(event.target);
if (t.hasClass('tv_link')) {
return false;
}
var box = $(box, this.treeview);
var bc = box.parent(); // bc is Box Container
var pid = box.attr('abbr');
var tv = this; // Store "this" for usage within jQuery functions where "this" is not this ;-)
var expanded;
var collapsed;
if (bc.hasClass('detailsLoaded')) {
collapsed = bc.find('.collapsedContent');
expanded = bc.find('.tv_box:not(.collapsedContent)');
} else {
// Cache the box content as an hidden person's box in the box's parent element
expanded = box;
collapsed = box.clone();
bc.append(collapsed.addClass('collapsedContent').css('display', 'none'));
// we add a waiting image at the right side of the box
var loading_image = this.loadingImage.find('img').clone().addClass('tv_box_loading').css('display', 'block');
box.prepend(loading_image);
tv.updating = true;
tv.setLoading();
// perform the Ajax request and load the result in the box
box.load(tv.ajaxDetails + '&pid=' + encodeURIComponent(pid), function () {
// If Lightbox module is active, we reinitialize it for the new links
if (typeof CB_Init === 'function') {
CB_Init();
}
box.css('width', tv.boxExpandedWidth * (tv.zoom / 100) + 'px');
loading_image.remove();
bc.addClass('detailsLoaded');
tv.setComplete();
tv.updating = false;
});
}
if (box.hasClass('boxExpanded')) {
expanded.css('display', 'none');
collapsed.css('display', 'block');
box.removeClass('boxExpanded');
} else {
expanded.css('display', 'block');
collapsed.css('display', 'none');
expanded.addClass('boxExpanded');
}
// we must adjust the draggable treeview size to its content size
this.getSize();
return false;
};
/**
* @param {string} name
* @param {string} value
* @param {number} days
*/
function createCookie (name, value, days) {
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = name + '=' + value + '; expires=' + date.toGMTString() + '; path=/';
} else {
document.cookie = name + '=' + value + '; path=/';
}
}
/**
* @param {string} name
* @returns {string|null}
*/
function readCookie (name) {
var name_equals = name + '=';
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1, c.length);
}
if (c.indexOf(name_equals) === 0) {
return c.substring(name_equals.length, c.length);
}
}
return null;
}