diff --git a/split/time-machine.css b/split/time-machine.css index b4a7caa..e52d618 100644 --- a/split/time-machine.css +++ b/split/time-machine.css @@ -464,3 +464,6 @@ a:hover { background-color: #8aa576; height: 7px; } +.element-wrapper { + display: inline-block; +} diff --git a/split/time-machine.js b/split/time-machine.js index 4a93bad..a69fbca 100644 --- a/split/time-machine.js +++ b/split/time-machine.js @@ -739,8 +739,8 @@ function finishedReceivingSources($status, context) { updateSplitViewHtml(); updateComboViewHtml(); makeTableHeadersDraggable(); + animateRows() fetchRelativeSources($status, context); - initPrevRowPositions(); } // Begin fetching the list of source descriptions for each relative, @@ -874,6 +874,7 @@ function makeMainHtml(context, changeLogMap, $mainTable) { $(document).keydown(handleMergeKeypress); initOptionsDisplay(); makeTableHeadersDraggable(); + animateRows(comboGrouper); } let columnPressed = false; @@ -1897,7 +1898,7 @@ function handleColumnClick(event, rowId) { } // =============== -// Map of groupId -> Grouper object that the groupId is found in. +// Map of groupId (and also personRowId and grouperId and tabId) -> Grouper object that the groupId or person row is found in. let grouperMap = {}; // Map of personRowId -> PersonRow object with that id let personRowMap = {}; @@ -1937,7 +1938,7 @@ class DisplayOptions { // Flag for whether to show summaries of 'keep' and 'split' just above the split group (if any). this.shouldShowSummaries = false; // Flag for whether to show hidden values in the summary rows in the combo view. (Does not affect split view). - this.shouldExpandHiddenValues = true; + this.shouldExpandHiddenValues = false; } } @@ -1987,7 +1988,11 @@ function handleOptionChange() { displayOptions.shouldRepeatInfoFromMerge = $("#merge-info-checkbox").prop("checked"); displayOptions.shouldShowAdditions = $("#additions-checkbox").prop("checked"); displayOptions.shouldShowDeletions = $("#deletions-checkbox").prop("checked"); + let prevVertical = displayOptions.vertical; displayOptions.vertical = $("#vertical-checkbox").prop("checked"); + if (prevVertical !== displayOptions.vertical) { + resetAnimation = true; + } displayOptions.shouldShowSummaries = updateIfVisible("summary-checkbox", displayOptions.shouldShowSummaries); displayOptions.shouldExpandHiddenValues = updateIfVisible("show-extra-values-checkbox", displayOptions.shouldExpandHiddenValues); updatePersonFactsDisplay(); @@ -2001,7 +2006,6 @@ function displayAvailableOptions() { setVisibility("settings", activeTab !== CHANGE_LOG_VIEW && activeTab !== SPLIT_VIEW); setVisibility("vertical-option", activeTab === FLAT_VIEW || activeTab === SOURCES_VIEW || activeTab === COMBO_VIEW || activeTab === HELP_VIEW); setVisibility("repeat-info-option", activeTab === MERGE_VIEW || activeTab === HELP_VIEW); - initPrevRowPositions(); } function setVisibility(elementId, isVisible) { @@ -2053,6 +2057,7 @@ class Grouper { grouperMap[mergeRow.id] = this; } grouperMap[this.id] = this; + grouperMap[tabId] = this; } sort(columnName) { @@ -2160,20 +2165,15 @@ class Grouper { } } - checkGroupOrder() { - // Make sure that the main person ID is always in the first group. - for (let g = 0; g < this.mergeGroups.length; g++) { - let mergeGroup = this.mergeGroups[g]; - for (let personRow of mergeGroup.personRows) { - if (personRow.personId === mainPersonId) { - if (g > 0) { - let mainGroup = this.mergeGroups.splice(g, 1)[0]; - this.mergeGroups.unshift(mainGroup); - } - return; + isMainPersonIdSelected() { + for (let group of this.mergeGroups) { + for (let mergeRow of group.personRows) { + if (mergeRow.personId === mainPersonId && mergeRow.isSelected) { + return true; } } } + return false; } } @@ -2613,6 +2613,18 @@ class PersonRow { return element.isVisible() || displayOptions.shouldExpandHiddenValues; } + // Wrap the contents of a table cell in a div with an id that can be used to animate the cell's contents. + // item: the object with an elementIndex that this cell is displaying, representing an element in the Split object. + // isKeep: true if this cell is displaying the 'keep' version of the element, false if it's displaying the 'split' version. + // contentHtml: the HTML content of the cell to display. + // isFacts: true if this cell is displaying facts for a spouse or child (which is not an independently-selectable element + // and thus doesn't have its own buttons or element id. These divs will get an id ending in ".facts") + function wrapElement(item, isKeep, contentHtml, isFacts) { + let cellId = getCellId(item.elementIndex, isKeep, isFacts); + return "
" + + (isFacts ? "" : getButtonsHtml(item, isKeep)) + contentHtml + "
"; + } + function getSummaryNamesHtml(person) { function getNameFormsHtml(name) { // Combine multiple name forms into a single string separated by "/", like "Kim Jeong-Un / 김정은 / 金正恩". @@ -2626,9 +2638,10 @@ class PersonRow { let namesHtml = ""; if (person.names && person.names.length > 0) { let namesHtmlList = []; - for (let name of person.names) { + for (let n = 0; n < person.names.length; n++) { + let name = person.names[n]; if (shouldDisplaySummaryElement(name)) { - namesHtmlList.push(getButtonsHtml(name, splitDirection === DIR_KEEP) + getNameFormsHtml(name)); + namesHtmlList.push(wrapElement(name, splitDirection === DIR_KEEP, getNameFormsHtml(name))); } } namesHtml += namesHtmlList.join("
"); @@ -2644,7 +2657,7 @@ class PersonRow { if (person.facts && person.facts.length > 0) { for (let fact of person.facts) { if (shouldDisplaySummaryElement(fact)) { - factsHtml += getButtonsHtml(fact, splitDirection === DIR_KEEP) + getFactHtml(fact, true, false) + "
"; + factsHtml += wrapElement(fact, splitDirection === DIR_KEEP, getFactHtml(fact, true, false)) + "
"; } } } @@ -2659,7 +2672,7 @@ class PersonRow { (usedColumns.has("father-name") && usedColumns.has("mother-name") ? " colspan='2'" : "") + ">"; for (let element of split.elements) { if (element.type === TYPE_PARENTS && (element.direction === direction || element.direction === DIR_COPY)) { - parentsHtml += getButtonsHtml(element, direction === DIR_KEEP) + getParentsHtml(element.item, encode(" & ")) + "
\n"; + parentsHtml += wrapElement(element, direction === DIR_KEEP, getParentsHtml(element.item, encode(" & "))) + "
\n"; } } return parentsHtml + "\n"; @@ -2689,18 +2702,33 @@ class PersonRow { isFirstSpouse = startNewRowIfNotFirst(isFirstSpouse, allRowsClass, noteCellHtmlHolder, this.isSummaryRow()); let familyBottomClass = spouseIndex === this.families.length - 1 ? bottomClass : ""; let childrenRowSpan = getRowspanParameter(displayOptions.shouldShowChildren ? spouseFamily.children.length : 1); - let spouseButtonsHtml = this.isSummaryRow() ? getButtonsHtml(spouseFamily.spouse.coupleRelationship, splitDirection === DIR_KEEP): ""; - addColumn("spouse-name", childrenRowSpan, spouseButtonsHtml + (spouseFamily.spouse ? spouseFamily.spouse.name : ""), familyBottomClass); - addColumn("spouse-facts", childrenRowSpan, spouseFamily.spouse ? spouseFamily.spouse.facts : "", familyBottomClass); + + let spouseNameHtml = spouseFamily.spouse ? spouseFamily.spouse.name : ""; + let spouseFactsHtml = spouseFamily.spouse ? spouseFamily.spouse.facts : ""; + if (this.isSummaryRow()) { + // Wrap the spouse name and facts in a div that can be animated. Also add buttons to the spouseNameHtml. + spouseNameHtml = wrapElement(spouseFamily.spouse.coupleRelationship, splitDirection === DIR_KEEP, spouseNameHtml); + spouseFactsHtml = wrapElement(spouseFamily.spouse.coupleRelationship, splitDirection === DIR_KEEP, spouseFactsHtml, true); + } + addColumn("spouse-name", childrenRowSpan, spouseNameHtml, familyBottomClass); + addColumn("spouse-facts", childrenRowSpan, spouseFactsHtml, familyBottomClass); + if (spouseFamily.children.length > 0 && displayOptions.shouldShowChildren) { let isFirstChild = true; for (let childIndex = 0; childIndex < spouseFamily.children.length; childIndex++) { let child = spouseFamily.children[childIndex]; isFirstChild = startNewRowIfNotFirst(isFirstChild, allRowsClass, noteCellHtmlHolder, this.isSummaryRow()); let childBottomClass = childIndex === spouseFamily.children.length - 1 ? familyBottomClass : ""; - let childButtonsHtml = this.isSummaryRow() ? getButtonsHtml(child.childAndParentsRelationship, splitDirection === DIR_KEEP): ""; - addColumn(COLUMN_CHILD_NAME, "", childButtonsHtml + child.name, childBottomClass); - addColumn(COLUMN_CHILD_FACTS, "", child.facts, childBottomClass); + + let childNameHtml = child.name; + let childFactsHtml = child.facts; + if (this.isSummaryRow()) { + // Wrap the child name and facts in a div that can be animated. Also add buttons to the childNameHtml. + childNameHtml = wrapElement(child.childAndParentsRelationship, splitDirection === DIR_KEEP, childNameHtml); + childFactsHtml = wrapElement(child.childAndParentsRelationship, splitDirection === DIR_KEEP, childFactsHtml, true); + } + addColumn(COLUMN_CHILD_NAME, "", childNameHtml, childBottomClass); + addColumn(COLUMN_CHILD_FACTS, "", childFactsHtml, childBottomClass); } } else { addColumn(COLUMN_CHILD_NAME, "", "", familyBottomClass); @@ -3083,25 +3111,38 @@ function findUsedColumns(personRows) { return usedColumns; } +function mainPersonIdSelected(grouperId) { + let grouper = grouperMap[grouperId]; + if (grouper.isMainPersonIdSelected()) { + alert("The main person Id (" + mainPersonId + ") must remain in the first group, so that it is above the 'person to keep' in the split summary."); + return true; + } + return false; +} + // Function called when the "add group" button is clicked in the Flat view. // If any rows are selected, they are moved to the new group. // If this is only the second group, then the first group begins displaying its header. function addGroup(grouperId) { + if (mainPersonIdSelected(grouperId)) { + return; + } let grouper = grouperMap[grouperId]; let mergeRows = grouper.removeSelectedRows(); let mergeGroup = new MergeGroup("Group " + (grouper.mergeGroups.length + 1), mergeRows, grouper); grouper.mergeGroups.push(mergeGroup); - grouper.checkGroupOrder(); updateFlatViewHtml(grouper); } function addSelectedToGroup(groupId) { + if (mainPersonIdSelected(groupId)) { + return; + } let grouper = grouperMap[groupId]; let mergeGroup = grouper.findGroup(groupId); if (mergeGroup) { let selectedRows = grouper.removeSelectedRows(); mergeGroup.personRows.push(...selectedRows); - grouper.checkGroupOrder(); updateFlatViewHtml(grouper); } } @@ -3115,47 +3156,120 @@ function updateTabsHtml() { } // Map of rowId -> {x, y} of where that row was last time. -let prevRowPositionsMap = new Map(); +let prevPositionMap = new Map(); +let resetAnimation = false; function initPrevRowPositions() { - animateRows(flatGrouper, true); - animateRows(sourceGrouper, true); - animateRows(comboGrouper, true); + switch (getCurrentTab()) { + case FLAT_VIEW: animateRows(flatGrouper, true); break; + case SOURCES_VIEW: animateRows(sourceGrouper, true); break; + case COMBO_VIEW: animateRows(comboGrouper, true); break; + } } -function animateRows(grouper, isInitialization) { - if (!grouper) { +function notSamePosition(a, b) { + return Math.abs(a.left - b.left) > .5 || Math.abs(a.top - b.top) > .5; +} + +const animationSpeed = 500; +function animateRows(grouper) { + if (!grouper || grouper !== grouperMap[getCurrentTab()]) { return; } - for (let group of grouper.mergeGroups) { - console.log("--- Grouper " + grouper.tabId); + if (!displayOptions.vertical) { + for (let group of grouper.mergeGroups) { + let newPositionMap = new Map(); + for (let personRow of group.personRows) { + let rowId = getRowId(grouper.tabId, personRow.id); + let $row = $("#" + rowId); + newPositionMap.set(rowId, {"left": $row.offset().left, "top": $row.offset().top}); + } + for (let personRow of group.personRows) { + let rowId = getRowId(grouper.tabId, personRow.id); + let $row = $("#" + rowId); + let prevPosition = prevPositionMap.get(rowId); + let newPosition = newPositionMap.get(rowId); + prevPositionMap.set(rowId, newPosition); + if (!resetAnimation && prevPosition && (prevPosition.left || prevPosition.top) && (newPosition.left || newPosition.top) && notSamePosition(prevPosition, newPosition)) { + // animate row's position from prevPosition to new position + $row.css({ + "position": "relative", + "left": prevPosition.left - newPosition.left, + "top": prevPosition.top - newPosition.top, + "width": "100%" + }); + $row.animate({"left": 0, "top": 0}, animationSpeed); + } + } + } + } + // Animate cells of summary elements + if (displayOptions.shouldShowSummaries && grouper.splitGroupId) { + function animateSummaryCell(cellId, prevPosition, newPosition, otherPosition) { + if (!resetAnimation && newPosition && (newPosition.left || newPosition.top)) { + if (otherPosition && !prevPosition) { + // If going from "Keep" to "Copy" or "Split", then animate from the old keep to the new split position + // (whether moving or copying). And vice versa. + prevPosition = otherPosition; + } + let $cell = $("#" + cellId); + if ($cell && prevPosition && (prevPosition.left || prevPosition.top)) { + $cell.css({"position": "relative", "left": prevPosition.left - newPosition.left, "top": prevPosition.top - newPosition.top, "width": "100%"}); + $cell.animate({"left": 0, "top": 0}, animationSpeed); + } + } + } + + function animateSummaryElement(element, doRelativeFacts) { + let keepCellId = getCellId(element.elementIndex, true, doRelativeFacts); + let splitCellId = getCellId(element.elementIndex, false, doRelativeFacts); + let prevKeepPos = prevPositionMap.get(keepCellId); + let prevSplitPos = prevPositionMap.get(splitCellId); + let newKeepPos = newPositionMap.get(keepCellId); + let newSplitPos = newPositionMap.get(splitCellId); + if (keepCellId === "combo-view_element-6_keep") { + console.log(keepCellId + ": " + JSON.stringify(prevKeepPos) + " -> " + JSON.stringify(newKeepPos)); + } + animateSummaryCell(keepCellId, prevKeepPos, newKeepPos, prevSplitPos); + animateSummaryCell(splitCellId, prevSplitPos, newSplitPos, prevKeepPos); + prevPositionMap.set(keepCellId, newKeepPos); + prevPositionMap.set(splitCellId, newSplitPos); + } + let newPositionMap = new Map(); - for (let personRow of group.personRows) { - let rowId = getRowId(grouper.tabId, personRow.id); - let $row = $("#" + rowId); - newPositionMap.set(rowId, {"left": $row.offset().left, "top": $row.offset().top}); + for (let element of split.elements) { + for (let isKeep of [true, false]) { + for (let isFacts of [false, true]) { + // (Spouse and Child summary elements can have a facts column that is not independently selectable) + if (!isFacts || (element.type === TYPE_SPOUSE || element.type === TYPE_CHILD)) { + let cellId = getCellId(element.elementIndex, isKeep, isFacts); + let $cell = $("#" + cellId); + if ($cell && $cell.offset()) { + newPositionMap.set(cellId, {"left": $cell.offset().left, "top": $cell.offset().top}); + } + } + } + } } - for (let personRow of group.personRows) { - let rowId = getRowId(grouper.tabId, personRow.id); - let $row = $("#" + rowId); - let prevPosition = prevRowPositionsMap.get(rowId); - let newPosition = newPositionMap.get(rowId); - prevRowPositionsMap.set(rowId, newPosition); - console.log(rowId + ": " + (prevPosition ? prevPosition.left + ", " + prevPosition.top + " => " : "") + newPosition.left + ", " + newPosition.top); - if (prevPosition && !isInitialization) { - // animate row's position from prevPosition to new position - $row.css({"position": "relative", "left": prevPosition.left - newPosition.left, "top": prevPosition.top - newPosition.top, "width": "100%"}); - $row.animate({"left": 0, "top": 0}, 500); + for (let element of split.elements) { + animateSummaryElement(element, false); + if (element.type === TYPE_SPOUSE || element.type === TYPE_CHILD) { + animateSummaryElement(element, true); } } } + resetAnimation = false; +} + +function getCellId(elementIndex, isKeep, isFacts) { + return getCurrentTab() + "_element-" + elementIndex + "_" + (isKeep ? "keep" : "split") + (isFacts ? "_facts" : ""); } function updateFlatViewHtml(grouper) { $("#" + grouper.tabId).html(getGrouperHtml(grouper)); highlightSelectedRows(grouper); - animateRows(grouper); makeTableHeadersDraggable(); + animateRows(grouper); } function highlightSelectedRows(grouper) { @@ -4618,7 +4732,7 @@ function showHiddenValuesButtonHtml(isVertical) { } function getRowId(tabId, personRowId) { - return tabId + "-" + personRowId; + return (displayOptions.vertical ? "v_" : "") + tabId + "-" + personRowId; } function getGrouperHtml(grouper) {