From a9101b5d6d9ff910246d6126f7bbb0e00f4f1524 Mon Sep 17 00:00:00 2001 From: Kelly Dwan Date: Tue, 9 Jul 2024 22:29:30 -0400 Subject: [PATCH] Add copy & expand buttons to syntax-highlighted code blocks (#2652) * Extend the core code block to add copy & expand buttons * Remove unused variable --- .../themes/pub/wporg-learn-2024/functions.php | 1 + .../pub/wporg-learn-2024/src/code/block.json | 5 + .../pub/wporg-learn-2024/src/code/index.php | 49 +++++++ .../pub/wporg-learn-2024/src/code/style.scss | 49 +++++++ .../pub/wporg-learn-2024/src/code/view.js | 122 ++++++++++++++++++ 5 files changed, 226 insertions(+) create mode 100644 wp-content/themes/pub/wporg-learn-2024/src/code/block.json create mode 100644 wp-content/themes/pub/wporg-learn-2024/src/code/index.php create mode 100644 wp-content/themes/pub/wporg-learn-2024/src/code/style.scss create mode 100644 wp-content/themes/pub/wporg-learn-2024/src/code/view.js diff --git a/wp-content/themes/pub/wporg-learn-2024/functions.php b/wp-content/themes/pub/wporg-learn-2024/functions.php index c1e9f0651..c5795d86f 100644 --- a/wp-content/themes/pub/wporg-learn-2024/functions.php +++ b/wp-content/themes/pub/wporg-learn-2024/functions.php @@ -5,6 +5,7 @@ use function WPOrg_Learn\Sensei\{get_my_courses_page_url}; // Block files +require_once __DIR__ . '/src/code/index.php'; require_once __DIR__ . '/src/course-grid/index.php'; require_once __DIR__ . '/src/course-outline/index.php'; require_once __DIR__ . '/src/learning-pathway-cards/index.php'; diff --git a/wp-content/themes/pub/wporg-learn-2024/src/code/block.json b/wp-content/themes/pub/wporg-learn-2024/src/code/block.json new file mode 100644 index 000000000..c2b05f87e --- /dev/null +++ b/wp-content/themes/pub/wporg-learn-2024/src/code/block.json @@ -0,0 +1,5 @@ +{ + "name": "wporg/code", + "style": "file:./style-view.css", + "viewScript": "file:./view.js" +} diff --git a/wp-content/themes/pub/wporg-learn-2024/src/code/index.php b/wp-content/themes/pub/wporg-learn-2024/src/code/index.php new file mode 100644 index 000000000..bfa8f64e4 --- /dev/null +++ b/wp-content/themes/pub/wporg-learn-2024/src/code/index.php @@ -0,0 +1,49 @@ + true ) ); + $metadata['file'] = $metadata_file; + + $style_handle = register_block_style_handle( $metadata, 'style', 0 ); + + $script_handle = register_block_script_handle( $metadata, 'viewScript', 0 ); + wp_localize_script( + $script_handle, + 'wporgCodeI18n', + array( + 'copy' => __( 'Copy', 'wporg-learn' ), + 'copied' => __( 'Code copied', 'wporg-learn' ), + 'expand' => __( 'Expand code', 'wporg-learn' ), + 'collapse' => __( 'Collapse code', 'wporg-learn' ), + ) + ); + + // Enqueue the assets only when the code block is on the page. + add_action( + 'render_block_core/code', + function( $block_content ) use ( $script_handle, $style_handle ) { + wp_enqueue_script( $script_handle ); + wp_enqueue_style( $style_handle ); + return $block_content; + } + ); +} diff --git a/wp-content/themes/pub/wporg-learn-2024/src/code/style.scss b/wp-content/themes/pub/wporg-learn-2024/src/code/style.scss new file mode 100644 index 000000000..7ee194e5a --- /dev/null +++ b/wp-content/themes/pub/wporg-learn-2024/src/code/style.scss @@ -0,0 +1,49 @@ +.wporg-code-block { + $border_radius: 2px; + + .wp-code-block-button-container { + padding: var(--wp--preset--spacing--10); + background-color: var(--wp--preset--color--light-grey-2); + border-radius: $border_radius $border_radius 0 0; + border-width: 1px 1px 0; + border-style: solid; + border-color: var(--wp--preset--color--light-grey-1); + + .wp-block-buttons { + justify-content: flex-end; + } + + .wp-block-button button { + padding-top: var(--wp--custom--button--spacing--padding--top) !important; + padding-bottom: var(--wp--custom--button--spacing--padding--bottom) !important; + padding-left: var(--wp--custom--button--spacing--padding--left) !important; + padding-right: var(--wp--custom--button--spacing--padding--right) !important; + background-color: var(--wp--preset--color--white); + text-wrap: nowrap; + font-size: var(--wp--custom--button--typography--font-size); + transition: none; + } + } + + .wp-code-block-button-container + pre { + margin-top: 0; + background-color: var(--wp--preset--color--white); + border-width: 0 1px 1px; + border-style: solid; + border-color: var(--wp--preset--color--light-grey-1); + } + + .wp-block-code { + border-radius: 0 0 $border_radius $border_radius; + + code { + font-family: var(--wp--preset--font-family--monospace); + background-color: unset; + } + } + + .line-numbers-rows > span::before { + text-align: left; + padding-inline-start: var(--wp--preset--spacing--10); + } +} diff --git a/wp-content/themes/pub/wporg-learn-2024/src/code/view.js b/wp-content/themes/pub/wporg-learn-2024/src/code/view.js new file mode 100644 index 000000000..f63a3a9db --- /dev/null +++ b/wp-content/themes/pub/wporg-learn-2024/src/code/view.js @@ -0,0 +1,122 @@ +/* global wporgCodeI18n */ +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import './style.scss'; + +// Index for +let _instanceID = 0; + +function init() { + // 27px (line height) * 12.7 for 12 lines + 18px top padding. + // The extra partial line is to show that there is more content. + const MIN_HEIGHT = 27 * 12.7 + 18; + + function collapseCodeBlock( element, button ) { + button.innerText = wporgCodeI18n.expand; + button.setAttribute( 'aria-expanded', 'false' ); + element.style.height = MIN_HEIGHT + 'px'; + } + + function expandCodeBlock( element, button ) { + button.innerText = wporgCodeI18n.collapse; + button.setAttribute( 'aria-expanded', 'true' ); + // Add 5px to ensure the vertical scrollbar is not displayed. + const height = parseInt( element.dataset.height, 10 ) + 5; + element.style.height = height + 'px'; + } + + // Run over all code blocks that use the syntax highlighter. + const codeBlocks = document.querySelectorAll( '.wp-block-code[class*=language]' ); + + codeBlocks.forEach( function ( element ) { + let timeoutId; + + // Create a unique ID for the `pre` element, which can be used for aria later. + const instanceId = 'wporg-source-code-' + _instanceID++; + element.id = instanceId; + + // Create the top-level container. This will contain the buttons & sits above the `pre`. + const container = document.createElement( 'div' ); + container.classList.add( 'wp-code-block-button-container' ); + + const buttonContainer = document.createElement( 'div' ); + buttonContainer.classList.add( 'wp-block-buttons' ); + + const copyButtonBlock = document.createElement( 'div' ); + copyButtonBlock.classList.add( 'wp-block-button', 'is-style-outline', 'is-small' ); + + const copyButton = document.createElement( 'button' ); + copyButton.classList.add( 'wp-block-button__link', 'wp-element-button' ); + copyButton.innerText = wporgCodeI18n.copy; + + copyButton.addEventListener( 'click', function ( event ) { + event.preventDefault(); + clearTimeout( timeoutId ); + const code = element.querySelector( 'code' ).innerText; + if ( ! code ) { + return; + } + + // This returns a promise which will resolve if the copy suceeded, + // and we can set the button text to tell the user it worked. + // We don't do anything if it fails. + window.navigator.clipboard.writeText( code ).then( function () { + copyButton.innerText = wporgCodeI18n.copied; + speak( wporgCodeI18n.copied ); + + // After 5 seconds, reset the button text. + timeoutId = setTimeout( function () { + copyButton.innerText = wporgCodeI18n.copy; + }, 5000 ); + } ); + } ); + + copyButtonBlock.append( copyButton ); + buttonContainer.append( copyButtonBlock ); + + // Check code block height. If it's too tall, add in the collapse button, + // and shrink down the `pre` to MIN_HEIGHT. + const originalHeight = element.clientHeight; + if ( originalHeight > MIN_HEIGHT ) { + element.dataset.height = originalHeight; + + const expandButtonBlock = document.createElement( 'div' ); + expandButtonBlock.classList.add( 'wp-block-button', 'is-style-outline', 'is-small' ); + + const expandButton = document.createElement( 'button' ); + expandButton.classList.add( 'wp-block-button__link', 'wp-element-button' ); + expandButton.setAttribute( 'aria-controls', instanceId ); + expandButton.innerText = wporgCodeI18n.expand; + + expandButton.addEventListener( 'click', function ( event ) { + event.preventDefault(); + if ( 'true' === expandButton.getAttribute( 'aria-expanded' ) ) { + collapseCodeBlock( element, expandButton ); + } else { + expandCodeBlock( element, expandButton ); + } + } ); + + collapseCodeBlock( element, expandButton ); + + expandButtonBlock.append( expandButton ); + buttonContainer.append( expandButtonBlock ); + } + + container.append( buttonContainer ); + + const wrapper = document.createElement( 'div' ); + wrapper.classList.add( 'wporg-code-block' ); + + element.replaceWith( wrapper ); + wrapper.append( container, element ); + } ); +} + +document.addEventListener( 'DOMContentLoaded', init );