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