Skip to content

Commit

Permalink
Add copy & expand buttons to syntax-highlighted code blocks (#2652)
Browse files Browse the repository at this point in the history
* Extend the core code block to add copy & expand buttons

* Remove unused variable
  • Loading branch information
ryelle authored Jul 10, 2024
1 parent 353cfe7 commit a9101b5
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 0 deletions.
1 change: 1 addition & 0 deletions wp-content/themes/pub/wporg-learn-2024/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 5 additions & 0 deletions wp-content/themes/pub/wporg-learn-2024/src/code/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "wporg/code",
"style": "file:./style-view.css",
"viewScript": "file:./view.js"
}
49 changes: 49 additions & 0 deletions wp-content/themes/pub/wporg-learn-2024/src/code/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
/**
* Extends the core code block to add copy & expand buttons.
*/

namespace WordPressdotorg\Theme\Learn_2024\Code_Block;

defined( 'WPINC' ) || die();

/**
* Actions and filters.
*/
add_action( 'init', __NAMESPACE__ . '\init' );

/**
* Add the scripts & styles to update the code block.
*
* The dependencies are autogenerated in block.json, and can be read with
* `wp_json_file_decode` & `register_block_script_handle.
*/
function init() {
$metadata_file = dirname( dirname( __DIR__ ) ) . '/build/code/block.json';
$metadata = wp_json_file_decode( $metadata_file, array( 'associative' => 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;
}
);
}
49 changes: 49 additions & 0 deletions wp-content/themes/pub/wporg-learn-2024/src/code/style.scss
Original file line number Diff line number Diff line change
@@ -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);
}
}
122 changes: 122 additions & 0 deletions wp-content/themes/pub/wporg-learn-2024/src/code/view.js
Original file line number Diff line number Diff line change
@@ -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 );

0 comments on commit a9101b5

Please sign in to comment.