1/** 2 * String.prototype.replaceAll() polyfill 3 * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ 4 * @author Chris Ferdinandi 5 * @license MIT 6 */ 7if (!String.prototype.replaceAll) { 8 String.prototype.replaceAll = function(str, newStr) { 9 // If a regex pattern 10 if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') { 11 return this.replace(str, newStr); 12 } 13 // If a string 14 return this.replace(new RegExp(str, 'g'), newStr); 15 } 16} 17 18const isMobile = (/Android|webOS|iPhone|iPad|iPod/i.test(navigator.userAgent)); 19 20function hideMyDiv(el, type, d) { 21 const sel = "#" + type + "-content"; 22 if (d) el.parent().siblings(sel).hide(); 23 else el.parent().siblings(sel).slideUp(150); 24 el.removeClass("active"); 25 el.find('span').html('▼'); 26} 27 28function showMyDiv(el, type, d) { 29 const sel = "#" + type + "-content"; 30 if (d) el.parent().siblings(sel).show(); 31 else el.parent().siblings(sel).slideDown(150); 32 el.addClass("active"); 33 el.find('span').html('▲'); 34} 35 36function ajaxSignup() { 37 // abort any pending request 38 if (signUpRequest) 39 signUpRequest.abort(); 40 41 const formData = $("#signup-form").eq(0).serializeArray(); 42 formData.push({name: 'csrf_token', value: window['CSRFToken']}); 43 44 var signUpRequest = $.ajax({ 45 url: "/signup_handler.php", 46 type: "post", 47 data: formData 48 }); 49 50 signUpRequest.done(function (response, textStatus, jqXHR) { 51 if (response.indexOf("successful") > 0) { 52 $("#signup-callback-div").removeClass("alert-error").addClass("alert-success"); 53 setTimeout(function () { 54 hideMyDiv($('#signup-trigger'), "signup"); 55 }, 1500); 56 } else { 57 $("#signup-callback-div").addClass("alert-error"); 58 } 59 $("#signup-callback-div").show(); 60 $("#signup-callback-text").html(response); 61 }); 62 63 signUpRequest.fail(function (jqXHR, textStatus, errorThrown) { 64 $("#signup-callback-div").addClass("alert-error").show().text(JSON.parse(jqXHR.responseText || "Erreur")); 65 }); 66 67 signUpRequest.always(function () { 68 try { 69 if (window.turnstile && document.querySelector('.cf-turnstile')) { 70 turnstile.reset(document.querySelector('.cf-turnstile')); 71 } 72 } catch(e) {} 73 document.getElementById('cf-turnstile-response').value = ''; 74 }); 75} 76 77function ajaxLogin(isLogout) { 78 79 if (typeof(isLogout) !== "boolean") { 80 isLogout = false; 81 } 82 83 // abort any pending request 84 if (loginRequest) 85 loginRequest.abort(); 86 87 const formData = $("#login-form").eq(0).serializeArray(); 88 formData.push({name: 'csrf_token', value: window['CSRFToken']}); 89 90 var loginRequest = $.ajax({ 91 url: "/login_handler.php", 92 type: "post", 93 data: formData 94 }); 95 96 const redirectAfter = function() { 97 if (window.location.href.indexOf("login_error") > -1) { 98 window.location = 'https://my.calcs.quest/collectors/'; 99 } else { 100 location.reload(); 101 } 102 }; 103 104 loginRequest.done(function (response, textStatus, jqXHR) { 105 console.log(response); 106 if (response.indexOf("successfully") > 0) { 107 $("#login-callback-div").removeClass("alert-warning").addClass("alert-success").text("Success"); 108 setTimeout(redirectAfter, 1000); 109 } else if (response.includes("Disconnected")) { 110 redirectAfter(); 111 } else { 112 $("#login-callback-div").addClass("alert-error"); 113 } 114 $("#login-callback-div").show(); 115 $("#login-callback-text").html(response); 116 }); 117 118 loginRequest.fail(function (jqXHR, textStatus, errorThrown) { 119 if (isLogout) { 120 redirectAfter(); 121 } else { 122 $("#login-callback-div").addClass("alert-error").show().text(JSON.parse(jqXHR.responseText || "Erreur")); 123 } 124 }); 125} 126function ajaxLogout() 127{ 128 ajaxLogin(true /* isLogout */); 129} 130 131function ajaxProfileEdit(theData, context, okCallback, failCallback) 132{ 133 const dataSerialized = theData.eq(0).serializeArray(); 134 dataSerialized.push({name: 'profileField', value: theData.attr("rel")}); 135 dataSerialized.push({name: 'targetProfileUID', value: window._targetProfileUID}); 136 dataSerialized.push({name: 'csrf_token', value: window['CSRFToken']}); 137 138 $.ajax({ 139 url: "/profileEdit_handler.php", 140 type: "post", 141 data: dataSerialized 142 }).done(function (response, textStatus, jqXHR) { 143 okCallback && okCallback("Updated successfully"); 144 backToNormalDisplay(context, true, response); 145 }).fail(function (jqXHR, textStatus, errorThrown) { 146 const msg = `Server error: ${jqXHR.responseJSON || jqXHR.responseText}`;
147 failCallback && failCallback(msg); 148 backToNormalDisplay(context, false, msg); 149 }); 150} 151 152function backToNormalDisplay($this, isGood, message) { 153 if ($this) { 154 $this.html("<span class='glyphicon glyphicon-pencil'></span>").removeData('editing').toggleClass("btn-primary"); 155 const el = $this.siblings().eq(0); 156 el.replaceWith($("<span />", { text: (message.indexOf("OK") < 0) ? el.attr("oldData") : el.val() })); 157 if (!isGood) { 158 alert("Error! " + (message || "")); 159 console.log('error', message); 160 } else { 161 // todo : popover right avec 'message' 162 console.log("popover serveur ok mais avec message : " + message); 163 } 164 } 165 setTimeout(() => { document.location.reload(); }, 100); 166} 167 168function select2CalcThumbs(selection, container) { 169 const isVariantSelect = container?.id?.startsWith("select2-variantsSelect") ?? false; 170 if ($(selection?.element).is("[aria-disabled='true']") || 171 (!isVariantSelect && window.currentlySelectedBrand && $(container).is('li[role="option"]') && !selection.text.startsWith(window.currentlySelectedBrand))) 172 { 173 return null; 174 } 175 if (!selection.id && !selection?.element?.dataset?.image) { 176 return selection.text; 177 } 178 const isProto = (!!selection.isproto) || ('1' === (selection.element ? selection.element.dataset.isproto : '0')); 179 const year = selection.year || (selection.element ? selection.element.dataset.year : ''); 180 if (!selection.origText) { 181 selection.origText = selection.text; 182 if (selection.text.toLowerCase().includes("plus")) { 183 selection.text += ` ${selection.origText.replaceAll(/\s+plus\b/gi, ' +')},${selection.origText.replaceAll(/\s+plus\b/gi, '+')}`; 184 } 185 if (selection.text.toLowerCase().includes("silver edition")) { 186 selection.text += ` ${selection.origText.replaceAll(/\bsilver edition\b/gi, ' SE')},${selection.origText.replaceAll(/\s+silver edition\b/gi, 'SE')}`; 187 } 188 if (selection.altNames && selection.altNames.length) { 189 selection.text += ` ${selection.altNames.toString()}`; 190 } 191 } 192 const origName = selection.origText; 193 const img = selection.image || (selection.element ? selection.element.dataset.image : null); 194 const thumb = $('<span class="select2-item-thumb-wrapper">').attr('title', origName); 195 thumb.toggleClass('empty', !img).append(img 196 ? $('<img class="select2-item-thumb" alt="thumbnail">').attr('src', img) 197 : $('<span class="select2-item-thumb">')); 198 const label = $('<span class="select2-item-label-wrapper">').attr('title', origName).text(origName); 199 if (selection.dates) { 200 label.attr({ 'data-reldate': selection.dates[0], 'data-discdate': selection.dates[1] }); 201 } 202 if (selection.text !== origName) { 203 label.append($('<span>').hide().text(selection.text.slice(origName.length))); 204 } 205 let result = thumb.add(label).add($('<span>').css('flex-grow', 1)); 206 if (isProto) { 207 result = result.add($('<span class="select2-item-proto-label-wrapper label label-primary" title="Prototype">').text('Proto')); 208 } 209 if (year && year.length) { 210 result = result.add($('<span class="select2-item-year-label-wrapper">').text(year)); 211 } 212 return result; 213} 214 215/** 216 * Keep exported member text literal in formula-enabled spreadsheet clients. 217 */ 218function neutralizeSpreadsheetFormula(value) { 219 return typeof value === 'string' && /^[\s\u0000-\u001f\u007f-\u009f]*[=+\-@]/u.test(value) ? "'" + value : value; 220} 221 222function prepareClipboardCell(value) { 223 // Tabs and line breaks inside a field must not create additional clipboard cells. 224 return neutralizeSpreadsheetFormula(typeof value === 'string' ? value.replace(/[\t\r\n\u2028\u2029]/g, ' ') : value); 225} 226 227function prepareSpreadsheetTextExport(data, forClipboard = false) { 228 const prepareCell = forClipboard ? prepareClipboardCell : neutralizeSpreadsheetFormula; 229 data.header = data.header.map(prepareCell); 230 data.body = data.body.map(row => row.map(prepareCell)); 231 if (data.footer) { 232 data.footer = data.footer.map(prepareCell); 233 } 234} 235 236/** 237 * Format timestamps to relative time (e.g., "2 hours ago") 238 */ 239function formatTimestampsNicely() { 240 document.querySelectorAll('.msgTime[data-ts]').forEach(function(timeElement) { 241 const timestamp = parseInt(timeElement.getAttribute('data-ts')); 242 if (timestamp) { 243 const date = new Date(timestamp * 1000); 244 const now = new Date(); 245 const diffMs = now - date; 246 const diffSecs = Math.floor(diffMs / 1000); 247 const diffMins = Math.floor(diffSecs / 60); 248 const diffHours = Math.floor(diffMins / 60); 249 const diffDays = Math.floor(diffHours / 24); 250 251 let timeString; 252 if (diffSecs < 60) { 253 timeString = 'just now'; 254 } else if (diffMins < 60) { 255 timeString = diffMins + ' minute' + (diffMins !== 1 ? 's' : '') + ' ago'; 256 } else if (diffHours < 24) { 257 timeString = diffHours + ' hour' + (diffHours !== 1 ? 's' : '') + ' ago'; 258 } else if (diffDays < 7) { 259 timeString = diffDays + ' day' + (diffDays !== 1 ? 's' : '') + ' ago'; 260 } else { 261 const options = { year: 'numeric', month: 'short', day: 'numeric' }; 262 timeString = date.toLocaleDateString(undefined, options); 263 } 264
265 timeElement.textContent = timeString; 266 timeElement.title = date.toLocaleString(); 267 } 268 }); 269} 270 271$(document).ready(function () 272{ 273 const itemType = (document.location.href.includes('/accessories') || (new URL(document.location)).searchParams.get('itemType') === 'accessory') ? 'accessory' : 'calc'; 274 const calcType = (window._calcType || (new URL(document.location)).searchParams.get('subtype') || 'GRAPH').toUpperCase() === 'DOTM' ? 'DOTM' : 'GRAPH'; 275 const profileContextQuery = itemType === 'calc' && calcType === 'DOTM' ? 'subtype=dotm' : `itemType=${itemType}`; 276 const darkModeStyleURL = $("#darkModeStyle").data('url'); 277 const toggleStyle = function(wantsDarkMode) { 278 if (wantsDarkMode) { 279 $("#darkModeStyle").attr('media', 'screen').html(`@import "${darkModeStyleURL}" screen`); 280 localStorage && localStorage.setItem('dark_mode', "1"); 281 document.cookie = 'dark_mode=1; expires=' + new Date(2147483647 * 1000).toUTCString(); 282 } else { 283 $("#darkModeStyle").attr('media', 'not-all'); 284 localStorage && localStorage.setItem('dark_mode', "0"); 285 document.cookie = 'dark_mode=0; expires=' + new Date(2147483647 * 1000).toUTCString(); 286 } 287 } 288 $("#darkModeDisabler").on('click', function() { toggleStyle(false); }); 289 $("#darkModeEnabler").on('click', function() { toggleStyle(true); }); 290 if (localStorage) { 291 const darkModeOverride = localStorage.getItem('dark_mode'); 292 if (darkModeOverride) { toggleStyle(darkModeOverride === '1'); } 293 } 294 295 let tooltipsDisabled = false; 296 if (!localStorage || !(tooltipsDisabled = (localStorage.getItem('disableTooltips') === 'true'))) 297 { 298 $("body").tooltip({selector:'.hasTooltip', container: 'body'}); 299 $("nav").tooltip({selector:'.hasLocalTooltip'}); 300 $("#profileTable,#setsContainer,#feedItemsContainer,#itemPageDisplay,#collecStats").tooltip({selector:'.hasHTMLTooltip', container: 'body', html:true}); 301 $("#userProfileMetadataContainer.verified").tooltip({container:'#userProfileMetadataContainer'}); 302 } 303 $("#disableTooltipsCheckbox").prop('checked', tooltipsDisabled).on('change', function() { 304 localStorage && localStorage.setItem('disableTooltips', this.checked); 305 }).parent().tooltip({container: 'body'}); 306 307 const applyScientificButtonsVisibility = function() { 308 const shouldHideScientificButtons = localStorage && localStorage.getItem('hideScientificButtons') === 'true'; 309 $(".scientific-calc-context-button:not(.active)").toggle(!shouldHideScientificButtons); 310 }; 311 $("#hideScientificButtonsCheckbox").prop('checked', localStorage && localStorage.getItem('hideScientificButtons') === 'true').on('change', function() { 312 localStorage && localStorage.setItem('hideScientificButtons', this.checked); 313 applyScientificButtonsVisibility(); 314 }); 315 applyScientificButtonsVisibility(); 316 317 let optionDisplayHostedForSomeoneElse = true; 318 if (localStorage) { 319 if (localStorage.getItem('optionDisplayHostedForSomeoneElse') === 'false') { 320 optionDisplayHostedForSomeoneElse = false; 321 } 322 $("#optionDisplayItemsHostedForSomeoneElse").prop('checked', optionDisplayHostedForSomeoneElse).on('change', function() { 323 localStorage.setItem('optionDisplayHostedForSomeoneElse', this.checked); 324 }) 325 } 326 327 if (!isMobile && itemType === 'calc' && /^\/(profile|user|u)\b/.test(document.location.pathname)) 328 { 329 $('#profileTable').popover({ 330 selector: 'tr td:first-child > div.hasPopover', 331 title: function () { return '<b>' + $(this).find('span.calcName').text() + '</b> <small>' + $(this).find('span.calcName').data('group') + '</small>'; }, 332 content: function () { const key = 'item_' + $(this).parent().data('itemid'); return userItemData && (key in userItemData) ? userItemData[key]['overall_descr'] : ''; }, 333 html: true, 334 container: 'body', 335 trigger: 'hover focus', 336 placement: $(document).width() < 1650 ? 'right' : 'left', 337 template: '<div class="popover itemInfo" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><img src="" alt=""/><div class="popover-content"></div></div>' 338 }).on('inserted.bs.popover', function (e) { 339 const target = $(e.target);
340 const key = 'item_' + target.parent().data('itemid'); 341 if (userItemData && (key in userItemData)) { 342 const popover = $("div.popover#" + target.attr('aria-describedby')); 343 popover.find('img').last().attr('src', userItemData[key]['thumb']); 344 } 345 }); 346 } 347 348 $('#login-trigger').click(function (e) { 349 e.stopPropagation(); 350 hideMyDiv($('#signup-trigger'), "signup", 1); 351 if ($(this).hasClass('active')) hideMyDiv($(this), "login"); 352 else showMyDiv($(this), "login"); 353 }); 354 355 $('#signup-trigger').click(function (e) { 356 e.stopPropagation(); 357 hideMyDiv($('#login-trigger'), "login", 1); 358 if ($(this).hasClass('active')) hideMyDiv($(this), "signup"); 359 else showMyDiv($(this), "signup"); 360 }); 361 362 $('#moncompte-trigger').click(function (e) { 363 e.stopPropagation(); 364 }); 365 366 $("#oauth-signup-form").on("submit", (ev) => { 367 if (!confirm('This will create your account - make sure you do not already have one, if so, log in first! Continue?')) { 368 ev.stopPropagation(); 369 ev.preventDefault(); 370 } 371 }); 372 373 $(document).click(function (e) { 374 if (!($('#login-trigger, #signup-trigger').hasClass('active'))) { return; } 375 if (e.target && $(e.target).parents("#login").index() < 0) { 376 hideMyDiv($('#login-trigger'), "login"); 377 hideMyDiv($('#signup-trigger'), "signup"); 378 e.preventDefault(); 379 e.stopPropagation(); 380 } 381 }); 382 383 /**************************************/ 384 385 $(".editProfileFieldButton").click(function (e) { 386 const $this = $(this); 387 let el; 388 if ($this.data('editing') !== '1') 389 { 390 $this.html("Save").data('editing', '1').attr('type', 'submit').toggleClass("btn-primary"); 391 el = $this.siblings().eq(0); 392 const tmp = $("<input />", { 393 val: el.data('textval') ? JSON.parse(el.data('textval')) : el.text(), 394 oldData: el.text(), 395 rel: $this.parent().prev().attr('id'), 396 name: 'profileValue', 397 type: el.data('opt') === 'num' ? 'number' : 'text', 398 maxlength: $this.parent().prev().attr('id') === 'bio' ? 150 : null 399 }); 400 el.replaceWith(tmp); 401 tmp.focus(); 402 } else { 403 el = $this.siblings().eq(0); 404 if (el.attr("oldData") != el.val()) { 405 ajaxProfileEdit(el, $this); 406 } else { 407 backToNormalDisplay($this, true, "No update."); 408 } 409 } 410 e.preventDefault(); 411 e.stopPropagation(); 412 }); 413 414 $("#titleSelect, #profileVisibilitySelect").on('change', function(e) { 415 ajaxProfileEdit($('<input />', { val: this.value, rel: $(this).attr('name'), name: 'profileValue' }), null, alert, alert); 416 e.stopPropagation(); 417 e.preventDefault(); 418 }); 419 420 /**************************************/ 421 422 $("#signup-submit").click(ajaxSignup); 423 $("#login-submit").click(ajaxLogin); 424 $("#logout-button").click(ajaxLogout); 425 426 $("#signup-form, #login-form").submit(function (e) { 427 return false; // ajax takes over. 428 }); 429 430 /**************************************/ 431 432 const isValidHttpUrl = function(string) { 433 let url; 434 try { 435 url = new URL(string); 436 } catch (_) { 437 return false; 438 } 439 return url.protocol === "http:" || url.protocol === "https:"; 440 } 441 442 let isEditing = false; 443 444 /* 445 * From https://datatables.net/forums/discussion/51536/hide-empty-columns-in-responsive-datatables 446 * check all cells of given datatable and hide each column containing only empty cells 447 * ATTENTION: this will only work if responsive-property in datatables is set to true 448 */ 449 const hideEmptyColumns = function(selector) { 450 const emptyColumnsIndexes = []; // store index of empty columns here 451 // check each column separately for empty cells 452 $(selector).find('th').each(function(i) { 453 if ($(this).index() <= 7) return; // special case for us... 454 // get all cells for current column 455 const cells = $(this).parents('table').find('tr td:nth-child(' + (i + 1) + ')'); 456 let emptyCells = 0; 457 458 cells.each(function(cell) { 459 // increase emptyCells if current cell is empty, trim string to remove possible space
459s in cell 460 if ($(this).html().trim() === '') { 461 emptyCells++; 462 } 463 }); 464 465 // if all cells are empty push current column to emptyColumns 466 if (emptyCells === $(cells).length) { 467 emptyColumnsIndexes.push($(this).index()); 468 } 469 }); 470 471 // only make changes if there are columns to hide 472 if (emptyColumnsIndexes.length > 0) { 473 const dt = $(selector).DataTable(); 474 dt.columns(emptyColumnsIndexes).visible(false); 475 if (isMobile) { 476 dt.columns.adjust().responsive.rebuild(); 477 dt.columns.adjust().responsive.recalc(); 478 } else { 479 dt.columns.adjust(); 480 } 481 } 482 } 483 484 let dtSearch = (myFilter) => {}; // will be replaced when ready 485 486 const setupDataTable = function() { 487 const theDOMTable = $('table.makedatatable'); 488 const isCollectorsTable = theDOMTable.data('label') === 'collectorsTable'; 489 const isProfileTable = theDOMTable.data('label') === 'profileTable'; 490 const isLatestTable = theDOMTable.data('label') === 'latestTable'; 491 const isSearchTable = theDOMTable.data('label') === 'searchTable'; 492 const isAchievementsTable = theDOMTable.data('label') === 'achievementsTable'; 493 const isSearchOrProfile = (isSearchTable || isProfileTable); 494 const isCollectionTable = (isSearchTable || isProfileTable || isLatestTable); 495 const needAdvancedFiltering = isSearchOrProfile; 496 const collectionColumnDefs = isCollectionTable ? [ 497 { 498 targets: [0], 499 orderData: [1, 3, 6, 8] 500 } 501 ] : []; 502 const profileColumnDefs = isProfileTable ? [ 503 { 504 targets: 'export-only', 505 visible: false, 506 searchable: false, 507 orderable: false, 508 searchPanes: { show: false }, 509 }, 510 ] : []; 511 const searchOrProfileColumnDefs = isSearchOrProfile ? [ 512 ...(isProfileTable ? [{ 513 targets: [12], 514 searchBuilderType: 'date' 515 }] : []), 516 { 517 targets: isProfileTable ? [0,8,10,11,12,13,14] : [0,8,9,10,11], 518 searchPanes: { show: false }, 519 }, 520 { 521 targets: isProfileTable ? [1,5,6,7,2,3,4,9] : [1,5,6,7,2,3,4,12], 522 searchPanes: { show: true }, 523 }, 524 ] : []; 525 $.fn.DataTable.ext.pager.numbers_length = 5; 526 527 const howManyItemsHostedForSomeoneElse = theDOMTable.find("tr[data-userishosternotowner]").length; 528 const table = theDOMTable.DataTable({ 529 retrieve: true, 530 pageLength: (isCollectorsTable || isSearchTable) ? 50 : 100, 531 dom: "<'row'<'col-sm-3'l><'col-sm-6'p><'col-sm-3'f>>" + "<'row'<'col-xs-12 col-sm-12 col-md-12'tr>>" + "<'row'<'col-sm-5'i><'col-sm-7'p>>", 532 colReorder: false, 533 responsive: (isMobile && isAchievementsTable) ? { 534 details: { 535 display: $.fn.dataTable.Responsive.display.childRowImmediate, 536 type: 'none', 537 target: '' 538 } 539 } : isMobile, 540 scrollX: !(isMobile && isAchievementsTable), 541 order: isCollectorsTable ? [[ 3, 'desc' ]] 542 : (isLatestTable ? [[ 12 , 'desc' ]] 543 : (isAchievementsTable ? [[ 7, 'desc' ], [ 2, 'desc' ], [ 0 , 'asc' ], [ 1 , 'asc' ]] 544 : [[ 0, 'asc' ], [ 6 , 'desc' ], [ 5 , 'desc' ], [ 7 , 'desc' ], [ 8 , 'desc' ]])), 545 buttons: [ 546 { extend: 'colvis', columns: ':not(.export-only)' }, 547 { extend: 'copy', title: null, customize: text => prepareClipboardCell(document.title) + '\n\n' + text, exportOptions: { customizeData: data => prepareSpreadsheetTextExport(data, true) } }, 548 { extend: 'csv', exportOptions: { customizeData: prepareSpreadsheetTextExport } }, 549 'excel', 550 { extend: 'print', title: () =>
550 $('<div>').text(document.title).html().replaceAll('*', '*') }, 551 ], 552 searchHighlight: true, 553 searchBuilder: (needAdvancedFiltering) ? { 554 columns: [1,2,3,4,5,6,7,9,10,12], 555 } : null, 556 columnDefs: [ 557 ...collectionColumnDefs, 558 ...profileColumnDefs, 559 ...searchOrProfileColumnDefs, 560 ], 561 preDrawCallback: function (settings) { 562 const needPaginate = (new $.fn.dataTable.Api(settings)).page.info().pages > 1; 563 $(this).closest('.dataTables_wrapper').find('.dataTables_paginate,.dataTables_length').toggle(needPaginate); 564 }, 565 initComplete: function() { 566 const dt = this; 567 const filterInputBox = $('div.dataTables_filter input[type=search]'); 568 dtSearch = function(myFilter) { 569 if (myFilter && myFilter.length) { 570 const origFilterValue = myFilter; 571 const regexMatches = myFilter.match(/^\/(.*)\/i?$/) 572 if (regexMatches) { 573 myFilter = regexMatches[1]; 574 } 575 const needCI = !regexMatches || origFilterValue.endsWith('/i'); 576 dt.api().search(myFilter.trim(), !!regexMatches, !regexMatches, needCI).draw(); 577 filterInputBox.val(origFilterValue); 578 } else { 579 dt.api().search('').draw(); 580 } 581 } 582 if (isProfileTable && $("#tableEditBtn").length === 0) { 583 hideEmptyColumns(this); // only hide if we can't edit 584 } 585 586 if (typeof(URLSearchParams) !== 'undefined') 587 { 588 const urlParams = (new URL(document.location)).searchParams; 589 const myFilter = (urlParams.get('filter') || '').trim(); 590 const itemId = (urlParams.get('itemid') || '').trim(); 591 592 if (myFilter.length || itemId.length) { 593 dtSearch(myFilter.length ? myFilter : `id=${itemId}`); 594 $('#profileTable_filter').fadeOut(150).fadeIn(150).fadeOut(150).fadeIn(150).fadeOut(150).fadeIn(150).fadeOut(150).fadeIn(150); 595 } 596 } 597 598 filterInputBox.off().on('click keyup', function(e) { 599 dtSearch(this.value); 600 // If search field is changed while in edit mode, apply edit stuff to new search results 601 isEditing && $("#tableEditBtn").click(); 602 }); 603 604 const dtTableTogglesAbove = $("#tableContainer > div.dataTables_wrapper > div:first-child > div:first-child"); 605 606 dtTableTogglesAbove.prepend('<div id="toggleRowDetailsDiv" class="checkbox"><label><input id="toggleRowDetailsInput" type="checkbox"> Toggle all collapsed/expanded details</label></div>'); 607 document.getElementById("toggleRowDetailsInput").addEventListener('change', (e) => { 608 dt.api().rows().every(function (){ 609 const tr = $(this.node()); 610 if (e.currentTarget.checked ^ tr.is('.dt-hasChild.parent')) { 611 tr.find("td:first").click(); 612 } 613 }); 614 }); 615 616 // Owner/hoster toggling thing 617 if (howManyItemsHostedForSomeoneElse > 0) { 618 dtTableTogglesAbove 619 .prepend('<div class="checkbox">' + 620 `<label><input id="hostingOption" type="checkbox" name="displayHostedForSomeoneElse" value="displayHostedForSomeoneElse" checked> Include the ${howManyItemsHostedForSomeoneElse} hosted for someone else</label>` + 621 '</div>'); 622 623 const checkbox = document.getElementById("hostingOption"); 624 $.fn.dataTable.ext.search.push((settings, searchData) => { 625 if (settings.nTable.id !== 'profileTable'){ 626 return true; 627 } 628 const displayHostedForSomeoneElse = checkbox.checked; 629 const isHostedForSomeoneElse = searchData[12]?.includes("Hosted for") ?? false; 630 return (!isHostedForSomeoneElse) || displayHostedForSomeoneElse; 631 }); 632 checkbox.addEventListener('change', () => { dt.api().search(filterInputBox.val()).draw(); }); 633 if (optionDisplayHostedForSomeoneElse === false) { 634 checkbox.checked = false;
635 dt.api().search(filterInputBox.val()).draw(); 636 } 637 } 638 639 theDOMTable.toggleClass("notReponsiveCollapsed", !theDOMTable.hasClass("collapsed")); 640 641 new $.fn.dataTable.FixedHeader(dt, { 642 headerOffset: $("nav.navbar").outerHeight() 643 }); 644 645 setTimeout(() => dt.api().columns.adjust(), 400); 646 } 647 }); 648 649 table.on('responsive-resize', (e, datatable, columns) => { 650 const count = columns.reduce((a, b) => b === false ? a + 1 : a, 0 ); 651 theDOMTable.toggleClass("notReponsiveCollapsed", count === 0); 652 }); 653 654 table.on('draw', function() { 655 if (!localStorage || !(localStorage.getItem('disableTooltips') === 'true')) { 656 theDOMTable.find('.hasTooltip').tooltip('destroy').tooltip({container: 'body'}); 657 } 658 }); 659 660 table.on('column-visibility.dt', function (e, settings, column, state) { 661 if (isMobile) { 662 table.columns.adjust().responsive.rebuild(); 663 table.columns.adjust().responsive.recalc(); 664 } else { 665 table.columns.adjust(); 666 } 667 }); 668 669 table.buttons().container().appendTo($('#afterTable')); 670 $('#afterTable a.btn').addClass('btn-sm'); 671 672 if (/^\/(profile|u|user|latest|search)\b/.test(document.location.pathname)) { 673 // Thumbnail toggler 674 $("#profileTable_wrapper > div:first-child > div:first-child").prepend('<div id="thumbTogglerInputWrapper" class="checkbox"><label><input id="thumbTogglerInput" type="checkbox" checked> Show thumbnails</label></div>'); 675 $('#thumbTogglerInput').on('change', function() { 676 $('#profileTable').toggleClass('noCalcimg'); 677 localStorage && localStorage.setItem('want_calc_thumbnails', this.checked); 678 }); 679 if (localStorage && localStorage.getItem('want_calc_thumbnails') === 'false') { 680 $('#profileTable').addClass('noCalcimg'); 681 $('#thumbTogglerInput').attr('checked', false); 682 } 683 if (needAdvancedFiltering) { 684 let advFiltersInited = false; 685 let advSearchInited = false; 686 $(`#profileTable_wrapper > div:first-child > div:last-child`).prepend('<div style="display:block;text-align: right"><div id="advFiltersTogglerInputWrapper" class="checkbox"><label><input id="advFiltersTogglerInput" type="checkbox"> Show advanced filters</label></div><div id="advSearchTogglerInputWrapper" class="checkbox"><label><input id="advSearchTogglerInput" type="checkbox"> Show advanced search</label></div></div>'); 687 $('#advFiltersTogglerInput').on('change', function() { 688 if (advFiltersInited) { 689 $("div.dtsp-panesContainer").toggle(this.checked); 690 } else { 691 new $.fn.dataTable.SearchPanesST(table, { 692 cascadePanes: true, 693 viewTotal: true, 694 // TODO: maybe check what to do with xxx + preDrawCallback to at least display/hide a loading overlay... 695 // dtOpts: { 696 // preDrawCallback: function() { 697 // console.log(this, 'draw finished') 698 // }, 699 // }, 700 }); 701 table.searchPanes.container().prependTo(table.table().container()); 702 table.searchPanes.resizePanes(); 703 advFiltersInited = true; 704 } 705 }); 706 $('#advSearchTogglerInput').on('change', function() { 707 if (advSearchInited) { 708 $("div.dtsb-searchBuilder").toggle(this.checked); 709 } else { 710 new $.fn.dataTable.SearchBuilder(table, {}); 711 table.searchBuilder.container().prependTo(table.table().container()); 712 advSearchInited = true; 713 } 714 }); 715 } 716 } 717 718 // inline-edit 719 let editables = []; 720 let previousTablePageLen = -1; 721 const renderPriceEditableValue = function(td, value) { 722 const setTitle = td.dataset.pricedSetTitle || ''; 723 const setUrl = td.dataset.pricedSetUrl || ''; 724 const setIndicator = setTitle 725 ? `<a href="${escapeHtml(setUrl || '#')}" class="price-set-indicator hasTooltip" title="${escapeHtml(setTitle)}" onclick="event.stopPropagation();" aria-label="Part of priced set"><i class="fa-solid fa-object-group"></i></a>` 726 : ''; 727 728 const sym = td.dataset.currencySymbol || ''; 729 const code = td.dataset.currencyCode || ''; 730 const pos = td.dataset.currencySymbPos || 'prefix'; 731 const hasValue = value !== null && typeof value !== 'undefined' && String(value) !== ''; 732 let priceHtml = ''; 733 734 if (hasValue && sym) {
735 let v = String(value); 736 if (v.endsWith('.00')) { v = v.slice(0, -3); } 737 const nbsp = ' '; 738 const amountHtml = (pos === 'suffix') ? `${escapeHtml(v)}${nbsp}${escapeHtml(sym)}` : `${escapeHtml(sym)}${nbsp}${escapeHtml(v)}`; 739 priceHtml = `<span title='${escapeHtml(code)}'>${amountHtml}</span>`; 740 } else if (hasValue) { 741 priceHtml = escapeHtml(String(value)); 742 } 743 744 td.innerHTML = setIndicator 745 ? `<span class="price-cell-value">${priceHtml}</span>${setIndicator}` 746 : priceHtml; 747 }; 748 $("#tableEditBtn").on('click', function() 749 { 750 previousTablePageLen = table.page.len() || -1; 751 table.page.len(-1).draw(); // show all 752 $(".removeCalcBtn, .transferItemBtn").removeClass('hide'); 753 if (editables.length) { 754 editables.forEach(element => $(element).editable('enable')); 755 } else { 756 let currentlyEditedOriginalSource = ''; 757 $(table.cells().nodes()).filter('.editable').each(function(idx, el){ 758 if ($(el).hasClass("priceEditable")) { 759 $(el).off('click.priceSetConfirm').on('click.priceSetConfirm', function(e) { 760 if (this.dataset.pricedSetTitle && !$(this).data('pricedSetQuickeditConfirmed')) { 761 const keepEditing = confirm(`${this.dataset.pricedSetTitle}\n\nThis item is already part of a priced set. Setting an item-specific price will not change the set price. Continue editing this item price?`); 762 if (!keepEditing) { 763 e.preventDefault(); 764 e.stopImmediatePropagation(); 765 return false; 766 } 767 $(this).data('pricedSetQuickeditConfirmed', true); 768 } 769 }); 770 } 771 editables.push($(el).editable({ 772 url: '/collectionEdit_handler.php', 773 pk: function () { 774 return $("#profileTable thead tr th").eq(table.cell( this ).index().column).data('colname'); 775 }, 776 params: function(p) { 777 p.csrf_token = window.CSRFToken; 778 p.user_id = window._targetProfileUID; 779 p.action = 'edit'; 780 p.row_id = $(this).closest('tr').data('rowid'); 781 if (typeof(p.value) === 'object' && p.value.length === 0) { 782 p.value = ''; 783 } 784 p.item_type = $("#addItemForm input[name='item_type']").val(); 785 p.subtype = $("#addItemForm input[name='subtype']").val() || calcType.toLowerCase(); 786 return p; 787 }, 788 source: function() { 789 if ($(this).hasClass("factorySelect")) { 790 return allFactories.map(function(obj){ return { value: obj.id, text: obj.code }; }); 791 } else if ($(this).hasClass("conditionSelect")) { 792 return [{value:'', text:''}].concat(allConditions); 793 } else if ($(this).hasClass("extrasList")) { 794 const isCdRom = $(this).attr('data-item-group') === 'CD-ROM'; 795 const extras = itemType === 'accessory' 796 ? allExtras.filter(extra => extra !== 'Faceplate(s)' && (!isCdRom || extra !== 'CD(s)')) 797 : allExtras; 798 return extras.map(function(txt){ return { value: txt, text: txt }; }); 799 } else if ($(this).hasClass("variantSelect")) { 800 currentlyEditedOriginalSource = $(this).html(); 801 const itemID = +$(this).data('itemid'); 802 const data = allItemVariants[itemID]?.map(arr => ({ value: arr[0], text: arr[1] })) ?? []; 803 if (data.length === 0) { 804 data.push({ value: '', text: '(no variant available)' }); 805 } else if (!itemsThatNeedVariants[itemID]) { 806 data.push({ value: '', text: '(not a variant)' }); 807 } 808 return data; 809 } else { 810 return null; 811 } 812 }, 813 display: ($(el).hasClass("photoURL") ? function(value) { 814 this.innerHTML = (!value.length || !isValidHttpUrl(value)) ? '' : `<a href="${escapeHtml(value)}" target='_blank'><span class='glyphicon glyphicon-picture'></span></a>`; 815 } : ($(el).hasClass("variantSelect") ? function(value, choices, responseValue) { 816 this.innerHTML = currentlyEditedOriginalSource; 817 if (responseValue) { 818 this.dataset.variantid = responseValue; 819 $(this).find('span.inlineVariantName').text((choices.filter(el => el.value === +responseValue)[0]?.text ?? '?')); 820 } else if (!value && el.dataset.itemid in allItemVariants) { 821 this.dataset.variantid = undefined; 822 $(this).find('span.inlineVariantName').html(`<span class='editable-empty'>Choose a variant...</span>`); 823 } 824 } : ($(el).hasClass("priceEditable") ? function(value) { 825 renderPriceEditableValue(this, value); 826 } : null))), 827 validate: ($(el).hasClass("photoURL") ? function(value) { 828 if (value && value.length && !isValidHttpUrl(value)) { return 'Invalid URL'; } 829 } : null),
830 success: ($(el).hasClass("priceEditable") ? function(response, newValue) { 831 // After amount saved, if a currency was chosen/changed, also save it 832 const $td = $(this); 833 const pendingCur = $td.data('pendingCurrencyId'); 834 if (typeof pendingCur !== 'undefined' && String(pendingCur) !== String(this.dataset.currencyId || '')) { 835 const rowId = $td.closest('tr').data('rowid'); 836 $.ajax({ 837 url: '/collectionEdit_handler.php', 838 type: 'post', 839 data: { csrf_token: window.CSRFToken, user_id: window._targetProfileUID, action: 'edit', item_type: $("#addItemForm input[name='item_type']").val(), subtype: $("#addItemForm input[name='subtype']").val() || calcType.toLowerCase(), row_id: rowId, pk: 'price_currency_id', value: pendingCur }, 840 success: () => { 841 const cur = (window.allCurrencies || []).find(c => String(c.id) === String(pendingCur)); 842 if (cur) { 843 this.dataset.currencyId = String(cur.id); 844 $td.data('currency-id', String(cur.id)); // Keep jQuery data cache in sync for future reads 845 this.dataset.currencyCode = cur.code || ''; 846 this.dataset.currencySymbol = cur.symbol || ''; 847 this.dataset.currencySymbPos = cur.symbol_position || 'prefix'; 848 // re-render with new currency 849 const v = (newValue === null || typeof newValue === 'undefined') ? '' : String(newValue); 850 renderPriceEditableValue(this, v); 851 } 852 $td.removeData('pendingCurrencyId'); 853 } 854 }); 855 } 856 } : null) 857 }).on('shown', function() { 858 const popup = $("div.popover.editable-popup"); 859 popup.css('visibility', 'hidden').detach().appendTo($('#profileTable_wrapper')); 860 if ($(`td.editable.hwRevTextInput[aria-describedby='${popup.attr('id')}']`).length) { 861 popup.find('input').attr('list', 'hwRevInputList'); 862 } 863 setTimeout(function(){ 864 popup.css('top', $("#thumbTogglerInput")[0].checked ? '+=15px' : '+=8px').css('visibility', 'visible'); 865 popup.find('input').focus(); 866 }, 20); 867 // If price cell, add currency selector and wire saving 868 if ($(this).hasClass('priceEditable')) { 869 const $td = $(this); 870 // Use live dataset first (jQuery .data caches the initial data-* and may be stale) 871 const curId = $td[0].dataset.currencyId || $td.data('currency-id') || String(defaultCurrencyID); 872 const $inputContainer = popup.find('.editable-input'); 873 const $sel = $('<select class="form-control input-sm" style="position: relative; z-index: 101"></select>'); 874 (window.allCurrencies || []).forEach(function(c){ 875 const opt = $(`<option></option>`).attr('value', c.id).text(`${c.code} (${c.symbol})`); 876 if (String(c.id) === String(curId || '')) { opt.attr('selected', 'selected'); } 877 $sel.append(opt); 878 }); 879 $inputContainer.append($sel); 880 // remember original amount to detect whether amount changed 881 try { $td.data('originalAmount', (popup.find('input').val() || '')); } catch(e) {} 882 // track selection for later save 883 $td.data('pendingCurrencyId', $sel.val()); 884 $sel.on('change', function(){ 885 $td.data('pendingCurrencyId', this.value); 886 }); 887 // ensure currency saves even if amount unchanged 888 popup.find('.editable-submit').off('click.price').on('click.price', async function(){ 889 const pending = $td.data('pendingCurrencyId'); 890 if (typeof pending === 'undefined' || String(pending) === String($td[0].dataset.currencyId || '')) { return; } 891 // If amount changed, let the x-editable success handler handle the currency save once 892 const currVal = (popup.find('input').val() || ''); 893 const origVal = ($td.data('originalAmount') || ''); 894 if (String(currVal) !== String(origVal)) { return; } 895 const rowId = $td.closest('tr').data('rowid'); 896 $.ajax({ 897 url: '/collectionEdit_handler.php', 898 type: 'post', 899 data: { csrf_token: window.CSRFToken, user_id: window._targetProfileUID, action: 'edit', item_type: $("#addItemForm input[name='item_type']").val(), subtype: $("#addItemForm input[name='subtype']").val() || calcType.toLowerCase(), row_i
899d: rowId, pk: 'price_currency_id', value: pending }, 900 success: () => { 901 const cur = (window.allCurrencies || []).find(c => String(c.id) === String(pending)); 902 if (cur) { 903 $td[0].dataset.currencyId = String(cur.id); 904 $td.data('currency-id', String(cur.id)); // Keep jQuery data cache in sync for future reads 905 $td[0].dataset.currencyCode = cur.code || ''; 906 $td[0].dataset.currencySymbol = cur.symbol || ''; 907 $td[0].dataset.currencySymbPos = cur.symbol_position || 'prefix'; 908 // Re-render using current input amount (even if unchanged) 909 const val = popup.find('input').val() || ''; 910 renderPriceEditableValue($td[0], val); 911 } 912 $td.removeData('pendingCurrencyId'); 913 $td.removeData('originalAmount'); 914 } 915 }); 916 }); 917 } 918 })); 919 }); 920 } 921 isEditing = true; 922 $("#tableEditBtn").addClass('hide'); 923 $("#tableEditBtnDone").removeClass('hide'); 924 }); 925 926 $("#tableEditBtnDone").on('click', function(e) { 927 e.stopPropagation(); 928 $(".removeCalcBtn, .transferItemBtn").addClass('hide'); 929 $("#tableEditBtn").removeClass('hide'); 930 $("#tableEditBtnDone").addClass('hide'); 931 const oldFilter = $('div.dataTables_filter input[type=search]').val(); 932 dtSearch(''); 933 $( table.cells().nodes() ).editable('disable'); 934 table.page.len(previousTablePageLen).draw(); 935 isEditing = false; 936 dtSearch(oldFilter); 937 }); 938 939 $("#profileTable").on("click", ".removeCalcBtn", function() { 940 if (confirm(`Are you sure you want to delete that ${itemType}?`)) 941 { 942 const row = $(this).closest('tr'); 943 $.ajax({ 944 url: "/collectionEdit_handler.php", 945 type: "post", 946 data: { 947 csrf_token: window.CSRFToken, 948 user_id: window._targetProfileUID, 949 action: 'delete', 950 row_id: row.data('rowid'), 951 item_type: itemType, 952 subtype: calcType.toLowerCase() 953 } 954 }).done(function (response, textStatus, jqXHR) 955 { 956 row.fadeOut(); 957 $("#collecStats").load(`/u/${window._targetProfileUID}?${profileContextQuery} #collecStats`); 958 }).fail(function (jqXHR, textStatus, errorThrown) 959 { 960 alert("There was an error deleting this item...") 961 console.log("Error", textStatus, errorThrown) 962 }); 963 } 964 }); 965 966 $("#profileTable").on("click", ".transferItemBtn", function() { 967 const itemId = $(this).data('item-id'); 968 $("#transferItemId").val(itemId).data('child-item-count', 0); 969 $("#transferToUser").val(''); 970 $("#transferMessage").val(''); 971 $("#transferChildItemsNotice").hide().text(''); 972 $("#confirmTransferBtn").prop('disabled', true).text('Checking...'); 973 $("#transferModal").modal('show'); 974 975 $.ajax({ 976 url: "/transfer_handler.php", 977 type: "post", 978 data: { 979 csrf_token: window.CSRFToken, 980 action: 'get_child_count', 981 user_item_id: itemId 982 } 983 }).done(function(response) { 984 const childItemCount = response.success ? Number(response.child_item_count || 0) : 0; 985 $("#transferItemId").data('child-item-count', Number.isFinite(c
985hildItemCount) ? childItemCount : 0); 986 if (childItemCount > 0) { 987 const childItemLabel = childItemCount === 1 ? 'child item' : 'child items'; 988 $("#transferChildItemsNotice") 989 .text(`This item has ${childItemCount} ${childItemLabel}. ${childItemCount === 1 ? 'It' : 'They'} will be transferred at the same time. You can detach ${childItemCount === 1 ? 'it' : 'them'} from the Sets page before transferring.`) 990 .show(); 991 } 992 }).fail(function(jqXHR, textStatus, errorThrown) { 993 alert("There was an error checking child items for this transfer."); 994 console.log("Error", textStatus, errorThrown); 995 $("#transferModal").modal('hide'); 996 }).always(function() { 997 $("#confirmTransferBtn").prop('disabled', false).text('Transfer'); 998 }); 999 }); 1000 1001 $("#confirmTransferBtn").on("click", function() { 1002 const itemId = $("#transferItemId").val(); 1003 const toUser = $("#transferToUser").val().trim(); 1004 const message = $("#transferMessage").val().trim(); 1005 1006 if (!toUser) { 1007 alert("Please enter a username to transfer to."); 1008 return; 1009 } 1010 1011 const childItemCount = Number($("#transferItemId").data('child-item-count') || 0); 1012 const childItemText = childItemCount > 0 1013 ? `\n\nThis will also transfer ${childItemCount} child item${childItemCount === 1 ? '' : 's'} at the same time. You can detach ${childItemCount === 1 ? 'it' : 'them'} from the Sets page before transferring.` 1014 : ''; 1015 if (!confirm("Transfer this item to " + toUser + "?" + childItemText)) { 1016 return; 1017 } 1018 1019 $.ajax({ 1020 url: "/transfer_handler.php", 1021 type: "post", 1022 data: { 1023 csrf_token: window.CSRFToken, 1024 action: 'initiate', 1025 user_item_id: itemId, 1026 to_user: toUser, 1027 message: message 1028 } 1029 }).done(function(response) { 1030 if (response.success) { 1031 alert(response.message); 1032 $("#transferModal").modal('hide'); 1033 if (response.message.includes("transferred successfully")) { 1034 location.reload(); 1035 } 1036 } else { 1037 alert(response.message || "There was an error initiating the transfer."); 1038 } 1039 }).fail(function(jqXHR, textStatus, errorThrown) { 1040 alert("There was an error initiating the transfer."); 1041 console.log("Error", textStatus, errorThrown); 1042 }); 1043 }); 1044 }; 1045 setupDataTable(); 1046 1047 1048 /**************************************/ 1049 1050 const checkProtoChoiceAndToggleWarning = function(callerEl) { 1051 setTimeout(() => { 1052 const isProto = (callerEl.querySelector(`[value="${callerEl.value}"]`).dataset.isproto === '1') || ($("span.select2 .select2-selection__rendered > span.select2-item-proto-label-wrapper.label").length > 0); 1053 $("input[name='is_prototype'][value='0']").attr('disabled', isProto); 1054 $(`input[name='is_prototype'][value='${isProto ? 1 : 0}']`).click(); 1055 }, 50); 1056 } 1057 1058 const variantSelect = $('#variantsSelect'); 1059 const variantSelectFormRow = $('#variantSelectFormRow'); 1060 const parentItemFormRow = $('#parentItemFormRow'); 1061 const parentUserItemSelect = $('#parentUserItemSelect'); 1062 const itemIdentificationSection = $('#itemIdentificationSection'); 1063 const accessoryRestrictedExtraInputs = $('#addItemForm input[name="extras[]"]').filter((_, element) => { 1064 return element.value === 'CD(s)' || element.value === 'Faceplate(s)'; 1065 }); 1066 1067 const faceplateExtraInput = $('input[name="extras[]"][value="Faceplate(s)"]'); 1068 faceplateExtraInput.prop('disabled', true); 1069 let parentItemAvailabilityRequest = null; 1070 1071 const getSelectedModelGroup = function() { 1072 const selectedItemId = $('#calcsSelect').val() || ''; 1073 return ($('#calcsSelect').select2('data')[0]?.itemGroup || window.itemGroupsById?.[selectedItemId] || '').toString(); 1074 }; 1075 1076 const setItemIdentificationVisible = function(visible) { 1077 if (!itemIdentificationSection.length) { 1078 return; 1079 } 1080 itemIdentificationSection.toggleClass('hide', !visible); 1081 if (!visible) { 1082 itemIdentificationSection.find('input[type="text"], input
1082:not([type]), textarea').val(''); 1083 itemIdentificationSection.find('select').val(''); 1084 itemIdentificationSection.find('#newTISerialInfo,#tiFactoryWarning,#tiDatestampWarning,#tiHwRevWarning').hide().text(''); 1085 $('#tiSpecificInfo,#ti85SpecificInfo,#tiSpecificImage,#casioSerialInfo').hide(); 1086 } 1087 }; 1088 1089 const resetParentUserItemSelect = function() { 1090 if (!parentUserItemSelect.length) { 1091 return; 1092 } 1093 parentUserItemSelect.val(null).trigger('change'); 1094 parentUserItemSelect.prop('disabled', true); 1095 parentItemFormRow.addClass('hide'); 1096 1097 if (parentItemAvailabilityRequest) { 1098 parentItemAvailabilityRequest.abort(); 1099 } 1100 1101 const accessoryId = $('#calcsSelect').val() || ''; 1102 if (!accessoryId) { 1103 return; 1104 } 1105 if (variantSelect.prop('required') && !variantSelect.val()) { 1106 return; 1107 } 1108 1109 const request = $.ajax({ 1110 url: '/collectionEdit_handler.php', 1111 type: 'POST', 1112 dataType: 'json', 1113 data: { 1114 csrf_token: window.CSRFToken, 1115 user_id: window._targetProfileUID, 1116 action: 'getCompatibleParentItems', 1117 item_type: 'accessory', 1118 accessory_id: accessoryId, 1119 acc_variant_id: variantSelect.val() || '', 1120 term: '' 1121 } 1122 }); 1123 parentItemAvailabilityRequest = request; 1124 1125 request.done(function(data) { 1126 if (parentItemAvailabilityRequest !== request) { 1127 return; 1128 } 1129 const hasCompatibleParents = (data.results || []).length > 0; 1130 parentUserItemSelect.prop('disabled', !hasCompatibleParents); 1131 parentItemFormRow.toggleClass('hide', !hasCompatibleParents); 1132 }).fail(function(xhr, status) { 1133 if (parentItemAvailabilityRequest !== request) { 1134 return; 1135 } 1136 if (status !== 'abort') { 1137 parentUserItemSelect.prop('disabled', true); 1138 parentItemFormRow.addClass('hide'); 1139 } 1140 }).always(function() { 1141 if (parentItemAvailabilityRequest === request) { 1142 parentItemAvailabilityRequest = null; 1143 } 1144 }); 1145 }; 1146 1147 if (parentUserItemSelect.length) { 1148 parentUserItemSelect.select2({ 1149 placeholder: 'No parent item', 1150 allowClear: true, 1151 dropdownParent: $('#addCalcModal'), 1152 templateResult: select2CalcThumbs, 1153 templateSelection: select2CalcThumbs, 1154 ajax: { 1155 url: '/collectionEdit_handler.php', 1156 type: 'POST', 1157 dataType: 'json', 1158 delay: 250, 1159 data: function(params) { 1160 return { 1161 csrf_token: window.CSRFToken, 1162 user_id: window._targetProfileUID, 1163 action: 'getCompatibleParentItems', 1164 item_type: 'accessory', 1165 accessory_id: $('#calcsSelect').val() || '', 1166 acc_variant_id: variantSelect.val() || '', 1167 term: params.term || '' 1168 }; 1169 }, 1170 processResults: function(data) { 1171 return { results: data.results || [] }; 1172 } 1173 } 1174 }); 1175 resetParentUserItemSelect(); 1176 } 1177 1178 $('#calcsSelect').on('change', function() { 1179 variantSelect.prop("disabled", true); 1180 variantSelect.html(""); 1181 variantSelect.append($("<option/>", {value: "", text: 'Standard (not a variant) / Other (=> comment)'})); 1182 1183 $('#datestampInput').trigger('keyup'); 1184 1185 const variantsForCalc = allItemVariants[this.value] || []; 1186 let hasOnlyBaseVariants = variantsForCalc.length >= 2; // may get toggled later. 1187 if (variantsForCalc.length === 0) { 1188 variantSelectFormRow.addClass('hide'); 1189 } else { 1190 variantSelectFormRow.removeClass('hide'); 1191 variantSelect.prop("disabled", false); 1192 $.each(variantsForCalc, function (_, value) { 1193 const year = value[3] ? (value[3].match(/\(([12][09]\d\d)\)/) || [])[1] : ''; 1194 variantSelect.append($("<option/>", { value: value[0], text: value[1], 'data-image': value[2], 'data-year': year, 'data-isproto': value[4] ? '1' : '0', 'data-isbase': value[5] })); 1195 if (value[5] === 0) { hasOnlyBaseVariants = false; } 1196 }); 1197 1198 const calcNeedsVariantSelect = hasOnlyBaseVariants || itemsThatNeedVariants[this.value]; 1199 variantSelect.prop("required", calcNeedsVariantSelect); 1200 1201 variantSelect.select2({ 1202 placeholder: calcNeedsVariantSelect ? "Choose a variant" : "Standard (not a variant) / Other (=> comment)", 1203 templateResult: select2CalcThumbs, 1204 templateSelection: select2CalcThumbs, 1205 dropdownParent: $('#addCalcModal'), 1206 allowClear: true, 1207 closeOnSelect: true 1208 }); 1209 if (calcNeedsVariantSelect) { 1210 setTimeout(() => { variantSelectFormRow.fadeOut(100).fadeIn(100).fadeOut(100).fadeIn(100); }, 200); 1211 } 1212 } 1213 1214 checkProtoChoiceAndToggleWarning(this); 1215 1216 const selectedCalcName = this.querySelector("option:checked").innerText || ''; 1217 const wizardRow = $('#wizardPromptRow'); 1218 const wizardLink = $('#wizardPromptLink'); 1219 faceplateExtraInput.prop('disabled', true);
1220 $('#tiSpecificInfo,#ti85SpecificInfo,#tiSpecificImage,#casioSerialInfo').hide(); 1221 if (/^(TI|Vernier)/.test(selectedCalcName)) { 1222 $('#tiSpecificInfo,#tiSpecificImage').css('display', 'inline-block'); 1223 if (selectedCalcName === 'TI-85') { 1224 $('#ti85SpecificInfo').css('display', 'inline-block'); 1225 } else if (/TI-84 Plus (C )?Silver Edition/.test(selectedCalcName)) { 1226 faceplateExtraInput.prop('disabled', false); 1227 } 1228 } else if (selectedCalcName.startsWith("Casio")) { 1229 $('#casioSerialInfo').css('display', 'inline-block'); 1230 } 1231 1232 const isCdRom = itemType === 'accessory' && getSelectedModelGroup() === 'CD-ROM'; 1233 setItemIdentificationVisible(!isCdRom); 1234 accessoryRestrictedExtraInputs.each((_, element) => { 1235 const input = $(element); 1236 const shouldHide = itemType === 'accessory' 1237 && (element.value === 'Faceplate(s)' || (isCdRom && element.value === 'CD(s)')); 1238 input.closest('label').toggleClass('hide', shouldHide); 1239 if (shouldHide) { 1240 input.prop('checked', false).prop('disabled', true); 1241 } else if (element.value === 'CD(s)') { 1242 input.prop('disabled', false); 1243 } 1244 }); 1245 1246 if (wizardRow.length && wizardLink.length) { 1247 const showWizard = !!window.itemWizardTypes?.[this.value]; 1248 if (showWizard) { 1249 const variantId = $('#variantsSelect').val() || ''; 1250 const calcId = this.value || ''; 1251 wizardLink.attr('href', `/calc_wizard.php?user_id=${window._targetProfileUID}&calc_id=${calcId}&calc_variant_id=${variantId}`); 1252 wizardRow.removeClass('hide'); 1253 } else { 1254 wizardRow.addClass('hide'); 1255 } 1256 } 1257 resetParentUserItemSelect(); 1258 }); 1259 1260 variantSelect.on('change', function() { 1261 checkProtoChoiceAndToggleWarning(this); 1262 const wizardRow = $('#wizardPromptRow'); 1263 const wizardLink = $('#wizardPromptLink'); 1264 if (wizardRow.length && wizardLink.length && !wizardRow.hasClass('hide')) { 1265 const calcId = $('#calcsSelect').val() || ''; 1266 const variantId = $('#variantsSelect').val() || ''; 1267 wizardLink.attr('href', `/calc_wizard.php?user_id=${window._targetProfileUID}&calc_id=${calcId}&calc_variant_id=${variantId}`); 1268 } 1269 resetParentUserItemSelect(); 1270 }); 1271 1272 const getDateOfWeek = function(w, y) { 1273 return new Date(y, 0, (1 + (w - 1) * 7)); // 1st of January + 7 days for each week 1274 } 1275 1276 $('#serialInput').on('keyup', function(e) { 1277 const serial = $(this).val().trim().toUpperCase(); 1278 const parts = []; 1279 for (const matches of serial.matchAll(/\b([A-H])(?:([1-9])(T)|(?:([0-4][0-9])|(5[0-2]))([0-9T]))([0-9A-F]{5})\b/ig)) 1280 { 1281 for (const match of matches) 1282 { 1283 // each part can only be up to 5 chars, so the extra check makes sure we don't add the whole part being caught for instance with other serials. 1284 if (match && match !== serial && match.length <= 5) { 1285 parts.push(match) 1286 } 1287 } 1288 } 1289 if (parts.length) 1290 { 1291 const [ yearCode, weekNum, unk, id ] = parts; 1292 const year = 2019 + (yearCode.charCodeAt(0) - 'A'.charCodeAt(0)); 1293 const theDate = getDateOfWeek(weekNum, year); 1294 $('#newTISerialInfo').html(`This TI-WalMart serial format means: assembled during week <b>${weekNum}</b> of <b>${year}</b> (<i>${theDate.toLocaleString('default', { month: "long", day: "numeric" })}</i>), line/~model <b>${unk}</b>, and calc has ID <b>${id}</b>.`).show(); 1295 } else { 1296 $('#newTISerialInfo').html('').hide(); 1297 } 1298 }); 1299 1300 // Function to check if the combination of factory, datecode, and hardware revision is valid 1301 function validateCalcDataInRange() { 1302 const $factoryWarning = $('#tiFactoryWarning'); 1303 const $dsWarning = $('#tiDatestampWarning'); 1304 const $hwRevWarning = $('#tiHwRevWarning'); 1305 1306 // Clear all warnings initially 1307 $factoryWarning.css('display', 'none').text(''); 1308 $dsWarning.css('display', 'none').text(''); 1309 $hwRevWarning.css('display', 'none').text(''); 1310 1311 const ds = $('#datestampInput').val().trim().toUpperCase(); 1312 const factoryCode = $('#factorySelect option:enabled:selected').text().split(' ')[0] || ''; 1313 const hwRev = $('#hwRevInput').val().trim().toUpperCase(); 1314 const selectedCalcName = $('#calcsSelect option:enabled:selected').text() || ''; 1315
1316 // Basic datecode validation 1317 if (ds.length > 0) { 1318 if (!/^\d{4}$/.test(ds)) { 1319 $dsWarning.css('display', 'inline-block').text('â ï¸ This should be 4 digits'); 1320 return; 1321 } 1322 if (!/^(0[1-9]|1[0-2])/.test(ds)) { 1323 $dsWarning.css('display', 'inline-block').text('â ï¸ Make sure this is the correct month'); 1324 return; 1325 } 1326 // TODO: in 2030, update me :D 1327 const regYear = new RegExp('(9[0-9]|[0-1][0-9]|2[0-' + (new Date().getFullYear() % 10) + '])$'); 1328 if (!regYear.test(ds)) { 1329 $dsWarning.css('display', 'inline-block').text('â ï¸ Make sure this is the correct year'); 1330 return; 1331 } 1332 const selectedCalcDates = $('#calcsSelect').select2('data')[0]?.dates ?? null; 1333 if (selectedCalcDates) { 1334 const calcYear = +(ds.substring(2, 4)); 1335 const calcFullYear = calcYear + ((calcYear < 70) ? 2000 : 1900); 1336 if (selectedCalcDates[0]) { 1337 const releaseYear = +selectedCalcDates[0].substring(0, 4); 1338 if (calcFullYear < releaseYear-1) { 1339 $dsWarning.css('display', 'inline-block').text(`â ï¸ This (${calcFullYear}) seems to be before release (${releaseYear})`); 1340 return; 1341 } 1342 } 1343 if (selectedCalcDates[1]) { 1344 const discYear = +selectedCalcDates[1].substring(0, 4); 1345 if (calcFullYear > discYear) { 1346 $dsWarning.css('display', 'inline-block').text(`â ï¸ This (${calcFullYear}) seems to be after discontinuation (${discYear})`); 1347 return; 1348 } 1349 } 1350 } 1351 } 1352 1353 // Check if the combination exists in calcSheetData 1354 if (typeof calcSheetData !== 'undefined' && calcSheetData && selectedCalcName) { 1355 try { 1356 // Find the calculator data 1357 const adjustedCalcName = selectedCalcName.replace(/[&()]/g, '').trim(); 1358 const calcData = calcSheetData[adjustedCalcName]; 1359 if (!calcData || !calcData.byFactory) { 1360 // No data for this calculator or invalid data structure 1361 return; 1362 } 1363 1364 // Check if the factory exists for this calculator (if factory is selected) 1365 if (factoryCode) { 1366 const factoryData = calcData.byFactory[factoryCode]; 1367 if (!factoryData) { 1368 // Get list of available factories 1369 const availableFactories = Object.keys(calcData.byFactory).filter(f => f !== '?').join(', '); 1370 if (availableFactories) { 1371 $factoryWarning.css('display', 'inline-block').text(`â ï¸ Are you sure it is Factory ${factoryCode}? For this model, we only know about ${availableFactories}`); 1372 } 1373 return; 1374 } 1375 1376 // If hardware revision is also provided, check if it's valid for this factory 1377 if (hwRev) { 1378 // Get the selected variant and prototype status 1379 const selectedVariant = $('#variantsSelect').data('select2') ? ($('#variantsSelect').select2('data')[0]?.text || '') : ''; 1380 const isViewScreenVariant = selectedVariant === 'ViewScreen' || selectedVariant === 'VB'; 1381 const isProtoSelected = $("input[name=is_prototype]:checked").val() === '1'; 1382 1383 // Filter available revisions based on the selected variant and prototype status 1384 const availableRevs = Object.keys(factoryData) 1385 .filter(r => { 1386 return !((/__(vsc|vb)$/i.test(r) && !isViewScreenVariant) 1387 || (!/__(vsc|vb)$/i.test(r) && isViewScreenVariant) 1388 || ( /EVT|DVT|PVT|PROTO|dev/i.test(r) && !isProtoSelected) 1389 || (!/EVT|DVT|PVT|PROTO|dev/i.test(r) && isProtoSelected)); 1390 }); 1391 1392 const revisedHwRev = hwRev.toUpperCase() + (isViewScreenVariant ? (selectedVariant === 'ViewScreen' ? '__vsc' : '__vb')
1392: ''); 1393 1394 // Check if the hardware revision exists for this factory 1395 if (!availableRevs.includes(revisedHwRev)) { 1396 let msg = `â ï¸ Are you sure it is revision ${hwRev}? It seems unknown to us for this ${isProtoSelected ? 'prototype ' : ''}${isViewScreenVariant ? `${selectedVariant} ` : ''}model by factory ${factoryCode}.`; 1397 if (availableRevs) { 1398 msg += ` We only know ${availableRevs.map((r) => r.replace(/(__vsc|__vb)$/, '')).join(', ')}`; 1399 } 1400 $hwRevWarning.css('display', 'inline-block').text(msg); 1401 return; 1402 } 1403 1404 // If datecode is also provided, check if it falls within the known ranges 1405 if (ds.length > 0) { 1406 // Convert datecode to YYYY-MM format for comparison 1407 const calcYear = +(ds.substring(2, 4)); 1408 const calcFullYear = calcYear + ((calcYear < 70) ? 2000 : 1900); 1409 const calcMonth = +(ds.substring(0, 2)); 1410 const dateToCheck = `${calcFullYear}-${calcMonth.toString().padStart(2, '0')}`; 1411 1412 // Check if the datecode falls within any of the date ranges for this revision 1413 const revData = factoryData[revisedHwRev] ?? []; 1414 let isInRange = false; 1415 for (const range of revData) { 1416 if (dateToCheck >= range.dateStart && dateToCheck <= range.dateEnd) { 1417 isInRange = true; 1418 break; 1419 } 1420 } 1421 1422 if (!isInRange) { 1423 // Format date ranges for display 1424 const dateRanges = revData.map(range => `${range.dateStart} to ${range.dateEnd}`).join(', '); 1425 if (dateRanges) { 1426 $dsWarning.css('display', 'inline-block').text(`â ï¸ Please make sure ${ds} (${dateToCheck}) is correct. We only know (so far) about [${dateRanges}] for factory ${factoryCode} rev ${hwRev}, for this ${isProtoSelected ? 'prototype ' : ''}${isViewScreenVariant ? `${selectedVariant} ` : ''} model.`); 1427 } 1428 } else { 1429 $hwRevWarning.css('display', 'inline-block').html(`ð This combination of information falls within our known data.`); 1430 } 1431 } 1432 } 1433 } 1434 } catch (error) { 1435 console.error("Error in calculator data validation:", error); 1436 } 1437 } 1438 } 1439 1440 $('#datestampInput').on('keyup', validateCalcDataInRange); 1441 $('#factorySelect').on('change', validateCalcDataInRange); 1442 $('#hwRevInput').on('keyup', validateCalcDataInRange); 1443 $('#calcsSelect').on('change', function() { 1444 $('#datestampInput').trigger('keyup'); 1445 validateCalcDataInRange(); 1446 }); 1447 1448 $('#addCalcModal').on('show.bs.modal', function() { 1449 if (isEditing) { 1450 $("#tableEditBtnDone").click(); 1451 } 1452 }); 1453 1454 $('#addItemForm').submit(function(e) { 1455 const isItemCalculator = !document.location.href.includes("accessories"); 1456 const itemCondition = $("#conditionSelect").val(); 1457 const isItemProto = $("input[name=is_prototype]:checked").val() === '1'; 1458 if (isItemCalculator && !isItemProto && (itemCondition === null || itemCondition !== 'New in box')) { 1459 if (!$("#serialInput").val()) { 1460 if (!confirm("Warning: you did not fill some fields (like the serial number), are you sure you want to submit it as-is? (note that all these elements are not actually 'secret', so there are no privacy concerns to be had - to the contrary they may later help you, if it gets stolen...)")) { 1461 e.preventDefault(); 1462 e.stopImmediatePropagation(); 1463 return false;
1464 } 1465 } 1466 } 1467 const submitBtn = $('#addItemForm button[type=submit]'); 1468 submitBtn.prop('disabled', true).text("Adding..."); 1469 e.preventDefault(); 1470 const manufName = $("#calcsSelect").find(":selected").text().split(/\b/g)[0] || ''; 1471 const dataToSubmit = $("#addItemForm").serialize() + `&manuf=${manufName}`; 1472 $.post("/collectionEdit_handler.php", dataToSubmit, function() { 1473 confetti && confetti({ particleCount: 100, spread: 70, origin: { y: 0.8 }, zIndex:9999 }); 1474 if (resetCalcsSelect2Form) { 1475 resetCalcsSelect2Form(); 1476 $('#variantsSelect').prop("disabled", true).val(""); 1477 } 1478 submitBtn.removeClass('btn-primary').addClass('btn-success').text('Success!').prop('disabled', true); 1479 $("#statsAndTableContainer").load(`/u/${window._targetProfileUID}?${profileContextQuery} #statsAndTableContainer`, setupDataTable); 1480 }).fail(function(err) { 1481 console.log("error: ", err); 1482 alert(`Error while adding the ${itemType} :(`); 1483 }).always(function() { 1484 setTimeout(function() { submitBtn.prop('disabled', false).removeClass('btn-success').addClass('btn-primary').text(`Add ${itemType}`); }, 1000); 1485 }); 1486 }); 1487 1488 if (/^\/(collectors|profile|u|user|latest|achievements|search)\b/.test(document.location.pathname)) 1489 { 1490 window.addEventListener('keydown', (e) => { 1491 // check if ctrl (or cmd on macOS) + F is pressed 1492 if ((e.ctrlKey || e.metaKey) && e.keyCode === 70) 1493 { 1494 $("div.dataTables_filter input[type=search]").focus(); 1495 e.stopPropagation(); 1496 e.preventDefault(); 1497 return false; 1498 } 1499 }); 1500 } 1501 1502 { 1503 const $dropdown = $(".dropdown"); 1504 const $dropdownToggle = $(".dropdown-toggle"); 1505 const $dropdownMenu = $(".dropdown-menu"); 1506 $(window).on("load resize", function () { 1507 if (this.matchMedia("(min-width: 768px)").matches) { 1508 $dropdown.hover( 1509 function () { 1510 const $this = $(this); 1511 $this.addClass("open"); 1512 $this.find($dropdownToggle).attr("aria-expanded", "true"); 1513 $this.find($dropdownMenu).addClass("open"); 1514 }, 1515 function () { 1516 const $this = $(this); 1517 $this.removeClass("open"); 1518 $this.find($dropdownToggle).attr("aria-expanded", "false"); 1519 $this.find($dropdownMenu).removeClass("open"); 1520 } 1521 ); 1522 } else { 1523 $dropdown.off("mouseenter mouseleave"); 1524 } 1525 }); 1526 } 1527});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.