This is a custom solution to easily add collapsible code blocks to WordPress posts. Tested with Classic Editor and Advanced Editor Tools plugins installed.
Add to wp-content/plugins/collapsible-pre-blocks/collapsible-pre-blocks.php:
<?php
/**
* Plugin Name: Collapsible Pre Blocks & Code Inserter
* Description: Collapses frontend <pre> tags with controls, adds a Code Sample button with custom languages to TinyMCE, and applies PrismJS syntax highlighting to both Editor and Frontend.
* Version: 2.4
* Author: d5.ca
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/* ==========================================================================
1. TINYMCE CODE SAMPLE TOOLBAR BUTTON & CUSTOM LANGUAGES
========================================================================== */
// Load the official TinyMCE codesample plugin from CDN
function cpb_enable_codesample_cdn( $plugins ) {$plugins['codesample'] = 'https://cdn.jsdelivr.net/npm/tinymce@4/plugins/codesample/plugin.min.js';
return $plugins;
}
add_filter( 'mce_external_plugins', 'cpb_enable_codesample_cdn' );
// Add the { } button to the Classic Editor main toolbar
function cpb_add_codesample_button( $buttons ) {
array_push( $buttons, 'codesample' );
return $buttons;
}
add_filter( 'mce_buttons', 'cpb_add_codesample_button' );
// Customize TinyMCE settings & inject PrismJS scripts inside the editor iframe
function cpb_custom_codesample_languages( $initArray ) {$languages = array(
array( 'text' => 'Bash / Shell', 'value' => 'bash' ),
array( 'text' => 'Batch File', 'value' => 'batch' ),
array( 'text' => 'PowerShell', 'value' => 'powershell' ),
array( 'text' => 'Go', 'value' => 'go' ),
array( 'text' => 'HTML/Markup', 'value' => 'markup' ),
array( 'text' => 'CSS', 'value' => 'css' ),
array( 'text' => 'JavaScript', 'value' => 'javascript' ),
array( 'text' => 'PHP', 'value' => 'php' ),
array( 'text' => 'Python', 'value' => 'python' ),
array( 'text' => 'SQL', 'value' => 'sql' ),
array( 'text' => 'JSON', 'value' => 'json' ),
array( 'text' => 'YAML', 'value' => 'yaml' ),
array( 'text' => 'C++', 'value' => 'cpp' )
);
// Inject custom language dropdown options
$initArray['codesample_languages'] = wp_json_encode($languages );
// Dynamically load PrismJS & Autoloader INSIDE the TinyMCE iframe context
$initArray['setup'] = "function(editor) {
editor.on('init', function() {
var doc = editor.getDoc();
// Add Prism Core JS to editor iframe
var prismScript = doc.createElement('script');
prismScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js';
doc.head.appendChild(prismScript);
// Add Prism Autoloader JS to editor iframe
prismScript.onload = function() {
var autoScript = doc.createElement('script');
autoScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js';
doc.head.appendChild(autoScript);
autoScript.onload = function() {
if (editor.iframeElement && editor.iframeElement.contentWindow.Prism) {
editor.iframeElement.contentWindow.Prism.highlightAll();
}
};
};
});
// Re-run highlighting when changes occur or code blocks are modified inside editor
editor.on('SetContent ExecCommand', function() {
setTimeout(function() {
if (editor.iframeElement && editor.iframeElement.contentWindow.Prism) {
editor.iframeElement.contentWindow.Prism.highlightAll();
}
}, 100);
});
}";
return $initArray;
}
add_filter( 'tiny_mce_before_init', 'cpb_custom_codesample_languages' );
/* ==========================================================================
2. EDITOR-SIDE SYNTAX HIGHLIGHTING STYLES (INSIDE TINYMCE IFRAME)
========================================================================== */
// Load Prism CSS inside the TinyMCE visual editor iframe
function cpb_add_editor_styles( $mce_css ) {
if ( ! empty( $mce_css ) ) {$mce_css .= ',';
}
$mce_css .= 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css';
return $mce_css;
}
add_filter( 'mce_css', 'cpb_add_editor_styles' );
/* ==========================================================================
3. FRONTEND PRISMJS SYNTAX HIGHLIGHTING ASSETS
========================================================================== */
function cpb_enqueue_syntax_highlighting() {
// Default Light theme (matches Classic Editor light-grey style)
wp_enqueue_style(
'cpb-prism-css',
'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css',
array(),
'1.29.0'
);
// Core Prism JS + Autoloader to dynamically fetch language packs on demand
wp_enqueue_script(
'cpb-prism-js',
'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js',
array(),
'1.29.0',
true
);
wp_enqueue_script(
'cpb-prism-autoloader',
'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js',
array('cpb-prism-js'),
'1.29.0',
true
);
}
add_action( 'wp_enqueue_scripts', 'cpb_enqueue_syntax_highlighting' );
/* ==========================================================================
4. FRONTEND COLLAPSIBLE PRE BLOCK JS SCRIPT
========================================================================== */
function cpb_render_inline_script() {
?>
<script id="cpb-collapsible-pre">
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('pre').forEach((pre) => {
// Count lines to detect short code blocks
const text = pre.innerText.trim();
const lineCount = text ? text.split('\n').length : 0;
const isShort = lineCount <= 8;
// Wrap pre element in container
const wrapper = document.createElement('div');
wrapper.className = 'pre-wrapper' + (isShort ? ' pre-short' : ' pre-collapsed');
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(pre);
// Build floating control toolbar
const controls = document.createElement('div');
controls.className = 'pre-controls';
// Collapsed state (> 8 lines) has a vertical scrollbar by default
if (!isShort) {
controls.classList.add('has-scrollbar');
}
controls.innerHTML = `
<button class="pre-btn pre-copy-btn" title="Copy code" aria-label="Copy code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</button>
${!isShort ? `
<button class="pre-btn pre-fullscreen-btn" title="Fullscreen" aria-label="Toggle Fullscreen">
<svg class="icon-fullscreen" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line></svg>
</button>
<button class="pre-btn pre-toggle-btn" title="Expand" aria-label="Expand code">
<svg class="icon-expand" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
` : ''}
`;
wrapper.appendChild(controls);
// Dynamic icon offset based on vertical scrollbar visibility
const checkScrollbar = () => {
if (wrapper.classList.contains('pre-short')) {
controls.classList.remove('has-scrollbar');
return;
}
if (wrapper.classList.contains('pre-collapsed')) {
controls.classList.add('has-scrollbar');
} else {
const hasVerticalScroll = pre.scrollHeight > pre.clientHeight;
if (hasVerticalScroll) {
controls.classList.add('has-scrollbar');
} else {
controls.classList.remove('has-scrollbar');
}
}
};
// Expand/Collapse Action
const toggleBtn = controls.querySelector('.pre-toggle-btn');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
const isCollapsed = wrapper.classList.toggle('pre-collapsed');
toggleBtn.title = isCollapsed ? 'Expand' : 'Collapse';
toggleBtn.style.transform = isCollapsed ? 'rotate(0deg)' : 'rotate(180deg)';
setTimeout(checkScrollbar, 50);
});
}
// Fullscreen Toggle Action
const fsBtn = controls.querySelector('.pre-fullscreen-btn');
if (fsBtn) {
fsBtn.addEventListener('click', () => {
const isFS = wrapper.classList.toggle('pre-fullscreen');
document.body.classList.toggle('pre-fs-active', isFS);
fsBtn.title = isFS ? 'Exit Fullscreen' : 'Fullscreen';
setTimeout(checkScrollbar, 50);
});
}
// Exit Fullscreen on ESC Key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && wrapper.classList.contains('pre-fullscreen')) {
wrapper.classList.remove('pre-fullscreen');
document.body.classList.remove('pre-fs-active');
if (fsBtn) fsBtn.title = 'Fullscreen';
setTimeout(checkScrollbar, 50);
}
});
// Copy Code to Clipboard Action
const copyBtn = controls.querySelector('.pre-copy-btn');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(pre.innerText).then(() => {
copyBtn.classList.add('copied');
setTimeout(() => copyBtn.classList.remove('copied'), 2000);
});
});
});
});
</script>
<?php
}
add_action( 'wp_footer', 'cpb_render_inline_script' );
Add to Appearance > Customize > Additional CSS:
/* Base wrapper */
.pre-wrapper {
position: relative;
margin-bottom: 1.714285714rem;
}
/* Base Pre Block Styling */
.pre-wrapper pre {
margin: 0;
padding-top: 2.5rem; /* Room for action icons */
white-space: pre;
word-wrap: normal;
overflow: auto;
max-height: 70vh; /* Default expanded height limit */
transition: max-height 0.25s ease-in-out;
}
/* Collapsed State (> 8 lines) */
.pre-wrapper.pre-collapsed pre {
max-height: 120px;
}
/* Short State (<= 8 lines): Auto height without vertical scrollbar */
.pre-wrapper.pre-short pre {
max-height: none;
overflow-y: visible;
overflow-x: auto;
}
/* Floating Action Buttons (Default right position when NO vertical scrollbar) */
.pre-controls {
position: absolute;
top: 8px;
right: 8px; /* Used when no scrollbar is present */
display: flex;
gap: 6px;
z-index: 10;
transition: right 0.2s ease;
}
/* Position applied when collapsed or when vertical scrollbar is present */
.pre-controls.has-scrollbar {
right: 24px; /* Shifts buttons safely to the left of the scroll track */
}
/* FULLSCREEN OVERLAY MODE */
.pre-wrapper.pre-fullscreen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 999999 !important;
margin: 0 !important;
padding: 0 !important;
background: #fff;
}
.pre-wrapper.pre-fullscreen pre {
max-height: 100vh !important;
height: 100vh !important;
border: none !important;
border-radius: 0 !important;
box-sizing: border-box;
}
.pre-wrapper.pre-fullscreen .pre-controls {
position: fixed;
top: 15px;
}
/* Prevent body scrolling while in fullscreen mode */
body.pre-fs-active {
overflow: hidden !important;
}
/* Icon Buttons */
.pre-btn {
background: rgba(240, 240, 240, 0.95);
border: 1px solid #ccc;
border-radius: 3px;
padding: 4px 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s, transform 0.2s;
color: #333;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.pre-btn:hover {
background: #e0e0e0;
}
.pre-copy-btn.copied {
background: #4caf50;
color: #fff;
border-color: #4caf50;
}