WordPress Collapsible Code Blocks – Custom Implementation

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;
}