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