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