✏️ 正在编辑: page-options.php
路径:
/home/h359620/public_html/wp-content/themes/datis/inc/theme-settings/page-options.php
提示:
您可以编辑任何文件(包括二进制文件),但请注意不当修改可能导致文件损坏。
<?php /** * File: page-options.php * Class: Datis_Page_Options * * CMB2-free implementation: * - Metabox: Project Details (project) * - Metabox: Project Card Features (project) * - Metabox: Project Media Gallery (project) * - Term Meta: project-categories (SVG Icon) * - Metabox: Team Social Links (team) * * IMPORTANT: No meta_key / id has been changed. */ if (!defined('ABSPATH')) { exit; } if (!class_exists('Datis_Page_Options')) { final class Datis_Page_Options { private static $instance = null; /** Post meta keys */ private $project_location_key = '_datis_project_location'; private $project_features_key = '_datis_project_features'; private $project_card_features_key = '_datis_project_card_features'; // Additional meta keys private $project_status_key = 'datis_project_status'; // in_progress|completed private $project_date_key = 'datis_project_date'; // YYYY-MM-DD (Gregorian) private $project_date_display_key = '_datis_project_date_display'; // UI only /** Project gallery meta key */ private $project_gallery_key = '_datis_project_gallery'; /** Term meta keys */ private $project_cat_svg_key = '_datis_project_cat_svg'; /** Team social meta keys */ private $team_instagram_key = '_datis_team_instagram'; private $team_telegram_key = '_datis_team_telegram'; private $team_whatsapp_key = '_datis_team_whatsapp'; public static function get_instance(): self { return self::$instance ?? (self::$instance = new self()); } private function __construct() { // Register metaboxes add_action('add_meta_boxes', [$this, 'register_metaboxes']); // Save post meta add_action('save_post_project', [$this, 'save_project_meta'], 10, 2); add_action('save_post_team', [$this, 'save_team_meta'], 10, 2); // Term meta UI + save add_action('project-categories_add_form_fields', [$this, 'render_project_category_term_add'], 10, 1); add_action('project-categories_edit_form_fields', [$this, 'render_project_category_term_edit'], 10, 2); add_action('created_project-categories', [$this, 'save_project_category_term_meta'], 10, 2); add_action('edited_project-categories', [$this, 'save_project_category_term_meta'], 10, 2); // Admin assets: datepicker + media uploader + repeaters add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']); add_action('admin_footer', [$this, 'print_admin_inline_js'], 99); // Date init (no CMB2 events) add_action('admin_footer', [$this, 'print_project_date_init_script'], 99); } /* ============================================================ * Register metaboxes * ============================================================ */ public function register_metaboxes(): void { // Project: Details add_meta_box( 'datis_project_details_box', esc_html__('Project Details', 'datis'), [$this, 'render_project_details_metabox'], 'project', 'normal', 'high' ); // Project: Card Features add_meta_box( 'datis_project_card_features_box', esc_html__('Project Card Features', 'datis'), [$this, 'render_project_card_features_metabox'], 'project', 'normal', 'high' ); // Project: Media Gallery add_meta_box( 'datis_project_media_gallery_box', esc_html__('Project Media Gallery', 'datis'), [$this, 'render_project_media_gallery_metabox'], 'project', 'normal', 'high' ); // Team: Social links add_meta_box( 'datis_team_social_links_box', esc_html__('Team Social Links', 'datis'), [$this, 'render_team_social_metabox'], 'team', 'normal', 'high' ); } /* ============================================================ * Render: Project Details * ============================================================ */ public function render_project_details_metabox(\WP_Post $post): void { wp_nonce_field('datis_project_meta_save', 'datis_project_meta_nonce'); $location = get_post_meta($post->ID, $this->project_location_key, true); $status = get_post_meta($post->ID, $this->project_status_key, true); $status = in_array($status, ['in_progress', 'completed'], true) ? $status : 'in_progress'; $stored_date = get_post_meta($post->ID, $this->project_date_key, true); // YYYY-MM-DD $display_date = get_post_meta($post->ID, $this->project_date_display_key, true); // UI only (not required) $features = get_post_meta($post->ID, $this->project_features_key, true); if (is_string($features)) { $maybe = maybe_unserialize($features); if (is_array($maybe)) $features = $maybe; } if (!is_array($features)) $features = []; ?> <style> .datis-repeater-wrap { margin-top: 14px; } .datis-repeater-item { border: 1px solid #dcdcde; background: #fff; padding: 12px; margin: 10px 0; border-radius: 6px; } .datis-repeater-head { display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom: 10px; } .datis-repeater-head strong { font-size: 13px; } .datis-grid { display:grid; grid-template-columns: 180px 1fr; gap:10px 12px; align-items:start; } .datis-grid label { font-weight: 600; } .datis-field-row { margin: 6px 0; } .datis-inline { display:inline-flex; gap:14px; align-items:center; } .datis-media-preview img { max-width: 90px; height:auto; display:block; margin-top:8px; } .datis-media-actions { display:flex; gap:8px; align-items:center; margin-top:8px; } .datis-small { font-size: 12px; color:#646970; } .datis-remove-item { color:#b32d2e; text-decoration:none; } .datis-add-item { margin-top: 10px; } </style> <div class="datis-grid"> <label for="<?php echo esc_attr($this->project_location_key); ?>"> <?php echo esc_html__('Project Location', 'datis'); ?> </label> <div> <input type="text" id="<?php echo esc_attr($this->project_location_key); ?>" name="<?php echo esc_attr($this->project_location_key); ?>" class="widefat" value="<?php echo esc_attr(is_string($location) ? $location : ''); ?>" placeholder="<?php echo esc_attr__('e.g. New York, USA', 'datis'); ?>" /> <p class="datis-small"><?php echo esc_html__('Enter the project address or location.', 'datis'); ?></p> </div> <label><?php echo esc_html__('Project Status', 'datis'); ?></label> <div class="datis-inline"> <label> <input type="radio" name="<?php echo esc_attr($this->project_status_key); ?>" value="in_progress" <?php checked($status, 'in_progress'); ?> /> <?php echo esc_html__('In Progress', 'datis'); ?> </label> <label> <input type="radio" name="<?php echo esc_attr($this->project_status_key); ?>" value="completed" <?php checked($status, 'completed'); ?> /> <?php echo esc_html__('Completed', 'datis'); ?> </label> <p class="datis-small" style="margin:0;"><?php echo esc_html__('Select project status.', 'datis'); ?></p> </div> <label><?php echo esc_html__('Project Date', 'datis'); ?></label> <div> <input type="text" name="<?php echo esc_attr($this->project_date_display_key); ?>" class="datis-project-date-display" readonly="readonly" autocomplete="off" style="width:100%;max-width:260px;cursor:pointer;" placeholder="<?php echo esc_attr__('Select date', 'datis'); ?>" value="<?php echo esc_attr(is_string($display_date) ? $display_date : ''); ?>" /> <input type="text" name="<?php echo esc_attr($this->project_date_key); ?>" class="datis-project-date-stored" autocomplete="off" readonly="readonly" style="position:absolute;left:-9999px;width:1px;height:1px;" value="<?php echo esc_attr(is_string($stored_date) ? $stored_date : ''); ?>" /> <p class="datis-small"><?php echo esc_html__('Select project date.', 'datis'); ?></p> </div> </div> <hr /> <h4 style="margin: 10px 0 0;"><?php echo esc_html__('Project Features', 'datis'); ?></h4> <p class="datis-small"><?php echo esc_html__('Add project feature items (icon + title + short text).', 'datis'); ?></p> <div class="datis-repeater-wrap" data-repeater="project_features"> <div class="datis-repeater-items"> <?php $i = 0; foreach ($features as $row) : if (!is_array($row)) continue; $icon_type = isset($row['icon_type']) ? (string)$row['icon_type'] : 'icon'; $icon_type = ($icon_type === 'image') ? 'image' : 'icon'; $icon_class = isset($row['icon_class']) ? (string)$row['icon_class'] : ''; $icon_image = $row['icon_image'] ?? ''; $title = isset($row['title']) ? (string)$row['title'] : ''; $desc = isset($row['desc']) ? (string)$row['desc'] : ''; $icon_image_url = $this->normalize_media_to_url($icon_image); ?> <div class="datis-repeater-item" data-index="<?php echo esc_attr($i); ?>"> <div class="datis-repeater-head"> <strong><?php echo esc_html(sprintf(__('Feature #%d', 'datis'), $i + 1)); ?></strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove Feature', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Icon Type', 'datis'); ?></label> <div class="datis-inline"> <label> <input type="radio" name="<?php echo esc_attr($this->project_features_key); ?>[<?php echo esc_attr($i); ?>][icon_type]" value="icon" <?php checked($icon_type, 'icon'); ?> /> <?php echo esc_html__('Icon Class', 'datis'); ?> </label> <label> <input type="radio" name="<?php echo esc_attr($this->project_features_key); ?>[<?php echo esc_attr($i); ?>][icon_type]" value="image" <?php checked($icon_type, 'image'); ?> /> <?php echo esc_html__('Image/SVG', 'datis'); ?> </label> </div> <label><?php echo esc_html__('Icon Class', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_features_key); ?>[<?php echo esc_attr($i); ?>][icon_class]" value="<?php echo esc_attr($icon_class); ?>" placeholder="fas fa-home" /> <p class="datis-small"><?php echo esc_html__('Enter the Font Awesome class (e.g., fas fa-home)', 'datis'); ?></p> </div> <label><?php echo esc_html__('Icon Image/SVG', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_features_key); ?>[<?php echo esc_attr($i); ?>][icon_image]" value="<?php echo esc_attr($icon_image_url); ?>" placeholder="<?php echo esc_attr__('Select or paste URL', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="image"> <?php echo esc_html__('Upload Icon', 'datis'); ?> </button> <button type="button" class="button datis-media-clear"> <?php echo esc_html__('Remove', 'datis'); ?> </button> </div> <div class="datis-media-preview"> <?php if ($icon_image_url) : ?> <img src="<?php echo esc_url($icon_image_url); ?>" alt="" /> <?php endif; ?> </div> </div> <label><?php echo esc_html__('Title', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_features_key); ?>[<?php echo esc_attr($i); ?>][title]" value="<?php echo esc_attr($title); ?>" placeholder="<?php echo esc_attr__('e.g. Space Type: Villa', 'datis'); ?>" /> </div> <label><?php echo esc_html__('Short Description', 'datis'); ?></label> <div> <textarea class="widefat" rows="3" name="<?php echo esc_attr($this->project_features_key); ?>[<?php echo esc_attr($i); ?>][desc]" placeholder="<?php echo esc_attr__('e.g. Two floors, 300m² area.', 'datis'); ?>" ><?php echo esc_textarea($desc); ?></textarea> </div> </div> </div> <?php $i++; endforeach; ?> </div> <button type="button" class="button datis-add-item" data-add="project_features"> <?php echo esc_html__('Add Feature', 'datis'); ?> </button> <!-- Template --> <script type="text/template" id="tmpl-datis-project-features-row"> <div class="datis-repeater-item" data-index="{{{index}}}"> <div class="datis-repeater-head"> <strong><?php echo esc_html__('Feature #', 'datis'); ?>{{{num}}}</strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove Feature', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Icon Type', 'datis'); ?></label> <div class="datis-inline"> <label> <input type="radio" name="<?php echo esc_attr($this->project_features_key); ?>[{{{index}}}][icon_type]" value="icon" checked /> <?php echo esc_html__('Icon Class', 'datis'); ?> </label> <label> <input type="radio" name="<?php echo esc_attr($this->project_features_key); ?>[{{{index}}}][icon_type]" value="image" /> <?php echo esc_html__('Image/SVG', 'datis'); ?> </label> </div> <label><?php echo esc_html__('Icon Class', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_features_key); ?>[{{{index}}}][icon_class]" value="" placeholder="fas fa-home" /> <p class="datis-small"><?php echo esc_html__('Enter the Font Awesome class (e.g., fas fa-home)', 'datis'); ?></p> </div> <label><?php echo esc_html__('Icon Image/SVG', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_features_key); ?>[{{{index}}}][icon_image]" value="" placeholder="<?php echo esc_attr__('Select or paste URL', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="image"><?php echo esc_html__('Upload Icon', 'datis'); ?></button> <button type="button" class="button datis-media-clear"><?php echo esc_html__('Remove', 'datis'); ?></button> </div> <div class="datis-media-preview"></div> </div> <label><?php echo esc_html__('Title', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_features_key); ?>[{{{index}}}][title]" value="" placeholder="<?php echo esc_attr__('e.g. Space Type: Villa', 'datis'); ?>" /> </div> <label><?php echo esc_html__('Short Description', 'datis'); ?></label> <div> <textarea class="widefat" rows="3" name="<?php echo esc_attr($this->project_features_key); ?>[{{{index}}}][desc]" placeholder="<?php echo esc_attr__('e.g. Two floors, 300m² area.', 'datis'); ?>"></textarea> </div> </div> </div> </script> </div> <?php } /* ============================================================ * Render: Project Card Features * ============================================================ */ public function render_project_card_features_metabox(\WP_Post $post): void { wp_nonce_field('datis_project_meta_save', 'datis_project_meta_nonce'); $features = get_post_meta($post->ID, $this->project_card_features_key, true); if (is_string($features)) { $maybe = maybe_unserialize($features); if (is_array($maybe)) $features = $maybe; } if (!is_array($features)) $features = []; ?> <p class="datis-small"><?php echo esc_html__('Add features to show on project cards (label + value + icon).', 'datis'); ?></p> <div class="datis-repeater-wrap" data-repeater="project_card_features"> <div class="datis-repeater-items"> <?php $i = 0; foreach ($features as $row) : if (!is_array($row)) continue; $icon_type = isset($row['icon_type']) ? sanitize_key($row['icon_type']) : 'class'; $icon_type = in_array($icon_type, ['class', 'image'], true) ? $icon_type : 'class'; $icon_class = isset($row['icon_class']) ? (string)$row['icon_class'] : ''; $icon_image = $row['icon_image'] ?? ''; $label = isset($row['label']) ? (string)$row['label'] : ''; $value = isset($row['value']) ? (string)$row['value'] : ''; $highlight = !empty($row['highlight']) ? '1' : '0'; $icon_image_url = $this->normalize_media_to_url($icon_image); ?> <div class="datis-repeater-item" data-index="<?php echo esc_attr($i); ?>"> <div class="datis-repeater-head"> <strong><?php echo esc_html(sprintf(__('Card Feature #%d', 'datis'), $i + 1)); ?></strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove Card Feature', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Icon Type', 'datis'); ?></label> <div class="datis-inline"> <label> <input type="radio" name="<?php echo esc_attr($this->project_card_features_key); ?>[<?php echo esc_attr($i); ?>][icon_type]" value="class" <?php checked($icon_type, 'class'); ?> /> <?php echo esc_html__('Icon Class', 'datis'); ?> </label> <label> <input type="radio" name="<?php echo esc_attr($this->project_card_features_key); ?>[<?php echo esc_attr($i); ?>][icon_type]" value="image" <?php checked($icon_type, 'image'); ?> /> <?php echo esc_html__('Image/SVG', 'datis'); ?> </label> </div> <label><?php echo esc_html__('Icon Class', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_card_features_key); ?>[<?php echo esc_attr($i); ?>][icon_class]" value="<?php echo esc_attr($icon_class); ?>" placeholder="fas fa-ruler-combined" /> </div> <label><?php echo esc_html__('Icon Image/SVG', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_card_features_key); ?>[<?php echo esc_attr($i); ?>][icon_image]" value="<?php echo esc_attr($icon_image_url); ?>" placeholder="<?php echo esc_attr__('Select or paste URL', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="image"> <?php echo esc_html__('Upload Icon', 'datis'); ?> </button> <button type="button" class="button datis-media-clear"> <?php echo esc_html__('Remove', 'datis'); ?> </button> </div> <div class="datis-media-preview"> <?php if ($icon_image_url) : ?> <img src="<?php echo esc_url($icon_image_url); ?>" alt="" /> <?php endif; ?> </div> </div> <label><?php echo esc_html__('Label', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_card_features_key); ?>[<?php echo esc_attr($i); ?>][label]" value="<?php echo esc_attr($label); ?>" placeholder="<?php echo esc_attr__('e.g. Area', 'datis'); ?>" /> </div> <label><?php echo esc_html__('Value', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_card_features_key); ?>[<?php echo esc_attr($i); ?>][value]" value="<?php echo esc_attr($value); ?>" placeholder="<?php echo esc_attr__('e.g. 320 m²', 'datis'); ?>" /> </div> <label><?php echo esc_html__('Highlight', 'datis'); ?></label> <div> <label> <input type="checkbox" name="<?php echo esc_attr($this->project_card_features_key); ?>[<?php echo esc_attr($i); ?>][highlight]" value="1" <?php checked($highlight, '1'); ?> /> <?php echo esc_html__('Make this feature highlighted on cards.', 'datis'); ?> </label> </div> </div> </div> <?php $i++; endforeach; ?> </div> <button type="button" class="button datis-add-item" data-add="project_card_features"> <?php echo esc_html__('Add Card Feature', 'datis'); ?> </button> <script type="text/template" id="tmpl-datis-project-card-features-row"> <div class="datis-repeater-item" data-index="{{{index}}}"> <div class="datis-repeater-head"> <strong><?php echo esc_html__('Card Feature #', 'datis'); ?>{{{num}}}</strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove Card Feature', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Icon Type', 'datis'); ?></label> <div class="datis-inline"> <label> <input type="radio" name="<?php echo esc_attr($this->project_card_features_key); ?>[{{{index}}}][icon_type]" value="class" checked /> <?php echo esc_html__('Icon Class', 'datis'); ?> </label> <label> <input type="radio" name="<?php echo esc_attr($this->project_card_features_key); ?>[{{{index}}}][icon_type]" value="image" /> <?php echo esc_html__('Image/SVG', 'datis'); ?> </label> </div> <label><?php echo esc_html__('Icon Class', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_card_features_key); ?>[{{{index}}}][icon_class]" value="" placeholder="fas fa-ruler-combined" /> </div> <label><?php echo esc_html__('Icon Image/SVG', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_card_features_key); ?>[{{{index}}}][icon_image]" value="" placeholder="<?php echo esc_attr__('Select or paste URL', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="image"><?php echo esc_html__('Upload Icon', 'datis'); ?></button> <button type="button" class="button datis-media-clear"><?php echo esc_html__('Remove', 'datis'); ?></button> </div> <div class="datis-media-preview"></div> </div> <label><?php echo esc_html__('Label', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_card_features_key); ?>[{{{index}}}][label]" value="" placeholder="<?php echo esc_attr__('e.g. Area', 'datis'); ?>" /> </div> <label><?php echo esc_html__('Value', 'datis'); ?></label> <div> <input type="text" class="widefat" name="<?php echo esc_attr($this->project_card_features_key); ?>[{{{index}}}][value]" value="" placeholder="<?php echo esc_attr__('e.g. 320 m²', 'datis'); ?>" /> </div> <label><?php echo esc_html__('Highlight', 'datis'); ?></label> <div> <label> <input type="checkbox" name="<?php echo esc_attr($this->project_card_features_key); ?>[{{{index}}}][highlight]" value="1" /> <?php echo esc_html__('Make this feature highlighted on cards.', 'datis'); ?> </label> </div> </div> </div> </script> </div> <?php } /* ============================================================ * Render: Project Media Gallery * ============================================================ */ public function render_project_media_gallery_metabox(\WP_Post $post): void { wp_nonce_field('datis_project_meta_save', 'datis_project_meta_nonce'); $gallery = get_post_meta($post->ID, $this->project_gallery_key, true); if (is_string($gallery)) { $maybe = maybe_unserialize($gallery); if (is_array($maybe)) $gallery = $maybe; } if (!is_array($gallery)) $gallery = []; ?> <p class="datis-small"><?php echo esc_html__('Add images and videos in a single sortable gallery.', 'datis'); ?></p> <div class="datis-repeater-wrap" data-repeater="project_gallery"> <div class="datis-repeater-items"> <?php $i = 0; foreach ($gallery as $row) : if (!is_array($row)) continue; $media_type = isset($row['media_type']) ? sanitize_key($row['media_type']) : 'image'; $media_type = ($media_type === 'video') ? 'video' : 'image'; // IMPORTANT: normalize to URL string (front expects a string) $media_file = $this->normalize_media_to_url($row['media_file'] ?? ''); $media_url = isset($row['media_url']) ? (string)$row['media_url'] : ''; $cover = $this->normalize_media_to_url($row['video_cover'] ?? ''); ?> <div class="datis-repeater-item" data-index="<?php echo esc_attr($i); ?>"> <div class="datis-repeater-head"> <strong><?php echo esc_html(sprintf(__('Media #%d', 'datis'), $i + 1)); ?></strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove Media', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Media Type', 'datis'); ?></label> <div class="datis-inline"> <label> <input type="radio" name="<?php echo esc_attr($this->project_gallery_key); ?>[<?php echo esc_attr($i); ?>][media_type]" value="image" <?php checked($media_type, 'image'); ?> /> <?php echo esc_html__('Image', 'datis'); ?> </label> <label> <input type="radio" name="<?php echo esc_attr($this->project_gallery_key); ?>[<?php echo esc_attr($i); ?>][media_type]" value="video" <?php checked($media_type, 'video'); ?> /> <?php echo esc_html__('Video', 'datis'); ?> </label> </div> <label><?php echo esc_html__('Media File', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_gallery_key); ?>[<?php echo esc_attr($i); ?>][media_file]" value="<?php echo esc_attr($media_file); ?>" placeholder="<?php echo esc_attr__('Upload/select an image or a video file.', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="mixed"> <?php echo esc_html__('Upload Media', 'datis'); ?> </button> <button type="button" class="button datis-media-clear"> <?php echo esc_html__('Remove', 'datis'); ?> </button> </div> <div class="datis-small"><?php echo esc_html__('Upload/select an image or a video file.', 'datis'); ?></div> </div> <label><?php echo esc_html__('Video URL (Optional)', 'datis'); ?></label> <div> <input type="url" class="widefat" name="<?php echo esc_attr($this->project_gallery_key); ?>[<?php echo esc_attr($i); ?>][media_url]" value="<?php echo esc_attr($media_url); ?>" placeholder="https://..." /> <p class="datis-small"><?php echo esc_html__('If your video is hosted (YouTube/Vimeo), paste the URL here instead of uploading.', 'datis'); ?></p> </div> <label><?php echo esc_html__('Video Cover (Poster)', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_gallery_key); ?>[<?php echo esc_attr($i); ?>][video_cover]" value="<?php echo esc_attr($cover); ?>" placeholder="<?php echo esc_attr__('Upload/select a cover image for video items', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="image"> <?php echo esc_html__('Upload Cover', 'datis'); ?> </button> <button type="button" class="button datis-media-clear"> <?php echo esc_html__('Remove', 'datis'); ?> </button> </div> <div class="datis-media-preview"> <?php if ($cover) : ?> <img src="<?php echo esc_url($cover); ?>" alt="" /> <?php endif; ?> </div> </div> </div> </div> <?php $i++; endforeach; ?> </div> <button type="button" class="button datis-add-item" data-add="project_gallery"> <?php echo esc_html__('Add Media', 'datis'); ?> </button> <script type="text/template" id="tmpl-datis-project-gallery-row"> <div class="datis-repeater-item" data-index="{{{index}}}"> <div class="datis-repeater-head"> <strong><?php echo esc_html__('Media #', 'datis'); ?>{{{num}}}</strong> <a href="#" class="datis-remove-item"><?php echo esc_html__('Remove Media', 'datis'); ?></a> </div> <div class="datis-grid"> <label><?php echo esc_html__('Media Type', 'datis'); ?></label> <div class="datis-inline"> <label><input type="radio" name="<?php echo esc_attr($this->project_gallery_key); ?>[{{{index}}}][media_type]" value="image" checked /> <?php echo esc_html__('Image', 'datis'); ?></label> <label><input type="radio" name="<?php echo esc_attr($this->project_gallery_key); ?>[{{{index}}}][media_type]" value="video" /> <?php echo esc_html__('Video', 'datis'); ?></label> </div> <label><?php echo esc_html__('Media File', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_gallery_key); ?>[{{{index}}}][media_file]" value="" placeholder="<?php echo esc_attr__('Upload/select an image or a video file.', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="mixed"><?php echo esc_html__('Upload Media', 'datis'); ?></button> <button type="button" class="button datis-media-clear"><?php echo esc_html__('Remove', 'datis'); ?></button> </div> <div class="datis-small"><?php echo esc_html__('Upload/select an image or a video file.', 'datis'); ?></div> </div> <label><?php echo esc_html__('Video URL (Optional)', 'datis'); ?></label> <div> <input type="url" class="widefat" name="<?php echo esc_attr($this->project_gallery_key); ?>[{{{index}}}][media_url]" value="" placeholder="https://..." /> <p class="datis-small"><?php echo esc_html__('If your video is hosted (YouTube/Vimeo), paste the URL here instead of uploading.', 'datis'); ?></p> </div> <label><?php echo esc_html__('Video Cover (Poster)', 'datis'); ?></label> <div> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_gallery_key); ?>[{{{index}}}][video_cover]" value="" placeholder="<?php echo esc_attr__('Upload/select a cover image for video items', 'datis'); ?>" /> <div class="datis-media-actions"> <button type="button" class="button datis-media-select" data-types="image"><?php echo esc_html__('Upload Cover', 'datis'); ?></button> <button type="button" class="button datis-media-clear"><?php echo esc_html__('Remove', 'datis'); ?></button> </div> <div class="datis-media-preview"></div> </div> </div> </div> </script> </div> <?php } /** Helper: get category SVG URL by term id (supports both URL string and array) */ public static function get_project_category_svg_url($term_id): string { $val = get_term_meta((int)$term_id, '_datis_project_cat_svg', true); // New shape (array): ['id' => 123, 'url' => 'https://...'] if (is_array($val)) { $url = isset($val['url']) ? (string) $val['url'] : ''; $url = $url ? esc_url_raw($url) : ''; // If url missing but id exists, rebuild from attachment if ($url === '' && !empty($val['id'])) { $u = wp_get_attachment_url((int)$val['id']); $url = $u ? esc_url_raw($u) : ''; } return $url; } // Legacy shape (string URL) if (is_string($val)) { $val = trim($val); return $val ? esc_url_raw($val) : ''; } return ''; } /* ============================================================ * Render: Team Social Links * ============================================================ */ public function render_team_social_metabox(\WP_Post $post): void { wp_nonce_field('datis_team_meta_save', 'datis_team_meta_nonce'); $instagram = get_post_meta($post->ID, $this->team_instagram_key, true); $telegram = get_post_meta($post->ID, $this->team_telegram_key, true); $whatsapp = get_post_meta($post->ID, $this->team_whatsapp_key, true); ?> <div class="datis-grid"> <label for="<?php echo esc_attr($this->team_instagram_key); ?>"><?php echo esc_html__('Instagram URL', 'datis'); ?></label> <div> <input type="url" class="widefat" id="<?php echo esc_attr($this->team_instagram_key); ?>" name="<?php echo esc_attr($this->team_instagram_key); ?>" value="<?php echo esc_attr(is_string($instagram) ? $instagram : ''); ?>" placeholder="https://www.instagram.com/username" /> <p class="datis-small"><?php echo esc_html__('Enter the Instagram profile URL for this team member.', 'datis'); ?></p> </div> <label for="<?php echo esc_attr($this->team_telegram_key); ?>"><?php echo esc_html__('Telegram URL', 'datis'); ?></label> <div> <input type="url" class="widefat" id="<?php echo esc_attr($this->team_telegram_key); ?>" name="<?php echo esc_attr($this->team_telegram_key); ?>" value="<?php echo esc_attr(is_string($telegram) ? $telegram : ''); ?>" placeholder="https://t.me/username" /> <p class="datis-small"><?php echo esc_html__('Enter the Telegram profile URL for this team member.', 'datis'); ?></p> </div> <label for="<?php echo esc_attr($this->team_whatsapp_key); ?>"><?php echo esc_html__('WhatsApp URL', 'datis'); ?></label> <div> <input type="url" class="widefat" id="<?php echo esc_attr($this->team_whatsapp_key); ?>" name="<?php echo esc_attr($this->team_whatsapp_key); ?>" value="<?php echo esc_attr(is_string($whatsapp) ? $whatsapp : ''); ?>" placeholder="https://wa.me/XXXXXXXXXXX" /> <p class="datis-small"><?php echo esc_html__('Enter the WhatsApp link for this team member.', 'datis'); ?></p> </div> </div> <?php } /* ============================================================ * Save: Project meta * ============================================================ */ public function save_project_meta(int $post_id, \WP_Post $post): void { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (wp_is_post_revision($post_id)) return; if (empty($_POST['datis_project_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['datis_project_meta_nonce'])), 'datis_project_meta_save')) { return; } if (!current_user_can('edit_post', $post_id)) return; // Location $location = isset($_POST[$this->project_location_key]) ? sanitize_text_field(wp_unslash($_POST[$this->project_location_key])) : ''; update_post_meta($post_id, $this->project_location_key, $location); // Status $status = isset($_POST[$this->project_status_key]) ? sanitize_key(wp_unslash($_POST[$this->project_status_key])) : 'in_progress'; $status = in_array($status, ['in_progress', 'completed'], true) ? $status : 'in_progress'; update_post_meta($post_id, $this->project_status_key, $status); // Date display (UI only) $display = isset($_POST[$this->project_date_display_key]) ? sanitize_text_field(wp_unslash($_POST[$this->project_date_display_key])) : ''; update_post_meta($post_id, $this->project_date_display_key, $display); // Date stored (YYYY-MM-DD) $stored = isset($_POST[$this->project_date_key]) ? trim(wp_unslash($_POST[$this->project_date_key])) : ''; $stored = $this->sanitize_ymd_date($stored); update_post_meta($post_id, $this->project_date_key, $stored); // Features repeater $features = isset($_POST[$this->project_features_key]) ? $_POST[$this->project_features_key] : []; $features = $this->sanitize_project_features_repeater($features); update_post_meta($post_id, $this->project_features_key, $features); // Card features repeater $card_features = isset($_POST[$this->project_card_features_key]) ? $_POST[$this->project_card_features_key] : []; $card_features = $this->sanitize_project_card_features_repeater($card_features); update_post_meta($post_id, $this->project_card_features_key, $card_features); // Gallery repeater (IMPORTANT: store media_file / video_cover as URL string) $gallery = isset($_POST[$this->project_gallery_key]) ? $_POST[$this->project_gallery_key] : []; $gallery = $this->sanitize_project_gallery_repeater($gallery); update_post_meta($post_id, $this->project_gallery_key, $gallery); } /* ============================================================ * Save: Team meta * ============================================================ */ public function save_team_meta(int $post_id, \WP_Post $post): void { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (wp_is_post_revision($post_id)) return; if (empty($_POST['datis_team_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['datis_team_meta_nonce'])), 'datis_team_meta_save')) { return; } if (!current_user_can('edit_post', $post_id)) return; $instagram = isset($_POST[$this->team_instagram_key]) ? esc_url_raw(wp_unslash($_POST[$this->team_instagram_key])) : ''; $telegram = isset($_POST[$this->team_telegram_key]) ? esc_url_raw(wp_unslash($_POST[$this->team_telegram_key])) : ''; $whatsapp = isset($_POST[$this->team_whatsapp_key]) ? esc_url_raw(wp_unslash($_POST[$this->team_whatsapp_key])) : ''; update_post_meta($post_id, $this->team_instagram_key, $instagram); update_post_meta($post_id, $this->team_telegram_key, $telegram); update_post_meta($post_id, $this->team_whatsapp_key, $whatsapp); } /* ============================================================ * Term meta: UI * ============================================================ */ public function render_project_category_term_add(string $taxonomy): void { wp_nonce_field('datis_term_meta_save', 'datis_term_meta_nonce'); ?> <div class="form-field"> <label for="<?php echo esc_attr($this->project_cat_svg_key); ?>"><?php echo esc_html__('SVG Icon', 'datis'); ?></label> <input type="text" class="widefat datis-media-url" name="<?php echo esc_attr($this->project_cat_svg_key); ?>" id="<?php echo esc_attr($this->project_cat_svg_key); ?>" value="" /> <p class="description"><?php echo esc_html__('Upload or select an SVG file to use as the category icon.', 'datis'); ?></p> <p> <button type="button" class="button datis-media-select" data-types="svg"><?php echo esc_html__('Upload SVG', 'datis'); ?></button> <button type="button" class="button datis-media-clear"><?php echo esc_html__('Remove', 'datis'); ?></button> </p> </div> <?php } public function render_project_category_term_edit(\WP_Term $term, string $taxonomy): void { wp_nonce_field('datis_term_meta_save', 'datis_term_meta_nonce'); $value = get_term_meta($term->term_id, $this->project_cat_svg_key, true); $value = is_string($value) ? $value : ''; ?> <tr class="form-field"> <th scope="row"> <label for="<?php echo esc_attr($this->project_cat_svg_key); ?>"><?php echo esc_html__('SVG Icon', 'datis'); ?></label> </th> <td> <input type="text" class="regular-text datis-media-url" name="<?php echo esc_attr($this->project_cat_svg_key); ?>" id="<?php echo esc_attr($this->project_cat_svg_key); ?>" value="<?php echo esc_attr($value); ?>" /> <p class="description"><?php echo esc_html__('Upload or select an SVG file to use as the category icon.', 'datis'); ?></p> <p> <button type="button" class="button datis-media-select" data-types="svg"><?php echo esc_html__('Upload SVG', 'datis'); ?></button> <button type="button" class="button datis-media-clear"><?php echo esc_html__('Remove', 'datis'); ?></button> </p> </td> </tr> <?php } public function save_project_category_term_meta(int $term_id, int $tt_id): void { if (empty($_POST['datis_term_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['datis_term_meta_nonce'])), 'datis_term_meta_save')) { return; } if (!current_user_can('manage_categories')) return; $val = isset($_POST[$this->project_cat_svg_key]) ? wp_unslash($_POST[$this->project_cat_svg_key]) : ''; $val = is_string($val) ? trim($val) : ''; $val = $val ? esc_url_raw($val) : ''; update_term_meta($term_id, $this->project_cat_svg_key, $val); } /* ============================================================ * Admin assets (media + datepicker) * ============================================================ */ public function enqueue_admin_assets(string $hook): void { // Media uploader for post + term screens if (is_admin()) { wp_enqueue_media(); } // Only on post edit/new screens if (!in_array($hook, ['post.php', 'post-new.php'], true)) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || $screen->post_type !== 'project') { return; } // Bootstrap 5 styles/scripts for admin (required by BS5 datepicker) wp_enqueue_style( 'datis-bootstrap-admin', DATIS_URI . '/assets/css/bootstrap.min.css', [], DATIS_VER ); wp_enqueue_style( 'bootstrap-utilities', DATIS_URI . '/assets/css/bootstrap-utilities.min.css', ['datis-bootstrap-admin'], DATIS_VER ); wp_enqueue_script( 'datis-bootstrap-bundle-admin', DATIS_URI . '/assets/js/bootstrap.bundle.min.js', ['jquery'], DATIS_VER, true ); $base = DATIS_URI . '/inc/assets/vendor/'; // moment + moment-jalaali (strict order) wp_enqueue_script('datis-moment', $base . 'moment/moment.js', ['jquery'], DATIS_VER, true); wp_enqueue_script('datis-moment-jalaali', $base . 'moment/moment-jalaali.js', ['datis-moment'], DATIS_VER, true); // Datepicker assets (must come AFTER bootstrap + moment-jalaali) wp_enqueue_style( 'datis-mdp', $base . 'mdp/mds.bs.datetimepicker.style.css', ['datis-bootstrap-admin'], DATIS_VER ); wp_enqueue_script( 'datis-mdp', $base . 'mdp/mds.bs.datetimepicker.js', ['jquery', 'datis-bootstrap-bundle-admin', 'datis-moment-jalaali'], DATIS_VER, true ); } public function print_admin_inline_js(): void { if (!is_admin()) return; $screen = function_exists('get_current_screen') ? get_current_screen() : null; $is_project = ($screen && isset($screen->post_type) && $screen->post_type === 'project'); $is_team = ($screen && isset($screen->post_type) && $screen->post_type === 'team'); // Term screens (taxonomy) $is_term = ($screen && isset($screen->taxonomy) && $screen->taxonomy === 'project-categories'); if (!$is_project && !$is_team && !$is_term) return; ?> <script> (function($){ "use strict"; function renderPreview($wrap){ var $input = $wrap.find('.datis-media-url').first(); var url = ($input.val() || '').trim(); var $prev = $wrap.find('.datis-media-preview').first(); if (!$prev.length) return; if (!url) { $prev.html(''); return; } // Image preview only if (/\.(png|jpe?g|webp|gif|svg)(\?.*)?$/i.test(url)) { $prev.html('<img src="'+ encodeURI(url) +'" alt="" />'); } else { $prev.html(''); } } function openMediaFrame($button){ var types = ($button.data('types') || 'image').toString(); // In repeater items we want the closest container that contains the input var $container = $button.closest('.datis-repeater-item'); if (!$container.length) $container = $button.closest('td, .form-field, .inside'); var $input = $container.find('.datis-media-url').first(); if (!$input.length) return; var libraryType = null; if (types === 'svg') { libraryType = 'image/svg+xml'; } else if (types === 'image') { libraryType = 'image'; } else if (types === 'mixed') { libraryType = null; // allow all (image + video) } var frame = wp.media({ title: '<?php echo esc_js(__('Select File', 'datis')); ?>', button: { text: '<?php echo esc_js(__('Use this file', 'datis')); ?>' }, multiple: false, library: libraryType ? { type: libraryType } : {} }); frame.on('select', function(){ var att = frame.state().get('selection').first().toJSON(); // Store URL string to keep compatibility with the front-end rendering if (att && att.url) { $input.val(att.url).trigger('change'); renderPreview($container); } }); frame.open(); } function bindMediaButtons(context){ var $ctx = context ? $(context) : $(document); $ctx.off('click.datisMediaSelect', '.datis-media-select'); $ctx.on('click.datisMediaSelect', '.datis-media-select', function(e){ e.preventDefault(); openMediaFrame($(this)); }); $ctx.off('click.datisMediaClear', '.datis-media-clear'); $ctx.on('click.datisMediaClear', '.datis-media-clear', function(e){ e.preventDefault(); var $container = $(this).closest('.datis-repeater-item'); if (!$container.length) $container = $(this).closest('td, .form-field, .inside'); var $input = $container.find('.datis-media-url').first(); if ($input.length) { $input.val('').trigger('change'); } renderPreview($container); }); $ctx.off('change.datisMediaUrl', '.datis-media-url'); $ctx.on('change.datisMediaUrl', '.datis-media-url', function(){ var $container = $(this).closest('.datis-repeater-item'); if (!$container.length) $container = $(this).closest('td, .form-field, .inside'); renderPreview($container); }); } function addRepeaterRow(type){ var $wrap = $('[data-repeater="'+type+'"]'); if (!$wrap.length) return; var $items = $wrap.find('.datis-repeater-items').first(); var index = $items.find('.datis-repeater-item').length; var tmplId = ''; if (type === 'project_features') tmplId = '#tmpl-datis-project-features-row'; if (type === 'project_card_features') tmplId = '#tmpl-datis-project-card-features-row'; if (type === 'project_gallery') tmplId = '#tmpl-datis-project-gallery-row'; var tmpl = $(tmplId).html(); if (!tmpl) return; var html = tmpl .replaceAll('{{{index}}}', index) .replaceAll('{{{num}}}', (index + 1)); var $row = $(html); $items.append($row); bindMediaButtons($row); } function bindRepeater(){ $(document).on('click', '.datis-add-item', function(e){ e.preventDefault(); var type = ($(this).data('add') || '').toString(); if (!type) return; addRepeaterRow(type); }); $(document).on('click', '.datis-remove-item', function(e){ e.preventDefault(); var $item = $(this).closest('.datis-repeater-item'); $item.remove(); }); } $(function(){ bindMediaButtons(document); bindRepeater(); // Initial previews $('.datis-repeater-item, .form-field, td').each(function(){ renderPreview($(this)); }); }); })(jQuery); </script> <?php } /* ============================================================ * Project date init (same logic; removed CMB2 events) * ============================================================ */ public function print_project_date_init_script(): void { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!$screen || $screen->post_type !== 'project') return; ?> <script> (function($){ "use strict"; function hasJalaliMoment() { try { return !!(window.moment && window.moment.fn && typeof window.moment.fn.jYear === "function"); } catch (e) { return false; } } /** * Format a date as Persian (Jalali) for display. * Priority: * 1) moment-jalaali (if actually patched) * 2) Intl Persian calendar (always reliable in modern browsers) * 3) fallback to Gregorian */ function formatPersianDisplay(dateLike) { // 1) moment-jalaali if available (REAL check: jYear exists) if (window.moment) { try { // If moment-jalaali exists but needs activation, try it (no harm) if (typeof window.moment.loadPersian === "function") { window.moment.loadPersian({ usePersianDigits: false }); } if (hasJalaliMoment()) { return window.moment(dateLike).format("jYYYY/jMM/jDD"); } } catch (e) {} } // 2) Intl Persian calendar (fa-IR-u-ca-persian) try { var d = (dateLike instanceof Date) ? dateLike : new Date(dateLike); if (!isNaN(d.getTime())) { // Produces something like ۱۴۰۴/۱۱/۲۱ or 1404/11/21 depending on locale digits // We keep digits Persian by default; if you want Latin digits, we can normalize. return new Intl.DateTimeFormat('fa-IR-u-ca-persian', { year: 'numeric', month: '2-digit', day: '2-digit' }).format(d).replace(/-/g, '/'); } } catch (e) {} // 3) Fallback: show Gregorian if (window.moment) { try { return window.moment(dateLike).format("YYYY-MM-DD"); } catch (e) {} } return ""; } /** * Parse a stored Gregorian YYYY-MM-DD and return a Date object. */ function parseGregorianYMD(ymd) { ymd = (ymd || "").trim(); if (!ymd) return null; // Strict parse: YYYY-MM-DD var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd); if (!m) return null; var y = parseInt(m[1], 10); var mo = parseInt(m[2], 10) - 1; var d = parseInt(m[3], 10); var dt = new Date(y, mo, d); if (isNaN(dt.getTime())) return null; return dt; } function syncDisplayFromStored($display, storedGregorian) { var dt = parseGregorianYMD(storedGregorian); if (!dt) { $display.val(""); return; } $display.val(formatPersianDisplay(dt)); } function initProjectDatePickers(context) { var $ctx = context ? $(context) : $(document); $ctx.find(".datis-project-date-display").each(function(){ var $display = $(this); if ($display.data("datisInited")) return; $display.data("datisInited", true); // Stored input is the sibling hidden input in our markup var $stored = $display.closest("div").find(".datis-project-date-stored").first(); if (!$stored.length) $stored = $display.nextAll(".datis-project-date-stored").first(); if (!$stored.length) return; $display.attr("autocomplete", "off").prop("readonly", true).css({ cursor: "pointer" }); $stored.attr("autocomplete", "off").prop("readonly", true); // Initial sync syncDisplayFromStored($display, $stored.val()); // BS5 picker if (window.mds && typeof window.mds.MdsPersianDateTimePicker === "function") { new window.mds.MdsPersianDateTimePicker($display[0], { placement: "bottom", modalMode: false, enableTimePicker: false, isGregorian: false, textFormat: "YYYY/MM/DD", dateFormat: "YYYY/MM/DD", onDayClick: function(setting){ try { if (setting && setting.selectedDate) { // ALWAYS store Gregorian YYYY-MM-DD // selectedDate is usually a Date object; moment can format it reliably var g = ""; if (window.moment) { g = window.moment(setting.selectedDate).format("YYYY-MM-DD"); } else { // Fallback if moment doesn't exist var dt = (setting.selectedDate instanceof Date) ? setting.selectedDate : new Date(setting.selectedDate); if (!isNaN(dt.getTime())) { var y = dt.getFullYear(); var m = String(dt.getMonth() + 1).padStart(2, "0"); var d = String(dt.getDate()).padStart(2, "0"); g = y + "-" + m + "-" + d; } } if (g) { $stored.val(g).trigger("change"); // Display MUST be Persian, using our formatter var parsed = parseGregorianYMD(g); $display.val(parsed ? formatPersianDisplay(parsed) : ""); } else { $stored.val(""); $display.val(""); } } } catch (e) { $stored.val(""); $display.val(""); } } }); // Force open on click $display.off("click.datisDate").on("click.datisDate", function(){ $display.trigger("focus"); }); return; } // Fallback: if no picker exists $display.prop("readonly", false); }); } $(function(){ initProjectDatePickers(document); // Re-init after adding repeater rows (our custom repeater) $(document).on("click", ".datis-add-item", function(){ setTimeout(function(){ initProjectDatePickers(document); }, 0); }); }); })(jQuery); </script> <?php } /* ============================================================ * Sanitizers * ============================================================ */ private function sanitize_ymd_date(string $value): string { $value = trim($value); if ($value === '') return ''; $dt = date_create_from_format('Y-m-d', $value); if ($dt && $dt->format('Y-m-d') === $value) return $value; $ts = strtotime($value); return $ts ? gmdate('Y-m-d', $ts) : ''; } private function sanitize_project_features_repeater($value): array { if (!is_array($value)) return []; $out = []; foreach ($value as $row) { if (!is_array($row)) continue; $icon_type = isset($row['icon_type']) ? sanitize_key($row['icon_type']) : 'icon'; $icon_type = ($icon_type === 'image') ? 'image' : 'icon'; $icon_class = isset($row['icon_class']) ? sanitize_text_field($row['icon_class']) : ''; $icon_image = isset($row['icon_image']) ? esc_url_raw($row['icon_image']) : ''; $title = isset($row['title']) ? sanitize_text_field($row['title']) : ''; $desc = isset($row['desc']) ? wp_kses($row['desc'], []) : ''; // Skip empty rows if ($title === '' && $desc === '' && $icon_class === '' && $icon_image === '') continue; $out[] = [ 'icon_type' => $icon_type, 'icon_class' => $icon_class, 'icon_image' => $icon_image, 'title' => $title, 'desc' => $desc, ]; } return $out; } private function sanitize_project_card_features_repeater($value): array { if (!is_array($value)) return []; $out = []; foreach ($value as $row) { if (!is_array($row)) continue; $icon_type = isset($row['icon_type']) ? sanitize_key($row['icon_type']) : 'class'; $icon_type = in_array($icon_type, ['class', 'image'], true) ? $icon_type : 'class'; $icon_class = isset($row['icon_class']) ? sanitize_text_field($row['icon_class']) : ''; $icon_image = isset($row['icon_image']) ? esc_url_raw($row['icon_image']) : ''; $label = isset($row['label']) ? sanitize_text_field($row['label']) : ''; $val = isset($row['value']) ? sanitize_text_field($row['value']) : ''; $hl = !empty($row['highlight']) ? '1' : '0'; if ($label === '' && $val === '' && $icon_class === '' && $icon_image === '') continue; $out[] = [ 'icon_type' => $icon_type, 'icon_class' => $icon_class, 'icon_image' => $icon_image, 'label' => $label, 'value' => $val, 'highlight' => $hl, ]; } return $out; } private function sanitize_project_gallery_repeater($value): array { if (!is_array($value)) return []; $out = []; foreach ($value as $row) { if (!is_array($row)) continue; $type = isset($row['media_type']) ? sanitize_key($row['media_type']) : 'image'; $type = ($type === 'video') ? 'video' : 'image'; // IMPORTANT: force URL string $file = isset($row['media_file']) ? esc_url_raw($row['media_file']) : ''; $url = isset($row['media_url']) ? esc_url_raw($row['media_url']) : ''; $cover = isset($row['video_cover']) ? esc_url_raw($row['video_cover']) : ''; if ($type === 'image' && $file === '') continue; if ($type === 'video' && $file === '' && $url === '') continue; $out[] = [ 'media_type' => $type, 'media_file' => $file, 'media_url' => $url, 'video_cover' => $cover, ]; } return $out; } /** * Legacy data compatibility helper: * - If numeric, treat as an attachment ID and convert to URL * - If array, try to extract a URL * - If string, return as-is (sanitized) */ private function normalize_media_to_url($value): string { if (is_numeric($value)) { $u = wp_get_attachment_url((int)$value); return $u ? esc_url_raw($u) : ''; } if (is_array($value)) { if (!empty($value['url'])) return esc_url_raw((string)$value['url']); if (!empty($value['id'])) { $u = wp_get_attachment_url((int)$value['id']); return $u ? esc_url_raw($u) : ''; } if (!empty($value[0]) && is_string($value[0])) return esc_url_raw($value[0]); return ''; } if (is_string($value)) { $value = trim($value); return $value ? esc_url_raw($value) : ''; } return ''; } } } /** * Bootstrap */ Datis_Page_Options::get_instance();
💾 保存文件
← 返回文件管理器