1/** 2 * webtrees: online genealogy 3 * Copyright (C) 2020 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 18(function (webtrees) { 19 const lang = document.documentElement.lang; 20 21 // Identify the script used by some text. 22 const scriptRegexes = { 23 Han: /[\u3400-\u9FCC]/, 24 Grek: /[\u0370-\u03FF]/, 25 Cyrl: /[\u0400-\u04FF]/, 26 Hebr: /[\u0590-\u05FF]/, 27 Arab: /[\u0600-\u06FF]/ 28 }; 29 30 /** 31 * Tidy the whitespace in a string. 32 * @param {string} str 33 * @returns {string} 34 */ 35 function trim (str) { 36 return str.replace(/\s+/g, ' ').trim(); 37 } 38 39 /** 40 * Look for non-latin characters in a string. 41 * @param {string} str 42 * @returns {string} 43 */ 44 webtrees.detectScript = function (str) { 45 for (const script in scriptRegexes) { 46 if (str.match(scriptRegexes[script])) { 47 return script; 48 } 49 } 50 51 return 'Latn'; 52 }; 53 54 /** 55 * In some languages, the SURN uses a male/default form, but NAME uses a gender-inflected form. 56 * @param {string} surname 57 * @param {string} sex 58 * @returns {string} 59 */ 60 function inflectSurname (surname, sex) { 61 if (lang === 'pl' && sex === 'F') { 62 return surname 63 .replace(/ski$/, 'ska') 64 .replace(/cki$/, 'cka') 65 .replace(/dzki$/, 'dzka') 66 .replace(/żki$/, 'żka'); 67 } 68 69 return surname; 70 } 71 72 /** 73 * Build a NAME from a NPFX, GIVN, SPFX, SURN and NSFX parts. 74 * Assumes the language of the document is the same as the language of the name. 75 * @param {string} npfx 76 * @param {string} givn 77 * @param {string} spfx 78 * @param {string} surn 79 * @param {string} nsfx 80 * @param {string} sex 81 * @returns {string} 82 */ 83 webtrees.buildNameFromParts = function (npfx, givn, spfx, surn, nsfx, sex) { 84 const usesCJK = webtrees.detectScript(npfx + givn + spfx + givn + surn + nsfx) === 'Han'; 85 const separator = usesCJK ? '' : ' '; 86 const surnameFirst = usesCJK || ['hu', 'jp', 'ko', 'vi', 'zh-Hans', 'zh-Hant'].indexOf(lang) !== -1; 87 const patronym = ['is'].indexOf(lang) !== -1; 88 const slash = patronym ? '' : '/'; 89 90 // GIVN and SURN may be a comma-separated lists. 91 npfx = trim(npfx); 92 givn = trim(givn.replace(/,/g, separator)); 93 spfx = trim(spfx); 94 surn = inflectSurname(trim(surn.replace(/,/g, separator)), sex); 95 nsfx = trim(nsfx); 96 97 const surname = trim(spfx + separator + surn); 98 99 const name = surnameFirst ? slash + surname + slash + separator + givn : givn + separator + slash + surname + slash; 100 101 return trim(npfx + separator + name + separator + nsfx); 102 }; 103 104 // Insert text at the current cursor position in a text field. 105 webtrees.pasteAtCursor = function (element, text) { 106 if (element !== null) { 107 const caret_pos = element.selectionStart + text.length; 108 const textBefore = element.value.substring(0, element.selectionStart); 109 const textAfter = element.value.substring(element.selectionEnd); 110 element.value = textBefore + text + textAfter; 111 element.setSelectionRange(caret_pos, caret_pos); 112 element.focus(); 113 } 114 }; 115}(window.webtrees = window.webtrees || {})); 116 117var pastefield; 118 119/** 120 * @param {string} datefield 121 * @param {string} dmy 122 */ 123function valid_date (datefield, dmy) { 124 var months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; 125 var hijri_months = ['MUHAR', 'SAFAR', 'RABIA', 'RABIT', 'JUMAA', 'JUMAT', 'RAJAB', 'SHAAB', 'RAMAD', 'SHAWW', 'DHUAQ', 'DHUAH']; 126 var hebrew_months = ['TSH', 'CSH', 'KSL', 'TVT', 'SHV', 'ADR', 'ADS', 'NSN', 'IYR', 'SVN', 'TMZ', 'AAV', 'ELL']; 127 var french_months = ['VEND', 'BRUM', 'FRIM', 'NIVO', 'PLUV', 'VENT', 'GERM', 'FLOR', 'PRAI', 'MESS', 'THER', 'FRUC', 'COMP']; 128 var jalali_months = ['FARVA', 'ORDIB', 'KHORD', 'TIR', 'MORDA', 'SHAHR', 'MEHR', 'ABAN', 'AZAR', 'DEY', 'BAHMA', 'ESFAN']; 129 130 var datestr = datefield.value; 131 // if a date has a date phrase marked by () this has to be excluded from altering 132 var datearr = datestr.split('('); 133 var datephrase = ''; 134 if (datearr.length > 1) { 135 datestr = datearr[0]; 136 datephrase = datearr[1]; 137 } 138 139 // Gedcom dates are upper case 140 datestr = datestr.toUpperCase(); 141 // Gedcom dates have no leading/trailing/repeated whitespace 142 datestr = datestr.replace(/\s+/g, ' '); 143 datestr = datestr.replace(/(^\s)|(\s$)/, ''); 144 // Gedcom dates have spaces between letters and digits, e.g. "01JAN2000" => "01 JAN 2000" 145 datestr = datestr.replace(/(\d)([A-Z])/g, '$1 $2'); 146 datestr = datestr.replace(/([A-Z])(\d)/g, '$1 $2'); 147 148 // Shortcut for quarter format, "Q1 1900" => "BET JAN 1900 AND MAR 1900". See [ 1509083 ] 149 if (datestr.match(/^Q ([1-4]) (\d\d\d\d)$/)) { 150 datestr = 'BET ' + months[RegExp.$1 * 3 - 3] + ' ' + RegExp.$2 + ' AND ' + months[RegExp.$1 * 3 - 1] + ' ' + RegExp.$2; 151 } 152 153 // Shortcut for @#Dxxxxx@ 01 01 1400, etc. 154 if (datestr.match(/^(@#DHIJRI@|HIJRI)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 155 datestr = '@#DHIJRI@' + RegExp.$2 + hijri_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 156 } 157 if (datestr.match(/^(@#DJALALI@|JALALI)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 158 datestr = '@#DJALALI@' + RegExp.$2 + jalali_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 159 } 160 if (datestr.match(/^(@#DHEBREW@|HEBREW)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 161 datestr = '@#DHEBREW@' + RegExp.$2 + hebrew_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 162 } 163 if (datestr.match(/^(@#DFRENCH R@|FRENCH)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 164 datestr = '@#DFRENCH R@' + RegExp.$2 + french_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 165 } 166 167 // e.g. 17.11.1860, 03/04/2005 or 1999-12-31. Use locale settings where DMY order is ambiguous. 168 var qsearch = /^([^\d]*)(\d+)[^\d](\d+)[^\d](\d+)$/i; 169 if (qsearch.exec(datestr)) { 170 var f0 = RegExp.$1; 171 var f1 = parseInt(RegExp.$2, 10); 172 var f2 = parseInt(RegExp.$3, 10); 173 var f3 = parseInt(RegExp.$4, 10); 174 var yyyy = new Date().getFullYear(); 175 var yy = yyyy % 100; 176 var cc = yyyy - yy; 177 if (dmy === 'DMY' && f1 <= 31 && f2 <= 12 || f1 > 13 && f1 <= 31 && f2 <= 12 && f3 > 31) { 178 datestr = f0 + f1 + ' ' + months[f2 - 1] + ' ' + (f3 >= 100 ? f3 : (f3 <= yy ? f3 + cc : f3 + cc - 100)); 179 } else { 180 if (dmy === 'MDY' && f1 <= 12 && f2 <= 31 || f2 > 13 && f2 <= 31 && f1 <= 12 && f3 > 31) { 181 datestr = f0 + f2 + ' ' + months[f1 - 1] + ' ' + (f3 >= 100 ? f3 : (f3 <= yy ? f3 + cc : f3 + cc - 100)); 182 } else { 183 if (dmy === 'YMD' && f2 <= 12 && f3 <= 31 || f3 > 13 && f3 <= 31 && f2 <= 12 && f1 > 31) { 184 datestr = f0 + f3 + ' ' + months[f2 - 1] + ' ' + (f1 >= 100 ? f1 : (f1 <= yy ? f1 + cc : f1 + cc - 100)); 185 } 186 } 187 } 188 } 189 190 // Shortcuts for date ranges 191 datestr = datestr.replace(/^[>]([\w ]+)$/, 'AFT $1'); 192 datestr = datestr.replace(/^[<]([\w ]+)$/, 'BEF $1'); 193 datestr = datestr.replace(/^([\w ]+)[-]$/, 'FROM $1'); 194 datestr = datestr.replace(/^[-]([\w ]+)$/, 'TO $1'); 195 datestr = datestr.replace(/^[~]([\w ]+)$/, 'ABT $1'); 196 datestr = datestr.replace(/^[*]([\w ]+)$/, 'EST $1'); 197 datestr = datestr.replace(/^[#]([\w ]+)$/, 'CAL $1'); 198 datestr = datestr.replace(/^([\w ]+) ?- ?([\w ]+)$/, 'BET $1 AND $2'); 199 datestr = datestr.replace(/^([\w ]+) ?~ ?([\w ]+)$/, 'FROM $1 TO $2'); 200 201 // Convert full months to short months 202 datestr = datestr.replace(/(JANUARY)/, 'JAN'); 203 datestr = datestr.replace(/(FEBRUARY)/, 'FEB'); 204 datestr = datestr.replace(/(MARCH)/, 'MAR'); 205 datestr = datestr.replace(/(APRIL)/, 'APR'); 206 datestr = datestr.replace(/(MAY)/, 'MAY'); 207 datestr = datestr.replace(/(JUNE)/, 'JUN'); 208 datestr = datestr.replace(/(JULY)/, 'JUL'); 209 datestr = datestr.replace(/(AUGUST)/, 'AUG'); 210 datestr = datestr.replace(/(SEPTEMBER)/, 'SEP'); 211 datestr = datestr.replace(/(OCTOBER)/, 'OCT'); 212 datestr = datestr.replace(/(NOVEMBER)/, 'NOV'); 213 datestr = datestr.replace(/(DECEMBER)/, 'DEC'); 214 215 // Americans frequently enter dates as SEP 20, 1999 216 // No need to internationalise this, as this is an english-language issue 217 datestr = datestr.replace(/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\.? (\d\d?)[, ]+(\d\d\d\d)/, '$2 $1 $3'); 218 219 // Apply leading zero to day numbers 220 datestr = datestr.replace(/(^| )(\d [A-Z]{3,5} \d{4})/, '$10$2'); 221 222 if (datephrase) { 223 datestr = datestr + ' (' + datephrase; 224 } 225 // Only update it if is has been corrected - otherwise input focus 226 // moves to the end of the field unnecessarily 227 if (datefield.value !== datestr) { 228 datefield.value = datestr; 229 } 230} 231 232var monthLabels = []; 233monthLabels[1] = 'January'; 234monthLabels[2] = 'February'; 235monthLabels[3] = 'March'; 236monthLabels[4] = 'April'; 237monthLabels[5] = 'May'; 238monthLabels[6] = 'June'; 239monthLabels[7] = 'July'; 240monthLabels[8] = 'August'; 241monthLabels[9] = 'September'; 242monthLabels[10] = 'October'; 243monthLabels[11] = 'November'; 244monthLabels[12] = 'December'; 245 246var monthShort = []; 247monthShort[1] = 'JAN'; 248monthShort[2] = 'FEB'; 249monthShort[3] = 'MAR'; 250monthShort[4] = 'APR'; 251monthShort[5] = 'MAY'; 252monthShort[6] = 'JUN'; 253monthShort[7] = 'JUL'; 254monthShort[8] = 'AUG'; 255monthShort[9] = 'SEP'; 256monthShort[10] = 'OCT'; 257monthShort[11] = 'NOV'; 258monthShort[12] = 'DEC'; 259 260var daysOfWeek = []; 261daysOfWeek[0] = 'S'; 262daysOfWeek[1] = 'M'; 263daysOfWeek[2] = 'T'; 264daysOfWeek[3] = 'W'; 265daysOfWeek[4] = 'T'; 266daysOfWeek[5] = 'F'; 267daysOfWeek[6] = 'S'; 268 269var weekStart = 0; 270 271/** 272 * @param {string} jan 273 * @param {string} feb 274 * @param {string} mar 275 * @param {string} apr 276 * @param {string} may 277 * @param {string} jun 278 * @param {string} jul 279 * @param {string} aug 280 * @param {string} sep 281 * @param {string} oct 282 * @param {string} nov 283 * @param {string} dec 284 */ 285function cal_setMonthNames (jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec) { 286 monthLabels[1] = jan; 287 monthLabels[2] = feb; 288 monthLabels[3] = mar; 289 monthLabels[4] = apr; 290 monthLabels[5] = may; 291 monthLabels[6] = jun; 292 monthLabels[7] = jul; 293 monthLabels[8] = aug; 294 monthLabels[9] = sep; 295 monthLabels[10] = oct; 296 monthLabels[11] = nov; 297 monthLabels[12] = dec; 298} 299 300/** 301 * @param {string} sun 302 * @param {string} mon 303 * @param {string} tue 304 * @param {string} wed 305 * @param {string} thu 306 * @param {string} fri 307 * @param {string} sat 308 */ 309function cal_setDayHeaders (sun, mon, tue, wed, thu, fri, sat) { 310 daysOfWeek[0] = sun; 311 daysOfWeek[1] = mon; 312 daysOfWeek[2] = tue; 313 daysOfWeek[3] = wed; 314 daysOfWeek[4] = thu; 315 daysOfWeek[5] = fri; 316 daysOfWeek[6] = sat; 317} 318 319/** 320 * @param {number} day 321 */ 322function cal_setWeekStart (day) { 323 if (day >= 0 && day < 7) { 324 weekStart = day; 325 } 326} 327 328/** 329 * @param {string} dateDivId 330 * @param {string} dateFieldId 331 * @returns {boolean} 332 */ 333function calendarWidget (dateDivId, dateFieldId) { 334 var dateDiv = document.getElementById(dateDivId); 335 var dateField = document.getElementById(dateFieldId); 336 337 if (dateDiv.style.visibility === 'visible') { 338 dateDiv.style.visibility = 'hidden'; 339 return false; 340 } 341 if (dateDiv.style.visibility === 'show') { 342 dateDiv.style.visibility = 'hide'; 343 return false; 344 } 345 346 /* Javascript calendar functions only work with precise gregorian dates "D M Y" or "Y" */ 347 var greg_regex = /((\d+ (JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC) )?\d+)/i; 348 var date; 349 if (greg_regex.exec(dateField.value)) { 350 date = new Date(RegExp.$1); 351 } else { 352 date = new Date(); 353 } 354 355 dateDiv.innerHTML = cal_generateSelectorContent(dateFieldId, dateDivId, date); 356 if (dateDiv.style.visibility === 'hidden') { 357 dateDiv.style.visibility = 'visible'; 358 return false; 359 } 360 if (dateDiv.style.visibility === 'hide') { 361 dateDiv.style.visibility = 'show'; 362 return false; 363 } 364 365 return false; 366} 367 368/** 369 * @param {string} dateFieldId 370 * @param {string} dateDivId 371 * @param {Date} date 372 * @returns {string} 373 */ 374function cal_generateSelectorContent (dateFieldId, dateDivId, date) { 375 var i, j; 376 var content = '<table border="1"><tr>'; 377 content += '<td><select class="form-control" id="' + dateFieldId + '_daySelect" onchange="return cal_updateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');">'; 378 for (i = 1; i < 32; i++) { 379 content += '<option value="' + i + '"'; 380 if (date.getDate() === i) { 381 content += ' selected="selected"'; 382 } 383 content += '>' + i + '</option>'; 384 } 385 content += '</select></td>'; 386 content += '<td><select class="form-control" id="' + dateFieldId + '_monSelect" onchange="return cal_updateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');">'; 387 for (i = 1; i < 13; i++) { 388 content += '<option value="' + i + '"'; 389 if (date.getMonth() + 1 === i) { 390 content += ' selected="selected"'; 391 } 392 content += '>' + monthLabels[i] + '</option>'; 393 } 394 content += '</select></td>'; 395 content += '<td><input class="form-control" type="text" id="' + dateFieldId + '_yearInput" size="5" value="' + date.getFullYear() + '" onchange="return cal_updateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');" /></td></tr>'; 396 content += '<tr><td colspan="3">'; 397 content += '<table width="100%">'; 398 content += '<tr>'; 399 j = weekStart; 400 for (i = 0; i < 7; i++) { 401 content += '<td '; 402 content += 'class="descriptionbox"'; 403 content += '>'; 404 content += daysOfWeek[j]; 405 content += '</td>'; 406 j++; 407 if (j > 6) { 408 j = 0; 409 } 410 } 411 content += '</tr>'; 412 413 var tdate = new Date(date.getFullYear(), date.getMonth(), 1); 414 var day = tdate.getDay(); 415 day = day - weekStart; 416 var daymilli = 1000 * 60 * 60 * 24; 417 tdate = tdate.getTime() - (day * daymilli) + (daymilli / 2); 418 tdate = new Date(tdate); 419 420 for (j = 0; j < 6; j++) { 421 content += '<tr>'; 422 for (i = 0; i < 7; i++) { 423 content += '<td '; 424 if (tdate.getMonth() === date.getMonth()) { 425 if (tdate.getDate() === date.getDate()) { 426 content += 'class="descriptionbox"'; 427 } else { 428 content += 'class="optionbox"'; 429 } 430 } else { 431 content += 'style="background-color:#EAEAEA; border: solid #AAAAAA 1px;"'; 432 } 433 content += '><a href="#" onclick="return cal_dateClicked(\'' + dateFieldId + '\', \'' + dateDivId + '\', ' + tdate.getFullYear() + ', ' + tdate.getMonth() + ', ' + tdate.getDate() + ');">'; 434 content += tdate.getDate(); 435 content += '</a></td>'; 436 var datemilli = tdate.getTime() + daymilli; 437 tdate = new Date(datemilli); 438 } 439 content += '</tr>'; 440 } 441 content += '</table>'; 442 content += '</td></tr>'; 443 content += '</table>'; 444 445 return content; 446} 447 448/** 449 * @param {string} dateFieldId 450 * @param {number} year 451 * @param {number} month 452 * @param {number} day 453 * @returns {boolean} 454 */ 455function cal_setDateField (dateFieldId, year, month, day) { 456 var dateField = document.getElementById(dateFieldId); 457 if (!dateField) { 458 return false; 459 } 460 if (day < 10) { 461 day = '0' + day; 462 } 463 dateField.value = day + ' ' + monthShort[month + 1] + ' ' + year; 464 return false; 465} 466 467/** 468 * @param {string} dateFieldId 469 * @param {string} dateDivId 470 * @returns {boolean} 471 */ 472function cal_updateCalendar (dateFieldId, dateDivId) { 473 var dateSel = document.getElementById(dateFieldId + '_daySelect'); 474 if (!dateSel) { 475 return false; 476 } 477 var monthSel = document.getElementById(dateFieldId + '_monSelect'); 478 if (!monthSel) { 479 return false; 480 } 481 var yearInput = document.getElementById(dateFieldId + '_yearInput'); 482 if (!yearInput) { 483 return false; 484 } 485 486 var month = parseInt(monthSel.options[monthSel.selectedIndex].value, 10); 487 month = month - 1; 488 489 var date = new Date(yearInput.value, month, dateSel.options[dateSel.selectedIndex].value); 490 cal_setDateField(dateFieldId, date.getFullYear(), date.getMonth(), date.getDate()); 491 492 var dateDiv = document.getElementById(dateDivId); 493 if (!dateDiv) { 494 alert('no dateDiv ' + dateDivId); 495 return false; 496 } 497 dateDiv.innerHTML = cal_generateSelectorContent(dateFieldId, dateDivId, date); 498 499 return false; 500} 501 502/** 503 * @param {string} dateFieldId 504 * @param {string} dateDivId 505 * @param {number} year 506 * @param {number} month 507 * @param {number} day 508 * @returns {boolean} 509 */ 510function cal_dateClicked (dateFieldId, dateDivId, year, month, day) { 511 cal_setDateField(dateFieldId, year, month, day); 512 calendarWidget(dateDivId, dateFieldId); 513 return false; 514} 515 516/** 517 * @param {string} id 518 */ 519function openerpasteid (id) { 520 if (window.opener.paste_id) { 521 window.opener.paste_id(id); 522 } 523 window.close(); 524} 525 526/** 527 * @param {string} value 528 */ 529function paste_id (value) { 530 pastefield.value = value; 531} 532 533/** 534 * @param {string} name 535 */ 536function pastename (name) { 537 if (nameElement) { 538 nameElement.innerHTML = name; 539 } 540 if (remElement) { 541 remElement.style.display = 'block'; 542 } 543} 544 545/** 546 * @param {string} value 547 */ 548function paste_char (value) { 549 if (document.selection) { 550 // IE 551 pastefield.focus(); 552 document.selection.createRange().text = value; 553 } else if (pastefield.selectionStart || pastefield.selectionStart === 0) { 554 // Mozilla/Chrome/Safari 555 pastefield.value = 556 pastefield.value.substring(0, pastefield.selectionStart) + 557 value + 558 pastefield.value.substring(pastefield.selectionEnd, pastefield.value.length); 559 pastefield.selectionStart = pastefield.selectionEnd = pastefield.selectionStart + value.length; 560 } else { 561 // Fallback? - just append 562 pastefield.value += value; 563 } 564 565 if (pastefield.id === 'NPFX' || pastefield.id === 'GIVN' || pastefield.id === 'SPFX' || pastefield.id === 'SURN' || pastefield.id === 'NSFX') { 566 updatewholename(); 567 } 568} 569 570/** 571 * Persistant checkbox options to hide/show extra data. 572 * @param element_id 573 */ 574function persistent_toggle (element_id) { 575 const element = document.getElementById(element_id); 576 const key = 'state-of-' + element_id; 577 const state = localStorage.getItem(key); 578 579 // Previously selected? 580 if (state === 'true') { 581 $(element).click(); 582 } 583 584 // Remember state for the next page load. 585 $(element).on('change', function () { localStorage.setItem(key, element.checked); }); 586} 587 588/** 589 * @param {string} field 590 * @param {string} pos 591 * @param {string} neg 592 */ 593function valid_lati_long (field, pos, neg) { 594 // valid LATI or LONG according to Gedcom standard 595 // pos (+) : N or E 596 // neg (-) : S or W 597 var txt = field.value.toUpperCase(); 598 txt = txt.replace(/(^\s*)|(\s*$)/g, ''); // trim 599 txt = txt.replace(/ /g, ':'); // N12 34 ==> N12.34 600 txt = txt.replace(/\+/g, ''); // +17.1234 ==> 17.1234 601 txt = txt.replace(/-/g, neg); // -0.5698 ==> W0.5698 602 txt = txt.replace(/,/g, '.'); // 0,5698 ==> 0.5698 603 // 0°34'11 ==> 0:34:11 604 txt = txt.replace(/\u00b0/g, ':'); // ° 605 txt = txt.replace(/\u0027/g, ':'); // ' 606 // 0:34:11.2W ==> W0.5698 607 txt = txt.replace(/^([0-9]+):([0-9]+):([0-9.]+)(.*)/g, function ($0, $1, $2, $3, $4) { 608 var n = parseFloat($1); 609 n += ($2 / 60); 610 n += ($3 / 3600); 611 n = Math.round(n * 1E4) / 1E4; 612 return $4 + n; 613 }); 614 // 0:34W ==> W0.5667 615 txt = txt.replace(/^([0-9]+):([0-9]+)(.*)/g, function ($0, $1, $2, $3) { 616 var n = parseFloat($1); 617 n += ($2 / 60); 618 n = Math.round(n * 1E4) / 1E4; 619 return $3 + n; 620 }); 621 // 0.5698W ==> W0.5698 622 txt = txt.replace(/(.*)([N|S|E|W]+)$/g, '$2$1'); 623 // 17.1234 ==> N17.1234 624 if (txt && txt.charAt(0) !== neg && txt.charAt(0) !== pos) { 625 txt = pos + txt; 626 } 627 field.value = txt; 628} 629 630/** 631 * Initialize autocomplete elements. 632 * @param {string} selector 633 */ 634function autocomplete (selector) { 635 // Use typeahead/bloodhound for autocomplete 636 $(selector).each(function () { 637 const that = this; 638 $(this).typeahead(null, { 639 display: 'value', 640 limit: 0, 641 source: new Bloodhound({ 642 datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), 643 queryTokenizer: Bloodhound.tokenizers.whitespace, 644 remote: { 645 url: this.dataset.autocompleteUrl, 646 replace: function (url, uriEncodedQuery) { 647 if (that.dataset.autocompleteExtra) { 648 const extra = $(document.querySelector(that.dataset.autocompleteExtra)).val(); 649 return url.replace('QUERY', uriEncodedQuery) + '&extra=' + encodeURIComponent(extra); 650 } 651 return url.replace('QUERY', uriEncodedQuery); 652 }, 653 wildcard: 'QUERY' 654 655 } 656 }) 657 }); 658 }); 659} 660 661/** 662 * Insert text at the current cursor position in an input field. 663 * @param {Element} e The input element. 664 * @param {string} t The text to insert. 665 */ 666function insertTextAtCursor (e, t) { 667 var scrollTop = e.scrollTop; 668 var selectionStart = e.selectionStart; 669 var prefix = e.value.substring(0, selectionStart); 670 var suffix = e.value.substring(e.selectionEnd, e.value.length); 671 e.value = prefix + t + suffix; 672 e.selectionStart = selectionStart + t.length; 673 e.selectionEnd = e.selectionStart; 674 e.focus(); 675 e.scrollTop = scrollTop; 676} 677 678// Send the CSRF token on all AJAX requests 679$.ajaxSetup({ 680 headers: { 681 'X-CSRF-TOKEN': $('meta[name=csrf]').attr('content') 682 } 683}); 684 685/** 686 * Initialisation 687 */ 688$(function () { 689 // Page elements that load automaticaly via AJAX. 690 // This prevents bad robots from crawling resource-intensive pages. 691 $('[data-ajax-url]').each(function () { 692 $(this).load($(this).data('ajaxUrl')); 693 }); 694 695 /** 696 * Select2 - format entries in the select list 697 * @param {Object} data 698 * @returns {string} 699 */ 700 function templateOptionForSelect2 (data) { 701 // This could be a "waiting..." message (data.loading is true) or a response from the server. 702 // Both are already HTML, so no need to reformat it. 703 return data.text; 704 } 705 706 // Autocomplete 707 autocomplete('input[data-autocomplete-url]'); 708 709 // Select2 - activate autocomplete fields 710 const lang = document.documentElement.lang; 711 const select2_languages = { 712 'zh-Hans': 'zh-CN', 713 'zh-Hant': 'zh-TW' 714 }; 715 $('select.select2').select2({ 716 language: select2_languages[lang] || lang, 717 // Needed for elements that are initially hidden. 718 width: '100%', 719 // Do not escape - we do it on the server. 720 escapeMarkup: function (x) { 721 return x; 722 } 723 }); 724 725 // If we clear the select (using the "X" button), we need an empty value 726 // (rather than no value at all) for (non-multiple) selects with name="array[]" 727 $('select.select2:not([multiple])') 728 .on('select2:unselect', function (evt) { 729 $(evt.delegateTarget).html('<option value="" selected></option>'); 730 }); 731 732 // Datatables - locale aware sorting 733 $.fn.dataTableExt.oSort['text-asc'] = function (x, y) { 734 return x.localeCompare(y, document.documentElement.lang, { sensitivity: 'base' }); 735 }; 736 $.fn.dataTableExt.oSort['text-desc'] = function (x, y) { 737 return y.localeCompare(x, document.documentElement.lang, { sensitivity: 'base' }); 738 }; 739 740 // DataTables - start hidden to prevent FOUC. 741 $('table.datatables').each(function () { 742 $(this).DataTable(); 743 $(this).removeClass('d-none'); 744 }); 745 746 // Save button state between pages 747 document.querySelectorAll('[data-toggle=button][data-persist]').forEach((element) => { 748 // Previously selected? 749 if (localStorage.getItem('state-of-' + element.dataset.persist) === 'T') { 750 element.click(); 751 } 752 // Save state on change 753 element.addEventListener('click', (event) => { 754 // Event occurs *before* the state changes, so reverse T/F. 755 localStorage.setItem('state-of-' + event.target.dataset.persist, event.target.classList.contains('active') ? 'F' : 'T'); 756 }); 757 }); 758 759 // Activate the on-screen keyboard 760 var osk_focus_element; 761 $('.wt-osk-trigger').click(function () { 762 // When a user clicks the icon, set focus to the corresponding input 763 osk_focus_element = document.getElementById($(this).data('id')); 764 osk_focus_element.focus(); 765 $('.wt-osk').show(); 766 }); 767 $('.wt-osk-script-button').change(function () { 768 $('.wt-osk-script').prop('hidden', true); 769 $('.wt-osk-script-' + $(this).data('script')).prop('hidden', false); 770 }); 771 $('.wt-osk-shift-button').click(function () { 772 document.querySelector('.wt-osk-keys').classList.toggle('shifted'); 773 }); 774 $('.wt-osk-keys').on('click', '.wt-osk-key', function () { 775 var key = $(this).contents().get(0).nodeValue; 776 var shift_state = $('.wt-osk-shift-button').hasClass('active'); 777 var shift_key = $('sup', this)[0]; 778 if (shift_state && shift_key !== undefined) { 779 key = shift_key.innerText; 780 } 781 webtrees.pasteAtCursor(osk_focus_element, key); 782 if ($('.wt-osk-pin-button').hasClass('active') === false) { 783 $('.wt-osk').hide(); 784 } 785 }); 786 787 $('.wt-osk-close').on('click', function () { 788 $('.wt-osk').hide(); 789 }); 790}); 791 792// Convert data-confirm and data-post-url attributes into useful behavior. 793document.addEventListener('click', (event) => { 794 const target = event.target.closest('a'); 795 796 if (target === null) { 797 return; 798 } 799 800 if ('confirm' in target.dataset && !confirm(target.dataset.confirm)) { 801 event.preventDefault(); 802 return; 803 } 804 805 if ('postUrl' in target.dataset) { 806 const token = document.querySelector('meta[name=csrf]').content; 807 808 fetch(target.dataset.postUrl, { 809 method: 'POST', 810 headers: { 811 'X-CSRF-TOKEN': token, 812 'X-Requested-with': 'XMLHttpRequest', 813 }, 814 }).then((response) => { 815 if ('reloadUrl' in target.dataset) { 816 // Go somewhere else. e.g. home page after logout. 817 document.location = target.dataset.reloadUrl; 818 } else { 819 // Reload the current page. e.g. change language. 820 document.location.reload(); 821 } 822 }).catch((error) => { 823 alert(error); 824 }); 825 } 826}); 827