-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathindex.html
4115 lines (3766 loc) · 169 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GEDCOM X Viewer</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/open-iconic/1.1.1/font/css/open-iconic-bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.14.0/jsoneditor.min.css">
<link rel="stylesheet" href="gx-view.css">
<link rel="stylesheet" type="text/css" href="graph/graph.css">
<link rel="stylesheet" href="main.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/5.14.0/jsoneditor.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-filestyle/2.1.0/bootstrap-filestyle.min.js"></script>
<!-- Relationship Graph support -->
<script src="gx-util.js"></script>
<script src="graph/model/model-util.js"></script>
<script src="graph/model/PersonNode.js"></script>
<script src="graph/model/FamilyNode.js"></script>
<script src="graph/model/RelationshipGraph.js"></script>
<script src="graph/view/Generation.js"></script>
<script src="graph/view/LinkedHashSet.js"></script>
<script src="graph/view/IntegerByRef.js"></script>
<script src="graph/view/PersonBox.js"></script>
<script src="graph/view/FamilyLine.js"></script>
<script src="graph/view/ChartCompressor.js"></script>
<script src="graph/view/RelationshipChart.js"></script>
<script src="graph/view/RelChartBuilder.js"></script>
<script src="graph/view/EditChart.js"></script>
<script src="graph/graph.js"></script>
<script src="gx-fix.js"></script>
<script src="gx-view.js"></script>
<script>
// let personaUrl = "https://familysearch.org/ark:/61903/1:1:XF48-HFC";
// let recordUrl = "https://familysearch.org/ark:/61903/1:2:99FJ-7LXP";
// let gedcomxUrl = "https://www.familysearch.org/ark:/61903/1:1:XTWB-H9T"; // marriage record
// let gedcomxUrl = "https://www.familysearch.org/ark:/61903/1:1:QK2M-H538"; // obituary
/**
* The GEDCOM X editor
*
* @type JSONEditor
*/
let editor = null;
/**
* The current selection of the GEDCOM X editor.
*
* @type Node
*/
let editorSelection = null;
/**
* The current record URL.
*
* @type string
*/
let currentUrl = null;
let sourceDocumentText = null;
let sourceDocumentName = null;
let editHooks = {};
/**
* Show a record.
*
* @param doc The doc to show.
* @param url The URL of the record, if applicable.
*/
function showRecord(doc, url) {
let el = $("#record");
el.empty();
el.append(buildRecordUI(doc, url, editHooks));
currentUrl = url;
//add all the node-path selection logic
$("*[json-node-path]").addClass("gx-node-select").click(function() {
findRecordNode($(this).attr("json-node-path"));
});
}
/* == RecordInfo ===
A RecordInfo object holds information about one record, as well as its corresponding text section, if any.
- Every text section will correspond to one RecordInfo.
- Each RecordInfo will contain one GedcomX record (possibly an empty one like "{}").
- Each RecordInfo can correspond to a text section, or can have no corresponding text section (yet).
Common flows:
- Load new GedcomX record => text cleared; One RecordInfo that points to the new record and no text section.
- Load new Record Set => text cleared; Array of RecordInfo (one per record in the record set), none of which point to a text section.
- Load text when no Gx record exists yet => One RecordInfo with empty ({}) record, which corresponds to the one text section.
- Split text section => insert a RecordInfo after the one that corresponds to the current text section. Empty record that corresponds to new section.
- Eventually, look to see if the following RecordInfo (a) has no corresponding text section yet, and (b) has names that appear in the new section.
If so, then associate the record there. This is especially helpful to "undo" an accidental delete of a section break.
- Merge text section => merge persons and records from the second RecordInfo into the first one, and delete the second one from the array.
- Load text when Gx record or record set already loaded:
- If one text section and one record, then point the first (and only) RecordInfo at the first (and only) text section.
- If one text section and >1 record, then point the first RecordInfo at the first (and only) text section
(User will have to split text section and drag other records over to them).
- If multiple text sections, then create an empty record for each, and user must drag the records to the correct sections.
(Eventually, create alignment code that looks at words in each section and names in each record to try to line them up)
- Create a record from scratch by adding persons and/or metadata => Create one RecordInfo object with no text section (yet).
So a RecordInfo may have a null sectionId, but all text sections should have exactly one RecordInfo that points at it.
*/
// Array of RecordInfo objects, one for each GedcomX record being edited, possibly from a multi-record article, such as a birth list.
let recordInfos = [];
let currentRecordIndex = 0; // Index in recordInfos[] of the currently-selected record.
// Current GedcomX record object, used by the pop-up bounding box window to get access to the record and image data.
function getCurrentGx() {
return editor.get();
}
// Relationship chart for the current GedcomX
let relChart;
let imageChildWindow;
let entityChildWindow;
let dirty = false;
function makeEmptyRecord(defaultCoverage) {
let gxRecord = {};
let sd = getOrAddMainSourceDescription(gxRecord);
if (!sd.coverage && defaultCoverage) {
// Make a deep copy of defaultCoverage.
sd.coverage = JSON.parse(JSON.stringify(defaultCoverage));
}
return gxRecord;
}
function RecordInfo(sectionId, gxRecord, defaultCoverage) {
this.gxRecord = hasRecordData(gxRecord) ? gxRecord : makeEmptyRecord(defaultCoverage);
// Get an array of (lower case) full text strings from all the name forms of all the people in the given record.
function getRecordNames(doc) {
let names = [];
if (doc.persons) {
for (let person of doc.persons) {
if (person.names) {
for (let name of person.names) {
if (name.nameForms) {
for (let nameForm of name.nameForms) {
if (nameForm.fullText && nameForm.fullText.length > 0) {
names.push(nameForm.fullText.toLowerCase());
}
}
}
}
}
}
}
return names;
}
this.names = getRecordNames(this.gxRecord);
this.summary = this.names.length > 0 ? this.names[0] : "<NoNames>";
// id of contenteditable <div> that corresponds to this record (if any so far).
this.sectionId = sectionId;
}
// Array of previous copies of the GedcomX RecordSet and text sections before a set of changes was made.
// [0] is the original GedcomX. Each time the graph is built, a copy of the latest GedcomX is added to the array (if isEditable is true).
let recordSetChangeHistory = [];
// Position in recordSetChangeHistory. Normally recordSetChangeHistory = recordSetChangeHistory.length.
// But if 'undo' has been done, it can be earlier. If 'redo' is done before any further changes, then it advances again.
// If a change is made when this position is not at the end, then all following elements are removed.
let recordSetChangePosition = 0;
function undoRecordSet() {
if (recordSetChangePosition > 1) {
let backupStr = recordSetChangeHistory[--recordSetChangePosition - 1];
recoverRecordSetFromBackupString(backupStr);
}
}
function redoRecordSet() {
if (recordSetChangePosition < recordSetChangeHistory.length) {
let backupStr = recordSetChangeHistory[recordSetChangePosition++];
recoverRecordSetFromBackupString(backupStr);
}
}
function updateRecordSetUndo(backupStr) {
recordSetChangeHistory[recordSetChangePosition++] = backupStr;
if (recordSetChangePosition < recordSetChangeHistory.length) {
// Did a change after doing multiple "undos". So ignore the rest of the change history.
recordSetChangeHistory.length = recordSetChangePosition;
}
}
function handleEditorKeydown(e) {
if (!$(document.activeElement).is(":input,[contenteditable]")) {
let key = e.key.toUpperCase();
if (e.ctrlKey || e.metaKey) {
// Handle ctrl/cmd keydown
if ((key === 'Z' && e.shiftKey) || key === 'Y') {
// Ctrl/Cmd-shift Z or Cmd-Y => Redo
redoRecordSet();
// Prevent graph.js from receiving redo, since it is already taken care of.
e.stopImmediatePropagation();
e.preventDefault(); // Prevent browser from treating Cmd-Y as "Show all history".
}
else if (key === 'Z') { // lower-case z, no shift
// Ctrl/Cmd-Z => Undo
undoRecordSet();
// Prevent graph.js from receiving Cmd-Z and re-doing the undo
e.stopImmediatePropagation();
e.preventDefault();
}
}
}
}
/**
* Having made a change to the given record, update the display of it.
*
* @param doc The record to update.
* @param fromUndoLog - flag for whether this update is coming from the undo history, in which case the undo history should not be updated.
* @param editorAlreadyUpdated (optional) Flag for whether the 'editor' is already updated (avoids closing editor when it is being edited still).
* @param url (optional) The url or filename of the record, if applicable. (Ignored if null)
* @param isClean - flag for whether this is a fresh update of the record, so the dirty bit should be cleared rather than set (like usual).
*/
function updateRecord(doc, fromUndoLog, editorAlreadyUpdated, url, isClean) {
url = url ? url : currentUrl;
currentUrl = url;
if (doc.records && doc.records.length > 0) {
doc = doc.records[0];
}
if (recordInfos === 'undefined') {
recordInfos = [];
}
if (recordInfos.length === 0) {
recordInfos.push(new RecordInfo(null, doc));
currentRecordIndex = 0;
}
else if (currentRecordIndex >= 0 && currentRecordIndex < recordInfos.length) {
recordInfos[currentRecordIndex].gxRecord = doc;
}
dirty = !isClean;
updateEstimatedBirthYear(doc);
let backup = {
"doc": doc,
"url": url,
"text": sourceDocumentText,
"filename": sourceDocumentName,
"recordInfos" : recordInfos,
"currentRecordIndex" : currentRecordIndex,
"textSections" : getTextSections()
}
let backupStr = JSON.stringify(backup);
sessionStorage.setItem("Gx-Backup", backupStr);
localStorage.setItem("Gx-Backup.localStorage", backupStr);
if (!fromUndoLog) {
updateRecordSetUndo(backupStr);
}
if (!editorAlreadyUpdated) {
editor.set(doc);
$("#source-viewer").show();
}
let sftSummaryHtml = getSftSummaryHtml(doc);
if (sftSummaryHtml) {
$("#sft-summary").html(sftSummaryHtml);
}
function getImageLink(doc) {
// todo: Include coordinates and handle multiple images.
// let imageArksAndRectangles = getImageArks(doc);
// if (!isEmpty(imageArksAndRectangles)) {
// return imageArksAndRectangles[0].url;
// }
// return null; // No image link found
return getImageArks(doc) ? "bbox/bbox.html" : null;
}
let imageLink = getImageLink(doc);
if (imageLink) {
$("#image-link").html('<a href="' + imageLink + '?currentUrl=' + currentUrl + '" target="reusable_image_viewer_tab">View image</a>');
if (imageChildWindow && imageChildWindow.location && !imageChildWindow.closed && !imageChildWindow.location.href.endsWith(currentUrl)) {
// Automatically load the image into the image viewer if it is already open.
window.open(imageLink + '?currentUrl=' + currentUrl, "reusable_image_viewer_tab");
}
}
$("#nbx-link").html('<a href="nbx/nbx.html?currentUrl=' + currentUrl + '" target="reusable_entity_tab">View entities</a>');
if (entityChildWindow && entityChildWindow.location && !entityChildWindow.closed && !entityChildWindow.location.href.endsWith(currentUrl)) {
// Automatically load the NBX into the entity viewer if it is already open.
window.open("nbx/nbx.html?currentUrl=" + currentUrl, "reusable_entity_tab");
}
showRecord(doc, url);
updateSelectablePersons(doc, $("#new-relationship-person-1"));
updateSelectablePersons(doc, $("#new-relationship-person-2"));
updateSelectablePersons(doc, $("#new-relationship-person-3"));
updateSelectablePersons(doc, $("#edit-relationship-person-1"));
updateSelectablePersons(doc, $("#edit-relationship-person-2"));
updateSelectablePersons(doc, $("#copy-person-fact-target-persons"));
// Rebuild the relationship graph. Set global variable (defined in RelChartBuilder.js).
relChart = buildGraph(doc, true, relChart, fromUndoLog);
}
/**
* Update the selectable persons of a record.
*
* @param doc The record.
* @param select The selection.
*/
function updateSelectablePersons(doc, select) {
select.empty();
$.each(doc.persons, function(i, person) {
select.append($("<option/>", {
value: person.id,
text: getBestNameValue(person)
}));
});
}
let nextSectionIndex = 1;
// Set the sectionId of each RecordInfo in recordInfos[] to the text section each record seems to have come from.
// If unclear, err on the side of leaving sections unassigned.
// function alignTextSectionsWithRecords() {
// let sourceFileElement = $("#source-file");
// let sectionTexts = [];
// let sectionIndex = 0;
// for (let sectionDiv = sourceFileElement.firstChild; sectionDiv; sectionDiv = sectionDiv.nextSibling) {
// //todo: See if this recurses
// sectionTexts[sectionIndex++] = sectionDiv.textContent;
// }
//
// let scores = [];
// for (let recordIndex = 0; recordIndex < recordInfos.length; recordIndex++) {
//
// }
// }
// Get HTML of the text sections.
function getTextSections() {
let sourceFileElement = $("#source-file")[0];
if (sourceFileElement) {
return sourceFileElement.innerHTML;
}
else {
return "";
}
}
// Tell whether the given GedcomX document has data, beyond its own SourceDescription.
function hasRecordData(doc) {
return doc && (Object.keys(doc).length > 2 || !isEmpty(doc.persons) || !isEmpty(doc.documents));
}
// See if 'text' is an <NBX> file. If so, extract the date from the <DAT> tag, and the place from the parentheses in the <PAP> tag,
// in order to get date & place "coverage" for this record.
// Then add these as Coverage to the record unless there is already date and/or place coverage there.
function addCoverageToRecord(gxRecord, text) {
function extractText(leftTag, rightTag, body) {
let pos1 = body.indexOf(leftTag);
if (pos1 >= 0) {
pos1 += leftTag.length;
let pos2 = body.indexOf(rightTag, pos1);
if (pos2 >= 0) {
return body.substring(pos1, pos2);
}
}
return null;
}
if (gxRecord && text.includes("<NBX>")) {
let sd = getSourceDescription(gxRecord);
if (sd && (!sd.coverage || sd.coverage.length === 0)) {
let coverage = {};
let date = coverage && coverage.temporal ? coverage.temporal.original : null;
let place = coverage && coverage.spatial ? coverage.spatial.original : null;
if (!date || !place) {
coverage = sd.coverage ? sd.coverage : {};
if (!date) {
date = extractText("<DAT>", "</DAT>", text);
if (date) {
date = normalizeDate(date);
coverage.temporal = {"original": date};
sd.coverage = [coverage];
}
}
if (!place) {
let paper = extractText("<PAP>", "</PAP>", text);
if (paper) {
let place = extractText("(", ")", paper);
if (place) {
let result = place.match(/^(.* )?([A-Z][A-Z])$/);
if (result && result.length > 2 && result[2]) {
let state = STATE_MAP[result[2]];
if (state) {
place = place.substring(0, place.length - 2) + state;
}
}
coverage.spatial = {"original": place};
sd.coverage = [coverage];
}
}
}
}
}
}
}
let STATE_MAP =
{
"AL": "Alabama",
"AK": "Alaska",
"AR": "Arkansas",
"AS": "American Samoa",
"AZ": "Arizona",
"CA": "California",
"CO": "Colorado",
"CT": "Connecticut",
"DC": "District of Columbia",
"DE": "Delaware",
"FL": "Florida",
"GA": "Georgia",
"GU": "Guam",
"HI": "Hawaii",
"IA": "Iowa",
"ID": "Idaho",
"IL": "Illinois",
"IN": "Indiana",
"KS": "Kansas",
"KY": "Kentucky",
"LA": "Louisiana",
"MA": "Massachusetts",
"MD": "Maryland",
"ME": "Maine",
"MI": "Michigan",
"MN": "Minnesota",
"MO": "Missouri",
"MP": "Northern Mariana Islands",
"MS": "Mississippi",
"MT": "Montana",
"NC": "North Carolina",
"ND": "North Dakota",
"NE": "Nebraska",
"NH": "New Hampshire",
"NJ": "New Jersey",
"NM": "New Mexico",
"NV": "Nevada",
"NY": "New York",
"OH": "Ohio",
"OK": "Oklahoma",
"OR": "Oregon",
"PA": "Pennsylvania",
"PR": "Puerto Rico",
"RI": "Rhode Island",
"SC": "South Carolina",
"SD": "South Dakota",
"TN": "Tennessee",
"TT": "Trust Territories",
"TX": "Texas",
"UT": "Utah",
"VA": "Virginia",
"VI": "Virgin Islands",
"VT": "Vermont",
"WA": "Washington",
"WI": "Wisconsin",
"WV": "West Virginia",
"WY": "Wyoming"};
/**
* Show the source of a record. There is a "source-file" DIV in the HTML that contains one 'section' that corresponds to each record.
* Usually there is just one text section and one GedcomX record.
* But if a "¶" is included in the original text, or is typed at any time (or cmd/ctrl-Enter typed), then additional sections are added for each "¶".
* The "¶" is removed and not displayed. But if 'delete' is typed at the beginning of a section, then it is joined with the previous section.
* When gathering the text from all the sections, each section is assumed to be separated by a newline followed by a "¶" character.
* Each text section always corresponds to a RecordInfo object in the global recordInfos[] array, which in turn points back at this text section.
* @param text The text to show.
* @param name Name/title for the text.
*/
function setRecordSourceDocument(text, name) {
if (text) {
if (sourceDocumentText) {
clearRecord();
}
text = text
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("&", "&")
.replaceAll("''", '"')
.replaceAll("'", "'")
.replaceAll("•", "•")
.replaceAll("•", "•");
}
sourceDocumentText = text;
sourceDocumentName = name;
let sourceFileElement = $("#source-file");
// Clear text sections and any pointers to them
sourceFileElement.empty();
if (recordInfos) {
for (let recordInfo of recordInfos) {
recordInfo.sectionId = null;
}
}
if (name) {
sourceFileElement.append($("<h1/>").append($("<small/>", {"class": "text-muted"}).text(name)));
let gxDownloadNode = $("#download-gx");
gxDownloadNode.attr("download", name.replace(/(.txt|.nbx|.nbx.xml)?$/, ".gx.json"));
}
if (text) {
let sectionDiv = makeTextSection(null, text);
// Clear sectionId on all recordInfos, and remove any that have empty gedcomx objects.
recordInfos = recordInfos.filter(function(recordInfo) {
recordInfo.sectionId = null;
return hasRecordData(recordInfo.gxRecord);
});
if (recordInfos.length > 0) {
recordInfos[0].sectionId = sectionDiv.id;
}
else {
recordInfos = [new RecordInfo(sectionDiv.id)]
}
// If the text is an <NBX> file, then copy the date from <DAT>, and the place from <PAP>Paper name (Place)</PAP>,
// into the coverage for the new Record (if it doesn't already have any)
addCoverageToRecord(recordInfos[0].gxRecord, text);
sourceFileElement.append(sectionDiv);
if (text.includes("¶")) {
splitText(sectionDiv);
}
selectTextSection(sectionDiv.id, true);
sectionDiv.focus();
//alignTextSectionsWithRecords();
$("#source-viewer").show();
}
}
// Find the index of the RecordInfo object in the global recordInfos[] that has the given sectionId
// (or -1 if not found, which should be impossible).
function findRecordInfo(textSectionId) {
for (let index = 0; index < recordInfos.length; index++) {
if (recordInfos[index].sectionId === textSectionId) {
return index;
}
}
return -1; // not found
}
// Find the RecordInfo objects in the global recordInfos[] that refer to the two given textSectionIds.
// Merge the GedcomX data from the second RecordInfo into the GedcomX info of the first one.
// Delete the second RecordInfo object from recordInfos[] and slide the others down.
function mergeRecords(textSectionId1, textSectionId2) {
let recordInfo1Index = findRecordInfo(textSectionId1);
let recordInfo2Index = findRecordInfo(textSectionId2);
if (recordInfo1Index >= 0 && recordInfo2Index >= 0) {
mergeGedcomx(recordInfos[recordInfo1Index].gxRecord, recordInfos[recordInfo2Index].gxRecord);
recordInfos.splice(recordInfo2Index, 1);
currentRecordIndex = recordInfo1Index;
}
}
// Append the text from the second text section's div into the first one's div, and remove div2.
// Merge the GedcomX from div2's corresponding entry in recordInfos[] into div1's entry, and remove div2's entry.
function mergeSections(div1, div2) {
let firstChild2 = div2.firstChild;
while (div2.firstChild) {
div1.appendChild(div2.firstChild);
}
mergeRecords(div1.id, div2.id);
div2.remove();
selectTextSection(div1.id, true);
let range = document.createRange();
range.selectNodeContents(firstChild2);
range.collapse(true);
let selection = document.getSelection();
range.setStart(firstChild2, 0);
range.setEnd(firstChild2, 0);
range.collapse(true);
selection.removeAllRanges();
setTimeout(function () {
// Strangely, if this isn't in a timeout section, then Chrome replaces the <div> with a <span>,
// thus making the two sections be on the same line. Whatever.
selection.addRange(range);
}, 1);
}
function cursorIsAtBeginningOfSection(node) {
let selection = window.getSelection();
if (selection && selection.focusOffset === 0) {
let selectedNode = selection.focusNode;
if (selectedNode) {
for (let currentNode = node; currentNode; currentNode = currentNode.firstChild) {
if (currentNode === selectedNode) {
return true;
}
}
}
}
return false;
}
function enableTextSectionActions(element) {
element
.on("input", function (e) {
if (!e.originalEvent.data || e.originalEvent.data.includes("¶")) {
splitText(this, true);
}
})
.keydown(function (e) {
let event = e.originalEvent;
let isCmd = event.getModifierState("Meta") || event.getModifierState("Control");
if (event.code === "Enter" && isCmd) {
// Cmd/ctrl-Enter will split the current text section at the cursor.
insertParagraphMarkAtCursor();
splitText(this, true);
}
else if (event.code === "Backspace" && cursorIsAtBeginningOfSection(this)) {
// Hitting delete when at the beginning of a section will delete the "section break" and merge the sections together.
let prevSection = this.previousSibling;
if (prevSection) {
mergeSections(prevSection, this);
}
}
})
.on("focus", function () {
selectTextSection(this.id, false);
});
}
function makeTextSection(firstChildNode, orTextToUse) {
let sectionId = "text-section-" + nextSectionIndex++;
let element = $("<div contenteditable='true' class='edit-section' id='" + sectionId + "'/>");
if (firstChildNode) {
element.append(firstChildNode);
}
else {
let lines = orTextToUse.split("\n");
for (let line of lines) {
let lineDiv = document.createElement("div");
lineDiv.textContent = line;
element.append(lineDiv);
}
}
element.droppable({
hoverClass: "section-drop-hover",
scope: "personDropScope",
drop: personDropOnTextSection
});
enableTextSectionActions(element);
return element[0];
}
function findRecordInfoWithPersonId(personId) {
for (let i = 0; i < recordInfos.length; i++) {
let recordInfo = recordInfos[i];
if (recordInfo.gxRecord && recordInfo.gxRecord.persons) {
for (let person of recordInfo.gxRecord.persons) {
if (person.id === personId) {
return i;
}
}
}
}
throw Error("Could not find dropped person with id " + personId);
}
// Get the person ID from the person1 or person2 person reference in a relationship
function getRelativePersonId(relative) {
if (relative.resource.startsWith("#")) {
return relative.resource.substring(1);
}
return relative.resourceId;
}
// Gather a set of person IDs that are related (directly or indirectly) to the given personId in the given GedcomX record.
function getRelatedPersonIds(personId, gxRecord) {
// personIds to move: Includes the original personId, plus anyone connected via relationships (directly or indirectly)
let personIds = new Set();
personIds.add(personId);
let added = true;
while (added) {
added = false;
if (gxRecord.relationships) {
for (let relationship of gxRecord.relationships) {
let person1Id = getRelativePersonId(relationship.person1);
let person2Id = getRelativePersonId(relationship.person2);
if (personIds.has(person1Id) && !personIds.has(person2Id)) {
personIds.add(person2Id);
added = true;
}
else if (personIds.has(person2Id) && !personIds.has(person1Id)) {
personIds.add(person1Id);
added = true;
}
}
}
}
return personIds;
}
/**
* Move a person (and any connected relatives and relationships) from one record to the other.
* @param personId - personId to move
* @param sourceRecord - GedcomX record to move from
* @param targetRecord - GedcomX record to move to
*/
function movePersonToOtherRecord(personId, sourceRecord, targetRecord) {
// personIds to move: Includes the original personId, plus anyone connected via relationships (directly or indirectly)
let personIdsToMove = getRelatedPersonIds(personId, sourceRecord);
if (!targetRecord.persons) {
targetRecord.persons = [];
}
for (let p = 0; p < sourceRecord.persons.length; p++) {
if (personIdsToMove.has(sourceRecord.persons[p].id)) {
targetRecord.persons.push(sourceRecord.persons[p]);
sourceRecord.persons.splice(p--, 1);
}
}
if (sourceRecord.relationships) {
for (let r = 0; r < sourceRecord.relationships.length; r++) {
let relationship = sourceRecord.relationships[r];
if (personIdsToMove.has(getRelativePersonId(relationship.person1)) || personIdsToMove.has(getRelativePersonId(relationship.person2))) {
if (!targetRecord.relationships) {
targetRecord.relationships = [relationship];
}
else {
targetRecord.relationships.push(relationship);
}
sourceRecord.relationships.splice(r--, 1);
}
}
}
}
/**
* Handle an event in which a person box was dropped on this text section.
* @param e - Event. Has the div id of the dropped control in e.originalEvent.target.id
* @param ui - UI object that has the div id of the dropped person if e doesn't have it.
*/
function personDropOnTextSection(e, ui) {
let textSectionId = e.target.id; // or e.target.id?
let personBoxId = ui.draggable.attr("id");
// Remove initial chart#: 1-box_... => box_...
personBoxId = personBoxId.replace(/^[0-9]*-/,"");
if (personBoxId.startsWith("box_") && personBoxId.indexOf("Plus") < 0) {
// (local) personId of the dragged & dropped person
let personId = currentRelChart.personBoxMap[personBoxId].personNode.personId;
// GedcomX record corresponding to the text section that the person was dropped onto
let sourceRecordIndex = findRecordInfoWithPersonId(personId);
let targetRecordIndex = findRecordInfo(textSectionId);
if (sourceRecordIndex !== targetRecordIndex) {
movePersonToOtherRecord(personId, recordInfos[sourceRecordIndex].gxRecord, recordInfos[targetRecordIndex].gxRecord);
// Continue displaying the same original record, in case there are more persons from this record that need to be dragged over.
updateRecord(recordInfos[currentRecordIndex].gxRecord);
}
}
}
/**
* Select the given text section, while deselecting all the others.
* Select the corresponding RecordInfo object's GedcomX record for display and editing.
* @param textSectionId - id of div to select
* @param forceSelection -
*/
function selectTextSection(textSectionId, forceSelection) {
let recordIndex = findRecordInfo(textSectionId);
if (recordIndex >= 0 && (forceSelection || recordIndex !== currentRecordIndex)) {
// Deselect the currently-selected section, if any
$(".selected-section").removeClass("selected-section");
let recordInfo = recordInfos[recordIndex];
// if (currentRecordIndex >= 0) {
// Store possibly-edited GedcomX
// recordInfos[currentRecordIndex].gxRecord = getCurrentGx();
// }
currentRecordIndex = recordIndex;
if (recordInfo.sectionId) {
let sectionDiv = $("#" + recordInfo.sectionId);
sectionDiv.addClass("selected-section");
sectionDiv.focus();
}
if (relChart) {
relChart = null; // avoid animation at beginning when switching to different records.
}
updateRecord(recordInfo.gxRecord, false, false, null, true);
}
}
// When the user types cmd/ctrl-Enter, then insert a paragraph mark at the current cursor location,
// in preparation for the text being split just as though the paragraph mark had been typed.
function insertParagraphMarkAtCursor() {
let selection = window.getSelection();
if (selection) {
let selectedNode = selection.focusNode;
if (selectedNode) {
selectedNode.textContent = selectedNode.textContent.substring(0, selection.focusOffset) + "¶" + selectedNode.textContent.substring(selection.focusOffset);
}
}
}
/**
* Insert a new RecordInfo object after, and point the new RecordInfo object at sectionId2
*/
function insertRecordInfo(sectionId1, sectionId2) {
let recordIndex1 = findRecordInfo(sectionId1);
let recordIndex2 = recordIndex1 + 1;
//todo: Check the following record to see if (a) it has no section Id yet; and (b) its names appear in the given text section.
// If so, then re-use the existing RecordInfo and point it at this sectionId. Otherwise, create a new RecordInfo with an empty record.
let newRecordInfo = new RecordInfo(sectionId2, null, getCoverageFromRecordInfo(recordInfos[recordIndex1]));
recordInfos.splice(recordIndex2, 0, newRecordInfo);
}
// Get the coverage of the record in the given RecordInfo
function getCoverageFromRecordInfo(recordInfo) {
if (recordInfo && recordInfo.gxRecord && recordInfo.gxRecord.sourceDescriptions) {
for (let sd of recordInfo.gxRecord.sourceDescriptions) {
if (sd.coverage && sd.resourceType === "http://gedcomx.org/Record" && ("#" + sd.id) === recordInfo.gxRecord.description) {
return sd.coverage;
}
}
}
return null;
}
/**
* Given a DIV for one text section, which probably has at least one "¶" in it,
* split all the text in this DIV past each ¶ into its own new section DIV.
* @param textSectionDiv - DOM node for a contenteditable text section.
* @param shouldSelectNewNode - Flag for whether to set the newly-created node as the currently-selected one, and put focus on it.
*/
function splitText(textSectionDiv, shouldSelectNewNode) {
let newChildrenPairs = splitDomOnParagraphMark(textSectionDiv);
let lastNode = textSectionDiv;
for (let childNodePair of newChildrenPairs) {
let childNode = childNodePair[0];
let childSibling = childNodePair[1];
let newSectionDiv = makeTextSection(childNode, null);
insertRecordInfo(lastNode.id, newSectionDiv.id);
while (childSibling) {
let nextSibling = childSibling.nextSibling;
newSectionDiv.append(childSibling);
childSibling = nextSibling;
}
lastNode.insertAdjacentElement('afterend', newSectionDiv);
lastNode = newSectionDiv;
}
if (shouldSelectNewNode) {
selectTextSection(lastNode.id, true);
}
}
/**
* Check the children of the given DOM node (and its descendants) for ¶ marks.
* For each one found, create a new child node of the same type as the one it's found in,
* and add the new child to the returned array. The new child will not be a child of 'node',
* but, rather, the child of a new text section.
* So, for example, if we have
* <div id="1">Births: <b>Alex, Jan 2. ¶Bob, Jan 5. ¶Carol, Jan 6.</b> All doing well.</div>
* then the dom looks like
* div id=1
* #text "Births: "
* b
* #text "Alex, Jan 2. ¶Bob, Jan 5. ¶Carol, Jan 6."
* #text " All doing well."
* and we want it to end up as
* div id=1
* #text "Births: "
* b
* #text "Alex, Jan 2. "
* div id=2
* b
* #text "Bob, Jan 5. "
* div id=3
* b
* #text "Carol, Jan 6."
* #text " All doing well."
* Calling this on the div would
* 1) Ignore the first text line, as there is no ¶
* 2) Recursively call on the b
* 1) Find the first ¶, so
* (a) truncate the b node's text to "Alex, Jan 2. "; and
* (b) create a new #text node with "Bob, Jan 5. ¶Carol, Jan 6.", and add that to the array of new children
* 2) While looping, find the second ¶ in the newly-created #text node, so
* (a) truncate the new #text to be "Bob, Jan 5. "
* (b) create another new #text node with "Carol, Jan 6." and add that to the array of new children
* 3) return the array of two new children
* 3) Upon receiving two new children, the for each one,
* (a) create a new <b> node with the child text, and add that to the list of new children.
* (b) return that array of two new children
* 4) The caller will then create two new 'text section divs' and insert them after the original section div.
*/
function splitDomOnParagraphMark(node) {
// Array of child nodes that need to each be the first child of a new section DIV.
let newChildren = [];
for (let childNode = node.firstChild; childNode; childNode = childNode.nextSibling) {
if (childNode.nodeName === '#text') {
let pos;
while ((pos = childNode.textContent.indexOf("¶")) >= 0) {
if (pos === 0) {
// Remove ¶ from beginning
childNode.textContent = childNode.textContent.substring(1);
// if (childNode !== node.firstChild) {
// If this is the first child of a section DIV, then the ¶ was redundant and no other structural change is needed.
// Otherwise, this node becomes the first child of a new div.
newChildren.push([childNode, null]);
// }
}
else {
// Split this node at the (first) ¶
let leftText = childNode.textContent.substring(0, pos);
let rightText = childNode.textContent.substring(pos + 1);
childNode.textContent = leftText;
let newNode = document.createTextNode(rightText);
newNode.textContent = rightText;
newChildren.push([newNode, null]);
// Cause 'newNode' to be checked for more ¶ in the while loop.
childNode = newNode;
}
}
}
else if (childNode.firstChild) {
// Not #text element, and has children, so try splitting those.
let subChildrenPairs = splitDomOnParagraphMark(childNode);
let lastNewElementPair = null;
for (let subChildPair of subChildrenPairs) {
// Split text down below, so create a new node of the same type as 'node' and add the subChild to it.
let subChild = subChildPair[0];
let subChildsNextSibling = subChildPair[1];
let newElement;
if (subChild === childNode.firstChild) {
// If we're splitting on the first child of a node, then steal the entire node.
newElement = childNode;
}
else {
newElement = document.createElement(childNode.nodeName);
newElement.appendChild(subChild);
while (subChildsNextSibling) {
let temp = subChildsNextSibling.nextSibling;
newElement.appendChild(subChildsNextSibling);
subChildsNextSibling = temp;
}
}
let newElementPair = [newElement, null];
newChildren.push(newElementPair);
lastNewElementPair = newElementPair;
}
if (lastNewElementPair && childNode.nextSibling) {
lastNewElementPair[1] = childNode.nextSibling;
}
}
}
return newChildren;
}
// Update the document text in the GedcomX, in response to edits happening in the text area.
function updateDocumentText(text) {
sourceDocumentText = text;
let doc = editor.get();
let sourceDocument = getSourceDocument(doc);
if (sourceDocument) {
sourceDocument.text = text;
editor.set(doc);
}
}
function loadSourceDocument(doc) {
if (doc.records && doc.records.length > 0) {
doc = doc.records[0];
}
let mainSourceDescription = getMainSourceDocumentSourceDescription(doc);
let sourceDocument = getSourceDocument(doc, mainSourceDescription);
if (sourceDocument) {
let title = mainSourceDescription.titles && mainSourceDescription.titles.length > 0 ? mainSourceDescription.titles[0].value : null;
setRecordSourceDocument(sourceDocument.text, title, sourceDocument);
}
}
/**
* Find a record node in the editor.
*
* @param path The path to the record node.
*/
function findRecordNode(path) {
if (editorSelection) {
editorSelection.setSelected(false);
}
if (editor.getMode() === "code") {