From 7dae2aab363fb114762c285d114101c773efff35 Mon Sep 17 00:00:00 2001 From: Rudrakshi Gupta <109859631+rudrakshi-gupta@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:49:18 +0530 Subject: [PATCH] Match WPBakery SEO Settings to Gutenberg Model --- .../wpbakery/wpbakery-video-selector-param.js | 228 +++++++++++++++++- .../class-wpb-godam-video.php | 77 +++++- 2 files changed, 290 insertions(+), 15 deletions(-) diff --git a/assets/src/js/wpbakery/wpbakery-video-selector-param.js b/assets/src/js/wpbakery/wpbakery-video-selector-param.js index 8951e54ff..e25160ee4 100644 --- a/assets/src/js/wpbakery/wpbakery-video-selector-param.js +++ b/assets/src/js/wpbakery/wpbakery-video-selector-param.js @@ -6,7 +6,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { stripHtmlTags, isFilenameLikeTitle } from '../../blocks/godam-player/utils/index.js'; +import { stripHtmlTags, isFilenameLikeTitle, appendTimezoneOffsetToUTC } from '../../blocks/godam-player/utils/index.js'; ( function( $ ) { 'use strict'; @@ -39,6 +39,18 @@ import { stripHtmlTags, isFilenameLikeTitle } from '../../blocks/godam-player/ut const attachmentDataCache = new LRUCache( ATTACHMENT_CACHE_MAX_ENTRIES ); + /** + * SEO fields that always come from the selected video and can't be edited, + * as in the Gutenberg "Video SEO Schema" modal and the Elementor popover. + */ + const SEO_READONLY_FIELDS = [ 'seo_content_url', 'seo_upload_date', 'seo_duration', 'seo_thumbnail_url' ]; + + /** + * SEO fields that show the Media Library values and become editable only + * when "Override SEO" is Yes. + */ + const SEO_OVERRIDABLE_FIELDS = [ 'seo_headline', 'seo_description', 'seo_family_friendly' ]; + // Initialize video selector on document ready and when WPBakery reloads the params $( document ).ready( initVideoSelector ); $( document ).on( 'vc.reload', initVideoSelector ); @@ -210,23 +222,231 @@ import { stripHtmlTags, isFilenameLikeTitle } from '../../blocks/godam-player/ut } ); $( document ).on( 'change', '.wpb_vc_param_value[name="seo_override"]', function() { - updateFilenameTitleWarning( getSeoScope( $( this ) ) ); + const $scope = getSeoScope( $( this ) ); + updateFilenameTitleWarning( $scope ); + syncSeoPanel( $scope ); + } ); + + $( document ).on( 'change', '.video_selector_field[name="id"]', function() { + syncSeoPanel( getSeoScope( $( this ) ) ); + } ); + + $( document ).on( 'click', '.godam-wpb-seo-reset', function( event ) { + event.preventDefault(); + resetSeoToMediaDefaults( getSeoScope( $( this ) ) ); } ); $( document ).on( 'input change', '.wpb_vc_param_value[name="seo_headline"]', function() { updateFilenameTitleWarning( getSeoScope( $( this ) ) ); } ); - // Check the title whenever an element's edit panel opens. + // Check the title and fill the SEO fields whenever an element's edit panel opens. $( document ).on( 'vc.reload', function() { $( '.wpb_vc_param_value[name="seo_override"]' ).each( function() { - updateFilenameTitleWarning( getSeoScope( $( this ) ) ); + const $scope = getSeoScope( $( this ) ); + updateFilenameTitleWarning( $scope ); + syncSeoPanel( $scope ); } ); } ); isSeoPrefillListenerBound = true; } + /** + * Get the editor wrapper of a param (label, field and description). + * + * @param {Object} $field - The param's value field (jQuery object). + * @return {Object} jQuery object. + */ + function getParamWrapper( $field ) { + const $wrapper = $field.closest( '.vc_shortcode-param' ); + return $wrapper.length ? $wrapper : $field.parent(); + } + + /** + * Make a param read-only (or editable again) without disabling it, so + * WPBakery still saves its value. + * + * @param {Object} $field - The param's value field (jQuery object). + * @param {boolean} readOnly - Whether the field should be read-only. + */ + function setParamReadOnly( $field, readOnly ) { + if ( ! $field.length ) { + return; + } + + const $wrapper = getParamWrapper( $field ); + + if ( $field.is( 'select' ) ) { + $field.css( 'pointer-events', readOnly ? 'none' : '' ); + $field.attr( 'tabindex', readOnly ? '-1' : null ); + $field.attr( 'aria-readonly', readOnly ? 'true' : null ); + } else { + $field.prop( 'readonly', readOnly ); + } + + $wrapper.toggleClass( 'godam-wpb-seo-readonly', readOnly ); + $field.css( 'background-color', readOnly ? '#f0f0f1' : '' ); + $wrapper.css( 'opacity', readOnly ? '0.75' : '' ); + } + + /** + * Set a param's value and notify WPBakery, only when it changed. + * + * @param {Object} $field - The param's value field (jQuery object). + * @param {string} value - New value. + */ + function setParamValue( $field, value ) { + if ( ! $field.length ) { + return; + } + + const next = undefined === value || null === value ? '' : String( value ); + if ( String( $field.val() ?? '' ) !== next ) { + $field.val( next ).trigger( 'change' ); + } + } + + /** + * SEO values for a video, the same way the Gutenberg modal builds them. + * + * @param {Object} attachmentData - `/wp/v2/media/` response. + * @return {Object} SEO values keyed by WPBakery param name. + */ + function getSeoParamValuesFromAttachment( attachmentData ) { + return { + // Google indexes HLS (.m3u8) but not DASH (.mpd), so prefer the HLS stream. + seo_content_url: getFirstNonEmpty( attachmentData?.meta?.rtgodam_hls_transcoded_url, attachmentData?.source_url ), + seo_headline: stripHtmlTags( getFirstNonEmpty( attachmentData?.title?.rendered, attachmentData?.title ) ), + seo_description: stripHtmlTags( getFirstNonEmpty( attachmentData?.description?.rendered, attachmentData?.description ) ), + seo_upload_date: attachmentData?.date_gmt ? appendTimezoneOffsetToUTC( attachmentData.date_gmt ) : '', + seo_duration: getFirstNonEmpty( attachmentData?.video_duration_iso8601 ), + seo_thumbnail_url: getFirstNonEmpty( attachmentData?.meta?.rtgodam_media_video_thumbnail ), + seo_family_friendly: '1', + }; + } + + /** + * Show the sync / override notice under "Override SEO", like the Gutenberg + * modal: an info notice when synced, a warning with "Reset to media library + * defaults" when overridden. + * + * @param {Object} $scope - Edit panel scope (jQuery object). + * @param {boolean} isOverride - Whether "Override SEO" is Yes. + * @param {boolean} hasVideo - Whether a video is selected. + */ + function renderSeoNotice( $scope, isOverride, hasVideo ) { + const $overrideField = getParamField( $scope, 'seo_override' ); + if ( ! $overrideField.length ) { + return; + } + + const $anchor = getParamWrapper( $overrideField ); + let $notice = $anchor.siblings( '.godam-wpb-seo-notice' ); + + if ( ! $notice.length ) { + $notice = $( '
' ); + $anchor.after( $notice ); + } + + if ( ! hasVideo ) { + $notice.hide(); + return; + } + + if ( isOverride ) { + $notice + .css( { borderLeftColor: '#dba617', background: '#fcf9e8' } ) + .empty() + .append( document.createTextNode( __( 'You have overridden the default SEO. Changes to this video in the media library will not update the SEO for this element.', 'godam' ) + ' ' ) ) + .append( + $( '' ).text( __( 'Reset to media library defaults', 'godam' ) ), + ) + .show(); + } else { + $notice + .css( { borderLeftColor: '#72aee6', background: '#f0f6fc' } ) + .text( __( 'SEO data is automatically synced from the media library. Any changes made to the video in the media library will be reflected here.', 'godam' ) ) + .show(); + } + } + + /** + * Fill and lock the SEO Settings fields to match the Gutenberg modal. + * + * Content URL, Upload Date, Duration and Thumbnail URL always come from + * the selected video and are read-only. Headline, Description and Is + * Family Friendly show the Media Library values and are read-only while + * "Override SEO" is No; they become editable (keeping their values) when + * it is Yes. + * + * @param {Object} $scope - Edit panel scope (jQuery object). + */ + function syncSeoPanel( $scope ) { + const $overrideField = getParamField( $scope, 'seo_override' ); + if ( ! $overrideField.length ) { + return; + } + + const isOverride = '1' === String( $overrideField.val() ); + const attachmentId = parseInt( getParamField( $scope, 'id' ).val(), 10 ); + + SEO_READONLY_FIELDS.forEach( ( name ) => setParamReadOnly( getParamField( $scope, name ), true ) ); + SEO_OVERRIDABLE_FIELDS.forEach( ( name ) => setParamReadOnly( getParamField( $scope, name ), ! isOverride ) ); + renderSeoNotice( $scope, isOverride, !! attachmentId ); + + if ( ! attachmentId ) { + return; + } + + // Only the latest call may write, so a slow lookup for a previously + // selected video can't overwrite the current one. + const requestId = ( $overrideField.data( 'godamSeoSyncId' ) || 0 ) + 1; + $overrideField.data( 'godamSeoSyncId', requestId ); + + fetchAttachmentData( attachmentId ).then( ( attachmentData ) => { + if ( ! attachmentData || $overrideField.data( 'godamSeoSyncId' ) !== requestId ) { + return; + } + + const values = getSeoParamValuesFromAttachment( attachmentData ); + const overrideNow = '1' === String( $overrideField.val() ); + + SEO_READONLY_FIELDS.forEach( ( name ) => setParamValue( getParamField( $scope, name ), values[ name ] ) ); + + if ( ! overrideNow ) { + SEO_OVERRIDABLE_FIELDS.forEach( ( name ) => setParamValue( getParamField( $scope, name ), values[ name ] ) ); + } + } ).catch( () => { + // Leave the fields as they are; the schema is built server-side anyway. + } ); + } + + /** + * "Reset to media library defaults": restore the Media Library values and + * switch "Override SEO" back to No. + * + * @param {Object} $scope - Edit panel scope (jQuery object). + */ + function resetSeoToMediaDefaults( $scope ) { + const attachmentId = parseInt( getParamField( $scope, 'id' ).val(), 10 ); + const $overrideField = getParamField( $scope, 'seo_override' ); + + if ( ! attachmentId || ! $overrideField.length ) { + return; + } + + fetchAttachmentData( attachmentId ).then( ( attachmentData ) => { + if ( ! attachmentData ) { + return; + } + + const values = getSeoParamValuesFromAttachment( attachmentData ); + SEO_OVERRIDABLE_FIELDS.forEach( ( name ) => setParamValue( getParamField( $scope, name ), values[ name ] ) ); + setParamValue( $overrideField, '0' ); + } ).catch( () => {} ); + } + /** * Show or hide a warning in the SEO Settings tab when the video's SEO title * looks like a raw file name (hyphens/underscores and no spaces, or the diff --git a/inc/classes/wpbakery-elements/class-wpb-godam-video.php b/inc/classes/wpbakery-elements/class-wpb-godam-video.php index e62a236f5..bab6c8090 100644 --- a/inc/classes/wpbakery-elements/class-wpb-godam-video.php +++ b/inc/classes/wpbakery-elements/class-wpb-godam-video.php @@ -331,6 +331,13 @@ public function godam_video() { ), // SEO Settings. + // + // Same fields, order and behaviour as the Gutenberg "Video SEO Schema" + // modal and the Elementor SEO popover. Every field shows once a video + // is selected; assets/src/js/wpbakery/wpbakery-video-selector-param.js + // fills them from the Media Library, keeps Content URL / Upload Date / + // Duration / Thumbnail read-only, and keeps Headline / Description / + // Family Friendly read-only until "Override SEO" is Yes. array( 'type' => 'dropdown', 'heading' => esc_html__( 'Override SEO', 'godam' ), @@ -340,7 +347,7 @@ public function godam_video() { esc_html__( 'Yes', 'godam' ) => '1', ), 'std' => '0', - 'description' => esc_html__( 'When disabled, SEO data is automatically synced from the media library. Enable to override with custom values.', 'godam' ), + 'description' => esc_html__( 'Enable to customize SEO for this element. When disabled, SEO data is synced from the media library.', 'godam' ), 'group' => esc_html__( 'SEO Settings', 'godam' ), 'save_always' => true, 'dependency' => array( @@ -348,17 +355,29 @@ public function godam_video() { 'not_empty' => true, ), ), + array( + 'type' => 'textfield', + 'heading' => esc_html__( 'Content URL', 'godam' ), + 'param_name' => 'seo_content_url', + 'value' => '', + 'description' => esc_html__( 'URL of the video file or HLS (.m3u8) stream that search engines index. Filled in from the selected video.', 'godam' ), + 'group' => esc_html__( 'SEO Settings', 'godam' ), + 'dependency' => array( + 'element' => 'id', + 'not_empty' => true, + ), + ), array( 'type' => 'textfield', 'heading' => esc_html__( 'Headline', 'godam' ), 'param_name' => 'seo_headline', 'value' => '', - 'description' => esc_html__( 'Title of the video for SEO.', 'godam' ), + 'description' => esc_html__( 'Title of the video', 'godam' ), 'group' => esc_html__( 'SEO Settings', 'godam' ), 'save_always' => true, 'dependency' => array( - 'element' => 'seo_override', - 'value' => '1', + 'element' => 'id', + 'not_empty' => true, ), ), array( @@ -366,29 +385,65 @@ public function godam_video() { 'heading' => esc_html__( 'Description', 'godam' ), 'param_name' => 'seo_description', 'value' => '', - 'description' => esc_html__( 'Description of the video for SEO. It is recommended to add a description for better video SEO.', 'godam' ), + 'description' => esc_html__( 'Description of the video. It is recommended to add a description for better video SEO.', 'godam' ), 'group' => esc_html__( 'SEO Settings', 'godam' ), 'save_always' => true, 'dependency' => array( - 'element' => 'seo_override', - 'value' => '1', + 'element' => 'id', + 'not_empty' => true, + ), + ), + array( + 'type' => 'textfield', + 'heading' => esc_html__( 'Upload Date', 'godam' ), + 'param_name' => 'seo_upload_date', + 'value' => '', + 'description' => esc_html__( 'ISO 8601 datetime format. Example: 2024-01-15T10:30:00+00:00', 'godam' ), + 'group' => esc_html__( 'SEO Settings', 'godam' ), + 'dependency' => array( + 'element' => 'id', + 'not_empty' => true, + ), + ), + array( + 'type' => 'textfield', + 'heading' => esc_html__( 'Duration', 'godam' ), + 'param_name' => 'seo_duration', + 'value' => '', + 'description' => esc_html__( 'ISO 8601 format. Example: PT1H30M', 'godam' ), + 'group' => esc_html__( 'SEO Settings', 'godam' ), + 'dependency' => array( + 'element' => 'id', + 'not_empty' => true, + ), + ), + array( + 'type' => 'textfield', + 'heading' => esc_html__( 'Video Thumbnail URL', 'godam' ), + 'param_name' => 'seo_thumbnail_url', + 'value' => '', + 'description' => esc_html__( 'URL of the video thumbnail. Example: https://www.example.com/thumbnail.jpg', 'godam' ), + 'group' => esc_html__( 'SEO Settings', 'godam' ), + 'dependency' => array( + 'element' => 'id', + 'not_empty' => true, ), ), array( 'type' => 'dropdown', - 'heading' => esc_html__( 'Family Friendly', 'godam' ), + 'heading' => esc_html__( 'Is Family Friendly', 'godam' ), 'param_name' => 'seo_family_friendly', 'value' => array( esc_html__( 'Yes', 'godam' ) => '1', esc_html__( 'No', 'godam' ) => '0', ), 'std' => '1', - 'description' => esc_html__( 'Is this video family friendly?', 'godam' ), + 'description' => esc_html__( 'Is the video suitable for all audiences?', 'godam' ), 'group' => esc_html__( 'SEO Settings', 'godam' ), 'save_always' => true, 'dependency' => array( - 'element' => 'seo_override', - 'value' => '1', + 'element' => 'id', + 'not_empty' => true, ), ), array(