start_controls_section( 'addoshvi_content_section', array( 'label' => __( 'Video Feed', 'addonn-shoppable-video' ), 'tab' => Controls_Manager::TAB_CONTENT, ) ); $this->add_control( 'layout', array( 'label' => __( 'Layout', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SELECT, 'default' => get_option( 'addoshvi_default_layout', 'carousel' ), 'options' => array( 'carousel' => __( 'Carousel (horizontal scroll)', 'addonn-shoppable-video' ), 'inline' => __( 'Inline (vertical TikTok-style feed)', 'addonn-shoppable-video' ), ), ) ); $this->add_control( 'heading_text', array( 'label' => __( 'Heading', 'addonn-shoppable-video' ), 'type' => Controls_Manager::TEXT, 'placeholder' => __( 'e.g. Shop the Look', 'addonn-shoppable-video' ), 'label_block' => true, ) ); $this->add_control( 'subheading_text', array( 'label' => __( 'Subheading', 'addonn-shoppable-video' ), 'type' => Controls_Manager::TEXTAREA, 'rows' => 2, 'placeholder' => __( 'Optional short line under the heading', 'addonn-shoppable-video' ), 'label_block' => true, ) ); // This widget deliberately offers only one way to pick videos: build // the list right here (repeater below). Picking from the full // wp-admin-managed video library ("all published" / "choose // specific") used to be a second mode here too, but having three // (later two) options in a "Which videos?" dropdown was confusing // for a Content-tab control — that library-driven use case still // works great, it's just what the [addoshvi_feed] shortcode is for, so // power users managing videos in wp-admin use the shortcode instead // of this widget. $repeater = new Repeater(); // Hidden — not shown to the admin. Left empty here on purpose: the // real addoshvi_video CPT post is created/updated by ADDOSHVI_Elementor_Sync // when the page is saved (matched via a stable per-item key, not // this field), so nothing needs to write into it client-side. $repeater->add_control( 'video_id', array( 'type' => Controls_Manager::HIDDEN, 'default' => '', ) ); $repeater->add_control( 'video', array( 'label' => __( 'Video', 'addonn-shoppable-video' ), 'type' => Controls_Manager::MEDIA, 'media_type' => 'video', ) ); $repeater->add_control( 'cover', array( 'label' => __( 'Cover Image (optional)', 'addonn-shoppable-video' ), 'type' => Controls_Manager::MEDIA, 'media_type' => 'image', ) ); $repeater->add_control( 'item_title', array( 'label' => __( 'Title (optional)', 'addonn-shoppable-video' ), 'type' => Controls_Manager::TEXT, ) ); $repeater->add_control( 'products', array( 'label' => __( 'Tag Products', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SELECT2, 'multiple' => true, 'label_block' => true, 'options' => $this->get_product_options(), 'description' => __( 'Type to search/filter. A video needs at least one product tagged to show on the front end.', 'addonn-shoppable-video' ), ) ); $repeater->add_control( 'whatsapp_enabled', array( 'label' => __( 'Show WhatsApp Button', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => __( 'Show', 'addonn-shoppable-video' ), 'label_off' => __( 'Hide', 'addonn-shoppable-video' ), 'return_value' => 'yes', 'default' => '', ) ); $repeater->add_control( 'whatsapp_number', array( 'label' => __( 'Override WhatsApp Number', 'addonn-shoppable-video' ), 'type' => Controls_Manager::TEXT, 'condition' => array( 'whatsapp_enabled' => 'yes' ), ) ); $this->add_control( 'video_items', array( 'label' => __( 'Videos', 'addonn-shoppable-video' ), 'type' => Controls_Manager::REPEATER, 'fields' => $repeater->get_controls(), 'title_field' => '{{{ item_title || "New Video" }}}', 'button_text' => __( '+ Add Video', 'addonn-shoppable-video' ), 'prevent_empty' => false, ) ); $this->end_controls_section(); $this->start_controls_section( 'addoshvi_carousel_section', array( 'label' => __( 'Carousel Settings', 'addonn-shoppable-video' ), 'tab' => Controls_Manager::TAB_CONTENT, 'condition' => array( 'layout' => 'carousel' ), ) ); $this->add_responsive_control( 'columns', array( 'label' => __( 'Cards Visible', 'addonn-shoppable-video' ), 'type' => Controls_Manager::NUMBER, 'min' => 1, 'max' => 8, 'default' => 3, 'tablet_default' => 2, 'mobile_default' => 1, ) ); $this->add_control( 'scroll_by', array( 'label' => __( 'Slides to Scroll', 'addonn-shoppable-video' ), 'type' => Controls_Manager::NUMBER, 'default' => 1, 'min' => 1, 'max' => 8, 'description' => __( 'How many cards the prev/next arrows move at a time.', 'addonn-shoppable-video' ), ) ); $this->add_control( 'arrows', array( 'label' => __( 'Show Arrows', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => __( 'Show', 'addonn-shoppable-video' ), 'label_off' => __( 'Hide', 'addonn-shoppable-video' ), 'return_value' => 'yes', 'default' => 'yes', ) ); $this->add_control( 'dots_nav', array( 'label' => __( 'Show Navigation Dots', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => __( 'Show', 'addonn-shoppable-video' ), 'label_off' => __( 'Hide', 'addonn-shoppable-video' ), 'return_value' => 'yes', 'default' => 'yes', 'description' => __( 'Replaces the browser\'s scrollbar with clickable dots below the carousel.', 'addonn-shoppable-video' ), ) ); $this->add_control( 'autoplay_heading', array( 'label' => __( 'Autoplay', 'addonn-shoppable-video' ), 'type' => Controls_Manager::HEADING, 'separator' => 'before', ) ); $this->add_control( 'autoplay', array( 'label' => __( 'Auto Slider', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => __( 'On', 'addonn-shoppable-video' ), 'label_off' => __( 'Off', 'addonn-shoppable-video' ), 'return_value' => 'yes', 'default' => '', ) ); $this->add_control( 'autoplay_speed', array( 'label' => __( 'Speed (ms between slides)', 'addonn-shoppable-video' ), 'type' => Controls_Manager::NUMBER, 'default' => 3000, 'min' => 1000, 'max' => 10000, 'step' => 250, 'condition' => array( 'autoplay' => 'yes' ), ) ); $this->add_control( 'infinite_loop', array( 'label' => __( 'Continue Scrolling (Loop)', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => __( 'On', 'addonn-shoppable-video' ), 'label_off' => __( 'Off', 'addonn-shoppable-video' ), 'return_value' => 'yes', 'default' => 'yes', 'description' => __( 'When it reaches the last video, jump back to the first and keep going instead of stopping.', 'addonn-shoppable-video' ), 'condition' => array( 'autoplay' => 'yes' ), ) ); $this->add_control( 'pause_on_hover', array( 'label' => __( 'Pause on Hover', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SWITCHER, 'label_on' => __( 'On', 'addonn-shoppable-video' ), 'label_off' => __( 'Off', 'addonn-shoppable-video' ), 'return_value' => 'yes', 'default' => 'yes', 'condition' => array( 'autoplay' => 'yes' ), ) ); $this->end_controls_section(); $this->start_controls_section( 'addoshvi_heading_style_section', array( 'label' => __( 'Heading', 'addonn-shoppable-video' ), 'tab' => Controls_Manager::TAB_STYLE, ) ); $this->add_control( 'heading_align', array( 'label' => __( 'Alignment', 'addonn-shoppable-video' ), 'type' => Controls_Manager::CHOOSE, 'options' => array( 'left' => array( 'title' => __( 'Left', 'addonn-shoppable-video' ), 'icon' => 'eicon-text-align-left', ), 'center' => array( 'title' => __( 'Center', 'addonn-shoppable-video' ), 'icon' => 'eicon-text-align-center', ), 'right' => array( 'title' => __( 'Right', 'addonn-shoppable-video' ), 'icon' => 'eicon-text-align-right', ), ), 'default' => 'left', 'selectors' => array( '{{WRAPPER}} .svp-feed-heading-wrap' => 'text-align: {{VALUE}};', ), ) ); $this->add_control( 'heading_heading', array( 'label' => __( 'Heading Text', 'addonn-shoppable-video' ), 'type' => Controls_Manager::HEADING, 'separator' => 'before', ) ); $this->add_control( 'heading_color', array( 'label' => __( 'Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-heading' => 'color: {{VALUE}};', ), ) ); $this->add_group_control( Group_Control_Typography::get_type(), array( 'name' => 'heading_typography', 'selector' => '{{WRAPPER}} .svp-feed-heading', ) ); $this->add_group_control( Group_Control_Text_Shadow::get_type(), array( 'name' => 'heading_text_shadow', 'selector' => '{{WRAPPER}} .svp-feed-heading', ) ); $this->add_control( 'subheading_heading', array( 'label' => __( 'Subheading Text', 'addonn-shoppable-video' ), 'type' => Controls_Manager::HEADING, 'separator' => 'before', ) ); $this->add_control( 'subheading_color', array( 'label' => __( 'Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-subheading' => 'color: {{VALUE}};', ), ) ); $this->add_group_control( Group_Control_Typography::get_type(), array( 'name' => 'subheading_typography', 'selector' => '{{WRAPPER}} .svp-feed-subheading', ) ); $this->add_group_control( Group_Control_Text_Shadow::get_type(), array( 'name' => 'subheading_text_shadow', 'selector' => '{{WRAPPER}} .svp-feed-subheading', ) ); $this->end_controls_section(); $this->start_controls_section( 'addoshvi_hotspot_style_section', array( 'label' => __( 'Shop Button', 'addonn-shoppable-video' ), 'tab' => Controls_Manager::TAB_STYLE, ) ); $this->add_control( 'hotspot_note', array( 'type' => Controls_Manager::RAW_HTML, 'raw' => __( 'The round bag-icon button on each video card that opens the product list. Applies to both Carousel and Inline layouts.', 'addonn-shoppable-video' ), ) ); $this->add_control( 'hotspot_color', array( 'label' => __( 'Icon Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed' => '--svp-hotspot-color: {{VALUE}};', ), ) ); $this->add_control( 'hotspot_bg_color', array( 'label' => __( 'Background Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed' => '--svp-hotspot-bg: {{VALUE}};', ), ) ); $this->add_control( 'hotspot_hover_color', array( 'label' => __( 'Icon Hover Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed' => '--svp-hotspot-hover-color: {{VALUE}};', ), ) ); $this->add_control( 'hotspot_hover_bg_color', array( 'label' => __( 'Background Hover Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed' => '--svp-hotspot-hover-bg: {{VALUE}};', ), ) ); $this->end_controls_section(); $this->start_controls_section( 'addoshvi_carousel_style_section', array( 'label' => __( 'Carousel Navigation', 'addonn-shoppable-video' ), 'tab' => Controls_Manager::TAB_STYLE, 'condition' => array( 'layout' => 'carousel' ), ) ); $this->add_control( 'arrows_style_heading', array( 'label' => __( 'Arrows', 'addonn-shoppable-video' ), 'type' => Controls_Manager::HEADING, ) ); $this->add_control( 'arrow_color', array( 'label' => __( 'Icon Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-carousel' => '--svp-arrow-color: {{VALUE}};', ), ) ); $this->add_control( 'arrow_bg_color', array( 'label' => __( 'Background Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-carousel' => '--svp-arrow-bg: {{VALUE}};', ), ) ); $this->add_control( 'arrow_hover_color', array( 'label' => __( 'Icon Hover Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-carousel' => '--svp-arrow-hover-color: {{VALUE}};', ), ) ); $this->add_control( 'arrow_hover_bg_color', array( 'label' => __( 'Background Hover Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-carousel' => '--svp-arrow-hover-bg: {{VALUE}};', ), ) ); $this->add_control( 'dots_style_heading', array( 'label' => __( 'Navigation Dots', 'addonn-shoppable-video' ), 'type' => Controls_Manager::HEADING, 'separator' => 'before', ) ); $this->add_control( 'dot_color', array( 'label' => __( 'Dot Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-carousel' => '--svp-dot-color: {{VALUE}};', ), ) ); $this->add_control( 'dot_active_color', array( 'label' => __( 'Active Dot Color', 'addonn-shoppable-video' ), 'type' => Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .svp-feed-carousel' => '--svp-dot-active-color: {{VALUE}};', ), ) ); $this->add_control( 'dot_size', array( 'label' => __( 'Dot Size', 'addonn-shoppable-video' ), 'type' => Controls_Manager::SLIDER, 'size_units' => array( 'px' ), 'range' => array( 'px' => array( 'min' => 4, 'max' => 20, ), ), 'selectors' => array( '{{WRAPPER}} .svp-feed-carousel' => '--svp-dot-size: {{SIZE}}{{UNIT}};', ), ) ); $this->end_controls_section(); } /** * Populate the SELECT2 control with published addoshvi_video posts. */ private function get_video_options() { if ( ! post_type_exists( ADDOSHVI_POST_TYPE ) ) { return array(); } $posts = get_posts( array( 'post_type' => ADDOSHVI_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 100, 'orderby' => 'title', 'order' => 'ASC', ) ); $options = array(); foreach ( $posts as $post ) { $options[ $post->ID ] = $post->post_title ? $post->post_title : sprintf( /* translators: %d: video post ID, used as a fallback title for an untitled video. */ __( 'Video #%d', 'addonn-shoppable-video' ), $post->ID ); } return $options; } /** * Preload published, purchasable WooCommerce products for the repeater's * "Tag Products" control — gives Select2's native type-to-filter search * inside the Elementor panel, and guarantees already-tagged products * always have a label (an AJAX-only source can't label a value that * hasn't been searched for yet in the current panel session). */ private function get_product_options() { if ( ! function_exists( 'wc_get_products' ) ) { return array(); } $products = wc_get_products( array( 'status' => 'publish', 'limit' => 300, 'orderby' => 'title', 'order' => 'ASC', ) ); $options = array(); foreach ( $products as $product ) { if ( ! $product->is_purchasable() ) { continue; } $options[ $product->get_id() ] = $product->get_name() . ' (' . html_entity_decode( wp_strip_all_tags( $product->get_price_html() ), ENT_QUOTES ) . ')'; } return $options; } protected function render() { $settings = $this->get_settings_for_display(); $mode = ! empty( $settings['video_select_mode'] ) ? $settings['video_select_mode'] : 'curate'; $atts = array( 'layout' => ! empty( $settings['layout'] ) ? $settings['layout'] : 'carousel', 'heading' => ! empty( $settings['heading_text'] ) ? $settings['heading_text'] : '', 'subheading' => ! empty( $settings['subheading_text'] ) ? $settings['subheading_text'] : '', 'ids' => '', 'limit' => ! empty( $settings['limit'] ) ? absint( $settings['limit'] ) : 10, 'columns' => ! empty( $settings['columns'] ) ? $settings['columns'] : 3, 'columns_tablet' => ! empty( $settings['columns_tablet'] ) ? $settings['columns_tablet'] : 2, 'columns_mobile' => ! empty( $settings['columns_mobile'] ) ? $settings['columns_mobile'] : 1, 'scroll_by' => ! empty( $settings['scroll_by'] ) ? $settings['scroll_by'] : 1, 'arrows' => isset( $settings['arrows'] ) && 'yes' === $settings['arrows'] ? 'yes' : 'no', 'dots' => isset( $settings['dots_nav'] ) && 'yes' === $settings['dots_nav'] ? 'yes' : 'no', 'autoplay' => isset( $settings['autoplay'] ) && 'yes' === $settings['autoplay'] ? 'yes' : 'no', 'autoplay_speed' => ! empty( $settings['autoplay_speed'] ) ? absint( $settings['autoplay_speed'] ) : 3000, 'pause_on_hover' => isset( $settings['pause_on_hover'] ) && 'yes' === $settings['pause_on_hover'] ? 'yes' : 'no', 'infinite' => isset( $settings['infinite_loop'] ) && 'yes' === $settings['infinite_loop'] ? 'yes' : 'no', ); // Legacy support: earlier versions of this widget had a "Which // videos?" dropdown that also let you pull from the full wp-admin- // managed video library ("all published" / "choose specific", // later merged into one "library" option). That dropdown is gone — // this widget is curate-only now, and the library-driven use case // lives in the [addoshvi_feed] shortcode instead — but a widget instance // saved back when the dropdown existed still has one of those old // mode strings stored, so this branch keeps it rendering exactly as // it did before, without needing anyone to reopen and resave it. if ( in_array( $mode, array( 'library', 'all', 'specific' ), true ) ) { $selected = ! empty( $settings['existing_videos'] ) ? $settings['existing_videos'] : ( ! empty( $settings['videos'] ) ? $settings['videos'] : array() ); if ( ! empty( $selected ) ) { $atts['ids'] = implode( ',', array_map( 'absint', (array) $selected ) ); } echo ADDOSHVI_Shortcode::render( $atts ); // phpcs:ignore WordPress.Security.EscapeOutput -- already-escaped template output. return; } // 'curate' — the only mode the Content tab offers now. Inside the // Elementor editor, render straight from the repeater's live // (possibly unsaved) settings — the real addoshvi_video CPT posts only // get created/updated by ADDOSHVI_Elementor_Sync once the page is // actually saved, so querying by ID here would show nothing while // the admin is still mid-edit. if ( \Elementor\Plugin::$instance->editor->is_edit_mode() ) { echo ADDOSHVI_Shortcode::render_curated_preview( ! empty( $settings['video_items'] ) ? $settings['video_items'] : array(), $atts ); // phpcs:ignore WordPress.Security.EscapeOutput return; } // Resolve each repeater row to the real addoshvi_video post ADDOSHVI_Elementor_Sync // created/updated for it on save — matched the same way the sync // class matches them, via the stable "page:widget:item" key. (Note: // 'video_id' is a HIDDEN, always-empty placeholder control on the // repeater — see its registration above — the actual uploaded video // lives at $item['video']['id']; a previous version of this method // mistakenly read 'video_id' here, which meant this branch never // found any ids and silently showed the site's latest videos instead // of this widget's own curated list.) $page_id = get_the_ID(); $widget_id = $this->get_id(); $ids = array(); foreach ( (array) ( ! empty( $settings['video_items'] ) ? $settings['video_items'] : array() ) as $item ) { $item_id = isset( $item['_id'] ) ? $item['_id'] : ''; if ( '' === $item_id ) { continue; } $item_key = $page_id . ':' . $widget_id . ':' . $item_id; $matches = get_posts( array( 'post_type' => ADDOSHVI_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 1, // No core API looks up a post by a custom meta value; scoped to this plugin's own addoshvi_video CPT (one row per synced video, not an unbounded table) and capped at posts_per_page => 1. 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => '_addoshvi_source_item_key', 'value' => $item_key, ), ), 'fields' => 'ids', ) ); if ( ! empty( $matches ) ) { $ids[] = $matches[0]; } } // Force zero results (rather than falling back to "latest N global // videos") when nothing has been synced yet, so the widget's own // empty-state / "Add Video" prompt shows instead. (ADDOSHVI_Shortcode::render() // checks '' !== ids rather than !empty( ids ), so an explicit "0" // here correctly matches nothing instead of being treated as "no // filter at all" — PHP's empty('0') is true, which used to make this // silently fall through to showing every published video site-wide.) $atts['ids'] = $ids ? implode( ',', $ids ) : '0'; echo ADDOSHVI_Shortcode::render( $atts ); // phpcs:ignore WordPress.Security.EscapeOutput -- already-escaped template output. } }