Gutenberg Block built with Gutenberg Native Components to create timeline in Gutenberg editor. * Requires at least: 6.0 * Requires PHP: 7.0 * Version: 2.2.1 * Author: Zakaria Binsaifullah * Author URI: https://makegutenblock.com * License: GPL-2.0-or-later * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: advanced-timeline-block * */ // Stop Direct Access if ( ! defined( 'ABSPATH' ) ) { exit; } // frontend style generation require_once plugin_dir_path( __FILE__ ) . 'includes/class-atlb-styles.php'; /** * Blocks Final Class */ final class ATLB_BLOCKS_CLASS { /** * Shared handle the per-block CSS is attached to when it can still reach * the document head. */ const STYLE_HANDLE = 'atlb-timeline-inline'; /** * Block ids whose CSS has already been emitted this request. * * @var array */ private $printed = array(); public function __construct() { // define constants $this->atlb_define_constants(); // block initialization add_action( 'init', [ $this, 'atlb_blocks_init' ] ); // blocks category add_filter( 'block_categories_all', [ $this, 'atlb_register_block_category' ], 10, 2 ); // enqueue block assets add_action( 'enqueue_block_assets', [ $this, 'atlb_external_libraries' ] ); // generate block css early enough to land in the document head add_action( 'wp_enqueue_scripts', [ $this, 'atlb_enqueue_block_styles' ] ); // render timeline block add_filter( 'render_block_atlb/timeline-block', [ $this, 'atlb_render_timeline_block' ], 10, 2 ); // swap legacy Font Awesome marker icons for inline SVG on the frontend add_filter( 'render_block_atlb/single-timeline-item', [ $this, 'atlb_render_timeline_item' ] ); // keep inline SVG markup intact when users without unfiltered_html save add_filter( 'wp_kses_allowed_html', [ $this, 'atlb_allow_svg_html' ], 10, 2 ); } /** * Initialize the plugin */ public static function init(){ static $instance = false; if( ! $instance ) { $instance = new self(); } return $instance; } /** * Define the plugin constants */ private function atlb_define_constants() { define( 'ATLB_VERSION', '2.2.1' ); define( 'ATLB_URL', plugin_dir_url( __FILE__ ) ); define( 'ATLB_INC_URL', ATLB_URL . 'includes/' ); define( 'ATLB_PATH', plugin_dir_path( __FILE__ ) ); } /** * Walk the main post's blocks up front so their CSS is attached to a * handle that is still printed in . * * Blocks that live outside the post content - template parts, widgets, * reusable blocks, query loops - are picked up by the render_block filter * instead, and print in the footer as they always have. */ public function atlb_enqueue_block_styles() { if ( ! is_singular() ) { return; } $post = get_post(); if ( ! $post || ! has_blocks( $post->post_content ) ) { return; } $this->atlb_collect_block_styles( parse_blocks( $post->post_content ) ); } /** * Recursively emit CSS for every timeline block in a parsed block tree. * * @param array $blocks Parsed blocks. */ private function atlb_collect_block_styles( $blocks ) { foreach ( $blocks as $block ) { if ( isset( $block['blockName'] ) && 'atlb/timeline-block' === $block['blockName'] ) { $this->atlb_add_block_css( isset( $block['attrs'] ) ? $block['attrs'] : [] ); } if ( ! empty( $block['innerBlocks'] ) ) { $this->atlb_collect_block_styles( $block['innerBlocks'] ); } } } /** * Emit one block's CSS, at most once per request. * * @param array $attributes Raw block attributes. */ private function atlb_add_block_css( $attributes ) { $id = isset( $attributes['id'] ) ? sanitize_html_class( $attributes['id'] ) : ''; if ( '' === $id || isset( $this->printed[ $id ] ) ) { return; } $css = ATLB_Styles::generate( $attributes ); if ( '' === $css ) { return; } // Which hook reaches a given block first is not fixed - block themes // render templates before wp_enqueue_scripts runs, so render_block can // fire first - therefore the handle is registered on demand rather than // being assumed to exist. // // The cutoff has to be wp_print_styles, not wp_head: wp_enqueue_scripts // is itself hooked to wp_head at priority 1, so did_action('wp_head') // is already true while the head pass is still running. if ( did_action( 'wp_print_styles' ) ) { // Too late for the shared handle; give this block its own so it // prints in the footer instead of being dropped. $handle = 'atlb-timeline-' . $id; } else { $handle = self::STYLE_HANDLE; } if ( ! wp_style_is( $handle, 'registered' ) ) { wp_register_style( $handle, false, [], ATLB_VERSION ); } if ( ! wp_style_is( $handle, 'enqueued' ) ) { wp_enqueue_style( $handle ); } wp_add_inline_style( $handle, $css ); $this->printed[ $id ] = true; } /** * Blocks Initialization */ public function atlb_blocks_init() { $blocks = ['timeline-block', 'timeline-item']; if( ! empty( $blocks ) ) { foreach( $blocks as $block ) { $block_path = ATLB_PATH . 'build/blocks/' . $block; if ( file_exists( $block_path ) ) { register_block_type( $block_path ); } } } } /** * Fallback for timeline blocks that are not in the main post content. * * Blocks already handled by the head pass are skipped by the id guard in * atlb_add_block_css(). * * @param string $block_content Rendered block markup. * @param array $block Parsed block, with raw comment attributes. * @return string */ public function atlb_render_timeline_block( $block_content, $block ) { if ( ! empty( $block['attrs'] ) ) { $this->atlb_add_block_css( $block['attrs'] ); } return $block_content; } /** * Register Block Category */ public function atlb_register_block_category( $categories, $post ) { return array_merge( array( array( 'slug' => 'atlb-block', 'title' => __( 'Timeline Block', 'advanced-timeline-block' ), ), ), $categories, ); } /** * Enqueue Block Assets */ public function atlb_external_libraries() { wp_register_style( 'atlb-block-editor-css', ATLB_INC_URL . 'css/editor.css', array(), ATLB_VERSION ); } /** * Replace legacy Font Awesome marker icons with inline SVG while rendering. * * Content saved by earlier releases stores the marker as * ``, which relied on the Font Awesome stylesheet * and webfonts that are no longer loaded. The class string is looked up in * the generated map (includes/legacy-icon-map.php) and the matching SVG is * injected inside the existing element, so the markup around it - and * all marker CSS - keeps working. Values that are not in the map are left * untouched. * * @param string $block_content Rendered item block markup. * @return string */ public function atlb_render_timeline_item( $block_content ) { if ( false === strpos( $block_content, 'fa-' ) ) { return $block_content; } return preg_replace_callback( '/]*><\/i>/', function ( $matches ) { if ( ! preg_match( '/\b(fa[bsrdl])\s+fa-([\w-]+)\b/', $matches[1], $icon ) ) { return $matches[0]; } $map = $this->atlb_legacy_icon_map(); $key = $icon[1] . ' fa-' . $icon[2]; if ( ! isset( $map[ $key ] ) ) { return $matches[0]; } return str_replace( '>', '>' . $map[ $key ] . '', $matches[0] ); }, $block_content ); } /** * Legacy Font Awesome class -> inline SVG map, required on first use. * * @return array */ private function atlb_legacy_icon_map() { static $map = null; if ( null === $map ) { $map = require ATLB_PATH . 'includes/legacy-icon-map.php'; } return $map; } /** * Allow inline SVG tags through kses for post content. * * Without this, users without the unfiltered_html capability (editors, * authors) would have the SVG icon markup stripped on save. The whitelist * covers SVG presentation attributes only - scripts, event handlers and * external references are never allowed through. * * @param array $tags Allowed tags and their attributes. * @param string $context The kses context. * @return array */ public function atlb_allow_svg_html( $tags, $context ) { if ( 'post' !== $context ) { return $tags; } $attributes = array_fill_keys( array( 'd', 'fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-opacity', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-dashoffset', 'opacity', 'viewbox', 'x', 'y', 'x1', 'x2', 'y1', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'width', 'height', 'transform', 'points', 'version', 'xmlns', 'preserveaspectratio', 'clip-rule', 'clip-path', 'offset', 'stop-color', 'stop-opacity', 'gradientunits', 'gradienttransform', 'spreadmethod', 'aria-hidden', 'focusable', 'class', 'role', ), true ); foreach ( array( 'svg', 'path', 'g', 'defs', 'title', 'desc', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'rect', 'lineargradient', 'radialgradient', 'stop', 'clippath' ) as $tag ) { $tags[ $tag ] = $attributes; } return $tags; } } /** * Kickoff */ ATLB_BLOCKS_CLASS::init();