UNPKG

expressive-code-fullscreen

Version:

Add fullscreen functionality to codeblocks in your documentation website.

1,089 lines (962 loc) 41.4 kB
// src/index.ts import { definePlugin, PluginStyleSettings } from "@expressive-code/core"; import { h } from "@expressive-code/core/hast"; var fullscreenStyleSettings = new PluginStyleSettings({ defaultValues: { fullscreen: { toolbarBg: "rgba(90, 88, 88, 0.95)", toolbarBorder: "rgba(255, 255, 255, 0.1)", buttonBg: "rgba(58, 57, 57, 0.9)", buttonBgHover: "rgba(120, 120, 120, 0.5)", buttonBgActive: "rgba(25, 25, 25, 0.9)", buttonText: "#ffffff", buttonBorder: "rgba(255, 255, 255, 0.2)", buttonFocus: "rgba(74, 144, 226, 0.6)", containerBg: "rgba(0, 0, 0, 0.85)", contentShadow: "rgba(0, 0, 0, 0.5)", hintBg: "rgba(20, 20, 20, 0.95)", hintText: "#ffffff", hintBorder: "rgba(255, 255, 255, 0.2)" } } }); function pluginFullscreen(options = {}) { const config = { enabled: true, fullscreenButtonTooltip: "Toggle fullscreen view", enableEscapeKey: true, exitOnBrowserBack: true, addToUntitledBlocks: true, showOnHoverOnly: true, animationDuration: 200, svgPathFullscreenOn: "M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z", svgPathFullscreenOff: "M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z", ...options }; return definePlugin({ name: "Expressive Code Fullscreen Plugin", styleSettings: fullscreenStyleSettings, baseStyles: ({ cssVar }) => ` @at-root { /* Fullscreen Plugin Theme Variables - Generated by PluginStyleSettings */ .cb-fullscreen__container { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; z-index: 2147483647 !important; overflow: auto !important; padding: 1.25rem !important; box-sizing: border-box !important; visibility: hidden !important; transform: scale(0.01) !important; transition: transform cubic-bezier(0.17, 0.67, 0.5, 0.71) ${config.animationDuration}ms !important; outline: none !important; display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: flex-start !important; isolation: isolate !important; backdrop-filter: blur(5px) !important; -webkit-backdrop-filter: blur(5px) !important; pointer-events: none !important; } .cb-fullscreen__content { width: 100% !important; max-width: 95% !important; display: flex !important; flex-direction: column !important; background-color: transparent !important; gap: 0.5rem !important; align-items: stretch !important; box-shadow: 0 1.25rem 3.75rem rgba(0, 0, 0, 0.5) !important; border-radius: 0.625rem !important; } .cb-fullscreen__container--open { visibility: visible !important; transform: scale(1) !important; pointer-events: auto !important; } .cb-fullscreen__font-controls { display: flex !important; align-items: center !important; gap: 0.25rem !important; background: ${cssVar("fullscreen.toolbarBg")} !important; border: 1px solid ${cssVar("fullscreen.toolbarBorder")} !important; border-radius: 8px !important; padding: 0.25rem !important; box-shadow: 0 1px 2px ${cssVar("fullscreen.contentShadow")} !important; justify-content: center !important; } .cb-fullscreen__font-btn { display: inline-flex !important; align-items: center !important; justify-content: center !important; width: 3rem !important; height: 3rem !important; padding: 0.5rem !important; margin-left: 0.5rem !important; background: ${cssVar("fullscreen.buttonBg")} !important; border: 1px solid ${cssVar("fullscreen.buttonBorder")} !important; border-radius: 6px !important; cursor: pointer !important; color: ${cssVar("fullscreen.buttonText")} !important; transition: all 0.2s ease !important; position: relative !important; min-width: 36px !important; min-height: 36px !important; } .cb-fullscreen__font-btn:hover { background: ${cssVar("fullscreen.buttonBgHover")} !important; transform: scale(1.05) !important; } .cb-fullscreen__font-btn:focus { outline: 2px solid ${cssVar("fullscreen.buttonFocus")} !important; outline-offset: 0.125rem !important; } .cb-fullscreen__font-btn:active { background: ${cssVar("fullscreen.buttonBgActive")} !important; transform: scale(0.95) !important; } .cb-fullscreen__font-btn svg { width: 1rem !important; height: 1rem !important; stroke-width: 2.5 !important; } .cb-fullscreen__font-btn--decrease[title]:hover::after { content: attr(title) !important; position: absolute !important; right: 100% !important; top: 50% !important; transform: translateY(-50%) !important; background-color: ${cssVar("fullscreen.hintBg")} !important; color: ${cssVar("fullscreen.hintText")} !important; padding: 0.375rem 0.5rem !important; border-radius: 0.25rem !important; font-size: 0.75rem !important; white-space: nowrap !important; z-index: 2147483647 !important; margin-right: 0.5rem !important; border: 1px solid ${cssVar("fullscreen.hintBorder")} !important; box-shadow: 0 0.125rem 0.5rem ${cssVar("fullscreen.contentShadow")} !important; pointer-events: none !important; } .cb-fullscreen__font-btn--increase[title]:hover::after { content: attr(title) !important; position: absolute !important; left: 100% !important; top: 50% !important; transform: translateY(-50%) !important; background-color: ${cssVar("fullscreen.hintBg")} !important; color: ${cssVar("fullscreen.hintText")} !important; padding: 0.375rem 0.5rem !important; border-radius: 0.25rem !important; font-size: 0.75rem !important; white-space: nowrap !important; z-index: 2147483647 !important; margin-left: 0.5rem !important; border: 1px solid ${cssVar("fullscreen.hintBorder")} !important; box-shadow: 0 0.125rem 0.5rem ${cssVar("fullscreen.contentShadow")} !important; pointer-events: none !important; } .cb-fullscreen__hint { position: absolute !important; bottom: 1.25rem !important; left: 50% !important; transform: translateX(-50%) !important; background-color: ${cssVar("fullscreen.hintBg")} !important; color: ${cssVar("fullscreen.hintText")} !important; padding: 0.75rem 1rem !important; border-radius: 0.5rem !important; font-size: 1rem !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; opacity: 0.85 !important; pointer-events: none !important; z-index: 10110 !important; backdrop-filter: blur(8px) !important; -webkit-backdrop-filter: blur(8px) !important; border: 1px solid ${cssVar("fullscreen.hintBorder")} !important; box-shadow: 0 4px 12px ${cssVar("fullscreen.contentShadow")} !important; } @keyframes simpleShow { to { opacity: 0.85; } } .cb-fullscreen__hint kbd { background-color: rgba(255, 255, 255, 0.2) !important; padding: 0.125rem 0.375rem !important; border-radius: 0.25rem !important; font-size: 0.75rem !important; font-weight: bold !important; margin: 0 0.125rem !important; color: #ffffff !important; border: 1px solid rgba(255, 255, 255, 0.1) !important; } .cb-fullscreen__sr-only { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; } #fullscreen-description { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important; } } .expressive-code.cb-fullscreen__active { align-self: center !important; flex: 1 !important; width: calc(100% - 2rem) !important; max-width: none !important; height: auto !important; margin: 1rem !important; margin-bottom: 4rem !important; background-color: #1e1e1e !important; border-radius: 0.625rem !important; box-sizing: border-box !important; box-shadow: 0 1.25rem 3.75rem rgba(0, 0, 0, 0.5) !important; } .expressive-code.cb-fullscreen__active pre, .expressive-code.cb-fullscreen__active code { font-size: calc(1em * var(--ec-font-scale, 1)) !important; } .expressive-code.cb-fullscreen__active .frame { font-size: calc(1em * var(--ec-font-scale, 1)) !important; } .cb-fullscreen__button { display: inline-flex; align-items: center; justify-content: center; width: 1.75rem; height: 1.75rem; padding: 0.25rem; background-color: rgba(255, 255, 255, 0.1) !important; border: 1px solid transparent; cursor: pointer; opacity: 0.7; transition: opacity 0.2s, background-color 0.2s, border-color 0.2s, transform 0.2s ease; border-radius: 20% !important; color: inherit; position: absolute; top: 4px; right: 8px; z-index: 100; } .expressive-code:not(.has-title) .cb-fullscreen__button, .expressive-code .frame:not(.has-title) ~ * .cb-fullscreen__button { top: 52px !important; right: 10px !important; } /* Terminal blocks - position button in figcaption header */ .expressive-code .frame.is-terminal .cb-fullscreen__button { position: absolute !important; top: 4px !important; right: 8px !important; z-index: 100 !important; } /* Hover-only visibility for untitled, non-terminal blocks ONLY */ ${config.showOnHoverOnly ? ` .expressive-code:not(.has-title) .cb-fullscreen__button:not(.frame.is-terminal *), .expressive-code .frame:not(.has-title):not(.is-terminal) ~ * .cb-fullscreen__button { opacity: 0; transition: opacity 0.2s ease, background-color 0.2s, border-color 0.2s, transform 0.2s ease; } .expressive-code:not(.has-title):hover .cb-fullscreen__button:not(.frame.is-terminal *), .expressive-code:hover .frame:not(.has-title):not(.is-terminal) ~ * .cb-fullscreen__button, .expressive-code:not(.has-title) .cb-fullscreen__button:focus:not(.frame.is-terminal *), .expressive-code .frame:not(.has-title):not(.is-terminal) ~ * .cb-fullscreen__button:focus, .expressive-code:not(.has-title) .cb-fullscreen__button:focus-visible:not(.frame.is-terminal *), .expressive-code .frame:not(.has-title):not(.is-terminal) ~ * .cb-fullscreen__button:focus-visible { opacity: 0.7; border: 2px solid #888888 !important; border-radius: 0.25rem !important; } /* Mobile/touch device fallback - show button on touch devices */ @media (hover: none) and (pointer: coarse) { .expressive-code:not(.has-title) .cb-fullscreen__button:not(.frame.is-terminal *), .expressive-code .frame:not(.has-title):not(.is-terminal) ~ * .cb-fullscreen__button { opacity: 0.7; } } ` : ""} .cb-fullscreen__button:hover { opacity: 1; background-color: rgba(0, 0, 0, 0.2) !important; border: 1px solid ${cssVar("fullscreen.buttonBorder")}; transform: scale(1.1); } .cb-fullscreen__button:focus { outline: 2px solid #4A90E2; outline-offset: 0.125rem; background-color: rgba(74, 144, 226, 0.2); } .cb-fullscreen__button:focus-visible { outline: 2px solid #4A90E2; outline-offset: 0.125rem; background-color: rgba(74, 144, 226, 0.2); } .cb-fullscreen__button .fullscreen-on { display: inline; } .cb-fullscreen__button .fullscreen-off { display: none; } .expressive-code.cb-fullscreen__active .cb-fullscreen__button .fullscreen-on { display: none !important; } .expressive-code.cb-fullscreen__active .cb-fullscreen__button .fullscreen-off { display: inline !important; } /* Custom tooltip for fullscreen button */ .cb-fullscreen__button[data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; right: 100%; top: 50%; transform: translateY(-50%); background-color: ${cssVar("fullscreen.hintBg")}; color: ${cssVar("fullscreen.hintText")}; padding: 0.5rem 0.75rem; border-radius: 0.375rem; font-size: 0.875rem; white-space: nowrap; z-index: 10001; margin-right: 0.5rem; border: 1px solid ${cssVar("fullscreen.hintBorder")}; box-shadow: 0 0.25rem 0.75rem ${cssVar("fullscreen.contentShadow")}; pointer-events: none; opacity: 0; animation: tooltipFadeIn 0.2s ease-out forwards; } @keyframes tooltipFadeIn { from { opacity: 0; transform: translateY(-50%) translateX(0.25rem); } to { opacity: 0.9; transform: translateY(-50%) translateX(0); } } `, hooks: { postprocessRenderedBlock: async (context) => { if (!config.enabled) return; const titleFromMeta = context.codeBlock.metaOptions.getString("title"); const titleFromProps = context.codeBlock.props?.title; if (!config.addToUntitledBlocks && !titleFromMeta && !titleFromProps) { return; } const fullscreenButton = h( "button", { class: "cb-fullscreen__button", type: "button", "aria-label": config.fullscreenButtonTooltip, "aria-expanded": "false", "data-tooltip": config.fullscreenButtonTooltip }, [ h( "svg", { class: "fullscreen-on", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", "aria-hidden": "true" }, [ h("path", { fill: "currentColor", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round", d: config.svgPathFullscreenOn }) ] ), h( "svg", { class: "fullscreen-off", xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", "aria-hidden": "true" }, [ h("path", { fill: "currentColor", stroke: "currentColor", "stroke-linecap": "round", "stroke-linejoin": "round", d: config.svgPathFullscreenOff }) ] ) ] ); const frameElement = context.renderData.blockAst.children.find( (child) => child.type === "element" && child.tagName === "figure" ); if (frameElement && frameElement.type === "element") { const frameClasses = frameElement.properties?.className || []; const classString = Array.isArray(frameClasses) ? frameClasses.join(" ") : String(frameClasses); const isTerminal = classString.includes("is-terminal"); const hasTitle = classString.includes("has-title"); if (!config.addToUntitledBlocks && !hasTitle && !isTerminal) { return; } const figcaption = frameElement.children?.find( (child) => child.type === "element" && child.tagName === "figcaption" ); if (figcaption && figcaption.type === "element") { figcaption.children = figcaption.children || []; figcaption.children.push(fullscreenButton); } else { frameElement.children = frameElement.children || []; frameElement.children.push(fullscreenButton); } } else { context.renderData.blockAst.children.push(fullscreenButton); } } }, jsModules: [ ` (function() { 'use strict'; // Configuration constants. const CONSTANTS = { MIN_FONT_SIZE: 60, MAX_FONT_SIZE: 500, DEFAULT_FONT_SIZE: 100, FONT_ADJUSTMENT: 7, DOUBLE_CLICK_THRESHOLD: 600, HINT_DISPLAY_TIME: 4000, FADE_TRANSITION_TIME: 500, MIN_BLOCK_HEIGHT: 95 }; // Plugin configuration. const pluginConfig = { fullscreenButtonTooltip: '${config.fullscreenButtonTooltip}', enableEscapeKey: ${config.enableEscapeKey}, exitOnBrowserBack: ${config.exitOnBrowserBack}, animationDuration: ${config.animationDuration} }; // Avoid duplicate initialization. if (window.expressiveCodeFullscreenInitialized) return; window.expressiveCodeFullscreenInitialized = true; // Initialize fullscreen state. const fullscreenState = { isFullscreenActive: false, scrollPosition: 0, originalCodeBlock: null, fontSize: CONSTANTS.DEFAULT_FONT_SIZE, focusTrapHandler: null, }; // Cache frequently used DOM elements. const domCache = { fullscreenContainer: null, get container() { if (!this.fullscreenContainer) { this.fullscreenContainer = document.querySelector('.cb-fullscreen__container'); } return this.fullscreenContainer; }, clearCache() { this.fullscreenContainer = null; } }; // Font size management. const fontManager = { storageKey: 'expressiveCodeFullscreenFontSize', loadFontSize() { try { const savedSize = localStorage.getItem(this.storageKey); if (savedSize) { const parsedSize = parseInt(savedSize, 10); if (parsedSize >= CONSTANTS.MIN_FONT_SIZE && parsedSize <= CONSTANTS.MAX_FONT_SIZE) { return parsedSize; } } } catch (e) { console.warn('Could not load font size from localStorage'); } return CONSTANTS.DEFAULT_FONT_SIZE; }, saveFontSize(size) { try { localStorage.setItem(this.storageKey, size.toString()); } catch (e) { console.warn('Could not save font size to localStorage'); } }, adjustFontSize(change, codeBlock) { const newSize = Math.max(CONSTANTS.MIN_FONT_SIZE, Math.min(CONSTANTS.MAX_FONT_SIZE, fullscreenState.fontSize + change)); fullscreenState.fontSize = newSize; this.saveFontSize(newSize); this.applyFontSize(codeBlock); }, resetFontSize(codeBlock) { fullscreenState.fontSize = CONSTANTS.DEFAULT_FONT_SIZE; this.saveFontSize(CONSTANTS.DEFAULT_FONT_SIZE); this.applyFontSize(codeBlock); }, applyFontSize(codeBlock) { if (codeBlock) { const scale = fullscreenState.fontSize / 100; codeBlock.style.setProperty('--ec-font-scale', scale); // Also apply directly to all text elements as backup. const textElements = codeBlock.querySelectorAll('pre, code, span, .frame'); textElements.forEach(el => { el.style.setProperty('font-size', \`calc(1em * \${scale})\`, 'important'); }); // Announce font size change to screen readers. this.announceFontSizeChange(fullscreenState.fontSize); } else { } }, announceFontSizeChange(fontSize) { const statusElement = document.getElementById('font-size-status'); if (statusElement) { const percentage = Math.round(fontSize); statusElement.textContent = \`Font size changed to \${percentage}%\`; // Clear the announcement after a brief delay to allow for multiple changes. setTimeout(() => { if (statusElement.textContent === \`Font size changed to \${percentage}%\`) { statusElement.textContent = ''; } }, 1000); } } }; // Initialize immediately and also on DOM ready. function initialize() { createFullscreenContainer(); initializeFullscreenButtons(); } // Initialize immediately if DOM is already loaded. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } // Also initialize when new content is added (for dynamic content). const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(function(node) { if (node.nodeType === 1 && (node.matches('.expressive-code') || node.querySelector('.expressive-code'))) { setTimeout(initializeFullscreenButtons, 100); } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); // Create fullscreen container. function createFullscreenContainer() { if (document.querySelector('.cb-fullscreen__container')) return; const container = document.createElement('div'); container.className = 'cb-fullscreen__container'; container.setAttribute('role', 'dialog'); container.setAttribute('aria-modal', 'true'); container.setAttribute('aria-label', 'Code block in fullscreen view'); container.setAttribute('aria-describedby', 'fullscreen-description'); container.setAttribute('tabindex', '-1'); // Ensure it's added directly to body, not any wrapper. document.body.appendChild(container); // Minimal inline styles - most styling in baseStyles. } // Initialize fullscreen buttons. function initializeFullscreenButtons() { const buttons = document.querySelectorAll('.cb-fullscreen__button'); buttons.forEach(button => { const codeBlock = button.closest('.expressive-code'); if (codeBlock) { const frame = codeBlock.querySelector('.frame'); // Check if block is tall enough to show fullscreen button if (frame && frame.offsetHeight < CONSTANTS.MIN_BLOCK_HEIGHT) { button.style.display = 'none'; return; } } button.addEventListener('click', handleFullscreenClick); button.addEventListener('keydown', function(event) { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleFullscreenClick.call(this, event); } }); }); } function handleFullscreenClick(event) { event.preventDefault(); event.stopPropagation(); const codeBlock = this.closest('.expressive-code'); if (codeBlock) { toggleFullscreen(codeBlock); } } function toggleFullscreen(codeBlock) { const fullscreenContainer = domCache.container; if (fullscreenState.isFullscreenActive) { exitFullscreen(fullscreenContainer); } else { enterFullscreen(codeBlock, fullscreenContainer); } } function enterFullscreen(codeBlock, fullscreenContainer) { fullscreenState.originalCodeBlock = codeBlock; fullscreenState.fontSize = fontManager.loadFontSize(); const originalButton = codeBlock.querySelector('.cb-fullscreen__button'); if (originalButton) { originalButton.setAttribute('aria-expanded', 'true'); } const clonedBlock = codeBlock.cloneNode(true); clonedBlock.classList.add('cb-fullscreen__active'); // Ensure the expressive-code class is present for CSS selectors to work. if (!clonedBlock.classList.contains('expressive-code')) { clonedBlock.classList.add('expressive-code'); } // Force full width with inline styles. // Styles handled in baseStyles (.expressive-code.cb-fullscreen__active). const fullscreenButtonInClone = clonedBlock.querySelector('.cb-fullscreen__button'); if (fullscreenButtonInClone) { // Force icon switching with JavaScript instead of CSS. const onIcon = fullscreenButtonInClone.querySelector('.fullscreen-on'); const offIcon = fullscreenButtonInClone.querySelector('.fullscreen-off'); if (onIcon && offIcon) { onIcon.style.display = 'none'; offIcon.style.display = 'inline'; } fullscreenButtonInClone.addEventListener('click', function(event) { event.preventDefault(); event.stopPropagation(); toggleFullscreen(clonedBlock); }); } saveScrollPosition(); setBodyOverflow(true); if (pluginConfig.enableEscapeKey) addKeyupListener(); if (pluginConfig.exitOnBrowserBack) { history.pushState({ fullscreenActive: true }, '', window.location.href); addPopStateListener(); } const pageBackgroundColor = getPageBackgroundColor(); const textColor = getContrastTextColor(pageBackgroundColor); fullscreenContainer.style.backgroundColor = pageBackgroundColor; fullscreenContainer.style.color = textColor; const contentWrapper = document.createElement('div'); contentWrapper.className = 'cb-fullscreen__content'; // Add hidden description for screen readers. const description = document.createElement('div'); description.id = 'fullscreen-description'; description.className = 'cb-fullscreen__sr-only'; // Styles handled in baseStyles. description.textContent = 'Use the font size controls to adjust text size. Press Escape to exit fullscreen.'; const fontControls = createFontSizeControls(); contentWrapper.appendChild(description); contentWrapper.appendChild(fontControls); contentWrapper.appendChild(clonedBlock); fullscreenContainer.appendChild(contentWrapper); addFontControlListeners(fontControls, clonedBlock); fontManager.applyFontSize(clonedBlock); if (pluginConfig.enableEscapeKey) { const hint = createFullscreenHint(); fullscreenContainer.appendChild(hint); setTimeout(() => { if (hint && hint.parentNode) { hint.style.setProperty('transition', 'opacity 0.9s ease', 'important'); hint.style.setProperty('opacity', '0', 'important'); setTimeout(() => { if (hint && hint.parentNode) { hint.remove(); } }, CONSTANTS.FADE_TRANSITION_TIME); } }, CONSTANTS.HINT_DISPLAY_TIME); } // Show the fullscreen container. fullscreenContainer.style.visibility = 'visible'; fullscreenContainer.style.transform = 'scale(1)'; fullscreenContainer.style.pointerEvents = 'auto'; fullscreenContainer.classList.add('cb-fullscreen__container--open'); fullscreenState.isFullscreenActive = true; // Force layout recalculation to ensure stretching works. setTimeout(() => { fullscreenContainer.offsetHeight; // Force reflow clonedBlock.style.width = '100%'; clonedBlock.style.maxWidth = 'none'; }, 0); fullscreenContainer.focus(); addFocusTrap(fullscreenContainer); } function exitFullscreen(fullscreenContainer) { setBodyOverflow(false); restoreScrollPosition(); if (pluginConfig.enableEscapeKey) removeKeyupListener(); if (pluginConfig.exitOnBrowserBack) { removePopStateListener(); // Only go back if we're exiting due to escape key or button click (not back button). if (history.state && history.state.fullscreenActive) { history.back(); } } removeFocusTrap(); // Hide the fullscreen container. fullscreenContainer.style.visibility = 'hidden'; fullscreenContainer.style.transform = 'scale(0.01)'; fullscreenContainer.style.pointerEvents = 'none'; fullscreenContainer.classList.remove('cb-fullscreen__container--open'); fullscreenContainer.innerHTML = ''; fullscreenState.isFullscreenActive = false; if (fullscreenState.originalCodeBlock) { const originalButton = fullscreenState.originalCodeBlock.querySelector('.cb-fullscreen__button'); if (originalButton) { originalButton.setAttribute('aria-expanded', 'false'); originalButton.blur(); } } fullscreenState.originalCodeBlock = null; } // Utility functions. function createFontSizeControls() { const controls = document.createElement('div'); controls.className = 'cb-fullscreen__font-controls'; // Styles are controlled by external CSS file. controls.setAttribute('role', 'toolbar'); controls.setAttribute('aria-label', 'Font size controls'); controls.setAttribute('aria-orientation', 'horizontal'); controls.innerHTML = \` <button class="cb-fullscreen__font-btn cb-fullscreen__font-btn--decrease" type="button" aria-label="Decrease font size (Double-click to reset to default)" aria-describedby="font-size-status" title="Decrease font size (Double-click to reset)" style="border-radius: 6px !important; border: none !important;"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" role="img"> <title>Minus icon</title> <path d="M5 12h14"/> </svg> </button> <button class="cb-fullscreen__font-btn cb-fullscreen__font-btn--increase" type="button" aria-label="Increase font size" aria-describedby="font-size-status" title="Increase font size" style="border-radius: 6px !important; border: none !important;"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" role="img"> <title>Plus icon</title> <path d="M12 5v14m-7-7h14"/> </svg> </button> <div id="font-size-status" class="cb-fullscreen__sr-only" aria-live="polite" aria-atomic="true" style="position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; white-space: nowrap !important; border: 0 !important;"></div> \`; return controls; } function createFullscreenHint() { const hint = document.createElement('div'); hint.className = 'cb-fullscreen__hint'; // Styles handled in baseStyles. hint.innerHTML = 'Press <kbd>Esc</kbd> to exit full screen'; return hint; } function getPageBackgroundColor() { const bodyBg = window.getComputedStyle(document.body).backgroundColor; if (bodyBg && bodyBg !== 'rgba(0, 0, 0, 0)' && bodyBg !== 'transparent') { return bodyBg; } const fallbackBg = window.getComputedStyle(document.documentElement).backgroundColor; if (fallbackBg && fallbackBg !== 'rgba(0, 0, 0, 0)' && fallbackBg !== 'transparent') { return fallbackBg; } return '#ffffff'; } function getContrastTextColor(backgroundColor) { const rgb = backgroundColor.match(/\\d+/g); if (rgb && rgb.length >= 3) { const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; return brightness > 128 ? '#000000' : '#ffffff'; } return '#000000'; } function saveScrollPosition() { fullscreenState.scrollPosition = window.scrollY || document.documentElement.scrollTop; } function restoreScrollPosition() { if (typeof fullscreenState.scrollPosition === 'number' && !isNaN(fullscreenState.scrollPosition)) { setTimeout(() => { window.scrollTo({ top: fullscreenState.scrollPosition, behavior: 'smooth', }); }, 0); } } function setBodyOverflow(hidden) { if (hidden) { document.body.style.overflow = 'hidden'; document.documentElement.style.overflow = 'hidden'; } else { document.body.style.overflow = ''; document.documentElement.style.overflow = ''; } } function handleKeyup(event) { if (event.key === 'Escape' && fullscreenState.isFullscreenActive) { const fullscreenContainer = domCache.container; if (fullscreenContainer) { exitFullscreen(fullscreenContainer); } } } function addKeyupListener() { document.removeEventListener('keyup', handleKeyup); document.addEventListener('keyup', handleKeyup); } function removeKeyupListener() { document.removeEventListener('keyup', handleKeyup); } function handlePopState(event) { if (fullscreenState.isFullscreenActive) { // Prevent the history.back() call in exitFullscreen from causing a loop. const isBackButtonPressed = !event.state || !event.state.fullscreenActive; if (isBackButtonPressed) { const fullscreenContainer = domCache.container; if (fullscreenContainer) { // Temporarily disable back button handling to prevent recursion. removePopStateListener(); exitFullscreen(fullscreenContainer); } } } } function addPopStateListener() { window.removeEventListener('popstate', handlePopState); window.addEventListener('popstate', handlePopState); } function removePopStateListener() { window.removeEventListener('popstate', handlePopState); } function addFocusTrap(container) { const focusableElements = container.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"]), [contenteditable="true"], summary, audio[controls], video[controls]' ); if (focusableElements.length === 0) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; function handleTabKey(event) { if (event.key === 'Tab') { if (event.shiftKey) { if (document.activeElement === firstElement) { event.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement) { event.preventDefault(); firstElement.focus(); } } } } container.addEventListener('keydown', handleTabKey); fullscreenState.focusTrapHandler = handleTabKey; } function removeFocusTrap() { const container = domCache.container; if (container && fullscreenState.focusTrapHandler) { container.removeEventListener('keydown', fullscreenState.focusTrapHandler); fullscreenState.focusTrapHandler = null; } } function addFontControlListeners(fontControls, codeBlock) { const decreaseBtn = fontControls.querySelector('.cb-fullscreen__font-btn--decrease'); const increaseBtn = fontControls.querySelector('.cb-fullscreen__font-btn--increase'); if (!decreaseBtn || !increaseBtn) { return; } let decreaseClickData = { lastClickTime: 0, clickCount: 0 }; // Enhanced keyboard support for decrease button. decreaseBtn.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); decreaseBtn.click(); } }); decreaseBtn.addEventListener('click', (event) => { const currentTime = Date.now(); const timeDifference = currentTime - decreaseClickData.lastClickTime; if (timeDifference < CONSTANTS.DOUBLE_CLICK_THRESHOLD) { decreaseClickData.clickCount++; if (decreaseClickData.clickCount === 2) { fontManager.resetFontSize(codeBlock); decreaseClickData.clickCount = 0; // Announce reset to screen readers. const statusElement = document.getElementById('font-size-status'); if (statusElement) { statusElement.textContent = 'Font size reset to default (100%)'; setTimeout(() => { statusElement.textContent = ''; }, 1000); } } } else { decreaseClickData.clickCount = 1; fontManager.adjustFontSize(-CONSTANTS.FONT_ADJUSTMENT, codeBlock); } decreaseClickData.lastClickTime = currentTime; // Keep focus for keyboard users. if (event.detail === 0) { // Keyboard activation. decreaseBtn.focus(); } else { // Blur for mouse clicks. decreaseBtn.blur(); } }); // Enhanced keyboard support for increase button. increaseBtn.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); increaseBtn.click(); } }); increaseBtn.addEventListener('click', (event) => { fontManager.adjustFontSize(CONSTANTS.FONT_ADJUSTMENT, codeBlock); // Keep focus for keyboard users. if (event.detail === 0) { // Keyboard activation. increaseBtn.focus(); } else { // Blur for mouse clicks. increaseBtn.blur(); } }); } })(); ` ] }); } export { pluginFullscreen }; //# sourceMappingURL=index.js.map