'now_playing', 'listen-button' => 'listen_button', ); /** * Register. */ public function register() { add_action( 'init', array( $this, 'register_blocks' ) ); // After the hand-made blocks: generate the rest from the element registry. add_action( 'init', array( $this, 'register_element_blocks' ), 30 ); add_action( 'init', array( Widgets::class, 'shortcodes' ) ); add_action( 'init', array( $this, 'register_patterns' ) ); add_action( 'enqueue_block_editor_assets', array( $this, 'editor_data' ) ); add_action( 'save_post_' . \StreamSera\PostTypes\Player::POST_TYPE, array( $this, 'clear_first_id' ) ); add_action( 'before_delete_post', array( $this, 'clear_first_id' ) ); add_filter( 'block_categories_all', array( $this, 'category' ), 10 ); add_filter( 'register_block_type_args', array( $this, 'block_args' ), 10, 2 ); } /** * Put every streamsera/* block in the StreamSera category. * * @param array $args Args. * @param string $name Block name. * @return array */ public function block_args( $args, $name ) { if ( 0 === strpos( (string) $name, 'streamsera/' ) ) { $args['category'] = 'streamsera'; } return $args; } /** * Register the dynamic blocks. */ public function register_blocks() { foreach ( self::BLOCKS as $name => $method ) { $dir = STREAMSERA_PATH . 'build/blocks/' . $name; if ( ! file_exists( $dir . '/block.json' ) ) { continue; } register_block_type( $dir, array( 'render_callback' => static function ( $attributes ) use ( $method ) { $attributes = is_array( $attributes ) ? $attributes : array(); $inner = call_user_func( array( Widgets::class, $method ), self::attributes( $attributes ) ); if ( '' === $inner ) { return ''; } return sprintf( '
%s
', get_block_wrapper_attributes(), wp_kses( $inner, streamsera_allowed_html() ) ); }, ) ); } } /** * Editor data for the hand-made blocks (listen button targets add-ons may extend). */ public function editor_data() { $handle = generate_block_asset_handle( 'streamsera/listen-button', 'editorScript' ); $targets = array(); foreach ( Widgets::listen_targets() as $value => $label ) { $targets[] = array( 'value' => (string) $value, 'label' => html_entity_decode( (string) $label, ENT_QUOTES ), ); } if ( wp_script_is( $handle, 'registered' ) ) { wp_add_inline_script( $handle, 'window.StreamSeraListenTargets = ' . wp_json_encode( $targets ) . ';', 'before' ); } } /** * Generated blocks: every page element without a hand-made block becomes `streamsera/`, * edited through one shared editor script that builds its controls from the field list. */ public function register_element_blocks() { $registry = \WP_Block_Type_Registry::get_instance(); $asset = STREAMSERA_PATH . 'build/blocks/elements/index.asset.php'; if ( ! file_exists( $asset ) ) { return; } $meta = require $asset; wp_register_script( 'streamsera-element-blocks', STREAMSERA_URL . 'build/blocks/elements/index.js', $meta['dependencies'], $meta['version'], true ); wp_set_script_translations( 'streamsera-element-blocks', 'streamsera', STREAMSERA_PATH . 'languages' ); $editor = array(); foreach ( Elements::all() as $id => $el ) { $name = $el['block']; if ( ! $name || $registry->is_registered( $name ) ) { continue; } $attributes = array(); $fields = array(); foreach ( $el['fields'] as $key => $field ) { $attr = Elements::attr( $key ); $numeric = Elements::is_numeric_type( $field['type'] ); $attributes[ $attr ] = array( 'type' => 'toggle' === $field['type'] ? 'boolean' : ( $numeric ? 'number' : 'string' ), // Numeric attributes need a numeric default ('' = "none chosen" becomes 0), or the editor preview request fails validation. 'default' => $numeric ? (int) $field['default'] : $field['default'], ); $fields[] = array( 'key' => $key, 'attr' => $attr, 'type' => $field['type'], 'label' => $field['label'], 'help' => $field['help'], 'section' => $field['section'], 'min' => $field['min'] ?? null, 'max' => $field['max'] ?? null, ); } register_block_type( $name, array( 'api_version' => 3, 'title' => $el['label'], 'description' => $el['description'], 'category' => 'streamsera', 'icon' => $el['icon']['dashicon'], 'keywords' => $el['keywords'], 'attributes' => $attributes, 'supports' => array( 'html' => false, 'align' => array( 'wide', 'full' ), 'spacing' => array( 'margin' => true ), ), 'editor_script' => 'streamsera-element-blocks', 'render_callback' => static function ( $attrs ) use ( $id, $el ) { $args = array(); foreach ( array_keys( $el['fields'] ) as $key ) { $attr = Elements::attr( $key ); if ( array_key_exists( $attr, (array) $attrs ) ) { $args[ $key ] = $attrs[ $attr ]; } } $inner = Elements::render( $id, $args ); return '' === $inner ? '' : sprintf( '
%s
', get_block_wrapper_attributes(), wp_kses( $inner, streamsera_allowed_html() ) ); }, ) ); $editor[] = array( 'id' => $id, 'name' => $name, 'title' => $el['label'], 'description' => $el['description'], 'icon' => $el['icon']['dashicon'], 'keywords' => $el['keywords'], 'attributes' => $attributes, 'fields' => $fields, ); } if ( ! $editor ) { return; } // Picker options (players, stations …) are only needed in the editor: query them there. add_action( 'enqueue_block_editor_assets', static function () use ( $editor ) { foreach ( $editor as $i => $block ) { $el = Elements::get( $block['id'] ); foreach ( $block['fields'] as $j => $field ) { $options = array(); foreach ( Elements::options( $el['fields'][ $field['key'] ] ) as $value => $label ) { $options[] = array( 'value' => (string) $value, 'label' => html_entity_decode( (string) $label, ENT_QUOTES ), ); } $editor[ $i ]['fields'][ $j ]['options'] = $options; } } wp_add_inline_script( 'streamsera-element-blocks', 'window.StreamSeraElements = ' . wp_json_encode( array( 'blocks' => $editor, 'pickers' => array_keys( Elements::picker_types() ) ) ) . ';', 'before' ); } ); } /** * Map block attributes (camelCase) to renderer args (snake_case). * * @param array $attributes Block attributes. * @return array */ private static function attributes( $attributes ) { $out = array(); // Alignment and extra classes belong to the block wrapper (get_block_wrapper_attributes()), not the inner markup. unset( $attributes['align'], $attributes['className'] ); foreach ( $attributes as $key => $value ) { $out[ strtolower( preg_replace( '/([a-z])([A-Z])/', '$1_$2', $key ) ) ] = $value; } return $out; } /** * Add an "StreamSera" block category (used by every StreamSera block). * * @param array $categories Categories. * @return array */ public function category( $categories ) { foreach ( $categories as $cat ) { if ( 'streamsera' === $cat['slug'] ) { return $categories; } } array_unshift( $categories, array( 'slug' => 'streamsera', 'title' => __( 'StreamSera', 'streamsera' ), 'icon' => null, ) ); return $categories; } /** * Block patterns for full-site editing themes. */ public function register_patterns() { if ( ! function_exists( 'register_block_pattern_category' ) ) { return; } register_block_pattern_category( 'streamsera', array( 'label' => __( 'StreamSera', 'streamsera' ) ) ); $player = self::first_id( \StreamSera\PostTypes\Player::POST_TYPE ); $name = get_bloginfo( 'name' ); $patterns = array( 'streamsera/hero-player' => array( 'title' => __( 'Hero player section', 'streamsera' ), 'description' => __( 'Full-width cover section with a headline, the live player and a listen button.', 'streamsera' ), 'content' => sprintf( '

%1$s

%2$s

', esc_html( $name ), esc_html__( 'Live, 24 hours a day. Press play and stay with us.', 'streamsera' ), $player, // Block attributes are JSON: let core encode the translated label. serialize_block_attributes( array( 'label' => __( 'Open the listen page', 'streamsera' ), 'variant' => 'pill', 'size' => 'lg', 'align' => 'center', ) ) ), ), 'streamsera/sidebar-now-playing' => array( 'title' => __( 'Sidebar: now playing', 'streamsera' ), 'description' => __( 'Compact now-playing card with a listen button. Fits a sidebar or footer column.', 'streamsera' ), 'content' => '

' . esc_html__( 'On air', 'streamsera' ) . '

', ), 'streamsera/station-landing' => array( 'title' => __( 'Station landing page', 'streamsera' ), 'description' => __( 'A station home page: the player, what is playing now and your stations.', 'streamsera' ), 'content' => sprintf( '

%2$s

%3$s

', $player, esc_html__( 'Now playing', 'streamsera' ), esc_html__( 'Our stations', 'streamsera' ) ), ), ); /** * Filter the block patterns StreamSera registers. * * @param array $patterns name → { title, description, content }. */ $patterns = apply_filters( 'streamsera_block_patterns', $patterns ); foreach ( $patterns as $slug => $pattern ) { register_block_pattern( $slug, array_merge( array( 'categories' => array( 'streamsera' ), 'keywords' => array( 'radio', 'streamsera', 'player' ), ), $pattern ) ); } } /** * First published post id of a type (0 if none). * * @param string $type Post type. * @return int */ public static function first_id( $type ) { // Cached (patterns register on every request); cleared when a post of that type is saved or deleted. $key = 'streamsera_first_' . $type; $cached = get_transient( $key ); if ( false !== $cached ) { return (int) $cached; } $ids = get_posts( array( 'post_type' => $type, 'post_status' => 'publish', 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true, 'suppress_filters' => false, ) ); $id = $ids ? (int) $ids[0] : 0; set_transient( $key, $id, DAY_IN_SECONDS ); return $id; } /** * Forget the cached first player after a player is saved or deleted. * * @param int $post_id Post ID. */ public function clear_first_id( $post_id ) { $type = get_post_type( $post_id ); if ( \StreamSera\PostTypes\Player::POST_TYPE === $type ) { delete_transient( 'streamsera_first_' . $type ); } } }