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 <script> tag to the bottom of 'authors.php'? Or is there something wrong with the script file?
ChatGPT said:
Thought for 1m 43s
It’s not failing because you moved your <script> tag to the bottom. Putting scripts at the bottom is usually the right move.
1) Your “TinyMCE failed to load” message is self-inflicted
In scripts.js, you only initialize TinyMCE — you never actually load the TinyMCE library. So tinymce is undefined and your own guard prints the error:
scripts
Also, your authors.php (as uploaded) doesn’t include any TinyMCE CDN <script ...tinymce...> tag at all — it only loads scripts.js:
authors
Fix options (pick one):
Option A (simple): add TinyMCE CDN script BEFORE scripts.js
<script src="https://cdn.jsdelivr.net/npm/tinymce@6/tinymce.min.js"></script>
<script src="../js/scripts.js"></script>
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):
<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method Not Allowed');
}
$author_name = trim($_POST['author_name'] ?? '');
if ($author_name === '') {
// You can redirect back with an error flag if you want.
http_response_code(400);
exit('Author name is required.');
}
require __DIR__ . '/database.php';
// Expecting $connection (mysqli) from database.php
$stmt = $connection->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 <script> tag to the bottom didn’t cause this.
Your scripts.js is trying to run tinymce.init(...), but TinyMCE itself isn’t being loaded anywhere (no TinyMCE CDN or local tinymce script tag). So your code hits your own “TinyMCE failed to load” branch.
Fix: actually load TinyMCE before scripts.js
Add this before scripts.js in whatever pages need TinyMCE:
<script src="https://cdn.jsdelivr.net/npm/tinymce@6/tinymce.min.js"></script>
<script src="../js/scripts.js"></script>
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 <script src="...tinymce.min.js"></script> 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:
<script src="https://cdn.jsdelivr.net/npm/tinymce@6/tinymce.min.js"></script>
<script src="../js/scripts.js"></script>
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:
<script src="https://cdn.jsdelivr.net/npm/tinymce@6/tinymce.min.js"></script>
<script src="../js/scripts.js"></script>
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:
<script src="https://cdn.tiny.cloud/1/fqnv67bzomwbq9c2746wnv4qgsi11vvump77vnetery2cq62/tinymce/7/tinymce.min.js" referrerpolicy="origin"></script>
<script src="../js/scripts.js" defer></script>
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 <script ...tinymce...> only on pages that need TinyMCE.
Keep scripts.js shared across your whole site.
Make TinyMCE init conditional on #content.
Do that and the editor loads when you want, and your expand/collapse and author-jump features stay untouched.
If you want, I can paste back your entire scripts.js with the TinyMCE block swapped in exactly where it belongs, so you can copy/paste once and be done.
No file chosenNo file chosen
ChatGPT can make mistakes. Check important info.