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