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 116 /** 117 * @param {Element} datefield 118 * @param {string} dmy 119 */ 120 webtrees.reformatDate = function (datefield, dmy) { 121 const months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; 122 const hijri_months = ['MUHAR', 'SAFAR', 'RABIA', 'RABIT', 'JUMAA', 'JUMAT', 'RAJAB', 'SHAAB', 'RAMAD', 'SHAWW', 'DHUAQ', 'DHUAH']; 123 const hebrew_months = ['TSH', 'CSH', 'KSL', 'TVT', 'SHV', 'ADR', 'ADS', 'NSN', 'IYR', 'SVN', 'TMZ', 'AAV', 'ELL']; 124 const french_months = ['VEND', 'BRUM', 'FRIM', 'NIVO', 'PLUV', 'VENT', 'GERM', 'FLOR', 'PRAI', 'MESS', 'THER', 'FRUC', 'COMP']; 125 const jalali_months = ['FARVA', 'ORDIB', 'KHORD', 'TIR', 'MORDA', 'SHAHR', 'MEHR', 'ABAN', 'AZAR', 'DEY', 'BAHMA', 'ESFAN']; 126 127 let datestr = datefield.value; 128 // if a date has a date phrase marked by () this has to be excluded from altering 129 let datearr = datestr.split('('); 130 let datephrase = ''; 131 if (datearr.length > 1) { 132 datestr = datearr[0]; 133 datephrase = datearr[1]; 134 } 135 136 // Gedcom dates are upper case 137 datestr = datestr.toUpperCase(); 138 // Gedcom dates have no leading/trailing/repeated whitespace 139 datestr = datestr.replace(/\s+/g, ' '); 140 datestr = datestr.replace(/(^\s)|(\s$)/, ''); 141 // Gedcom dates have spaces between letters and digits, e.g. "01JAN2000" => "01 JAN 2000" 142 datestr = datestr.replace(/(\d)([A-Z])/g, '$1 $2'); 143 datestr = datestr.replace(/([A-Z])(\d)/g, '$1 $2'); 144 145 // Shortcut for quarter format, "Q1 1900" => "BET JAN 1900 AND MAR 1900". See [ 1509083 ] 146 if (datestr.match(/^Q ([1-4]) (\d\d\d\d)$/)) { 147 datestr = 'BET ' + months[RegExp.$1 * 3 - 3] + ' ' + RegExp.$2 + ' AND ' + months[RegExp.$1 * 3 - 1] + ' ' + RegExp.$2; 148 } 149 150 // Shortcut for @#Dxxxxx@ 01 01 1400, etc. 151 if (datestr.match(/^(@#DHIJRI@|HIJRI)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 152 datestr = '@#DHIJRI@' + RegExp.$2 + hijri_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 153 } 154 if (datestr.match(/^(@#DJALALI@|JALALI)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 155 datestr = '@#DJALALI@' + RegExp.$2 + jalali_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 156 } 157 if (datestr.match(/^(@#DHEBREW@|HEBREW)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 158 datestr = '@#DHEBREW@' + RegExp.$2 + hebrew_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 159 } 160 if (datestr.match(/^(@#DFRENCH R@|FRENCH)( \d?\d )(\d?\d)( \d?\d?\d?\d)$/)) { 161 datestr = '@#DFRENCH R@' + RegExp.$2 + french_months[parseInt(RegExp.$3, 10) - 1] + RegExp.$4; 162 } 163 164 // All digit dates 165 datestr = datestr.replaceAll(/(\d\d)(\d\d)(\d\d)(\d\d)/g, function () { 166 if (RegExp.$1 > '12' && RegExp.$3 <= '12' && RegExp.$4 <= '31') { 167 return RegExp.$4 + ' ' + months[RegExp.$3 - 1] + ' ' + RegExp.$1 + RegExp.$2; 168 } 169 if (RegExp.$1 <= '31' && RegExp.$2 <= '12' && RegExp.$3 > '12') { 170 return RegExp.$1 + ' ' + months[RegExp.$2 - 1] + ' ' + RegExp.$3 + RegExp.$4; 171 } 172 return RegExp.$1 + RegExp.$2 + RegExp.$3 + RegExp.$4; 173 }); 174 175 // e.g. 17.11.1860, 3/4/2005 or 1999-12-31. Use locale settings since DMY order is ambiguous. 176 datestr = datestr.replaceAll(/(\d+)([./-])(\d+)([./-])(\d+)/g, function () { 177 let f1 = parseInt(RegExp.$1, 10); 178 let f2 = parseInt(RegExp.$3, 10); 179 let f3 = parseInt(RegExp.$5, 10); 180 let yyyy = new Date().getFullYear(); 181 let yy = yyyy % 100; 182 let cc = yyyy - yy; 183 if (dmy === 'DMY' && f1 <= 31 && f2 <= 12 || f1 > 13 && f1 <= 31 && f2 <= 12 && f3 > 31) { 184 return f1 + ' ' + months[f2 - 1] + ' ' + (f3 >= 100 ? f3 : (f3 <= yy ? f3 + cc : f3 + cc - 100)); 185 } 186 if (dmy === 'MDY' && f1 <= 12 && f2 <= 31 || f2 > 13 && f2 <= 31 && f1 <= 12 && f3 > 31) { 187 return f2 + ' ' + months[f1 - 1] + ' ' + (f3 >= 100 ? f3 : (f3 <= yy ? f3 + cc : f3 + cc - 100)); 188 } 189 if (dmy === 'YMD' && f2 <= 12 && f3 <= 31 || f3 > 13 && f3 <= 31 && f2 <= 12 && f1 > 31) { 190 return f3 + ' ' + months[f2 - 1] + ' ' + (f1 >= 100 ? f1 : (f1 <= yy ? f1 + cc : f1 + cc - 100)); 191 } 192 return RegExp.$1 + RegExp.$2 + RegExp.$3 + RegExp.$4 + RegExp.$5; 193 }); 194 195 datestr = datestr 196 // Shortcuts for date ranges 197 .replace(/^[>]([\w ]+)$/, 'AFT $1') 198 .replace(/^[<]([\w ]+)$/, 'BEF $1') 199 .replace(/^([\w ]+)[-]$/, 'FROM $1') 200 .replace(/^[-]([\w ]+)$/, 'TO $1') 201 .replace(/^[~]([\w ]+)$/, 'ABT $1') 202 .replace(/^[*]([\w ]+)$/, 'EST $1') 203 .replace(/^[#]([\w ]+)$/, 'CAL $1') 204 .replace(/^([\w ]+) ?- ?([\w ]+)$/, 'BET $1 AND $2') 205 .replace(/^([\w ]+) ?~ ?([\w ]+)$/, 'FROM $1 TO $2') 206 // Convert full months to short months 207 .replaceAll('JANUARY', 'JAN') 208 .replaceAll('FEBRUARY', 'FEB') 209 .replaceAll('MARCH', 'MAR') 210 .replaceAll('APRIL', 'APR') 211 .replaceAll('JUNE', 'JUN') 212 .replaceAll('JULY', 'JUL') 213 .replaceAll('AUGUST', 'AUG') 214 .replaceAll('SEPTEMBER', 'SEP') 215 .replaceAll('OCTOBER', 'OCT') 216 .replaceAll('NOVEMBER', 'NOV') 217 .replaceAll('DECEMBER', 'DEC') 218 // Americans enter dates as SEP 20, 1999 219 .replaceAll(/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)\.? (\d\d?)[, ]+(\d\d\d\d)/g, '$2 $1 $3') 220 // Apply leading zero to day numbers 221 .replaceAll(/(^| )(\d [A-Z]{3,5} \d{4})/g, '$10$2'); 222 223 if (datephrase) { 224 datestr = datestr + ' (' + datephrase; 225 } 226 227 // Only update it if is has been corrected - otherwise input focus 228 // moves to the end of the field unnecessarily 229 if (datefield.value !== datestr) { 230 datefield.value = datestr; 231 } 232 }; 233 234 let monthLabels = []; 235 monthLabels[1] = 'January'; 236 monthLabels[2] = 'February'; 237 monthLabels[3] = 'March'; 238 monthLabels[4] = 'April'; 239 monthLabels[5] = 'May'; 240 monthLabels[6] = 'June'; 241 monthLabels[7] = 'July'; 242 monthLabels[8] = 'August'; 243 monthLabels[9] = 'September'; 244 monthLabels[10] = 'October'; 245 monthLabels[11] = 'November'; 246 monthLabels[12] = 'December'; 247 248 let monthShort = []; 249 monthShort[1] = 'JAN'; 250 monthShort[2] = 'FEB'; 251 monthShort[3] = 'MAR'; 252 monthShort[4] = 'APR'; 253 monthShort[5] = 'MAY'; 254 monthShort[6] = 'JUN'; 255 monthShort[7] = 'JUL'; 256 monthShort[8] = 'AUG'; 257 monthShort[9] = 'SEP'; 258 monthShort[10] = 'OCT'; 259 monthShort[11] = 'NOV'; 260 monthShort[12] = 'DEC'; 261 262 let daysOfWeek = []; 263 daysOfWeek[0] = 'S'; 264 daysOfWeek[1] = 'M'; 265 daysOfWeek[2] = 'T'; 266 daysOfWeek[3] = 'W'; 267 daysOfWeek[4] = 'T'; 268 daysOfWeek[5] = 'F'; 269 daysOfWeek[6] = 'S'; 270 271 let weekStart = 0; 272 273 /** 274 * @param {string} jan 275 * @param {string} feb 276 * @param {string} mar 277 * @param {string} apr 278 * @param {string} may 279 * @param {string} jun 280 * @param {string} jul 281 * @param {string} aug 282 * @param {string} sep 283 * @param {string} oct 284 * @param {string} nov 285 * @param {string} dec 286 * @param {string} sun 287 * @param {string} mon 288 * @param {string} tue 289 * @param {string} wed 290 * @param {string} thu 291 * @param {string} fri 292 * @param {string} sat 293 * @param {number} day 294 */ 295 webtrees.calLocalize = function (jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec, sun, mon, tue, wed, thu, fri, sat, day) { 296 monthLabels[1] = jan; 297 monthLabels[2] = feb; 298 monthLabels[3] = mar; 299 monthLabels[4] = apr; 300 monthLabels[5] = may; 301 monthLabels[6] = jun; 302 monthLabels[7] = jul; 303 monthLabels[8] = aug; 304 monthLabels[9] = sep; 305 monthLabels[10] = oct; 306 monthLabels[11] = nov; 307 monthLabels[12] = dec; 308 daysOfWeek[0] = sun; 309 daysOfWeek[1] = mon; 310 daysOfWeek[2] = tue; 311 daysOfWeek[3] = wed; 312 daysOfWeek[4] = thu; 313 daysOfWeek[5] = fri; 314 daysOfWeek[6] = sat; 315 316 if (day >= 0 && day < 7) { 317 weekStart = day; 318 } 319 }; 320 321 /** 322 * @param {string} dateDivId 323 * @param {string} dateFieldId 324 * @returns {boolean} 325 */ 326 webtrees.calendarWidget = function (dateDivId, dateFieldId) { 327 let dateDiv = document.getElementById(dateDivId); 328 let dateField = document.getElementById(dateFieldId); 329 330 if (dateDiv.style.visibility === 'visible') { 331 dateDiv.style.visibility = 'hidden'; 332 return false; 333 } 334 if (dateDiv.style.visibility === 'show') { 335 dateDiv.style.visibility = 'hide'; 336 return false; 337 } 338 339 /* Javascript calendar functions only work with precise gregorian dates "D M Y" or "Y" */ 340 let greg_regex = /((\d+ (JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC) )?\d+)/i; 341 let date; 342 if (greg_regex.exec(dateField.value)) { 343 date = new Date(RegExp.$1); 344 } else { 345 date = new Date(); 346 } 347 348 dateDiv.innerHTML = calGenerateSelectorContent(dateFieldId, dateDivId, date); 349 if (dateDiv.style.visibility === 'hidden') { 350 dateDiv.style.visibility = 'visible'; 351 return false; 352 } 353 if (dateDiv.style.visibility === 'hide') { 354 dateDiv.style.visibility = 'show'; 355 return false; 356 } 357 358 return false; 359 }; 360 361 /** 362 * @param {string} dateFieldId 363 * @param {string} dateDivId 364 * @param {Date} date 365 * @returns {string} 366 */ 367 function calGenerateSelectorContent (dateFieldId, dateDivId, date) { 368 let i, j; 369 let content = '<table border="1"><tr>'; 370 content += '<td><select class="form-control" id="' + dateFieldId + '_daySelect" onchange="return webtrees.calUpdateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');">'; 371 for (i = 1; i < 32; i++) { 372 content += '<option value="' + i + '"'; 373 if (date.getDate() === i) { 374 content += ' selected="selected"'; 375 } 376 content += '>' + i + '</option>'; 377 } 378 content += '</select></td>'; 379 content += '<td><select class="form-control" id="' + dateFieldId + '_monSelect" onchange="return webtrees.calUpdateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');">'; 380 for (i = 1; i < 13; i++) { 381 content += '<option value="' + i + '"'; 382 if (date.getMonth() + 1 === i) { 383 content += ' selected="selected"'; 384 } 385 content += '>' + monthLabels[i] + '</option>'; 386 } 387 content += '</select></td>'; 388 content += '<td><input class="form-control" type="text" id="' + dateFieldId + '_yearInput" size="5" value="' + date.getFullYear() + '" onchange="return webtrees.calUpdateCalendar(\'' + dateFieldId + '\', \'' + dateDivId + '\');" /></td></tr>'; 389 content += '<tr><td colspan="3">'; 390 content += '<table width="100%">'; 391 content += '<tr>'; 392 j = weekStart; 393 for (i = 0; i < 7; i++) { 394 content += '<td '; 395 content += 'class="descriptionbox"'; 396 content += '>'; 397 content += daysOfWeek[j]; 398 content += '</td>'; 399 j++; 400 if (j > 6) { 401 j = 0; 402 } 403 } 404 content += '</tr>'; 405 406 let tdate = new Date(date.getFullYear(), date.getMonth(), 1); 407 let day = tdate.getDay(); 408 day = day - weekStart; 409 let daymilli = 1000 * 60 * 60 * 24; 410 tdate = tdate.getTime() - (day * daymilli) + (daymilli / 2); 411 tdate = new Date(tdate); 412 413 for (j = 0; j < 6; j++) { 414 content += '<tr>'; 415 for (i = 0; i < 7; i++) { 416 content += '<td '; 417 if (tdate.getMonth() === date.getMonth()) { 418 if (tdate.getDate() === date.getDate()) { 419 content += 'class="descriptionbox"'; 420 } else { 421 content += 'class="optionbox"'; 422 } 423 } else { 424 content += 'style="background-color:#EAEAEA; border: solid #AAAAAA 1px;"'; 425 } 426 content += '><a href="#" onclick="return webtrees.calDateClicked(\'' + dateFieldId + '\', \'' + dateDivId + '\', ' + tdate.getFullYear() + ', ' + tdate.getMonth() + ', ' + tdate.getDate() + ');">'; 427 content += tdate.getDate(); 428 content += '</a></td>'; 429 let datemilli = tdate.getTime() + daymilli; 430 tdate = new Date(datemilli); 431 } 432 content += '</tr>'; 433 } 434 content += '</table>'; 435 content += '</td></tr>'; 436 content += '</table>'; 437 438 return content; 439 } 440 441 /** 442 * @param {string} dateFieldId 443 * @param {number} year 444 * @param {number} month 445 * @param {number} day 446 * @returns {boolean} 447 */ 448 function calSetDateField (dateFieldId, year, month, day) { 449 let dateField = document.getElementById(dateFieldId); 450 dateField.value = (day < 10 ? '0' : '') + day + ' ' + monthShort[month + 1] + ' ' + year; 451 return false; 452 } 453 454 /** 455 * @param {string} dateFieldId 456 * @param {string} dateDivId 457 * @returns {boolean} 458 */ 459 webtrees.calUpdateCalendar = function (dateFieldId, dateDivId) { 460 let dateSel = document.getElementById(dateFieldId + '_daySelect'); 461 if (!dateSel) { 462 return false; 463 } 464 let monthSel = document.getElementById(dateFieldId + '_monSelect'); 465 if (!monthSel) { 466 return false; 467 } 468 let yearInput = document.getElementById(dateFieldId + '_yearInput'); 469 if (!yearInput) { 470 return false; 471 } 472 473 let month = parseInt(monthSel.options[monthSel.selectedIndex].value, 10); 474 month = month - 1; 475 476 let date = new Date(yearInput.value, month, dateSel.options[dateSel.selectedIndex].value); 477 calSetDateField(dateFieldId, date.getFullYear(), date.getMonth(), date.getDate()); 478 479 let dateDiv = document.getElementById(dateDivId); 480 if (!dateDiv) { 481 alert('no dateDiv ' + dateDivId); 482 return false; 483 } 484 dateDiv.innerHTML = calGenerateSelectorContent(dateFieldId, dateDivId, date); 485 486 return false; 487 }; 488 489 /** 490 * @param {string} dateFieldId 491 * @param {string} dateDivId 492 * @param {number} year 493 * @param {number} month 494 * @param {number} day 495 * @returns {boolean} 496 */ 497 webtrees.calDateClicked = function (dateFieldId, dateDivId, year, month, day) { 498 calSetDateField(dateFieldId, year, month, day); 499 webtrees.calendarWidget(dateDivId, dateFieldId); 500 return false; 501 }; 502 503 /** 504 * Persistent checkbox options to hide/show extra data. 505 * @param {string} element_id 506 */ 507 webtrees.persistentToggle = function (element_id) { 508 const element = document.getElementById(element_id); 509 const key = 'state-of-' + element_id; 510 const state = localStorage.getItem(key); 511 512 // Previously selected? 513 if (state === 'true') { 514 element.click(); 515 } 516 517 // Remember state for the next page load. 518 element.addEventListener('change', function () { 519 localStorage.setItem(key, element.checked); 520 }); 521 }; 522 523 /** 524 * @param {Element} field 525 * @param {string} pos 526 * @param {string} neg 527 */ 528 function reformatLatLong (field, pos, neg) { 529 // valid LATI or LONG according to Gedcom standard 530 // pos (+) : N or E 531 // neg (-) : S or W 532 let txt = field.value.toUpperCase(); 533 txt = txt.replace(/(^\s*)|(\s*$)/g, ''); // trim 534 txt = txt.replace(/ /g, ':'); // N12 34 ==> N12.34 535 txt = txt.replace(/\+/g, ''); // +17.1234 ==> 17.1234 536 txt = txt.replace(/-/g, neg); // -0.5698 ==> W0.5698 537 txt = txt.replace(/,/g, '.'); // 0,5698 ==> 0.5698 538 // 0°34'11 ==> 0:34:11 539 txt = txt.replace(/\u00b0/g, ':'); // ° 540 txt = txt.replace(/\u0027/g, ':'); // ' 541 // 0:34:11.2W ==> W0.5698 542 txt = txt.replace(/^([0-9]+):([0-9]+):([0-9.]+)(.*)/g, function ($0, $1, $2, $3, $4) { 543 let n = parseFloat($1); 544 n += ($2 / 60); 545 n += ($3 / 3600); 546 n = Math.round(n * 1E4) / 1E4; 547 return $4 + n; 548 }); 549 // 0:34W ==> W0.5667 550 txt = txt.replace(/^([0-9]+):([0-9]+)(.*)/g, function ($0, $1, $2, $3) { 551 let n = parseFloat($1); 552 n += ($2 / 60); 553 n = Math.round(n * 1E4) / 1E4; 554 return $3 + n; 555 }); 556 // 0.5698W ==> W0.5698 557 txt = txt.replace(/(.*)(NSEW])$/g, '$2$1'); 558 // 17.1234 ==> N17.1234 559 if (txt && txt.charAt(0) !== neg && txt.charAt(0) !== pos) { 560 txt = pos + txt; 561 } 562 field.value = txt; 563 } 564 565 /** 566 * @param {Element} field 567 */ 568 webtrees.reformatLatitude = function (field) { 569 return reformatLatLong(field, 'N', 'S'); 570 }; 571 572 /** 573 * @param {Element} field 574 */ 575 webtrees.reformatLongitude = function (field) { 576 return reformatLatLong(field, 'E', 'W'); 577 }; 578 579 /** 580 * Initialize autocomplete elements. 581 * @param {string} selector 582 */ 583 webtrees.autocomplete = function (selector) { 584 // Use typeahead/bloodhound for autocomplete 585 $(selector).each(function () { 586 const that = this; 587 $(this).typeahead(null, { 588 display: 'value', 589 limit: 0, 590 source: new Bloodhound({ 591 datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), 592 queryTokenizer: Bloodhound.tokenizers.whitespace, 593 remote: { 594 url: this.dataset.autocompleteUrl, 595 replace: function (url, uriEncodedQuery) { 596 if (that.dataset.autocompleteExtra) { 597 const extra = $(document.querySelector(that.dataset.autocompleteExtra)).val(); 598 return url.replace('QUERY', uriEncodedQuery) + '&extra=' + encodeURIComponent(extra); 599 } 600 return url.replace('QUERY', uriEncodedQuery); 601 }, 602 wildcard: 'QUERY' 603 } 604 }) 605 }); 606 }); 607 }; 608}(window.webtrees = window.webtrees || {})); 609 610// Send the CSRF token on all AJAX requests 611$.ajaxSetup({ 612 headers: { 613 'X-CSRF-TOKEN': $('meta[name=csrf]').attr('content') 614 } 615}); 616 617/** 618 * Initialisation 619 */ 620$(function () { 621 // Page elements that load automatically via AJAX. 622 // This prevents bad robots from crawling resource-intensive pages. 623 $('[data-ajax-url]').each(function () { 624 $(this).load($(this).data('ajaxUrl')); 625 }); 626 627 // Autocomplete 628 webtrees.autocomplete('input[data-autocomplete-url]'); 629 630 // Select2 - activate autocomplete fields 631 const lang = document.documentElement.lang; 632 const select2_languages = { 633 'zh-Hans': 'zh-CN', 634 'zh-Hant': 'zh-TW' 635 }; 636 $('select.select2').select2({ 637 language: select2_languages[lang] || lang, 638 // Needed for elements that are initially hidden. 639 width: '100%', 640 // Do not escape - we do it on the server. 641 escapeMarkup: function (x) { 642 return x; 643 } 644 }); 645 646 // If we clear the select (using the "X" button), we need an empty value 647 // (rather than no value at all) for (non-multiple) selects with name="array[]" 648 $('select.select2:not([multiple])') 649 .on('select2:unselect', function (evt) { 650 $(evt.delegateTarget).html('<option value="" selected></option>'); 651 }); 652 653 // Datatables - locale aware sorting 654 $.fn.dataTableExt.oSort['text-asc'] = function (x, y) { 655 return x.localeCompare(y, document.documentElement.lang, { sensitivity: 'base' }); 656 }; 657 $.fn.dataTableExt.oSort['text-desc'] = function (x, y) { 658 return y.localeCompare(x, document.documentElement.lang, { sensitivity: 'base' }); 659 }; 660 661 // DataTables - start hidden to prevent FOUC. 662 $('table.datatables').each(function () { 663 $(this).DataTable(); 664 $(this).removeClass('d-none'); 665 }); 666 667 // Save button state between pages 668 document.querySelectorAll('[data-toggle=button][data-persist]').forEach((element) => { 669 // Previously selected? 670 if (localStorage.getItem('state-of-' + element.dataset.persist) === 'T') { 671 element.click(); 672 } 673 // Save state on change 674 element.addEventListener('click', (event) => { 675 // Event occurs *before* the state changes, so reverse T/F. 676 localStorage.setItem('state-of-' + event.target.dataset.persist, event.target.classList.contains('active') ? 'F' : 'T'); 677 }); 678 }); 679 680 // Activate the on-screen keyboard 681 let osk_focus_element; 682 $('.wt-osk-trigger').click(function () { 683 // When a user clicks the icon, set focus to the corresponding input 684 osk_focus_element = document.getElementById($(this).data('id')); 685 osk_focus_element.focus(); 686 $('.wt-osk').show(); 687 }); 688 $('.wt-osk-script-button').change(function () { 689 $('.wt-osk-script').prop('hidden', true); 690 $('.wt-osk-script-' + $(this).data('script')).prop('hidden', false); 691 }); 692 $('.wt-osk-shift-button').click(function () { 693 document.querySelector('.wt-osk-keys').classList.toggle('shifted'); 694 }); 695 $('.wt-osk-keys').on('click', '.wt-osk-key', function () { 696 let key = $(this).contents().get(0).nodeValue; 697 let shift_state = $('.wt-osk-shift-button').hasClass('active'); 698 let shift_key = $('sup', this)[0]; 699 if (shift_state && shift_key !== undefined) { 700 key = shift_key.innerText; 701 } 702 webtrees.pasteAtCursor(osk_focus_element, key); 703 if ($('.wt-osk-pin-button').hasClass('active') === false) { 704 $('.wt-osk').hide(); 705 } 706 }); 707 708 $('.wt-osk-close').on('click', function () { 709 $('.wt-osk').hide(); 710 }); 711}); 712 713// Convert data-confirm and data-post-url attributes into useful behavior. 714document.addEventListener('click', (event) => { 715 const target = event.target.closest('a,button'); 716 717 if (target === null) { 718 return; 719 } 720 721 if ('confirm' in target.dataset && !confirm(target.dataset.confirm)) { 722 event.preventDefault(); 723 return; 724 } 725 726 if ('postUrl' in target.dataset) { 727 const token = document.querySelector('meta[name=csrf]').content; 728 729 fetch(target.dataset.postUrl, { 730 method: 'POST', 731 headers: { 732 'X-CSRF-TOKEN': token, 733 'X-Requested-with': 'XMLHttpRequest', 734 }, 735 }).then(() => { 736 if ('reloadUrl' in target.dataset) { 737 // Go somewhere else. e.g. home page after logout. 738 document.location = target.dataset.reloadUrl; 739 } else { 740 // Reload the current page. e.g. change language. 741 document.location.reload(); 742 } 743 }).catch((error) => { 744 alert(error); 745 }); 746 } 747}); 748