(function() { var merge = function() { var merged = {}; for (var i = 0; i < arguments.length; i++) { var source = arguments[i]; if (typeof source !== 'object') { continue; } // Each argument must be an object for (var key in source) { var value = source[key]; if (typeof value !== 'object') { // Simple value merged[key] = value; } else { // Value is an object if (typeof merged[key] === 'undefined') { merged[key] = deep_copy(value); } else { merged[key] = merge(merged[key], value); } } } } return merged; }; window['merge'] = merge; var deep_copy = function(source) { var copied = {}; if (typeof source !== 'object') { return copied; } for (var key in source) { var value = source[key]; if (typeof value === 'object') { value = deep_copy(value); } copied[key] = value; } return copied; }; var nextNodeID = (function() { var nodes = 0; return function() { return 'ft_node_' + nodes++; } })() window['FamilyTreeJS'] = { AUTHOR: 'Chandler Prall <chandler.prall@gmail.com>', VERSION: '.1', FamilyTree: function() { /** * FamilyTree private members */ var tree_element = null; var scroll_info = { 'scrolling':false, 'x1':0, 'x2':0, 'y1':0, 'y2':0 }; this.config = { compressable: true, node: { fontcolor: 'black', background: 'white', height: 30, width: 100, borderwidth: 1, bordercolor: 'black', spacingVertical: 40, spacingHorizontal: 15 }, line: { offsetY: 0, width: 2, color: 'random' } }; var people = []; // Holds all people in the family tree /** * Class Person */ var Person = function(identity, config, details) { this.node_id = nextNodeID(); this.identity = identity; this.details = (typeof details !== 'undefined') ? details : {}; this.parents = []; this.children = []; this.leveled = false; this.level = null; this.rendered = false; this.on_grid = false; this.starting_pos = null; this.connected = false; this.node = null; this.config = config; this.line_color = null; this.birth = function(identity, details) { var config = deep_copy(this.config); if (typeof details !== 'undefined' && typeof details.config !== 'undefined') { config = merge(config, details.config); } // Create new person var person = new Person(identity, deep_copy(config), details); // Add parent/child relatonship person.parents.push(this); this.children.push(person); if (typeof details !== 'undefined' && typeof details.partner !== 'undefined') { person.parents.push(details.partner); details.partner.children.push(person); } people.push(person); return person; }; this.Level = function(level) { this.level = level; this.leveled = true; for (var i = 0; i < this.parents.length; i++) { if (!this.parents[i].leveled) { this.parents[i].Level(level - 1); } } for (var i = 0; i < this.children.length; i++) { if (!this.children[i].leveled) { this.children[i].Level(level + 1); } } }; this.GetMaxNodeWidth = function() { var width = 0; var has_children = false; for (var i = 0; i < this.children.length; i++) { if (this.children[i].parents[0] === this) { has_children = true; width += this.children[i].GetMaxNodeWidth(); } } return width + ((has_children) ? 0 : 1); }; this.FillGrid = function(grid, starting_pos) { if (this.on_grid) { return grid; } if (typeof grid[this.level] === 'undefined') { grid[this.level] = []; }; this.on_grid = true; // Make sure our main parent is on the grid if (this.parents.length > 0) { if (typeof grid[this.level-1] === 'undefined') { grid[this.level-1] = []; } //grid = this.parents[0].FillGrid(grid, grid[this.level-1].length); grid = this.parents[0].FillGrid(grid, starting_pos); if (this.parents[0].starting_pos > starting_pos) { starting_pos = this.parents[0].starting_pos; } } while (typeof grid[this.level][starting_pos] !== 'undefined' && grid[this.level][starting_pos] !== null) { starting_pos++; } this.starting_pos = starting_pos; while (starting_pos > grid[this.level].length) { grid[this.level].push(null); } grid[this.level][starting_pos] = this; for (var i = 0; i < this.GetMaxNodeWidth() - 1; i++) { grid[this.level].push(null); } // Lists of partners & children var partners = []; var children = []; for (var i = 0; i < this.children.length; i++) { if (this === this.children[i].parents[0]) { children.push(this.children[i]); } for (var j = 0; j < this.children[i].parents.length; j++) { if (this !== this.children[i].parents[j]) { var found = false; for (var k = 0; k < partners.length; k++) { if (partners[k] === this.children[i].parents[j]) { found = true; } } if (!found) { partners.push(this.children[i].parents[j]); } } } } // Put the partners on our grid for (var i = 0; i < partners.length; i++) { grid = partners[i].FillGrid(grid, starting_pos + this.GetMaxNodeWidth()); } // Add our children var children_aggregate_size = 0; for (var i = 0; i < children.length; i++) { grid = children[i].FillGrid(grid, starting_pos + children_aggregate_size); children_aggregate_size += children[i].GetMaxNodeWidth(); } return grid; }; this.IsChildNo = function() { if (this.parents.length === 0) { return 0; } parent = this.parents[0]; for (var i = 0; i < parent.children.length; i++) { if (parent.children[i].node_id === this.node_id) { return i; } } }; this.DrawConnections = function(target) { this.connected = true; for (var i = 0; i < this.children.length; i++) { var child = this.children[i]; if (!child.connected) { for (var j = 0; j < child.parents.length; j++) { var parent = child.parents[j]; if (this.line_color === null) { switch (this.config.line.color) { case 'random': this.line_color = 'rgb(' + parseInt(Math.random() * 255) + ',' + parseInt(Math.random() * 255) + ',' + parseInt(Math.random() * 255) + ')'; break; case 'inherit': this.line_color = this.parents[0].line_color; this.config.line.color = this.line_color; default: this.line_color = this.config.line.color; } } if (child.parents.length > 0 || this !== parent) { if (child.parents.length > 1) { var other_parent = parent; } // Draw line down from me var shape = AutoshapeJS.createShape({ 'shape': 'Box', 'color': this.line_color, 'width': this.config.line.width + 'px', 'height': ((this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px', 'position': 'absolute', 'left': ((this.config.node.borderwidth * 2) + this.node.element.offsetLeft + (this.config.node.width / 2)) + 'px', 'top': ((this.config.node.borderwidth * 2) + this.node.element.offsetTop + this.config.node.height)+1 + 'px' }); shape.attachTo(target); if (typeof other_parent !== 'undefined') { // Draw line down from other parent var shape = AutoshapeJS.createShape({ 'shape': 'Box', 'color': this.line_color, 'width': this.config.line.width + 'px', 'height': (((this.level - other_parent.level)*this.config.node.spacingVertical) + ((this.level - other_parent.level) * (this.config.node.spacingVertical / 2))) + ((this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px', 'position': 'absolute', 'left': ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetLeft + (this.config.node.width / 2)) + 'px', 'top': ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetTop + this.config.node.height)+1 + 'px' }); shape.attachTo(target); // Draw line across to other parent var shape = AutoshapeJS.createShape({ 'shape': 'Box', 'color': this.line_color, 'width': (this.node.element.offsetLeft < other_parent.node.element.offsetLeft) ? ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetLeft - this.node.element.offsetLeft) + 'px' : ((this.config.node.borderwidth * 2) + this.node.element.offsetLeft - other_parent.node.element.offsetLeft) + 'px', 'height': this.config.line.width + 'px', 'position': 'absolute', 'left': (this.node.element.offsetLeft < other_parent.node.element.offsetLeft) ? ((this.config.node.borderwidth * 2) + this.node.element.offsetLeft + (this.config.node.width / 2)) + 'px' : ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetLeft + (this.config.node.width / 2)) + 'px', 'top': ((this.config.node.borderwidth * 2) + other_parent.node.element.offsetTop + this.config.node.height + (this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px' }); shape.attachTo(target); } // Draw line across to child var shape_width = child.node.element.offsetLeft - this.node.element.offsetLeft; if (shape_width < 0) shape_width *= -1; var shape_left = (this.node.element.offsetLeft < child.node.element.offsetLeft) ? this.node.element.offsetLeft + (this.config.node.borderwidth*2) + (this.config.node.width / 2) : child.node.element.offsetLeft + (this.config.node.borderwidth*2) + (this.config.node.width / 2) ; var shape = AutoshapeJS.createShape({ 'shape': 'Box', 'color': this.line_color, 'width': shape_width + 'px', 'height': this.config.line.width + 'px', 'position': 'absolute', 'left': shape_left + 'px', 'top': ((this.config.node.borderwidth * 2) + this.node.element.offsetTop + this.config.node.height + (this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px' }); shape.attachTo(target); // Draw line down to child var shape = AutoshapeJS.createShape({ 'shape': 'Box', 'color': this.line_color, 'width': this.config.line.width + 'px', 'height': (this.config.node.spacingVertical / 2) + 1 + this.config.node.height - (this.config.line.offsetY) + 'px', 'position': 'absolute', 'left': ((this.config.node.borderwidth * 2) + child.node.element.offsetLeft + (this.config.node.width / 2)) + 'px', 'top': ((this.config.node.borderwidth * 2) + child.node.element.offsetTop - (this.config.node.spacingVertical / 2) + this.config.line.offsetY) + 'px' }); shape.attachTo(target); child.DrawConnections(target); } } } } } }; /** * AddPerson function * * used to add a 'parentless' person to the family tree (no higher-level nodes) */ this.AddPerson = function(identity, details) { var config = deep_copy(this.config); if (typeof details !== 'undefined' && typeof details.config !== 'undefined') { config = merge(config, details.config); } var person = new Person(identity, config, details); people.push(person); return person; }; var MouseCoordinatesFromEvent = function(e) { var pos = {x:null, y:null}; if (e.pageX || e.pageY) { pos.x = e.pageX; pos.y = e.pageY; } else if (e.clientX || e.clientY) { pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; } return pos; }; /** * Render function * * renders the family tree */ this.Render = function(element) { tree_element = element; tree_element.style.position = 'relative'; tree_element.innerHTML = ''; // Generate all the people's levels so we know where they are at people[0].Level(0); min_level = 0; for (var i = 0; i < people.length; i++) { if (people[i].level < min_level) { min_level = people[i].level; } } if (min_level < 0) { var level_increase = min_level * -1 for (var i = 0; i < people.length; i++) { people[i].level += level_increase; } } var grid = {}; grid = people[0].FillGrid(grid, 0); // Compress the grid while_loop: while (true) { for (var i in grid) { level = grid[i]; for (var j = 1; j < level.length; j++) { node = level[j]; if (node !== null && (level[j-1] === null) && node.config.compressable === true) { // It's a candidate to move, nothing to it's left. Is anything around it may conflict with? // Are we already above our first parent? If so we shouldn't wander away if (node.parents.length) { if (grid[node.level-1] === null || typeof grid[node.level-1] !== 'undefined') { if (grid[node.level-1][j] !== null && grid[node.level-1][j] === node.parents[0]) { continue; } } } var clear_above = true; var clear_below = true; /* if (typeof grid[node.level-1] !== 'undefined') { if (grid[node.level-1][j-1] !== null && typeof grid[node.level-1][j-1] !== 'undefined' && grid[node.level-1][j-1].children.length > 0) { // We're blocked if the thing above isn't our parent clear_above = false; for (var k = 0; k < node.parents.length; k++) { if (grid[node.level-1][j-1] === node.parents[k]) { clear_above = true; } } } } */ if (typeof grid[node.level+1] !== 'undefined') { if (grid[node.level+1][j-1] !== null && typeof grid[node.level+1][j-1] !== 'undefined' && grid[node.level+1][j-1].parents.length > 0) { // We're blocked if the thing below isn't our child clear_below = false; for (var k = 0; k < node.children.length; k++) { if (grid[node.level+1][j-1] === node.children[k]) { clear_below = true; } } } } if (clear_above && clear_below) { grid[i][j-1] = node; grid[i][j] = null; continue while_loop; } } } } break; } var all_nodes = []; for (var level in grid) { var nodes = grid[level]; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (node !== null) { this.RenderNode(node, tree_element, level, i); all_nodes.push(node); } } } for (var node in all_nodes) { node = all_nodes[node]; node.DrawConnections(tree_element); } // Setup the drag ability tree_element.onselectstart = function() { return false; }; tree_element.unselectable = 'on'; tree_element.style.MozUserSelect = 'none'; tree_element.style.cursor = 'move'; tree_element.onmousedown = function(e) { scroll_info.scrolling = true; e = e || window.event; var mouse_coordinates = MouseCoordinatesFromEvent(e); scroll_info.x = mouse_coordinates.x; scroll_info.y = mouse_coordinates.y; }; tree_element.onmouseup = function(e) { if (scroll_info.scrolling) { scroll_info.scrolling = false; e = e || window.event; var mouse_coordinates = MouseCoordinatesFromEvent(e); if (scroll_info.x !== null && scroll_info.y !== null) { delta = { x: scroll_info.x - mouse_coordinates.x, y: scroll_info.y - mouse_coordinates.y } tree_element.scrollLeft += delta.x; tree_element.scrollTop += delta.y; } } }; tree_element.onmousemove = function(e) { if (scroll_info.scrolling) { e = e || window.event; var mouse_coordinates = MouseCoordinatesFromEvent(e); if (scroll_info.x !== null && scroll_info.y !== null) { delta = { x: scroll_info.x - mouse_coordinates.x, y: scroll_info.y - mouse_coordinates.y } tree_element.scrollLeft += delta.x; tree_element.scrollTop += delta.y; } scroll_info.x = mouse_coordinates.x; scroll_info.y = mouse_coordinates.y; } } tree_element.onmouseout = function(e) { e = e || window.event; // Check to see if we are, in fact, still in the tree_element var element = e.relatedTarget || e.toElement; if (typeof element !== 'undefined') { while (element !== null && element !== tree_element) { element = element.parentNode; } if (element !== tree_element) { scroll_info.scrolling = false; } } } }; this.RenderNode = function(person, element, level, position) { var node_left = position * (this.config.node.width + this.config.node.spacingHorizontal); var shape = AutoshapeJS.createShape({ 'shape': 'Box', 'color': person.config.node.background, 'borderwidth': person.config.node.borderwidth + 'px', 'bordercolor': person.config.node.bordercolor, 'width': this.config.node.width + 'px', 'height': this.config.node.height + 'px', 'position': 'absolute', 'left': node_left + 'px', 'top': (level * (this.config.node.height + this.config.node.spacingVertical)) + 'px', 'opacity': (person.identity.length > 0) ? 1 : 0 }); shape.attachTo(element); shape.element.innerHTML = person.identity; if (typeof person.details.blurb !== 'undefined') { shape.element.innerHTML += '<div class="blurb">' + person.details.blurb + '</div>'; } shape.element.style.color = person.config.node.fontcolor; shape.element.style.textAlign = 'center'; shape.element.className = 'familytree-node'; person.node = shape; }; this.center = function() { var view_center = { x: tree_element.clientWidth / 2, y: tree_element.clientHeight / 2 } if (arguments.length == 0) { // Center the whole tree var center = { x: tree_element.scrollWidth / 2, y: tree_element.scrollHeight / 2 } } else { // Center on a specific node var node = arguments[0]; var center = { x: node.node.element.offsetLeft + (node.config.node.width / 2), y: node.node.element.offsetTop + (node.config.node.height / 2), } } tree_element.scrollLeft = center.x - view_center.x; tree_element.scrollTop = center.y - view_center.y; } } }; })();