^
Update this timeline entry
Author_Add failed to load
Source: Amy and ChatGPT #1356
Project ID
Chapter
No chapter
Timeline title
Start date
End date
Filename received
Filename assigned
Content
Enable editor
Use plain text
Code entry
Skip to content You said: author_add_run.php File authors.php File scripts.js JavaScript I have attached a file 'authors.php' that contains a form to Add Author, which posts to 'author_add_run.php' that I've also attached. Upon submission of a new author, Chrome said 'author_add_run.php' is not currently working. The inspector says "TinyMCE failed to load". TinyMCE is in the 'scripts.js' file also attached. Is the failure to load because I moved the Option B (better): only try to init TinyMCE if the editor exists Right now you always run: tinymce.init({ selector: '#content', ... }) scripts Change your TinyMCE block to: document.addEventListener('DOMContentLoaded', () => { const editor = document.querySelector('#content'); if (!editor) return; // <-- don’t even try on pages without it if (typeof tinymce === 'undefined') { console.error('TinyMCE failed to load.'); return; } tinymce.init({ selector: '#content', ... }); }); That prevents the console error from screaming on pages that don’t even have #content. 2) The reason Chrome says author_add_run.php is not currently working is almost certainly PHP, not TinyMCE Your form only posts author_name: authors …but author_add_run.php expects both author_name and project_id: author_add_run That means $_POST['project_id'] is an undefined index. Depending on your server settings, that can produce warnings/notices (or fatal behavior if configured strictly). Also: You do include "database.php"; with no path author_add_run — if database.php isn’t in the same directory as author_add_run.php, that’s a fatal include error. You redirect to ../pages/authors.php author_add_run but your uploaded authors.php is clearly being referenced like it’s at the site root (it includes ./pages/leftnav.php, ./css/styles.css) authors — so that redirect path may be wrong too. Minimum sane fixes: If you don’t actually use project_id for authors: Delete it from the PHP. if ($_SERVER["REQUEST_METHOD"] === "POST") { $author_name = trim($_POST['author_name'] ?? ''); require __DIR__ . "/database.php"; $stmt = $connection->prepare("INSERT INTO authors (author_name) VALUES (?)"); $stmt->bind_param("s", $author_name); $stmt->execute(); header("Location: ../authors.php"); // adjust to your real location exit; } If you DO need project_id: Add an input to the form (hidden or select), so it actually posts it. Bottom line TinyMCE error: your page isn’t loading TinyMCE at all (and/or you’re initializing it on pages that don’t have #content). That’s a front-end console error. scripts author_add_run.php not working: your PHP is expecting project_id that your form doesn’t send, plus likely path issues for database.php and the redirect. authors author_add_run If you tell me where database.php actually lives (relative path from author_add_run.php) and whether project_id is required, I can rewrite author_add_run.php to be bulletproof and match your folder layout. Sources You said: 'database.php' is in the same directory as 'author_add_run.php'. 'project_id' is not required. ChatGPT said: Cool — then the PHP crash is almost certainly because you’re trying to read $_POST['project_id'] even though the form never sends it. That’s a classic “works until it doesn’t” problem. Fix author_add_run.php (project_id not required) Replace your file with this (same behavior, but won’t blow up, and adds basic safety): prepare("INSERT INTO authors (author_name) VALUES (?)"); if (!$stmt) { http_response_code(500); exit('DB prepare failed: ' . $connection->error); } $stmt->bind_param("s", $author_name); if (!$stmt->execute()) { http_response_code(500); exit('DB execute failed: ' . $stmt->error); } $stmt->close(); // Redirect back to your authors page (adjust path if needed) header("Location: ../pages/authors.php"); exit; That removes project_id completely and prevents undefined-index nonsense. About the TinyMCE error Moving your Better fix: don’t even try TinyMCE on pages that don’t have the editor Wrap it so it only initializes when #content exists: document.addEventListener('DOMContentLoaded', () => { const editor = document.querySelector('#content'); if (!editor) return; if (typeof tinymce === 'undefined') { console.error('TinyMCE failed to load.'); return; } tinymce.init({ selector: '#content' /* ...rest... */ }); }); What to check after you patch Submit “Add Author” again — it should redirect cleanly (no white screen, no “not working”). The TinyMCE console error will disappear only on pages where you either: load TinyMCE properly, or stop initializing it when the editor isn’t present. If you paste the exact “Add Author” form section (or upload your current authors.php after your latest edits), I’ll tell you the correct redirect path too — right now you redirect to ../pages/authors.php, which might be right or might be one folder off depending on where author_add_run.php lives. You said: I replaced 'author_add_run.php' with your code and that resolved the problem. I am now able to add an author through the form. How can I add your TinyMCE code to 'scripts.js' without messing up the expandAll and collapseAll functions? ChatGPT said: Easy: don’t “add TinyMCE code” in a way that runs globally. Make it conditional and self-contained so it only runs on pages that actually have the editor. That way it won’t touch your expand/collapse code at all. Drop-in pattern for scripts.js Keep your existing expandAll / collapseAll functions exactly as they are. Add this TinyMCE block at the bottom of scripts.js (or anywhere, as long as it’s not inside another function that prevents it from running): (() => { document.addEventListener('DOMContentLoaded', () => { // Only run TinyMCE if the target textarea exists on this page const editorEl = document.querySelector('#content'); if (!editorEl) return; // TinyMCE library must be loaded via before scripts.js if (typeof window.tinymce === 'undefined') { console.error('TinyMCE failed to load: tinymce is undefined. Did you include the TinyMCE script tag?'); return; } // Avoid double-initializing if scripts.js runs twice for any reason if (window.tinymce.get('content')) return; window.tinymce.init({ selector: '#content', height: 600, menubar: false, plugins: 'lists link code', toolbar: 'undo redo | bold italic | bullist numlist | link | code' }); }); })(); This won’t interfere with expandAll / collapseAll because: it only runs after DOM is ready it exits immediately if #content isn’t on the page it doesn’t overwrite any globals you likely rely on One non-negotiable: load the TinyMCE library in the page In any PHP page that needs the editor, include TinyMCE before scripts.js: If you don’t include that, scripts.js can’t magically summon TinyMCE. Why your expand/collapse won’t break Your expand/collapse functions are triggered by clicks/handlers. The TinyMCE init block runs once on load and then gets out of the way. As long as you’re not wrapping your whole scripts.js in something that hides those functions from the onclick handlers, you’re fine. If you want, paste the current scripts.js and I’ll splice this into your exact structure (without changing anything you already have). You said: Here is the current 'scripts.js' file contents /* Collapsible sections per Josh Howard */ const collapsibleItems = document.getElementsByClassName("collapsible"); for (let i = 0; i < collapsibleItems.length; i++) { const collapsibleItem = collapsibleItems[i]; collapsibleItem.addEventListener("click", function() { this.classList.toggle("active"); const content = this.nextElementSibling; content.classList.toggle('expanded'); }); } /* Expand/collapse all by Josh */ const expandAll = () => { const collapsibleItems = document.getElementsByClassName("collapsible"); for (let i = 0; i < collapsibleItems.length; i++) { const content = collapsibleItems[i].nextElementSibling; if (!content.classList.contains('expanded')) { content.classList.add('expanded'); } } }; const collapseAll = () => { const collapsibleItems = document.getElementsByClassName("collapsible"); for (let i = 0; i < collapsibleItems.length; i++) { const content = collapsibleItems[i].nextElementSibling; if (content.classList.contains('expanded')) { content.classList.remove('expanded'); } } }; /* TinyMCE from CDN for WYSIWYG editor from MIT open source */ document.addEventListener('DOMContentLoaded', function () { const expandAllBtn = document.querySelector('#expand-all-button'); const collapseAllBtn = document.querySelector('#collapse-all-button'); if (expandAllBtn) expandAllBtn.addEventListener('click', expandAll); if (collapseAllBtn) collapseAllBtn.addEventListener('click', collapseAll); if (typeof tinymce === 'undefined') { console.error('TinyMCE failed to load.'); return; } console.log('TinyMCE found, initializing...'); tinymce.init({ selector: '#content', menubar: false, toolbar: 'undo redo | bold italic underline | alignleft aligncenter alignright | bullist numlist | link | removeformat', plugins: 'lists link', branding: false, height: 400 }); }); /* ========================================== Author Jump + Arrow Navigation (authors.php) by ChatGPT 1/12/2026 ========================================== */ (() => { const input = document.getElementById('authorJump'); const list = document.getElementById('authorList'); if (!input || !list) return; const norm = (s) => (s || '') .toString() .toLowerCase() .replace(/['".,()]/g, '') .replace(/\s+/g, ' ') .trim(); let matches = []; let matchIndex = -1; let lastQuery = ''; let lastHit = null; function clearHighlights() { list.querySelectorAll('.author-hit') .forEach(a => a.classList.remove('author-hit')); } function getLinks() { return Array.from(list.querySelectorAll('a.author-link')); } function computeMatches(rawQuery) { // Keep commas for "Last, First" intent const cleaned = (rawQuery || '') .toString() .toLowerCase() .replace(/['".()]/g, '') // NOTE: comma is NOT removed here .replace(/\s+/g, ' ') .trim(); if (!cleaned) return []; const hasComma = cleaned.includes(','); const [left, right = ''] = cleaned.split(',', 2); const qLast = norm(left); const qFirst = norm(right); const items = getLinks().map(a => { const fullRaw = (a.textContent || '').toString(); const full = norm(fullRaw); // last is authoritative from data-last const last = norm(a.dataset.last || ''); // first name chunk: // - if "Last, First" → after comma // - else "First Last" → everything except last word let first = ''; if (fullRaw.includes(',')) { first = norm(fullRaw.split(',')[1] || ''); } else { const parts = fullRaw.trim().split(/\s+/); if (parts.length > 1) first = norm(parts.slice(0, -1).join(' ')); } return { a, last, first, full }; }); // helper: rank matches by exact → startsWith → contains const rank = (arr, field, q) => { const exact = arr.filter(x => x[field] === q).map(x => x.a); const starts = arr.filter(x => x[field].startsWith(q) && x[field] !== q).map(x => x.a); const contains = arr.filter(x => x[field].includes(q) && !x[field].startsWith(q)).map(x => x.a); return [...exact, ...starts, ...contains]; }; // If user typed a comma, they’re saying: "last, first" // Still: last must match FIRST; then narrow by first. if (hasComma && qLast) { const lastCandidates = items.filter(x => x.last.includes(qLast)); if (!lastCandidates.length) return []; // no last-name match at all → no match // If no first part, just rank by last if (!qFirst) return rank(lastCandidates, 'last', qLast); // Narrow within last matches by first const firstRanked = rank(lastCandidates, 'first', qFirst); return firstRanked.length ? firstRanked : []; // only return if first actually matches } // 1) LAST-NAME SEARCH ONLY (the priority you asked for) const lastRanked = rank(items, 'last', qLast); if (lastRanked.length) return lastRanked; // 2) ONLY IF ZERO last-name matches exist, then search FIRST names const firstRanked = rank(items, 'first', qLast); if (firstRanked.length) return firstRanked; // 3) (optional fallback) full-string contains, *after* last+first fail // If you want to be stricter, delete this. const fullRanked = rank(items, 'full', qLast); return fullRanked; } function setHit(el, {behavior = 'auto', fromKeys = false} = {}) { if (!el) return; clearHighlights(); el.classList.add('author-hit'); lastHit = el; // Scroll behavior: // - typing: auto (prevents snap-back flicker) // - arrow keys: smooth (feels nice) requestAnimationFrame(() => { el.scrollIntoView({ behavior, block: 'center', inline: 'nearest' }); }); // Don’t refocus input here—you're already typing in it. // Refocusing can trigger browser “caret visibility” scroll snaps. } function jump(fromKeys = false) { const q = norm(input.value); if (!q) { input.classList.remove('no-match'); matches = []; matchIndex = -1; lastQuery = ''; lastHit = null; clearHighlights(); return; } // Recompute matches only if query changed if (q !== lastQuery) { matches = computeMatches(q); matchIndex = matches.length ? 0 : -1; lastQuery = q; } if (!matches.length) { input.classList.add('no-match'); lastHit = null; clearHighlights(); // Only return to the input when there is truly no match input.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; } input.classList.remove('no-match'); const hit = matches[matchIndex] || matches[0]; // Don’t re-scroll if nothing changed if (hit === lastHit && !fromKeys) return; setHit(hit, { behavior: fromKeys ? 'smooth' : 'auto', fromKeys }); } // While typing: jump to best match (instant scroll, no flicker) input.addEventListener('input', () => jump(false)); // Arrow navigation + Enter to open input.addEventListener('keydown', (e) => { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); // Ensure matches are current for the existing text const q = norm(input.value); if (q !== lastQuery) { matches = computeMatches(q); matchIndex = matches.length ? 0 : -1; lastQuery = q; } if (!matches.length) return; if (e.key === 'ArrowDown') { matchIndex = (matchIndex + 1) % matches.length; } else { matchIndex = (matchIndex - 1 + matches.length) % matches.length; } jump(true); return; } if (e.key === 'Enter') { e.preventDefault(); const hit = list.querySelector('a.author-link.author-hit'); if (hit) window.location.href = hit.href; } }); })(); ChatGPT said: You’re close, but your TinyMCE block has one booby trap: The real issue You call tinymce.init({ selector: '#content', ... }) on every page as soon as DOM is ready. If the page doesn’t have #content (and most pages won’t), TinyMCE may still try to initialize and can throw side-effects / noise. More importantly: your console error “TinyMCE failed to load” will fire on any page that doesn’t include the TinyMCE library, even if that page never needs an editor. That’s how you “mess things up”: global JS doing page-specific work. The fix Make TinyMCE initialization conditional and isolated, and keep your expand/collapse wiring intact. Here’s your TinyMCE section rewritten safely. Replace your current “TinyMCE from CDN…” block with this: /* TinyMCE init (only when #content exists) */ document.addEventListener('DOMContentLoaded', function () { // Keep your buttons wiring here (fine) const expandAllBtn = document.querySelector('#expand-all-button'); const collapseAllBtn = document.querySelector('#collapse-all-button'); if (expandAllBtn) expandAllBtn.addEventListener('click', expandAll); if (collapseAllBtn) collapseAllBtn.addEventListener('click', collapseAll); // TinyMCE should ONLY run on pages that actually have the editor element const editorEl = document.querySelector('#content'); if (!editorEl) return; // TinyMCE library must be loaded in the page before scripts.js if (typeof window.tinymce === 'undefined') { console.error('TinyMCE failed to load. Include tinymce.min.js before scripts.js on this page.'); return; } // Prevent double init if scripts load twice if (window.tinymce.get('content')) return; window.tinymce.init({ selector: '#content', menubar: false, toolbar: 'undo redo | bold italic underline | alignleft aligncenter alignright | bullist numlist | link | removeformat', plugins: 'lists link', branding: false, height: 400 }); }); Why this won’t mess up expandAll/collapseAll Your expand/collapse functions stay global (const expandAll = ...), so your button listeners still work. TinyMCE only runs if #content exists, so it stops being a global landmine. Your author-jump IIFE is separate and already safely gated by if (!input || !list) return; — good pattern. One required HTML rule (still true) On pages that need TinyMCE, you must load TinyMCE before scripts.js: If you don’t want to include TinyMCE on most pages (smart), only include that CDN script on the pages with the editor. If you tell me which page actually contains #content (or paste that page header/footer script tags), I’ll tell you the cleanest exact placement so TinyMCE loads only where needed. You said: The script is already in my 'content_add.php' file like this: ChatGPT said: That’s almost right — but the defer on scripts.js is the problem in combo with how your TinyMCE init currently runs. What’s happening tinymce.min.js loads normally (blocking). scripts.js loads with defer, meaning it waits until the HTML is parsed, then runs. Your TinyMCE init runs on DOMContentLoaded (fine), but you’re also doing this in the same handler: if (typeof tinymce === 'undefined') { ... return; } tinymce.init({ selector: '#content', ... }); So on pages that don’t load TinyMCE (most pages), you get the “failed to load” message even though nothing is wrong. And on the pages that do load TinyMCE, you’re still trying to init #content even if that element doesn’t exist (depending on page). That’s the part to harden. Make your setup bulletproof Keep your HTML exactly as-is. It’s fine. Change your TinyMCE block in scripts.js so it only runs when #content exists. Here’s the clean version you should use (same as before, but I’m being explicit since you showed your tags): /* TinyMCE init (only when #content exists) */ document.addEventListener('DOMContentLoaded', function () { // Wire buttons (safe on any page) const expandAllBtn = document.querySelector('#expand-all-button'); const collapseAllBtn = document.querySelector('#collapse-all-button'); if (expandAllBtn) expandAllBtn.addEventListener('click', expandAll); if (collapseAllBtn) collapseAllBtn.addEventListener('click', collapseAll); // Only attempt TinyMCE on pages that actually have the editor element const editorEl = document.querySelector('#content'); if (!editorEl) return; // content_add.php loads TinyMCE; other pages probably won't if (typeof window.tinymce === 'undefined') { console.error('TinyMCE failed to load. (content_add.php should include tinymce.min.js before scripts.js)'); return; } // Avoid double init if (window.tinymce.get('content')) return; window.tinymce.init({ selector: '#content', menubar: false, toolbar: 'undo redo | bold italic underline | alignleft aligncenter alignright | bullist numlist | link | removeformat', plugins: 'lists link', branding: false, height: 400 }); }); One more “tell it like it is” improvement Since you’re already using defer, you don’t need DOMContentLoaded for the expand/collapse button wiring or the author jump IIFE — defer guarantees the DOM is parsed before the script runs. But leaving it doesn’t hurt. Just don’t make global assumptions (like TinyMCE always existing). If you want zero drama Leave