PageSourceSearch

https://worksheets.theteacherscorner.net/assets/js/font-manager.js?v=1790345379

js theteacherscorner.net collected 2026-09-25 14:09:51 UTC 35,736 bytes, 810 lines download raw bytes

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.