1function initializeFontManager() { 2 const $ = window.jQuery; 3 const fontManager = $('#fontManager'); 4 if (fontManager.length === 0) { 5 return; // Don't run if the font manager isn't on the page 6 } 7 8 // --- STATE --- 9 let state = { 10 loadedFontFaces: new Set(), 11 selectedFonts: [], 12 userLocalFonts: [], // Fonts the user has saved from their system 13 autoSelectedFonts: [], // For special handwriting fonts that are auto-selected 14 currentCategoryFonts: [], // All fonts for the currently displayed category 15 activeFilters: [], 16 isMember: false, // Will be updated on init 17 maxFonts: 4, // Default for non-members 18 }; 19 20 // --- SELECTORS --- 21 const selectors = { 22 tabs: '#fm-tabs .fm-tab', 23 tagFiltersContainer: '#fm-tag-filters', 24 gridContainer: '#fm-grid-container', 25 loader: '.fm-loader', 26 formInputsContainer: '#fm-form-inputs', 27 selectedCountBadge: '#fm-selected-count-badge' 28 }; 29 30 // --- INITIALIZATION --- 31 function init() { 32 // Read membership status from the data attribute 33 const isMemberStatus = fontManager.data('is-member'); 34 state.isMember = (isMemberStatus === true || isMemberStatus === 'true'); 35 state.maxFonts = state.isMember ? 15 : 4; 36 37 // Read handwriting page status from the data attribute 38 const isHandwritingPageStatus = fontManager.data('is-handwriting-page'); 39 state.isHandwritingPage = (isHandwritingPageStatus === true || isHandwritingPageStatus === 'true'); 40 41 // Loosen the check to handle 'true' as a string from the data attribute 42 // Update max fonts badge 43 $('#fm-max-fonts-badge').text(state.maxFonts); 44 45 loadState(); // Load saved user fonts first 46 bindEvents(); 47 48 // Auto-select fonts for new users if no previous selection is found 49 if (state.selectedFonts.length === 0) { 50 autoSelectInitialFonts(); 51 } 52 53 // Load fonts for the initially active tab 54 const initialTab = $(selectors.tabs + '.active'); 55 if (initialTab.length) { 56 const initialCategoryId = initialTab.data('category-id'); 57 loadCategory(initialCategoryId, initialTab.text().trim()); 58 } else { 59 // Fallback if no tab is active 60 updateUI(); 61 } 62 } 63 64 // --- EVENT BINDING --- 65 function bindEvents() { 66 // Tab clicks 67 fontManager.on('click', selectors.tabs, function() { 68 const $this = $(this); 69 if ($this.hasClass('active')) return; 70 71 $(selectors.tabs).removeClass('active'); 72 $this.addClass('active'); 73 state.activeFilters = []; // Reset filters on tab change 74 const categoryName = $this.text().trim(); // Get category name from tab text 75 loadCategory($this.data('category-id'), categoryName); 76 }); 77 78 // Font item clicks - delegate to the container 79 fontManager.on('click', '.fm-font-item', function() { 80 handleFontClick($(this)); 81 }); 82 83 // Tag filter clicks 84 fontManager.on('click', '.fm-tag-filter', function() { 85 const $this = $(this); 86 const tag = $this.data('tag'); 87 $this.toggleClass('active'); 88 89 if ($this.hasClass('active')) { 90 state.activeFilters.push(tag); 91 } else { 92 state.activeFilters = state.activeFilters.filter(t => t !== tag); 93 } 94 applyTagFilters(); 95 }); 96 97 } 98 99 // --- DATA & STATE LOGIC --- 100 function loadCategory(categoryId, categoryName) { 101 const grid = $(selectors.gridContainer); 102 const loader = $(selectors.loader); 103 $(selectors.tagFiltersContainer).empty(); 104 105 if (categoryId === 'my-stash') { 106 handleMyStashTab(); 107 return; 108 } 109 110 grid.empty().html('<div class="fm-loader active"></div>'); // Show loader immediately 111 112 if (categoryId === 0) { 113 const handwritingBanner = ` 114 <div class="fm-handwriting-banner"> 115 Handwriting Fonts are made for this worksheet and HIGHLY Recommended. 116 </div> 117 `; 118 grid.html(handwritingBanner); // Add banner first 119 } 120 121 $.ajax({
122 url: '/includes/ajax/get_fonts.php', 123 type: 'GET', 124 data: { 125 category_id: categoryId, 126 t: new Date().getTime() // Cache busting 127 }, 128 dataType: 'json', 129 success: function(response) { 130 const serverFonts = response.fonts || []; 131 132 // Clear previous auto-selections and filter user selections if necessary 133 state.autoSelectedFonts = []; 134 if (categoryId === 0) { // Handwriting Tab 135 // Find free handwriting fonts from the response and auto-select them 136 state.autoSelectedFonts = serverFonts.filter(f => parseInt(f.is_handwriting) === 1 && parseInt(f.is_premium) === 0); 137 injectFontFaces(state.autoSelectedFonts); // Ensure they are available to render 138 } 139 // Note: Removed the else clause that was filtering out handwriting fonts when switching tabs 140 // This preserves user's handwriting font selections across tab switches 141 142 const systemFonts = getAvailableSystemFontsForCategory(categoryId); 143 144 // The definitive list of fonts for this category is the server fonts plus the mapped system fonts. 145 const allFontsForCategory = [...systemFonts, ...serverFonts]; 146 147 // Remove potential duplicates if a system font was ever added to the DB by mistake. 148 const uniqueFonts = allFontsForCategory.filter((font, index, self) => 149 index === self.findIndex((f) => f.name === font.name) 150 ); 151 152 // Don't include selected system fonts from other categories 153 // System fonts should only appear in their assigned categories 154 state.currentCategoryFonts = uniqueFonts; // Store the full list for the category 155 156 injectFontFaces(uniqueFonts); 157 renderGrid(uniqueFonts); // Render the grid with the final combined list 158 renderTagFilters(response.available_tags || []); 159 160 // Update UI to ensure counters and selections are synced. 161 updateUI(); 162 }, 163 error: function() { 164 grid.html('<div class="fm-error">Error loading fonts.</div>'); 165 } 166 }); 167 } 168 169 function handleFontClick($item) { 170 const isPremium = parseInt($item.data('is-premium')) === 1; 171 console.log(`Font clicked. isPremium: ${isPremium}, state.isMember: ${state.isMember}`); // DEBUG 172 173 if ($item.hasClass('disabled')) { 174 // Check if this is a premium font vs an auto-selected font 175 const fontData = $item.data('font-data'); 176 const isPremium = parseInt($item.data('is-premium')) === 1; 177 178 if (fontData && (fontData.is_auto_selected || parseInt(fontData.is_handwriting) === 1)) { 179 // Handle auto-selected fonts that can't be deselected 180 if (fontData.name === 'Arial') { 181 Swal.fire('Arial is Required', 'Arial is required and cannot be unselected.', 'info'); 182 } else if (fontData.name === 'TTCPrint Dashed') { 183 Swal.fire('TTCPrint Dashed is Required', 'TTCPrint Dashed is required and cannot be unselected.', 'info'); 184 } else { 185 Swal.fire('Cannot Deselect', 'This font is required and cannot be deselected.', 'info'); 186 } 187 } else if (isPremium && !state.isMember) { 188 // Handle premium fonts for non-members 189 Swal.fire('Premium Membership Required', 'Please subscribe to get access to this and many other options!', 'warning'); 190 } 191 return; 192 } 193 194 const fontId = $item.data('font-id'); 195 const isSelected = $item.hasClass('selected'); 196 197 if (isSelected) { 198 // Check if this is an auto-selected font that can't be deselected 199 const fontData = $item.data('font-data'); 200 if (fontData && fontData.is_auto_selected) { 201 if (fontData.name === 'Arial') { 202 Swal.fire('Arial is Required', 'Arial is required and cannot be unselected.', 'info'); 203 } else if (fontData.name === 'TTCPrint Dashed') { 204 Swal.fire('TTCPrint Dashed is Required', 'TTCPrint Dashed is required and cannot be unselected.', 'info'); 205 } else { 206 Swal.fire('Cannot Deselect', 'This font is required and cannot be deselected.', 'info'); 207 } 208 return; 209 } 210 // Deselect - NOTE: Auto-selected fonts cannot be deselected from the UI. 211 // This logic only applies to user-selected fonts. 212 state.selectedFonts = state.selectedFonts.filter(font => font.id !== fontId); 213 } else { 214 // Select 215 // The max font limit only applies to user-selected fonts that are NOT: 216 // - handwriting fonts (is_handwriting=1) 217 // - auto-selected fonts (like Arial) 218 // - TTCPrint fonts (font_type=4) on handwriting pages 219 const countableSelectedFonts = state.selectedFonts.filter(f => 220 parseInt(f.is_handwriting) !== 1 && 221 !f.is_auto_selected && 222 !(parseInt(f.font_type) === 4) // TTCPrint fonts don't count 223 ); 224 if (countableSelectedFonts.length >= state.maxFonts) { 225 Swal.fire('Maximum fonts selected', `You can select up to ${state.maxFonts} fonts. Please deselect another font to add this one.`, 'warning'); 226 return; 227 } 228 // Store the entire font data object to preserve all properties like is_handwriting 229 const fontData = $item.data('font-data'); 230 if (fontData) { 231 state.selectedFonts.push(fontData); 232 } 233 } 234 235 updateUI(); 236 } 237 238 function saveState() { 239 try { 240 // A font is premium if it's marked as premium OR it's a local font (from My Stash) 241 const premiumFonts = state.selectedFonts.filter(f => parseInt(f.is_premium) === 1 || String(f.id).startsWith('local-')); 242 const freeFonts = state.selectedFonts.filter(f => parseInt(f.is_premium) !== 1 && !String(f.id).startsWith('local-')); 243 244 localStorage.setItem('ttcSelectedFreeFonts', JSON.stringify(freeFonts)); 245 localStorage.setItem('ttcSelectedPremiumFonts', JSON.stringify(premiumFonts)); 246 localStorage.setItem('ttcUserLocalFonts', JSON.stringify(state.userLocalFonts)); // Still save the available local fonts separately 247 } catch (e) { 248 console.error("Could not save state to localStorage", e); 249 } 250 } 251 252 function loadState() { 253 try { 254 let selectedFonts = []; 255 const savedFree = localStorage.getItem('ttcSelectedFreeFonts'); 256 if (savedFree) { 257 selectedFonts = selectedFonts.concat(JSON.parse(savedFree)); 258 } 259 260 // Only load premium and local fonts if the user is a member 261 if (state.isMember) { 262 const savedPremium = localStorage.getItem('ttcSelectedPremiumFonts'); 263 if (savedPremium) { 264 selectedFonts = selectedFonts.concat(JSON.parse(savedPremium)); 265 } 266 const savedLocal = localStorage.getItem('ttcUserLocalFonts'); 267 if (savedLocal) { 268 state.userLocalFonts = JSON.parse(savedLocal); 269 } 270 } else { 271 // If not a member, ensure local fonts are cleared from the state 272 state.userLocalFonts = []; 273 } 274 275 state.selectedFonts = selectedFonts; 276 277 // De-duplicate fonts by name, prioritizing system fonts if duplicates exist. 278 if (state.selectedFonts.length > 0) { 279 const fontGroups = {};
280 state.selectedFonts.forEach(font => { 281 if (!fontGroups[font.name]) { 282 fontGroups[font.name] = []; 283 } 284 fontGroups[font.name].push(font); 285 }); 286 287 const uniqueSelectedFonts = []; 288 for (const name in fontGroups) { 289 const fonts = fontGroups[name]; 290 if (fonts.length > 1) { 291 // If a duplicate name exists, always prefer the system font. 292 const systemFont = fonts.find(f => String(f.id).startsWith('system-')); 293 if (systemFont) { 294 uniqueSelectedFonts.push(systemFont); 295 } else { 296 uniqueSelectedFonts.push(fonts[0]); // Fallback to the first one found 297 } 298 } else { 299 uniqueSelectedFonts.push(fonts[0]); 300 } 301 } 302 state.selectedFonts = uniqueSelectedFonts; 303 } 304 305 // Ensure Arial is always auto-selected and marked as such 306 ensureArialAutoSelected(); 307 308 } catch (e) { 309 console.error("Could not load state from localStorage", e); 310 state.selectedFonts = []; 311 state.userLocalFonts = []; 312 } 313 } 314 315 function ensureArialAutoSelected() { 316 // Check if Arial is already in the selected fonts 317 const arialFont = state.selectedFonts.find(f => f.name === 'Arial'); 318 319 if (arialFont) { 320 // Mark existing Arial as auto-selected 321 arialFont.is_auto_selected = true; 322 } else if (_doesFontExist('Arial')) { 323 // Add Arial as auto-selected if it doesn't exist but is available 324 state.selectedFonts.unshift({ 325 id: 'system-Arial', 326 name: 'Arial', 327 file_name: 'Arial', 328 font_type: 1, 329 is_premium: 0, 330 is_auto_selected: true 331 }); 332 } 333 } 334 335 // --- UI & RENDERING --- 336 function injectFontFaces(fonts) { 337 let styles = ''; 338 fonts.forEach(font => { 339 if (!state.loadedFontFaces.has(font.name)) { 340 // Skip system fonts and local fonts - they don't need @font-face rules 341 const isSystemFont = String(font.id).startsWith('system-'); 342 const isLocalFont = String(font.id).startsWith('local-'); 343 344 if (isSystemFont || isLocalFont) { 345 // Just mark as loaded to avoid reprocessing 346 state.loadedFontFaces.add(font.name); 347 return; 348 } 349 350 // Only inject @font-face for server-hosted fonts 351 const urlBase = `/includes/fonts/font-files/${font.file_name}`; 352 styles += ` 353 @font-face { 354 font-family: "${font.name}"; 355 src: url('${urlBase}.eot'); 356 src: url('${urlBase}.eot?#iefix') format('embedded-opentype'), 357 url('${urlBase}.woff2') format('woff2'), 358 url('${urlBase}.woff') format('woff'), 359 url('${urlBase}.ttf') format('truetype'); 360 font-weight: normal; 361 font-style: normal; 362 } 363 `; 364 state.loadedFontFaces.add(font.name); 365 } 366 }); 367 368 if (styles) { 369 $('head').append(`<style type="text/css">${styles}</style>`); 370 } 371 } 372 373 function updateUI() { 374 const allSelectedFonts = [...state.selectedFonts, ...state.autoSelectedFonts]; 375 console.log('Updating UI. User selected:', JSON.stringify(state.selectedFonts, null, 2)); // DEBUG 376 console.log('Updating UI. Auto selected:', JSON.stringify(state.autoSelectedFonts, null, 2)); // DEBUG 377 // Re-render the grid to reflect selections and sorting 378 const currentTab = $(selectors.tabs + '.active').data('category-id'); 379 380 if (currentTab === 'my-stash') { 381 // My Stash tab has its own rendering logic. 382 renderMyStashGrid(); 383 } else { 384 // Re-render the grid using the full list of fonts for this category from the state. 385 // This ensures that sorting is applied correctly without losing any fonts. 386 if (state.currentCategoryFonts.length > 0) { 387 renderGrid(state.currentCategoryFonts); 388 } 389 } 390 391 // Update other UI elements
392 // Do not count handwriting fonts, auto-selected fonts, or TTCPrint fonts towards the user's selection limit display. 393 const countableFonts = allSelectedFonts.filter(f => 394 parseInt(f.is_handwriting) !== 1 && 395 !f.is_auto_selected && 396 parseInt(f.font_type) !== 4 397 ); 398 $(selectors.selectedCountBadge).text(countableFonts.length); 399 $('#fm-max-fonts-badge').text(state.maxFonts); 400 updateHiddenFormInputs(); 401 saveState(); 402 403 // Expose selected local fonts globally for the PDF/Image generation script 404 // Expose ONLY local fonts globally. 405 window.ttcSelectedLocalFonts = state.selectedFonts 406 .filter(font => String(font.id).startsWith('local-')) 407 .map(font => font.name); 408 } 409 410 function renderGrid(fonts) { 411 const grid = $(selectors.gridContainer); 412 // If a banner exists, don't empty the entire grid. Just remove font items. 413 if (grid.find('.fm-handwriting-banner').length > 0) { 414 grid.find('.fm-font-item').remove(); 415 } else { 416 grid.empty(); 417 } 418 419 if (!fonts || fonts.length === 0) { 420 grid.html('<div class="fm-error" style="position: static; background: none; border: none;">No fonts to display.</div>'); 421 return; 422 } 423 424 // Sort to bring selected fonts to the top, then system fonts, then by name. 425 fonts.sort((a, b) => { 426 const allSelectedIds = new Set([...state.selectedFonts, ...state.autoSelectedFonts].map(f => f.id)); 427 const a_selected = allSelectedIds.has(a.id); 428 const b_selected = allSelectedIds.has(b.id); 429 // Ensure ID is treated as a string before calling startsWith 430 const a_isSystem = String(a.id).startsWith('system-'); 431 const b_isSystem = String(b.id).startsWith('system-'); 432 433 if (a_selected !== b_selected) { 434 return b_selected - a_selected; // Selected fonts first 435 } 436 if (a_isSystem !== b_isSystem) { 437 return b_isSystem - a_isSystem; // Then system fonts 438 } 439 return a.name.localeCompare(b.name); // Then alphabetically 440 }); 441 442 fonts.forEach(font => { 443 const allSelectedIds = new Set([...state.selectedFonts, ...state.autoSelectedFonts].map(f => f.id)); 444 const isSelected = allSelectedIds.has(font.id); 445 const isPremium = parseInt(font.is_premium) === 1; 446 const isAutoSelected = state.autoSelectedFonts.some(f => f.id === font.id); 447 448 // Disable click events for auto-selected fonts so they can't be deselected. 449 // Also disable premium fonts for non-members. 450 // Also disable fonts that are marked as is_auto_selected (like Arial) 451 const isDisabled = (isPremium && !state.isMember) || isAutoSelected || (font.is_auto_selected === true); 452 453 const fontItem = $(` 454 <div class="fm-font-item"> 455 <div class="fm-font-name"></div> 456 <div class="fm-font-preview">AaBbCc</div> 457 </div> 458 `); 459 460 // Add classes and data 461 fontItem.toggleClass('selected', isSelected); 462 fontItem.toggleClass('premium', isPremium); 463 fontItem.toggleClass('disabled', isDisabled); 464 fontItem.data('font-id', font.id); 465 fontItem.data('font-name', font.name); 466 fontItem.data('font-type', font.font_type); 467 fontItem.data('is-premium', font.is_premium); 468 fontItem.data('tags', font.tags ? font.tags.join(' ') : ''); 469 fontItem.data('font-data', font); // Store the whole object 470 471 // Set text and style 472 fontItem.find('.fm-font-name').text(font.name); 473 fontItem.find('.fm-font-preview').css('font-family', `"${font.name}", sans-serif`); 474 475 grid.append(fontItem); 476 }); 477 } 478 479 function renderTagFilters(tags) { 480 const container = $(selectors.tagFiltersContainer); 481 container.empty(); 482 if (!tags || tags.length === 0) return; 483
484 tags.forEach(tag => { 485 const tagButton = $(` 486 <button type="button" class="fm-tag-filter"></button> 487 `); 488 tagButton.text(tag).data('tag', tag); 489 container.append(tagButton); 490 }); 491 } 492 493 function applyTagFilters() { 494 const grid = $(selectors.gridContainer); 495 grid.find('.fm-font-item').each(function() { 496 const $item = $(this); 497 if (state.activeFilters.length === 0) { 498 $item.show(); 499 return; 500 } 501 502 const itemTags = $item.data('tags').split(' '); 503 const matches = state.activeFilters.every(filterTag => itemTags.includes(filterTag)); 504 $item.toggle(matches); 505 }); 506 } 507 508 function updateHiddenFormInputs() { 509 const container = $(selectors.formInputsContainer); 510 container.empty(); 511 512 // The form inputs must be associated with the main form, 'ttcForm'. 513 // The original system used checkboxes, but since this is now dynamic, 514 // we just need to ensure the correct `name` and `value` are submitted. 515 // Using hidden inputs is cleaner as they don't need to be visible or checked. 516 517 const allSelectedFonts = [...state.selectedFonts, ...state.autoSelectedFonts]; 518 519 // Add default Arial font if no fonts are selected at all. 520 if (allSelectedFonts.length === 0) { 521 container.append(`<input type="hidden" name="font-checkbox1[]" value="Arial|||Arial" form="ttcForm">`); // Arial is its own file name 522 } 523
524 allSelectedFonts.forEach(font => { 525 // Only include handwriting fonts (is_handwriting=1) if we're on the handwriting page 526 if (parseInt(font.is_handwriting) === 1 && !state.isHandwritingPage) { 527 return; // Skip handwriting fonts on non-handwriting pages 528 } 529 530 // The name attribute is dynamically created to match the old system 531 const inputName = `font-checkbox${font.font_type}[]`; 532 const inputValue = `${font.name}|||${font.file_name}`; // Combine name and file_name 533 const input = `<input type="hidden" name="${inputName}" value="${inputValue}" form="ttcForm">`; 534 container.append(input); 535 }); 536 } 537 538 // --- SYSTEM FONT DETECTION & AUTO-SELECTION --- 539 540 // Permission-less font detection using canvas measurement 541 function _doesFontExist(fontName) { 542 const fallback = "monospace"; 543 if (fontName.toLowerCase() === fallback) return true; 544 545 const testString = "abcdefghijklmnopqrstuvwxyz0123456789"; 546 const canvas = document.createElement("canvas"); 547 const context = canvas.getContext("2d"); 548 549 context.font = `72px ${fallback}`; 550 const fallbackWidth = context.measureText(testString).width; 551 552 context.font = `72px "${fontName}", ${fallback}`; 553 const testWidth = context.measureText(testString).width; 554 555 return testWidth !== fallbackWidth; 556 } 557 558 function getAvailableSystemFontsForCategory(categoryId) { 559 // --- CENTRAL CONFIGURATION FOR SYSTEM FONTS --- 560 // To add a free system font to a category, add its name to the array. 561 // Use the numeric category ID as the key. 562 // 0=Handwriting, 1=Boring, 2=Fancy, 3=Fun, 4=Absurd 563 const systemFontsByCategoryId = { 564 '1': ['Arial', 'Verdana', 'Georgia', 'Times New Roman', 'Courier New', 'Georgia', 'Roboto', 'Tahoma'], // Boring 565 '2': ['Papyrus'], // Fancy 566 '3': ['Comic Sans MS', 'Impact'] // Fun 567 }; 568 569 // Ensure categoryId is treated as a string for object key lookup.
570 const fontsForCategory = systemFontsByCategoryId[String(categoryId)] || []; 571 const availableFonts = []; 572 573 fontsForCategory.forEach(fontName => { 574 if (_doesFontExist(fontName)) { 575 const fontObj = { 576 id: 'system-' + fontName.replace(/\s/g, ''), 577 name: fontName, 578 file_name: fontName, 579 font_type: 1, // Basic font type 580 is_premium: 0, // Explicitly set to 0 (not false) to match database format 581 tags: ['system', 'basic'] 582 }; 583 584 // Mark Arial as auto-selected so it appears disabled 585 if (fontName === 'Arial') { 586 fontObj.is_auto_selected = true; 587 } 588 589 availableFonts.push(fontObj); 590 } 591 }); 592 return availableFonts; 593 } 594 595 function autoSelectInitialFonts() { 596 // Always auto-select Arial if available - it doesn't count against max and can't be unselected 597 const autoSelectedFonts = []; 598 599 if (_doesFontExist('Arial')) { 600 autoSelectedFonts.push({ 601 id: 'system-Arial', 602 name: 'Arial', 603 file_name: 'Arial', 604 font_type: 1, 605 is_premium: 0, 606 is_auto_selected: true // Mark as auto-selected so it can't be unselected 607 }); 608 } 609 610 // For new users, also select some additional fonts up to the limit 611 const additionalCandidates = ['Comic Sans MS', 'Verdana', 'Georgia', 'Times New Roman']; 612 const availableAdditional = []; 613 614 additionalCandidates.forEach(fontName => { 615 if (_doesFontExist(fontName)) { 616 availableAdditional.push({ 617 id: 'system-' + fontName.replace(/\s/g, ''), 618 name: fontName, 619 file_name: fontName, 620 font_type: 1, 621 is_premium: 0 622 }); 623 } 624 }); 625 626 // Select up to 3 additional fonts (since Arial is already selected) 627 state.selectedFonts = [...autoSelectedFonts, ...availableAdditional.slice(0, 3)]; 628 } 629 630 // --- "MY STASH" FUNCTIONALITY --- 631 632 function handleMyStashTab() { 633 const grid = $(selectors.gridContainer); 634 grid.empty(); // Clear the grid completely 635 636 if (!state.isMember) { 637 // Non-member message 638 const nonMemberHTML = ` 639 <div class="fm-stash-promo"> 640 <h3>â Premium Feature</h3> 641 <p>Premium Members can use the fonts that are installed on their own computer!</p> 642 <button type="button" class="fm-button-primary" id="fm-subscribe-link">Learn More About Premium</button> 643 </div> 644 `; 645 grid.html(nonMemberHTML); 646 $('#fm-subscribe-link').on('click', function() { 647 if (typeof subpop === 'function') { 648 subpop(); // Call the global subscription pop-up function 649 } else { 650 window.open('/members/members-subscribe.php', '_blank'); 651 } 652 }); 653 return; 654 } 655 656 // Member view - Banner first 657 const memberBannerHTML = ` 658 <div class="fm-stash-banner"> 659 <p class="fm-stash-warning"><b>NOTE:</b> These fonts are installed on your local computer. We are testing the ability for them to be used in PDFs and Images. Does not work in <b>Safari</b>, and may not work in <b>Firefox</b>. Should work in Chrome on Mac and Windows. </p> 660 <button type="button" class="fm-button-primary" id="fm-manage-local-fonts">Manage My Computer Fonts</button> 661 </div> 662 `; 663 grid.html(memberBannerHTML); 664 $('#fm-manage-local-fonts').on('click', manageLocalFonts); 665 666 // Now render the fonts into the same grid 667 renderMyStashGrid(); 668 } 669 670 function renderMyStashGrid() { 671 const grid = $(selectors.gridContainer); 672 673 // Clear only font items and old messages, leave the banner 674 grid.find('.fm-font-item').remove(); 675 grid.find('.fm-stash-empty-message').remove(); 676 677 if (state.userLocalFonts.length === 0) { 678 grid.append('<p class="fm-stash-empty-message" style="text-align: center; color: #666; grid-column: 1 / -1;">You have not added any local fonts to your stash yet.</p>'); 679 return; 680 } 681
682 state.userLocalFonts.forEach(fontName => { 683 const font = { 684 id: 'local-' + fontName.replace(/\s/g, ''), 685 name: fontName, 686 file_name: fontName, // For local fonts, name and file_name are the same 687 font_type: 4, // Use 4 for local/personal fonts 688 is_premium: false, 689 tags: ['local'] 690 }; 691 692 const isSelected = state.selectedFonts.some(f => f.id === font.id); 693 694 const fontItem = $(` 695 <div class="fm-font-item"> 696 <div class="fm-font-name"></div> 697 <div class="fm-font-preview">AaBbCc</div> 698 </div> 699 `); 700 701 fontItem.toggleClass('selected', isSelected); 702 fontItem.data('font-id', font.id); 703 fontItem.data('font-name', font.name); 704 fontItem.data('font-type', font.font_type); 705 fontItem.data('is-premium', font.is_premium); 706 fontItem.data('font-data', font); 707 708 fontItem.find('.fm-font-name').text(font.name); 709 fontItem.find('.fm-font-preview').css('font-family', `"${font.name}", sans-serif`); 710 711 grid.append(fontItem); 712 }); 713 } 714 715 async function manageLocalFonts() { 716 if (!('queryLocalFonts' in window)) { 717 Swal.fire('Browser Not Supported', 'Sorry, your browser does not support accessing local fonts.', 'error'); 718 return; 719 } 720 721 let systemFonts = []; 722 try { 723 const permissionStatus = await navigator.permissions.query({ name: 'local-fonts' }); 724 if (permissionStatus.state === 'denied') { 725 Swal.fire('Permission Denied', "You've previously denied permission to access local fonts. Please reset the permission in your browser settings.", 'warning'); 726 return; 727 } 728 systemFonts = await window.queryLocalFonts(); 729 } catch (err) { 730 Swal.fire('Error', 'Could not access local fonts. Please grant permission when prompted.', 'error'); 731 return; 732 } 733 734 const availableFonts = [...new Set(systemFonts.map(f => f.family))].sort(); 735 const savedLocalFonts = state.userLocalFonts; 736 737 const availableFontsHtml = availableFonts 738 .filter(font => !savedLocalFonts.includes(font)) 739 .map(font => ` 740 <div class="font-item" style="display: flex; align-items: center; padding: 5px;"> 741 <input type="checkbox" class="font-select" value="${font}" style="margin-right: 10px;"> 742 <span style="font-family: '${font}'; font-size: 16px;">${font}</span> 743 </div> 744 `).join(''); 745 746 const savedFontsHtml = savedLocalFonts.length ? ` 747 <div class="saved-fonts-section" style="margin-bottom: 20px;"> 748 <h3 style="margin-bottom: 10px;">Your Saved Fonts</h3> 749 ${savedLocalFonts.map(font => ` 750 <div class="font-item" style="display: flex; align-items: center; padding: 5px;"> 751 <input type="checkbox" class="font-select" value="${font}" checked style="margin-right: 10px;"> 752 <span style="font-family: '${font}'; font-size: 16px;">${font}</span> 753 </div> 754 `).join('')} 755 </div> 756 ` : ''; 757 758 Swal.fire({ 759 title: 'Manage My Computer Fonts', 760 html: ` 761 <div id="fontlist_instructions" style="text-align: left; margin-bottom: 15px;">Select the fonts from your computer you'd like to use.</div> 762 <div id="fontlist" style="text-align: left; max-height: 400px; overflow-y: auto; border: 1px solid #eee; padding: 10px;"> 763 ${savedFontsHtml} 764 <h3 style="margin-top: ${savedLocalFonts.length ? '20px' : '0'}; margin-bottom: 10px;">Available System Fonts</h3> 765 ${availableFontsHtml} 766 </div> 767 `, 768 showCancelButton: true, 769 confirmButtonText: 'Save My Stash', 770 customClass: { 771 popup: 'swal-wide' 772 }, 773 preConfirm: () => { 774 return Array.from(document.querySelectorAll('#fontlist .font-select:checked')) 775 .map(cb => cb.value); 776 } 777 }).then((result) => { 778 if (result.isConfirmed) { 779 state.userLocalFonts = result.value; 780 781 // Remove any deselected local fonts from the main selection 782 const deselectedLocalFonts = state.selectedFonts.filter(font => { 783 return String(font.id).startsWith('local-') && !state.userLocalFonts.includes(font.name); 784 }); 785 786 state.selectedFonts = state.selectedFonts.filter(font => { 787 return !deselectedLocalFonts.some(deselected => deselected.id === font.id); 788 }); 789 790 saveState(); 791 renderMyStashGrid(); 792 updateUI(); 793 } 794 }); 795 } 796 797 798 // --- START THE APP --- 799 init(); 800} 801 802function waitForJQuery() { 803 if (window.jQuery) { 804 initializeFontManager(); 805 } else { 806 setTimeout(waitForJQuery, 50); 807 } 808} 809 810waitForJQuery();
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.