xref: /webtrees/resources/js/webtrees.js (revision 9ed80f829cfb208ae92f7928345696619569bf4e)
1/**
2 * webtrees: online genealogy
3 * Copyright (C) 2019 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
16'use strict';
17
18let webtrees = function () {
19    const lang = document.documentElement.lang;
20
21    /**
22     * Tidy the whitespace in a string.
23     */
24    function trim(str) {
25        return str.replace(/\s+/g, " ").trim();
26
27    }
28
29    /**
30     * Look for non-latin characters in a string.
31     */
32    function detectScript(str) {
33        if (str.match(/[\u3400-\u9FCC]/)) {
34            return "cjk";
35        } else if (str.match(/[\u0370-\u03FF]/)) {
36            return "greek";
37        } else if (str.match(/[\u0400-\u04FF]/)) {
38            return "cyrillic";
39        } else if (str.match(/[\u0590-\u05FF]/)) {
40            return "hebrew";
41        } else if (str.match(/[\u0600-\u06FF]/)) {
42            return "arabic";
43        }
44
45        return "latin";
46    }
47
48    /**
49     * In some languages, the SURN uses a male/default form, but NAME uses a gender-inflected form.
50     */
51    function inflectSurname(surname, sex) {
52        if (lang === "pl" && sex === "F") {
53            return surname
54                .replace(/ski$/, "ska")
55                .replace(/cki$/, "cka")
56                .replace(/dzki$/, "dzka")
57                .replace(/żki$/, "żka");
58        }
59
60        return surname;
61    }
62
63    /**
64     * Build a NAME from a NPFX, GIVN, SPFX, SURN and NSFX parts.
65     *
66     * Assumes the language of the document is the same as the language of the name.
67     */
68    function buildNameFromParts(npfx, givn, spfx, surn, nsfx, sex) {
69        const usesCJK      = detectScript(npfx + givn + spfx + givn + surn + nsfx) === "cjk";
70        const separator    = usesCJK ? "" : " ";
71        const surnameFirst = usesCJK || ['hu', 'jp', 'ko', 'vi', 'zh-Hans', 'zh-Hant'].indexOf(lang) !== -1;
72        const patronym     = ['is'].indexOf(lang) !== -1;
73        const slash        = patronym ? "" : "/";
74
75        // GIVN and SURN may be a comma-separated lists.
76        npfx = trim(npfx);
77        givn = trim(givn.replace(",", separator));
78        spfx = trim(spfx);
79        surn = inflectSurname(trim(surn.replace(",", separator)), sex);
80        nsfx = trim(nsfx);
81
82        const surname = trim(spfx + separator + surn);
83
84        const name = surnameFirst ? slash + surname + slash + separator + givn : givn + separator + slash + surname + slash;
85
86        return trim(npfx + separator + name + separator + nsfx);
87    }
88
89    // Public methods
90    return {
91        buildNameFromParts: buildNameFromParts,
92        detectScript:       detectScript,
93    };
94}();
95
96function expand_layer(sid)
97{
98    $('#' + sid + '_img').toggleClass('icon-plus icon-minus');
99    $('#' + sid).slideToggle('fast');
100    $('#' + sid + '-alt').toggle(); // hide something when we show the layer - and vice-versa
101    return false;
102}
103
104// Accept the changes to a record - and reload the page
105function accept_changes(xref, ged)
106{
107    $.post(
108        'index.php?route=accept-changes',
109        {
110            xref: xref,
111            ged: ged,
112        },
113        function () {
114            document.location.reload();
115        }
116    );
117    return false;
118}
119
120// Reject the changes to a record - and reload the page
121function reject_changes(xref, ged)
122{
123    $.post(
124        'index.php?route=reject-changes',
125        {
126            xref: xref,
127            ged: ged,
128        },
129        function () {
130            document.location.reload();
131        }
132    );
133    return false;
134}
135
136// Delete a record - and reload the page
137function delete_record(xref, gedcom)
138{
139    $.post(
140        'index.php?route=delete-record',
141        {
142            xref: xref,
143            ged: gedcom,
144        },
145        function () {
146            document.location.reload();
147        }
148    );
149
150    return false;
151}
152
153// Delete a fact - and reload the page
154function delete_fact(message, ged, xref, fact_id)
155{
156    if (confirm(message)) {
157        $.post(
158            'index.php?route=delete-fact',
159            {
160                xref: xref,
161                fact_id: fact_id,
162                ged: ged
163            },
164            function () {
165                document.location.reload();
166            }
167        );
168    }
169    return false;
170}
171
172// Copy a fact to the clipboard
173function copy_fact(ged, xref, fact_id)
174{
175    $.post(
176        'index.php?route-copy-fact',
177        {
178            xref: xref,
179            fact_id: fact_id,
180            ged: ged,
181        },
182        function () {
183            document.location.reload();
184        }
185    );
186    return false;
187}
188
189// Paste a fact from the clipboard
190function paste_fact(ged, xref, element)
191{
192    $.post(
193        'index.php?route=paste-face',
194        {
195            xref: xref,
196            fact_id: $(element).val(), // element is the <select> containing the option
197            ged: ged,
198        },
199        function () {
200            document.location.reload();
201        }
202    );
203    return false;
204}
205
206// Delete a user - and reload the page
207function delete_user(message, user_id)
208{
209    if (confirm(message)) {
210        $.post(
211            'index.php?route=delete-user',
212            {
213                user_id: user_id,
214            },
215            function () {
216                document.location.reload();
217            }
218        );
219    }
220    return false;
221}
222
223// Masquerade as another user - and reload the page.
224function masquerade(user_id)
225{
226    $.post(
227        'index.php?route=masquerade',
228        {
229            user_id: user_id,
230        },
231        function () {
232            document.location.reload();
233        }
234    );
235    return false;
236}
237
238var pastefield;
239function addmedia_links(field, iid, iname)
240{
241    pastefield = field;
242    insertRowToTable(iid, iname);
243    return false;
244}
245
246function valid_date(datefield, dmy)
247{
248    var months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'];
249    var hijri_months = ['MUHAR', 'SAFAR', 'RABIA', 'RABIT', 'JUMAA', 'JUMAT', 'RAJAB', 'SHAAB', 'RAMAD', 'SHAWW', 'DHUAQ', 'DHUAH'];
250    var hebrew_months = ['TSH', 'CSH', 'KSL', 'TVT', 'SHV', 'ADR', 'ADS', 'NSN', 'IYR', 'SVN', 'TMZ', 'AAV', 'ELL'];
251    var french_months = ['VEND', 'BRUM', 'FRIM', 'NIVO', 'PLUV', 'VENT', 'GERM', 'FLOR', 'PRAI', 'MESS', 'THER', 'FRUC', 'COMP'];
252    var jalali_months = ['FARVA', 'ORDIB', 'KHORD', 'TIR', 'MORDA', 'SHAHR', 'MEHR', 'ABAN', 'AZAR', 'DEY', 'BAHMA', 'ESFAN'];
253
254    var datestr = datefield.value;
255  // if a date has a date phrase marked by () this has to be excluded from altering
256    var datearr = datestr.split('(');
257    var datephrase = '';
258    if (datearr.length > 1) {
259        datestr = datearr[0];
260        datephrase = datearr[1];
261    }
262
263  // Gedcom dates are upper case
264    datestr = datestr.toUpperCase();
265  // Gedcom dates have no leading/trailing/repeated whitespace
266    datestr = datestr.replace(/\s+/, ' ');
267    datestr = datestr.replace(/(^\s)|(\s$)/, '');
268  // Gedcom dates have spaces between letters and digits, e.g. "01JAN2000" => "01 JAN 2000"
269    datestr = datestr.replace(/(\d)([A-Z])/, '$1 $2');
270    datestr = datestr.replace(/([A-Z])(\d)/, '$1 $2');
271
272  // Shortcut for quarter format, "Q1 1900" => "BET JAN 1900 AND MAR 1900". See [ 1509083 ]
273    if (datestr.match(/^Q ([1-4]) (\d\d\d\d)$/)) {
274        datestr = 'BET ' + months[RegExp.$1 * 3 - 3] + ' ' + RegExp.$2 + ' AND ' + months[RegExp.$1 * 3 - 1] + ' ' + RegExp.$2;
275    }
276
277  // Shortcut for @#Dxxxxx@ 01 01 1400, etc.
278    if (datestr.match(/^(@#DHIJRI@|HIJRI)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) {
279        datestr = '@#DHIJRI@' + RegExp.$2 + hijri_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4;
280    }
281    if (datestr.match(/^(@#DJALALI@|JALALI)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) {
282        datestr = '@#DJALALI@' + RegExp.$2 + jalali_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4;
283    }
284    if (datestr.match(/^(@#DHEBREW@|HEBREW)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) {
285        datestr = '@#DHEBREW@' + RegExp.$2 + hebrew_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4;
286    }
287    if (datestr.match(/^(@#DFRENCH R@|FRENCH)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) {
288        datestr = '@#DFRENCH R@' + RegExp.$2 + french_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4;
289    }
290
291  // e.g. 17.11.1860, 03/04/2005 or 1999-12-31. Use locale settings where DMY order is ambiguous.
292    var qsearch = /^([^\d]*)(\d+)[^\d](\d+)[^\d](\d+)$/i;
293    if (qsearch.exec(datestr)) {
294        var f0 = RegExp.$1;
295        var f1 = parseInt(RegExp.$2, 10);
296        var f2 = parseInt(RegExp.$3, 10);
297        var f3 = parseInt(RegExp.$4, 10);
298        var yyyy = new Date().getFullYear();
299        var yy = yyyy % 100;
300        var cc = yyyy - yy;
301        if (dmy === 'DMY' && f1 <= 31 && f2 <= 12 || f1 > 13 && f1 <= 31 && f2 <= 12 && f3 > 31) {
302            datestr = f0 + f1 + ' ' + months[f2 - 1] + ' ' + (f3 >= 100 ? f3 : (f3 <= yy ? f3 + cc : f3 + cc - 100));
303        } else {
304            if (dmy === 'MDY' && f1 <= 12 && f2 <= 31 || f2 > 13 && f2 <= 31 && f1 <= 12 && f3 > 31) {
305                datestr = f0 + f2 + ' ' + months[f1 - 1] + ' ' + (f3 >= 100 ? f3 : (f3 <= yy ? f3 + cc : f3 + cc - 100));
306            } else {
307                if (dmy === 'YMD' && f2 <= 12 && f3 <= 31 || f3 > 13 && f3 <= 31 && f2 <= 12 && f1 > 31) {
308                    datestr = f0 + f3 + ' ' + months[f2 - 1] + ' ' + (f1 >= 100 ? f1 : (f1 <= yy ? f1 + cc : f1 + cc - 100));
309                }
310            }
311        }
312    }
313
314  // Shortcuts for date ranges
315    datestr = datestr.replace(/^[>]([\w ]+)$/, 'AFT $1');
316    datestr = datestr.replace(/^[<]([\w ]+)$/, 'BEF $1');
317    datestr = datestr.replace(/^([\w ]+)[-]$/, 'FROM $1');
318    datestr = datestr.replace(/^[-]([\w ]+)$/, 'TO $1');
319    datestr = datestr.replace(/^[~]([\w ]+)$/, 'ABT $1');
320    datestr = datestr.replace(/^[*]([\w ]+)$/, 'EST $1');
321    datestr = datestr.replace(/^[#]([\w ]+)$/, 'CAL $1');
322    datestr = datestr.replace(/^([\w ]+) ?- ?([\w ]+)$/, 'BET $1 AND $2');
323    datestr = datestr.replace(/^([\w ]+) ?~ ?([\w ]+)$/, 'FROM $1 TO $2');
324
325  // Convert full months to short months
326    datestr = datestr.replace(/(JANUARY)/, 'JAN');
327    datestr = datestr.replace(/(FEBRUARY)/, 'FEB');
328    datestr = datestr.replace(/(MARCH)/, 'MAR');
329    datestr = datestr.replace(/(APRIL)/, 'APR');
330    datestr = datestr.replace(/(MAY)/, 'MAY');
331    datestr = datestr.replace(/(JUNE)/, 'JUN');
332    datestr = datestr.replace(/(JULY)/, 'JUL');
333    datestr = datestr.replace(/(AUGUST)/, 'AUG');
334    datestr = datestr.replace(/(SEPTEMBER)/, 'SEP');
335    datestr = datestr.replace(/(OCTOBER)/, 'OCT');
336    datestr = datestr.replace(/(NOVEMBER)/, 'NOV');
337    datestr = datestr.replace(/(DECEMBER)/, 'DEC');
338
339  // Americans frequently enter dates as SEP 20, 1999
340  // No need to internationalise this, as this is an english-language issue
341    datestr = datestr.replace(/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\.? (\d\d?)[, ]+(\d\d\d\d)/, '$2 $1 $3');
342
343  // Apply leading zero to day numbers
344    datestr = datestr.replace(/(^| )(\d [A-Z]{3,5} \d{4})/, '$10$2');
345
346    if (datephrase) {
347        datestr = datestr + ' (' + datephrase;
348    }
349  // Only update it if is has been corrected - otherwise input focus
350  // moves to the end of the field unnecessarily
351    if (datefield.value !== datestr) {
352        datefield.value = datestr;
353    }
354}
355
356var menutimeouts = [];
357
358function show_submenu(elementid, parentid)
359{
360    var pagewidth = document.body.scrollWidth + document.documentElement.scrollLeft;
361    var element = document.getElementById(elementid);
362
363    if (element && element.style) {
364        if (document.all) {
365            pagewidth = document.body.offsetWidth;
366        } else {
367            pagewidth = document.body.scrollWidth + document.documentElement.scrollLeft - 55;
368            if (document.documentElement.dir === 'rtl') {
369                boxright = element.offsetLeft + element.offsetWidth + 10;
370            }
371        }
372
373      // -- make sure the submenu is the size of the largest child
374        var maxwidth = 0;
375        var count = element.childNodes.length;
376        for (var i = 0; i < count; i++) {
377            var child = element.childNodes[i];
378            if (child.offsetWidth > maxwidth + 5) {
379                maxwidth = child.offsetWidth;
380            }
381        }
382        if (element.offsetWidth < maxwidth) {
383            element.style.width = maxwidth + 'px';
384        }
385        var pelement, boxright;
386        pelement = document.getElementById(parentid);
387        if (pelement) {
388            element.style.left = pelement.style.left;
389            boxright = element.offsetLeft + element.offsetWidth + 10;
390            if (boxright > pagewidth) {
391                var menuleft = pagewidth - element.offsetWidth;
392                element.style.left = menuleft + 'px';
393            }
394        }
395
396        if (element.offsetLeft < 0) {
397            element.style.left = '0px';
398        }
399
400      // -- put scrollbars on really long menus
401        if (element.offsetHeight > 500) {
402            element.style.height = '400px';
403            element.style.overflow = 'auto';
404        }
405
406        element.style.visibility = 'visible';
407    }
408    clearTimeout(menutimeouts[elementid]);
409    menutimeouts[elementid] = null;
410}
411
412function hide_submenu(elementid)
413{
414    if (typeof menutimeouts[elementid] !== 'number') {
415        return;
416    }
417    var element = document.getElementById(elementid);
418    if (element && element.style) {
419        element.style.visibility = 'hidden';
420    }
421    clearTimeout(menutimeouts[elementid]);
422    menutimeouts[elementid] = null;
423}
424
425function timeout_submenu(elementid)
426{
427    if (typeof menutimeouts[elementid] !== 'number') {
428        menutimeouts[elementid] = setTimeout("hide_submenu('" + elementid + "')", 100);
429    }
430}
431
432var monthLabels = [];
433monthLabels[1] = 'January';
434monthLabels[2] = 'February';
435monthLabels[3] = 'March';
436monthLabels[4] = 'April';
437monthLabels[5] = 'May';
438monthLabels[6] = 'June';
439monthLabels[7] = 'July';
440monthLabels[8] = 'August';
441monthLabels[9] = 'September';
442monthLabels[10] = 'October';
443monthLabels[11] = 'November';
444monthLabels[12] = 'December';
445
446var monthShort = [];
447monthShort[1] = 'JAN';
448monthShort[2] = 'FEB';
449monthShort[3] = 'MAR';
450monthShort[4] = 'APR';
451monthShort[5] = 'MAY';
452monthShort[6] = 'JUN';
453monthShort[7] = 'JUL';
454monthShort[8] = 'AUG';
455monthShort[9] = 'SEP';
456monthShort[10] = 'OCT';
457monthShort[11] = 'NOV';
458monthShort[12] = 'DEC';
459
460var daysOfWeek = [];
461daysOfWeek[0] = 'S';
462daysOfWeek[1] = 'M';
463daysOfWeek[2] = 'T';
464daysOfWeek[3] = 'W';
465daysOfWeek[4] = 'T';
466daysOfWeek[5] = 'F';
467daysOfWeek[6] = 'S';
468
469var weekStart = 0;
470
471function cal_setMonthNames(jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec)
472{
473    monthLabels[1] = jan;
474    monthLabels[2] = feb;
475    monthLabels[3] = mar;
476    monthLabels[4] = apr;
477    monthLabels[5] = may;
478    monthLabels[6] = jun;
479    monthLabels[7] = jul;
480    monthLabels[8] = aug;
481    monthLabels[9] = sep;
482    monthLabels[10] = oct;
483    monthLabels[11] = nov;
484    monthLabels[12] = dec;
485}
486
487function cal_setDayHeaders(sun, mon, tue, wed, thu, fri, sat)
488{
489    daysOfWeek[0] = sun;
490    daysOfWeek[1] = mon;
491    daysOfWeek[2] = tue;
492    daysOfWeek[3] = wed;
493    daysOfWeek[4] = thu;
494    daysOfWeek[5] = fri;
495    daysOfWeek[6] = sat;
496}
497
498function cal_setWeekStart(day)
499{
500    if (day >= 0 && day < 7) {
501        weekStart = day;
502    }
503}
504
505function calendarWidget(dateDivId, dateFieldId)
506{
507    var dateDiv = document.getElementById(dateDivId);
508    var dateField = document.getElementById(dateFieldId);
509
510    if (dateDiv.style.visibility === 'visible') {
511        dateDiv.style.visibility = 'hidden';
512        return false;
513    }
514    if (dateDiv.style.visibility === 'show') {
515        dateDiv.style.visibility = 'hide';
516        return false;
517    }
518
519  /* Javascript calendar functions only work with precise gregorian dates "D M Y" or "Y" */
520    var greg_regex = /((\d+ (JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC) )?\d+)/i;
521    var date;
522    if (greg_regex.exec(dateField.value)) {
523        date = new Date(RegExp.$1);
524    } else {
525        date = new Date();
526    }
527
528    dateDiv.innerHTML = cal_generateSelectorContent(dateFieldId, dateDivId, date);
529    if (dateDiv.style.visibility === 'hidden') {
530        dateDiv.style.visibility = 'visible';
531        return false;
532    }
533    if (dateDiv.style.visibility === 'hide') {
534        dateDiv.style.visibility = 'show';
535        return false;
536    }
537
538    return false;
539}
540
541function cal_generateSelectorContent(dateFieldId, dateDivId, date)
542{
543    var i, j;
544    var content = '<table border="1"><tr>';
545    content += '<td><select class="form-control" id="' + dateFieldId + '_daySelect" onchange="return cal_updateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');">';
546    for (i = 1; i < 32; i++) {
547        content += '<option value="' + i + '"';
548        if (date.getDate() === i) {
549            content += ' selected="selected"';
550        }
551        content += '>' + i + '</option>';
552    }
553    content += '</select></td>';
554    content += '<td><select class="form-control" id="' + dateFieldId + '_monSelect" onchange="return cal_updateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');">';
555    for (i = 1; i < 13; i++) {
556        content += '<option value="' + i + '"';
557        if (date.getMonth() + 1 === i) {
558            content += ' selected="selected"';
559        }
560        content += '>' + monthLabels[i] + '</option>';
561    }
562    content += '</select></td>';
563    content += '<td><input class="form-control" type="text" id="' + dateFieldId + '_yearInput" size="5" value="' + date.getFullYear() + '" onchange="return cal_updateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');" /></td></tr>';
564    content += '<tr><td colspan="3">';
565    content += '<table width="100%">';
566    content += '<tr>';
567    j = weekStart;
568    for (i = 0; i < 7; i++) {
569        content += '<td ';
570        content += 'class="descriptionbox"';
571        content += '>';
572        content += daysOfWeek[j];
573        content += '</td>';
574        j++;
575        if (j > 6) {
576            j = 0;
577        }
578    }
579    content += '</tr>';
580
581    var tdate = new Date(date.getFullYear(), date.getMonth(), 1);
582    var day = tdate.getDay();
583    day = day - weekStart;
584    var daymilli = 1000 * 60 * 60 * 24;
585    tdate = tdate.getTime() - (day * daymilli) + (daymilli / 2);
586    tdate = new Date(tdate);
587
588    for (j = 0; j < 6; j++) {
589        content += '<tr>';
590        for (i = 0; i < 7; i++) {
591            content += '<td ';
592            if (tdate.getMonth() === date.getMonth()) {
593                if (tdate.getDate() === date.getDate()) {
594                    content += 'class="descriptionbox"';
595                } else {
596                    content += 'class="optionbox"';
597                }
598            } else {
599                content += 'style="background-color:#EAEAEA; border: solid #AAAAAA 1px;"';
600            }
601            content += '><a href="#" onclick="return cal_dateClicked(\'' + dateFieldId + '\', \'' + dateDivId + '\', ' + tdate.getFullYear() + ', ' + tdate.getMonth() + ', ' + tdate.getDate() + ');">';
602            content += tdate.getDate();
603            content += '</a></td>';
604            var datemilli = tdate.getTime() + daymilli;
605            tdate = new Date(datemilli);
606        }
607        content += '</tr>';
608    }
609    content += '</table>';
610    content += '</td></tr>';
611    content += '</table>';
612
613    return content;
614}
615
616function cal_setDateField(dateFieldId, year, month, day)
617{
618    var dateField = document.getElementById(dateFieldId);
619    if (!dateField) {
620        return false;
621    }
622    if (day < 10) {
623        day = '0' + day;
624    }
625    dateField.value = day + ' ' + monthShort[month + 1] + ' ' + year;
626    return false;
627}
628
629function cal_updateCalendar(dateFieldId, dateDivId)
630{
631    var dateSel = document.getElementById(dateFieldId + '_daySelect');
632    if (!dateSel) {
633        return false;
634    }
635    var monthSel = document.getElementById(dateFieldId + '_monSelect');
636    if (!monthSel) {
637        return false;
638    }
639    var yearInput = document.getElementById(dateFieldId + '_yearInput');
640    if (!yearInput) {
641        return false;
642    }
643
644    var month = parseInt(monthSel.options[monthSel.selectedIndex].value, 10);
645    month = month - 1;
646
647    var date = new Date(yearInput.value, month, dateSel.options[dateSel.selectedIndex].value);
648    cal_setDateField(dateFieldId, date.getFullYear(), date.getMonth(), date.getDate());
649
650    var dateDiv = document.getElementById(dateDivId);
651    if (!dateDiv) {
652        alert('no dateDiv ' + dateDivId);
653        return false;
654    }
655    dateDiv.innerHTML = cal_generateSelectorContent(dateFieldId, dateDivId, date);
656
657    return false;
658}
659
660function cal_dateClicked(dateFieldId, dateDivId, year, month, day)
661{
662    cal_setDateField(dateFieldId, year, month, day);
663    calendarWidget(dateDivId, dateFieldId);
664    return false;
665}
666
667function openerpasteid(id)
668{
669    if (window.opener.paste_id) {
670        window.opener.paste_id(id);
671    }
672    window.close();
673}
674
675function paste_id(value)
676{
677    pastefield.value = value;
678}
679
680function pastename(name)
681{
682    if (nameElement) {
683        nameElement.innerHTML = name;
684    }
685    if (remElement) {
686        remElement.style.display = 'block';
687    }
688}
689
690function paste_char(value)
691{
692    if (document.selection) {
693      // IE
694        pastefield.focus();
695        document.selection.createRange().text = value;
696    } else if (pastefield.selectionStart || pastefield.selectionStart === 0) {
697      // Mozilla/Chrome/Safari
698        pastefield.value =
699        pastefield.value.substring(0, pastefield.selectionStart) +
700        value +
701        pastefield.value.substring(pastefield.selectionEnd, pastefield.value.length);
702        pastefield.selectionStart = pastefield.selectionEnd = pastefield.selectionStart + value.length;
703    } else {
704      // Fallback? - just append
705        pastefield.value += value;
706    }
707
708    if (pastefield.id === 'NPFX' || pastefield.id === 'GIVN' || pastefield.id === 'SPFX' || pastefield.id === 'SURN' || pastefield.id === 'NSFX') {
709        updatewholename();
710    }
711}
712
713/**
714 * Persistant checkbox options to hide/show extra data.
715
716 * @param element_id
717 */
718function persistent_toggle(element_id)
719{
720    let element = document.getElementById(element_id);
721    let key     = 'state-of-' + element_id;
722    let state   = localStorage.getItem(key);
723
724    // Previously selected?
725    if (state === 'true') {
726        $(element).click();
727    }
728
729    // Remember state for the next page load.
730    $(element).on('change', function() { localStorage.setItem(key, element.checked); });
731}
732
733function valid_lati_long(field, pos, neg)
734{
735  // valid LATI or LONG according to Gedcom standard
736  // pos (+) : N or E
737  // neg (-) : S or W
738    var txt = field.value.toUpperCase();
739    txt = txt.replace(/(^\s*)|(\s*$)/g, ''); // trim
740    txt = txt.replace(/ /g, ':'); // N12 34 ==> N12.34
741    txt = txt.replace(/\+/g, ''); // +17.1234 ==> 17.1234
742    txt = txt.replace(/-/g, neg); // -0.5698 ==> W0.5698
743    txt = txt.replace(/,/g, '.'); // 0,5698 ==> 0.5698
744  // 0°34'11 ==> 0:34:11
745    txt = txt.replace(/\u00b0/g, ':'); // °
746    txt = txt.replace(/\u0027/g, ':'); // '
747  // 0:34:11.2W ==> W0.5698
748    txt = txt.replace(/^([0-9]+):([0-9]+):([0-9.]+)(.*)/g, function ($0, $1, $2, $3, $4) {
749        var n = parseFloat($1);
750        n += ($2 / 60);
751        n += ($3 / 3600);
752        n = Math.round(n * 1E4) / 1E4;
753        return $4 + n;
754    });
755  // 0:34W ==> W0.5667
756    txt = txt.replace(/^([0-9]+):([0-9]+)(.*)/g, function ($0, $1, $2, $3) {
757        var n = parseFloat($1);
758        n += ($2 / 60);
759        n = Math.round(n * 1E4) / 1E4;
760        return $3 + n;
761    });
762  // 0.5698W ==> W0.5698
763    txt = txt.replace(/(.*)([N|S|E|W]+)$/g, '$2$1');
764  // 17.1234 ==> N17.1234
765    if (txt && txt.charAt(0) !== neg && txt.charAt(0) !== pos) {
766        txt = pos + txt;
767    }
768    field.value = txt;
769}
770
771// This is the default way for webtrees to show image galleries.
772// Custom themes may use a different viewer.
773function activate_colorbox(config)
774{
775    $.extend($.colorbox.settings, {
776      // Don't scroll window with document
777        fixed: true,
778        current: '',
779        previous: '\uf048',
780        next: '\uf051',
781        slideshowStart: '\uf04b',
782        slideshowStop: '\uf04c',
783        close: '\uf00d'
784    });
785    if (config) {
786        $.extend($.colorbox.settings, config);
787    }
788
789  // Trigger an event when we click on an (any) image
790    $('body').on('click', 'a.gallery', function () {
791      // Enable colorbox for images
792        $('a[type^=image].gallery').colorbox({
793            photo: true,
794            maxWidth: '95%',
795            maxHeight: '95%',
796            rel: 'gallery', // Turn all images on the page into a slideshow
797            slideshow: true,
798            slideshowAuto: false,
799          // Add wheelzoom to the displayed image
800            onComplete: function () {
801              // Disable click on image triggering next image
802              // https://github.com/jackmoore/colorbox/issues/668
803                $('.cboxPhoto').unbind('click');
804
805                wheelzoom(document.querySelectorAll('.cboxPhoto'));
806            }
807        });
808
809      // Enable colorbox for audio using <audio></audio>, where supported
810      // $('html.video a[type^=video].gallery').colorbox({
811      //  rel:         'nofollow' // Slideshows are just for images
812      // });
813
814      // Enable colorbox for video using <video></video>, where supported
815      // $('html.audio a[type^=audio].gallery').colorbox({
816      //  rel:         'nofollow', // Slideshows are just for images
817      // });
818
819      // Allow all other media types remain as download links
820    });
821}
822
823// Initialize autocomplete elements.
824function autocomplete(selector)
825{
826  // Use typeahead/bloodhound for autocomplete
827    $(selector).each(function () {
828        let that = this;
829        $(this).typeahead(null, {
830            display: 'value',
831            source: new Bloodhound({
832                datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
833                queryTokenizer: Bloodhound.tokenizers.whitespace,
834                remote: {
835                    url: this.dataset.autocompleteUrl,
836                    replace: function(url, uriEncodedQuery) {
837                        if (that.dataset.autocompleteExtra) {
838                            let extra = $(document.querySelector(that.dataset.autocompleteExtra)).val();
839                            return url.replace("QUERY",uriEncodedQuery) + '&extra=' + encodeURIComponent(extra)
840                        }
841                        return url.replace("QUERY",uriEncodedQuery);
842                    },
843                    wildcard: 'QUERY',
844
845                }
846            })
847        });
848    });
849}
850
851/**
852 * Insert text at the current cursor position in an input field.
853 *
854 * @param e The input element.
855 * @param t The text to insert.
856 */
857function insertTextAtCursor(e, t)
858{
859    var scrollTop = e.scrollTop;
860    var selectionStart = e.selectionStart;
861    var prefix = e.value.substring(0, selectionStart);
862    var suffix = e.value.substring(e.selectionEnd, e.value.length);
863    e.value = prefix + t + suffix;
864    e.selectionStart = selectionStart + t.length;
865    e.selectionEnd = e.selectionStart;
866    e.focus();
867    e.scrollTop = scrollTop;
868}
869
870
871/**
872 * Draws a google pie chart.
873 *
874 * @param {String} elementId        The element id of the HTML element the chart is rendered too
875 * @param {Array}  data             The chart data array
876 * @param {Array}  colors           The chart color array
877 * @param {String} title            The chart title
878 * @param {String} labeledValueText The type of how to display the slice text
879 */
880function drawPieChart(elementId, data, colors, title, labeledValueText)
881{
882    var data    = google.visualization.arrayToDataTable(data);
883    var options = {
884        title: title,
885        height: '100%',
886        width: '100%',
887        pieStartAngle: 0,
888        pieSliceText: 'none',
889        pieSliceTextStyle: {
890            color: '#777'
891        },
892        pieHole: 0.4,  // Donut
893        //is3D: true,  // 3D (not together with pieHole)
894        legend: {
895            alignment: 'center',
896            // Flickers on mouseover :(
897            labeledValueText: labeledValueText || 'value',
898            position: 'labeled'
899        },
900        chartArea: {
901            left: 0,
902            top: '5%',
903            height: '90%',
904            width: '100%'
905        },
906        tooltip: {
907            trigger: 'none',
908            text: 'both'
909        },
910        backgroundColor: 'transparent',
911        colors: colors
912    };
913
914    var chart = new google.visualization.PieChart(document.getElementById(elementId));
915
916    chart.draw(data, options);
917}
918
919/**
920 * Draws a google column chart.
921 *
922 * @param {String} elementId The element id of the HTML element the chart is rendered too
923 * @param {Array}  data      The chart data array
924 * @param {Object} options   The chart specific options to overwrite the default ones
925 */
926function drawColumnChart(elementId, data, options)
927{
928    var defaults = {
929        title: '',
930        subtitle: '',
931        titleTextStyle: {
932            color: '#757575',
933            fontName: 'Roboto',
934            fontSize: '16px',
935            bold: false,
936            italic: false
937        },
938        height: '100%',
939        width: '100%',
940        vAxis: {
941            title: ''
942        },
943        hAxis: {
944            title: ''
945        },
946        legend: {
947            position: 'none'
948        },
949        backgroundColor: 'transparent'
950    };
951
952    options = Object.assign(defaults, options);
953
954    var chart = new google.visualization.ColumnChart(document.getElementById(elementId));
955    var data  = google.visualization.arrayToDataTable(data);
956
957    chart.draw(data, options);
958}
959
960/**
961 * Draws a google combo chart.
962 *
963 * @param {String} elementId The element id of the HTML element the chart is rendered too
964 * @param {Array}  data      The chart data array
965 * @param {Object} options   The chart specific options to overwrite the default ones
966 */
967function drawComboChart(elementId, data, options)
968{
969    var defaults = {
970        title: '',
971        subtitle: '',
972        titleTextStyle: {
973            color: '#757575',
974            fontName: 'Roboto',
975            fontSize: '16px',
976            bold: false,
977            italic: false
978        },
979        height: '100%',
980        width: '100%',
981        vAxis: {
982            title: ''
983        },
984        hAxis: {
985            title: ''
986        },
987        legend: {
988            position: 'none'
989        },
990        seriesType: 'bars',
991        series: {
992            2: {
993                type: 'line'
994            }
995        },
996        colors: [],
997        backgroundColor: 'transparent'
998    };
999
1000    options = Object.assign(defaults, options);
1001
1002    var chart = new google.visualization.ComboChart(document.getElementById(elementId));
1003    var data  = google.visualization.arrayToDataTable(data);
1004
1005    chart.draw(data, options);
1006}
1007
1008/**
1009 * Draws a google geo chart.
1010 *
1011 * @param {String} elementId The element id of the HTML element the chart is rendered too
1012 * @param {Array}  data      The chart data array
1013 * @param {Object} options   The chart specific options to overwrite the default ones
1014 */
1015function drawGeoChart(elementId, data, options)
1016{
1017    var defaults = {
1018        title: '',
1019        subtitle: '',
1020        height: '100%',
1021        width: '100%'
1022    };
1023
1024    options = Object.assign(defaults, options);
1025
1026    var chart = new google.visualization.GeoChart(document.getElementById(elementId));
1027    var data  = google.visualization.arrayToDataTable(data);
1028
1029    chart.draw(data, options);
1030}
1031
1032// Send the CSRF token on all AJAX requests
1033$.ajaxSetup({
1034    headers: {
1035        'X-CSRF-TOKEN': $('meta[name=csrf]').attr('content')
1036    }
1037});
1038
1039// Initialisation
1040$(function () {
1041  // Page elements that load automaticaly via AJAX.
1042  // This prevents bad robots from crawling resource-intensive pages.
1043    $("[data-ajax-url]").each(function () {
1044        $(this).load($(this).data('ajaxUrl'));
1045    });
1046
1047  // Select2 - format entries in the select list
1048    function templateOptionForSelect2(data)
1049    {
1050        if (data.loading) {
1051          // If we're waiting for the server, this will be a "waiting..." message
1052            return data.text;
1053        } else {
1054          // The response from the server is already in HTML, so no need to format it here.
1055            return data.text;
1056        }
1057    }
1058
1059    // Autocomplete
1060    autocomplete('input[data-autocomplete-url]');
1061
1062    // Select2 - activate autocomplete fields
1063    $("select.select2").select2({
1064        width: "100%",
1065        // Do not escape.
1066        escapeMarkup: function (x) {
1067            return x;
1068        },
1069        // Same formatting for both selections and rsult
1070        //templateResult: templateOptionForSelect2,
1071        //templateSelection: templateOptionForSelect2
1072    })
1073    // If we clear the select (using the "X" button), we need an empty
1074    // value (rather than no value at all) for inputs with name="array[]"
1075    .on("select2:unselect", function (evt) {
1076        $(evt.delegateTarget).append("<option value=\"\" selected=\"selected\"></option>");
1077    });
1078
1079    // Datatables - locale aware sorting
1080    $.fn.dataTableExt.oSort['text-asc'] = function (x, y) {
1081        return x.localeCompare(y, document.documentElement.lang, {'sensitivity': 'base'});
1082    };
1083    $.fn.dataTableExt.oSort['text-desc'] = function (x, y) {
1084        return y.localeCompare(x, document.documentElement.lang, {'sensitivity': 'base'});
1085    };
1086
1087  // DataTables - start hidden to prevent FOUC.
1088    $('table.datatables').each(function () {
1089        $(this).DataTable(); $(this).removeClass('d-none'); });
1090
1091  // Create a new record while editing an existing one.
1092  // Paste the XREF and description into the Select2 element.
1093    $('.wt-modal-create-record').on('show.bs.modal', function (event) {
1094      // Find the element ID that needs to be updated with the new value.
1095        $('form', $(this)).data('element-id', $(event.relatedTarget).data('element-id'));
1096        $('form .form-group input:first', $(this)).focus();
1097    });
1098
1099  // Submit the modal form using AJAX, and paste the returned record ID/NAME into the parent form.
1100    $('.wt-modal-create-record form').on('submit', function (event) {
1101        event.preventDefault();
1102        var elementId = $(this).data('element-id');
1103        $.ajax({
1104            url: 'index.php',
1105            type: 'POST',
1106            data: new FormData(this),
1107            async: false,
1108            cache: false,
1109            contentType: false,
1110            processData: false,
1111            success: function (data) {
1112                $('#' + elementId).select2().empty().append(new Option(data.text, data.id)).val(data.id).trigger('change');
1113            },
1114            failure: function (data) {
1115                alert(data.error_message);
1116            }
1117        });
1118      // Clear the form
1119        this.reset();
1120      // Close the modal
1121        $(this).closest('.wt-modal-create-record').modal('hide');
1122    });
1123
1124  // Activate the langauge selection menu.
1125    $('.menu-language').on('click', '[data-language]', function () {
1126        $.post('index.php?route=language', {
1127            language: $(this).data('language')
1128        }, function () {
1129            document.location.reload();
1130        });
1131
1132        return false;
1133    });
1134
1135  // Activate the theme selection menu.
1136    $('.menu-theme').on('click', '[data-theme]', function () {
1137        $.post('index.php?route=theme', {
1138            theme: $(this).data('theme')
1139        }, function () {
1140            document.location.reload();
1141        });
1142
1143        return false;
1144    });
1145
1146  // Activate the on-screen keyboard
1147    var osk_focus_element;
1148    $('.wt-osk-trigger').click(function () {
1149      // When a user clicks the icon, set focus to the corresponding input
1150        osk_focus_element = document.getElementById($(this).data('id'));
1151        osk_focus_element.focus();
1152        $('.wt-osk').show();
1153
1154    });
1155
1156    $('.wt-osk-script-button').change(function () {
1157        $('.wt-osk-script').prop('hidden', true);
1158        $('.wt-osk-script-' + $(this).data('script')).prop('hidden', false);
1159    });
1160    $('.wt-osk-shift-button').click(function () {
1161        document.querySelector('.wt-osk-keys').classList.toggle('shifted');
1162    });
1163    $('.wt-osk-keys').on('click', '.wt-osk-key', function () {
1164        var key = $(this).contents().get(0).nodeValue;
1165        var shift_state = $('.wt-osk-shift-button').hasClass('active');
1166        var shift_key = $('sup', this)[0];
1167        if (shift_state && shift_key !== undefined) {
1168            key = shift_key.innerText;
1169        }
1170        if (osk_focus_element !== null) {
1171            var cursorPos = osk_focus_element.selectionStart;
1172            var v = osk_focus_element.value;
1173            var textBefore = v.substring(0, cursorPos);
1174            var textAfter  = v.substring(cursorPos, v.length);
1175            osk_focus_element.value = textBefore + key + textAfter;
1176            if ($('.wt-osk-pin-button').hasClass('active') === false) {
1177                $('.wt-osk').hide();
1178            }
1179        }
1180    });
1181
1182    $('.wt-osk-close').on('click', function () {
1183        $('.wt-osk').hide();
1184    });
1185});
1186