diff --git a/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php new file mode 100644 index 0000000..deb097e --- /dev/null +++ b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php @@ -0,0 +1,942 @@ +plugin = $plugin; + add_action( 'admin_menu', array( $this, 'admin_menu' ) ); + add_action( 'admin_init', array( $this, 'handle_actions' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + } + + public function admin_menu() { + add_menu_page( + __( 'Ansico CPT & Taxonomies', 'ansico-cpt-and-taxonomies' ), + __( 'Ansico CPT', 'ansico-cpt-and-taxonomies' ), + 'manage_options', + 'ansico-cptax', + array( $this, 'render_cpts_page' ), + 'dashicons-database-add', + 58 + ); + + add_submenu_page( + 'ansico-cptax', + __( 'Custom Post Types', 'ansico-cpt-and-taxonomies' ), + __( 'Custom Post Types', 'ansico-cpt-and-taxonomies' ), + 'manage_options', + 'ansico-cptax', + array( $this, 'render_cpts_page' ) + ); + + add_submenu_page( + 'ansico-cptax', + __( 'Taxonomies', 'ansico-cpt-and-taxonomies' ), + __( 'Taxonomies', 'ansico-cpt-and-taxonomies' ), + 'manage_options', + 'ansico-cptax-taxonomies', + array( $this, 'render_taxonomies_page' ) + ); + + add_submenu_page( + 'ansico-cptax', + __( 'Tools', 'ansico-cpt-and-taxonomies' ), + __( 'Tools', 'ansico-cpt-and-taxonomies' ), + 'manage_options', + 'ansico-cptax-tools', + array( $this, 'render_tools_page' ) + ); + } + + public function enqueue_assets( $hook_suffix ) { + if ( false === strpos( $hook_suffix, 'ansico-cptax' ) ) { + return; + } + + wp_enqueue_style( + 'ansico-cptax-admin', + ANSICO_CPTAX_URL . 'assets/admin.css', + array(), + ANSICO_CPTAX_VERSION + ); + + wp_enqueue_script( + 'ansico-cptax-admin', + ANSICO_CPTAX_URL . 'assets/admin.js', + array(), + ANSICO_CPTAX_VERSION, + true + ); + } + + public function handle_actions() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + if ( empty( $_POST['ansico_cptax_action'] ) ) { + return; + } + + $action = sanitize_text_field( wp_unslash( $_POST['ansico_cptax_action'] ) ); + + if ( 'export_json' === $action ) { + check_admin_referer( 'ansico_cptax_export' ); + $this->download_export(); + } + + check_admin_referer( 'ansico_cptax_save' ); + + if ( 'save_cpt' === $action ) { + $record = $this->sanitize_cpt_request(); + if ( ! empty( $record['is_override'] ) ) { + $records = $this->plugin->get_cpt_overrides(); + $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' ); + $this->plugin->save_cpt_overrides( $records ); + } else { + $records = $this->plugin->get_cpts(); + $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' ); + $this->plugin->save_cpts( $records ); + } + flush_rewrite_rules(); + wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax&updated=1' ) ); + exit; + } + + if ( 'delete_cpt' === $action ) { + $key = sanitize_key( wp_unslash( $_POST['key'] ?? '' ) ); + $is_override = ! empty( $_POST['is_override'] ); + $records = $is_override ? $this->plugin->get_cpt_overrides() : $this->plugin->get_cpts(); + $records = array_values( array_filter( $records, function( $cpt ) use ( $key ) { + return ( $cpt['key'] ?? '' ) !== $key; + } ) ); + if ( $is_override ) { + $this->plugin->save_cpt_overrides( $records ); + } else { + $this->plugin->save_cpts( $records ); + } + flush_rewrite_rules(); + wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax&deleted=1' ) ); + exit; + } + + if ( 'save_taxonomy' === $action ) { + $record = $this->sanitize_taxonomy_request(); + if ( ! empty( $record['is_override'] ) ) { + $records = $this->plugin->get_taxonomy_overrides(); + $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' ); + $this->plugin->save_taxonomy_overrides( $records ); + } else { + $records = $this->plugin->get_taxonomies(); + $records = $this->upsert_by_key( $records, sanitize_key( wp_unslash( $_POST['current_key'] ?? '' ) ), $record, 'key' ); + $this->plugin->save_taxonomies( $records ); + } + flush_rewrite_rules(); + wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax-taxonomies&updated=1' ) ); + exit; + } + + if ( 'delete_taxonomy' === $action ) { + $key = sanitize_key( wp_unslash( $_POST['key'] ?? '' ) ); + $is_override = ! empty( $_POST['is_override'] ); + $records = $is_override ? $this->plugin->get_taxonomy_overrides() : $this->plugin->get_taxonomies(); + $records = array_values( array_filter( $records, function( $taxonomy ) use ( $key ) { + return ( $taxonomy['key'] ?? '' ) !== $key; + } ) ); + if ( $is_override ) { + $this->plugin->save_taxonomy_overrides( $records ); + } else { + $this->plugin->save_taxonomies( $records ); + } + flush_rewrite_rules(); + wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax-taxonomies&deleted=1' ) ); + exit; + } + + if ( 'import_json' === $action ) { + $replace_existing = ! empty( $_POST['replace_existing'] ); + $result = $this->handle_import_json( $replace_existing ); + $status = ! empty( $result['error'] ) ? 'import_error=1' : 'imported=1'; + $message = ! empty( $result['message'] ) ? rawurlencode( $result['message'] ) : ''; + wp_safe_redirect( admin_url( 'admin.php?page=ansico-cptax-tools&' . $status . '&message=' . $message ) ); + exit; + } + } + + private function download_export() { + $filename = 'ansico-cptax-export-' . gmdate( 'Y-m-d-His' ) . '.json'; + $payload = $this->plugin->get_export_payload(); + + nocache_headers(); + header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) ); + header( 'Content-Disposition: attachment; filename=' . $filename ); + echo wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); + exit; + } + + private function handle_import_json( $replace_existing ) { + if ( empty( $_FILES['import_file']['tmp_name'] ) ) { + return array( + 'error' => true, + 'message' => __( 'No JSON file was uploaded.', 'ansico-cpt-and-taxonomies' ), + ); + } + + $raw = file_get_contents( $_FILES['import_file']['tmp_name'] ); // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown + if ( false === $raw || '' === $raw ) { + return array( + 'error' => true, + 'message' => __( 'The uploaded file could not be read.', 'ansico-cpt-and-taxonomies' ), + ); + } + + $payload = json_decode( $raw, true ); + if ( ! is_array( $payload ) ) { + return array( + 'error' => true, + 'message' => __( 'The uploaded file is not valid JSON.', 'ansico-cpt-and-taxonomies' ), + ); + } + + $cpts = $this->normalize_import_records( $payload['cpts'] ?? array(), 'cpt' ); + $taxonomies = $this->normalize_import_records( $payload['taxonomies'] ?? array(), 'taxonomy' ); + $cpt_overrides = $this->normalize_import_records( $payload['cpt_overrides'] ?? array(), 'cpt' ); + $taxonomy_overrides = $this->normalize_import_records( $payload['taxonomy_overrides'] ?? array(), 'taxonomy' ); + + if ( $replace_existing ) { + $this->plugin->save_cpts( $cpts ); + $this->plugin->save_taxonomies( $taxonomies ); + $this->plugin->save_cpt_overrides( $cpt_overrides ); + $this->plugin->save_taxonomy_overrides( $taxonomy_overrides ); + } else { + $this->plugin->save_cpts( $this->merge_by_key( $this->plugin->get_cpts(), $cpts ) ); + $this->plugin->save_taxonomies( $this->merge_by_key( $this->plugin->get_taxonomies(), $taxonomies ) ); + $this->plugin->save_cpt_overrides( $this->merge_by_key( $this->plugin->get_cpt_overrides(), $cpt_overrides ) ); + $this->plugin->save_taxonomy_overrides( $this->merge_by_key( $this->plugin->get_taxonomy_overrides(), $taxonomy_overrides ) ); + } + + flush_rewrite_rules(); + + return array( + 'error' => false, + 'message' => __( 'JSON import completed.', 'ansico-cpt-and-taxonomies' ), + ); + } + + private function normalize_import_records( $records, $type ) { + $normalized = array(); + if ( ! is_array( $records ) ) { + return $normalized; + } + + foreach ( $records as $record ) { + if ( 'taxonomy' === $type ) { + $record = $this->plugin->normalize_taxonomy_record( $record ); + } else { + $record = $this->plugin->normalize_cpt_record( $record ); + } + + if ( empty( $record['key'] ) ) { + continue; + } + $normalized[] = $record; + } + + return $normalized; + } + + private function merge_by_key( $existing, $incoming ) { + foreach ( $incoming as $record ) { + $existing = $this->upsert_by_key( $existing, $record['key'] ?? '', $record, 'key' ); + } + + return array_values( $existing ); + } + + private function upsert_by_key( $records, $current_key, $record, $key_field ) { + $updated = false; + foreach ( $records as $index => $existing ) { + if ( ( $existing[ $key_field ] ?? '' ) === $current_key ) { + $records[ $index ] = $record; + $updated = true; + break; + } + } + + if ( ! $updated ) { + $records[] = $record; + } + + return array_values( $records ); + } + + +private function sanitize_cpt_request() { + $supports = isset( $_POST['supports'] ) && is_array( $_POST['supports'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['supports'] ) ) : array(); + $taxonomies = isset( $_POST['taxonomies'] ) && is_array( $_POST['taxonomies'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['taxonomies'] ) ) : array(); + $custom_fields = $this->sanitize_custom_fields(); + $template_settings = array( + 'single_mode' => sanitize_key( wp_unslash( $_POST['single_mode'] ?? 'plugin' ) ), + 'archive_mode' => sanitize_key( wp_unslash( $_POST['archive_mode'] ?? 'plugin' ) ), + 'single_show_featured' => ! empty( $_POST['single_show_featured'] ) ? 1 : 0, + 'single_show_meta' => ! empty( $_POST['single_show_meta'] ) ? 1 : 0, + 'single_show_terms' => ! empty( $_POST['single_show_terms'] ) ? 1 : 0, + 'single_show_custom_fields' => ! empty( $_POST['single_show_custom_fields'] ) ? 1 : 0, + 'archive_show_featured' => ! empty( $_POST['archive_show_featured'] ) ? 1 : 0, + 'archive_show_excerpt' => ! empty( $_POST['archive_show_excerpt'] ) ? 1 : 0, + 'archive_show_meta' => ! empty( $_POST['archive_show_meta'] ) ? 1 : 0, + 'archive_columns' => absint( wp_unslash( $_POST['archive_columns'] ?? 3 ) ), + 'archive_intro' => sanitize_textarea_field( wp_unslash( $_POST['archive_intro'] ?? '' ) ), + ); + + return $this->plugin->normalize_cpt_record( + array( + 'key' => sanitize_key( wp_unslash( $_POST['key'] ?? '' ) ), + 'singular_label' => sanitize_text_field( wp_unslash( $_POST['singular_label'] ?? '' ) ), + 'plural_label' => sanitize_text_field( wp_unslash( $_POST['plural_label'] ?? '' ) ), + 'slug' => sanitize_title( wp_unslash( $_POST['slug'] ?? '' ) ), + 'exclude_from_search' => ! empty( $_POST['exclude_from_search'] ) ? 1 : 0, + 'can_export' => ! empty( $_POST['can_export'] ) ? 1 : 0, + 'supports' => $supports, + 'taxonomies' => $taxonomies, + 'custom_fields' => $custom_fields, + 'template_settings' => $template_settings, + 'is_override' => ! empty( $_POST['is_override'] ) ? 1 : 0, + ) + ); +} + + +private function sanitize_taxonomy_request() { + $object_type = isset( $_POST['object_type'] ) && is_array( $_POST['object_type'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['object_type'] ) ) : array(); + $template_settings = array( + 'archive_mode' => sanitize_key( wp_unslash( $_POST['archive_mode'] ?? 'plugin' ) ), + 'archive_show_featured' => ! empty( $_POST['archive_show_featured'] ) ? 1 : 0, + 'archive_show_excerpt' => ! empty( $_POST['archive_show_excerpt'] ) ? 1 : 0, + 'archive_show_meta' => ! empty( $_POST['archive_show_meta'] ) ? 1 : 0, + 'archive_columns' => absint( wp_unslash( $_POST['archive_columns'] ?? 3 ) ), + 'archive_intro' => sanitize_textarea_field( wp_unslash( $_POST['archive_intro'] ?? '' ) ), + ); + + return $this->plugin->normalize_taxonomy_record( + array( + 'key' => sanitize_key( wp_unslash( $_POST['key'] ?? '' ) ), + 'singular_label' => sanitize_text_field( wp_unslash( $_POST['singular_label'] ?? '' ) ), + 'plural_label' => sanitize_text_field( wp_unslash( $_POST['plural_label'] ?? '' ) ), + 'slug' => sanitize_title( wp_unslash( $_POST['slug'] ?? '' ) ), + 'hierarchical' => ! empty( $_POST['hierarchical'] ) ? 1 : 0, + 'object_type' => $object_type, + 'template_settings' => $template_settings, + 'is_override' => ! empty( $_POST['is_override'] ) ? 1 : 0, + ) + ); +} + + + private function sanitize_custom_fields() { + $labels = isset( $_POST['field_label'] ) && is_array( $_POST['field_label'] ) ? wp_unslash( $_POST['field_label'] ) : array(); + $keys = isset( $_POST['field_key'] ) && is_array( $_POST['field_key'] ) ? wp_unslash( $_POST['field_key'] ) : array(); + $types = isset( $_POST['field_type'] ) && is_array( $_POST['field_type'] ) ? wp_unslash( $_POST['field_type'] ) : array(); + $descriptions = isset( $_POST['field_description'] ) && is_array( $_POST['field_description'] ) ? wp_unslash( $_POST['field_description'] ) : array(); + $options = isset( $_POST['field_options'] ) && is_array( $_POST['field_options'] ) ? wp_unslash( $_POST['field_options'] ) : array(); + $custom_fields = array(); + + foreach ( $keys as $index => $key ) { + $key = sanitize_key( $key ); + if ( ! $key ) { + continue; + } + $raw_options = preg_split( '/[ +,]+/', (string) ( $options[ $index ] ?? '' ) ); + $normalized_options = array(); + foreach ( $raw_options as $option ) { + $option = sanitize_text_field( $option ); + if ( '' !== $option ) { + $normalized_options[] = $option; + } + } + $custom_fields[] = array( + 'label' => sanitize_text_field( $labels[ $index ] ?? '' ), + 'key' => $key, + 'type' => sanitize_key( $types[ $index ] ?? 'text' ), + 'description' => sanitize_text_field( $descriptions[ $index ] ?? '' ), + 'options' => $normalized_options, + ); + } + + return $custom_fields; + } + + private function render_page_header( $title, $description, $active_tab ) { + ?> +
+

+

+ + render_notices(); + } + + private function render_notices() { + if ( ! empty( $_GET['updated'] ) ) { + echo '

' . esc_html__( 'Saved successfully.', 'ansico-cpt-and-taxonomies' ) . '

'; + } + if ( ! empty( $_GET['deleted'] ) ) { + echo '

' . esc_html__( 'Deleted successfully.', 'ansico-cpt-and-taxonomies' ) . '

'; + } + if ( ! empty( $_GET['imported'] ) ) { + $message = sanitize_text_field( wp_unslash( $_GET['message'] ?? __( 'Import completed.', 'ansico-cpt-and-taxonomies' ) ) ); + echo '

' . esc_html( $message ) . '

'; + } + if ( ! empty( $_GET['import_error'] ) ) { + $message = sanitize_text_field( wp_unslash( $_GET['message'] ?? __( 'Import failed.', 'ansico-cpt-and-taxonomies' ) ) ); + echo '

' . esc_html( $message ) . '

'; + } + } + + public function render_cpts_page() { + $managed = $this->plugin->get_cpts(); + $overrides = $this->plugin->get_cpt_overrides(); + $edit_key = sanitize_key( wp_unslash( $_GET['edit'] ?? '' ) ); + $import_key = sanitize_key( wp_unslash( $_GET['import'] ?? '' ) ); + $editing = null; + + foreach ( array_merge( $managed, $overrides ) as $record ) { + if ( $edit_key && ( $record['key'] ?? '' ) === $edit_key ) { + $editing = $record; + break; + } + } + + if ( ! $editing && $import_key ) { + $all_post_types = $this->plugin->get_editable_post_types(); + if ( isset( $all_post_types[ $import_key ] ) ) { + $editing = $this->map_post_type_object_to_record( $all_post_types[ $import_key ] ); + $editing['is_override'] = 1; + } + } + + $editing = $editing ?: $this->plugin->normalize_cpt_record( array( 'supports' => array( 'title', 'editor' ) ) ); + $all_taxonomies = $this->plugin->get_editable_taxonomies(); + $all_post_types = $this->plugin->get_editable_post_types(); + $field_types = $this->get_field_types(); + $template_row = $this->get_field_row_template( $field_types ); + + $this->render_page_header( + __( 'Ansico CPT & Taxonomies', 'ansico-cpt-and-taxonomies' ), + __( 'Create new custom post types or import existing post types for plugin-based overrides.', 'ansico-cpt-and-taxonomies' ), + 'cpts' + ); + ?> +
+
+
+

+

+
+
+ + + + + +
+
+ + > +

+
+
+ + +

+
+
+ + +
+
+ + +
+
+ +
+
+

+
+ +
+
+

+ 'Title', 'editor' => 'Editor', 'thumbnail' => 'Featured image', 'author' => 'Author', 'comments' => 'Comments' ) as $feature => $label ) : ?> + + +
+
+ +
+
+

+

+
+
+ $taxonomy ) : ?> + + +
+
+ + +
+
+

+

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+

+ + + + +
+
+

+ + + +
+
+

+
+ +
+
+
+

+

+
+ +
+
+ + + + + + + + + + + + + + + render_field_row( $field, $field_types ); ?> + + + +
+ +
+
+ + +
+
+ + +
+
+ plugin->get_taxonomies(); + $overrides = $this->plugin->get_taxonomy_overrides(); + $edit_key = sanitize_key( wp_unslash( $_GET['edit'] ?? '' ) ); + $import_key = sanitize_key( wp_unslash( $_GET['import'] ?? '' ) ); + $editing = null; + + foreach ( array_merge( $managed, $overrides ) as $record ) { + if ( $edit_key && ( $record['key'] ?? '' ) === $edit_key ) { + $editing = $record; + break; + } + } + + if ( ! $editing && $import_key ) { + $all_taxonomies = $this->plugin->get_editable_taxonomies(); + if ( isset( $all_taxonomies[ $import_key ] ) ) { + $editing = $this->map_taxonomy_object_to_record( $all_taxonomies[ $import_key ] ); + $editing['is_override'] = 1; + } + } + + $editing = $editing ?: $this->plugin->normalize_taxonomy_record( array( 'object_type' => array( 'post' ) ) ); + $post_types = $this->plugin->get_editable_post_types(); + + $this->render_page_header( + __( 'Taxonomies', 'ansico-cpt-and-taxonomies' ), + __( 'Create new taxonomies or import existing ones for plugin-based overrides.', 'ansico-cpt-and-taxonomies' ), + 'taxonomies' + ); + ?> +
+
+
+

+

+
+
+ + + + + +
+
+ + > +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

+ +

+
+ +
+
+

+
+
+ + + +
+
+ + +
+
+

+

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+

+
+ + +
+
+ + +
+ + plugin->get_export_payload(); + $this->render_page_header( + __( 'Tools', 'ansico-cpt-and-taxonomies' ), + __( 'Export or import plugin-managed custom post types, taxonomies, overrides, and custom fields as JSON.', 'ansico-cpt-and-taxonomies' ), + 'tools' + ); + ?> +
+
+

+

+ +
+ + + +
+
+
+

+

+
+ + +

+

+ +
+
+
+ + ' . esc_html__( 'Name', 'ansico-cpt-and-taxonomies' ) . '' . esc_html__( 'Key', 'ansico-cpt-and-taxonomies' ) . '' . esc_html__( 'Slug', 'ansico-cpt-and-taxonomies' ) . '' . esc_html__( 'Actions', 'ansico-cpt-and-taxonomies' ) . ''; + if ( empty( $records ) ) { + echo '' . esc_html__( 'No items found.', 'ansico-cpt-and-taxonomies' ) . ''; + } else { + foreach ( $records as $cpt ) { + echo ''; + echo '' . esc_html( $cpt['plural_label'] ?? $cpt['key'] ) . ''; + echo '' . esc_html( $cpt['key'] ?? '' ) . ''; + echo '' . esc_html( $cpt['slug'] ?? '' ) . ''; + echo '' . esc_html__( 'Edit', 'ansico-cpt-and-taxonomies' ) . ' '; + echo '
'; + wp_nonce_field( 'ansico_cptax_save' ); + echo ''; + echo ''; + echo ''; + submit_button( __( 'Delete', 'ansico-cpt-and-taxonomies' ), 'delete small', '', false, array( 'onclick' => "return confirm('Are you sure?');" ) ); + echo '
'; + echo ''; + } + } + echo ''; + } + + private function render_taxonomy_table( $records, $is_override ) { + echo ''; + if ( empty( $records ) ) { + echo ''; + } else { + foreach ( $records as $taxonomy ) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + } + echo '
' . esc_html__( 'Name', 'ansico-cpt-and-taxonomies' ) . '' . esc_html__( 'Key', 'ansico-cpt-and-taxonomies' ) . '' . esc_html__( 'Slug', 'ansico-cpt-and-taxonomies' ) . '' . esc_html__( 'Actions', 'ansico-cpt-and-taxonomies' ) . '
' . esc_html__( 'No items found.', 'ansico-cpt-and-taxonomies' ) . '
' . esc_html( $taxonomy['plural_label'] ?? $taxonomy['key'] ) . '' . esc_html( $taxonomy['key'] ?? '' ) . '' . esc_html( $taxonomy['slug'] ?? '' ) . '' . esc_html__( 'Edit', 'ansico-cpt-and-taxonomies' ) . ' '; + echo '
'; + wp_nonce_field( 'ansico_cptax_save' ); + echo ''; + echo ''; + echo ''; + submit_button( __( 'Delete', 'ansico-cpt-and-taxonomies' ), 'delete small', '', false, array( 'onclick' => "return confirm('Are you sure?');" ) ); + echo '
'; + } + + private function get_field_types() { + return array( + 'text' => __( 'Text', 'ansico-cpt-and-taxonomies' ), + 'textarea' => __( 'Textarea', 'ansico-cpt-and-taxonomies' ), + 'number' => __( 'Number', 'ansico-cpt-and-taxonomies' ), + 'url' => __( 'URL', 'ansico-cpt-and-taxonomies' ), + 'email' => __( 'Email', 'ansico-cpt-and-taxonomies' ), + 'date' => __( 'Date', 'ansico-cpt-and-taxonomies' ), + 'checkbox' => __( 'Checkbox', 'ansico-cpt-and-taxonomies' ), + 'select' => __( 'Select', 'ansico-cpt-and-taxonomies' ), + 'radio' => __( 'Radio', 'ansico-cpt-and-taxonomies' ), + ); + } + + private function render_field_row( $field, $field_types ) { + echo $this->get_field_row_markup( $field, $field_types ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + private function get_field_row_template( $field_types ) { + return str_replace( array( "\n", "\r", "\t" ), '', $this->get_field_row_markup( array(), $field_types ) ); + } + + private function get_field_row_markup( $field, $field_types ) { + ob_start(); + ?> + + + + + + + + + + + name, $feature ) ) { + $supports[] = $feature; + } + } + + $taxonomies = get_object_taxonomies( $obj->name, 'names' ); + + return $this->plugin->normalize_cpt_record( + array( + 'key' => $obj->name, + 'singular_label' => $obj->labels->singular_name ?? $obj->name, + 'plural_label' => $obj->labels->name ?? $obj->name, + 'slug' => is_array( $obj->rewrite ) && ! empty( $obj->rewrite['slug'] ) ? $obj->rewrite['slug'] : $obj->name, + 'exclude_from_search' => ! empty( $obj->exclude_from_search ) ? 1 : 0, + 'can_export' => ! empty( $obj->can_export ) ? 1 : 0, + 'supports' => $supports, + 'taxonomies' => is_array( $taxonomies ) ? $taxonomies : array(), + 'custom_fields' => array(), + ) + ); + } + + private function map_taxonomy_object_to_record( $obj ) { + return $this->plugin->normalize_taxonomy_record( + array( + 'key' => $obj->name, + 'singular_label' => $obj->labels->singular_name ?? $obj->name, + 'plural_label' => $obj->labels->name ?? $obj->name, + 'slug' => is_array( $obj->rewrite ) && ! empty( $obj->rewrite['slug'] ) ? $obj->rewrite['slug'] : $obj->name, + 'hierarchical' => ! empty( $obj->hierarchical ) ? 1 : 0, + 'object_type' => is_array( $obj->object_type ) ? $obj->object_type : array(), + ) + ); + } +} diff --git a/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php:Zone.Identifier b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/admin/class-ansico-cptax-admin.php:Zone.Identifier differ diff --git a/ansico-cpt-and-taxonomies/ansico-cpt-and-taxonomies.php b/ansico-cpt-and-taxonomies/ansico-cpt-and-taxonomies.php new file mode 100644 index 0000000..677ab28 --- /dev/null +++ b/ansico-cpt-and-taxonomies/ansico-cpt-and-taxonomies.php @@ -0,0 +1,32 @@ + section, +.ansico-cptax-grid > aside { + display: grid; + gap: 20px; + align-content: start; +} + +.ansico-cptax-card { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 12px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0,0,0,.04); +} + +.ansico-cptax-card--nested { + margin-top: 20px; + background: #fcfcfd; +} + +.ansico-cptax-card__header { + margin-bottom: 16px; +} + +.ansico-cptax-card__header h2, +.ansico-cptax-card__header h3, +.ansico-cptax-card h2, +.ansico-cptax-card h3 { + margin-top: 0; +} + +.ansico-cptax-card__header--spread { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; +} + +.ansico-cptax-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(220px, 1fr)); + gap: 18px; +} + +.ansico-cptax-form-grid input[type="text"] { + width: 100%; +} + +.ansico-cptax-option-columns { + display: grid; + grid-template-columns: repeat(2, minmax(220px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.ansico-inline-option, +.ansico-checkbox-grid label { + display: block; + margin-bottom: 8px; +} + +.ansico-checkbox-grid { + columns: 2 260px; +} + +.ansico-cptax-fields-wrap { + overflow-x: auto; +} + +.ansico-cptax-fields-table td, +.ansico-cptax-fields-table th { + vertical-align: top; +} + +.ansico-cptax-fields-table textarea, +.ansico-cptax-fields-table select, +.ansico-cptax-fields-table input[type="text"] { + width: 100%; +} + +.ansico-cptax-stat-list { + margin: 0 0 18px 18px; +} + +@media (max-width: 1100px) { + .ansico-cptax-grid--wide { + grid-template-columns: 1fr; + } +} + +@media (max-width: 782px) { + .ansico-cptax-form-grid, + .ansico-cptax-option-columns { + grid-template-columns: 1fr; + } + + .ansico-cptax-card__header--spread { + flex-direction: column; + } +} diff --git a/ansico-cpt-and-taxonomies/assets/admin.css:Zone.Identifier b/ansico-cpt-and-taxonomies/assets/admin.css:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/assets/admin.css:Zone.Identifier differ diff --git a/ansico-cpt-and-taxonomies/assets/admin.js b/ansico-cpt-and-taxonomies/assets/admin.js new file mode 100644 index 0000000..1f1e3ba --- /dev/null +++ b/ansico-cpt-and-taxonomies/assets/admin.js @@ -0,0 +1,54 @@ +(function () { + function slugify(value) { + return (value || '') + .toString() + .toLowerCase() + .trim() + .replace(/[^a-z0-9-_\s]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); + } + + function bindSlugHelpers() { + document.querySelectorAll('.ansico-slug-source').forEach(function (source) { + var form = source.closest('form'); + if (!form) return; + var target = form.querySelector('.ansico-slug-target'); + if (!target || target.value) return; + source.addEventListener('input', function () { + if (!target.dataset.userEdited) { + target.value = slugify(source.value); + } + }); + target.addEventListener('input', function () { + if (target.value) { + target.dataset.userEdited = '1'; + } + }); + }); + } + + function bindFieldRows() { + var addButton = document.getElementById('ansico-add-field'); + var tableBody = document.querySelector('#ansico-fields-table tbody'); + var template = document.getElementById('tmpl-ansico-field-row'); + if (!addButton || !tableBody || !template) return; + + addButton.addEventListener('click', function () { + tableBody.insertAdjacentHTML('beforeend', template.innerHTML); + }); + + tableBody.addEventListener('click', function (event) { + var removeButton = event.target.closest('.ansico-remove-field'); + if (removeButton) { + var row = removeButton.closest('.ansico-field-row'); + if (row) row.remove(); + } + }); + } + + document.addEventListener('DOMContentLoaded', function () { + bindSlugHelpers(); + bindFieldRows(); + }); +})(); diff --git a/ansico-cpt-and-taxonomies/assets/admin.js:Zone.Identifier b/ansico-cpt-and-taxonomies/assets/admin.js:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/assets/admin.js:Zone.Identifier differ diff --git a/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php new file mode 100644 index 0000000..4bb33d7 --- /dev/null +++ b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php @@ -0,0 +1,601 @@ +register_taxonomies(); + self::instance()->register_post_types(); + flush_rewrite_rules(); + } + + public static function deactivate() { + flush_rewrite_rules(); + } + + public function get_cpts() { + $cpts = get_option( self::CPT_OPTION, array() ); + return is_array( $cpts ) ? array_map( array( $this, 'normalize_cpt_record' ), $cpts ) : array(); + } + + public function get_taxonomies() { + $taxonomies = get_option( self::TAX_OPTION, array() ); + return is_array( $taxonomies ) ? array_map( array( $this, 'normalize_taxonomy_record' ), $taxonomies ) : array(); + } + + public function get_cpt_overrides() { + $cpts = get_option( self::CPT_OVERRIDE_OPTION, array() ); + return is_array( $cpts ) ? array_map( array( $this, 'normalize_cpt_record' ), $cpts ) : array(); + } + + public function get_taxonomy_overrides() { + $tax = get_option( self::TAX_OVERRIDE_OPTION, array() ); + return is_array( $tax ) ? array_map( array( $this, 'normalize_taxonomy_record' ), $tax ) : array(); + } + + public function save_cpts( $cpts ) { + update_option( self::CPT_OPTION, array_values( array_map( array( $this, 'normalize_cpt_record' ), $cpts ) ), false ); + } + + public function save_taxonomies( $taxonomies ) { + update_option( self::TAX_OPTION, array_values( array_map( array( $this, 'normalize_taxonomy_record' ), $taxonomies ) ), false ); + } + + public function save_cpt_overrides( $cpts ) { + update_option( self::CPT_OVERRIDE_OPTION, array_values( array_map( array( $this, 'normalize_cpt_record' ), $cpts ) ), false ); + } + + public function save_taxonomy_overrides( $taxonomies ) { + update_option( self::TAX_OVERRIDE_OPTION, array_values( array_map( array( $this, 'normalize_taxonomy_record' ), $taxonomies ) ), false ); + } + + public function normalize_cpt_record( $record ) { + $record = is_array( $record ) ? $record : array(); + $custom_fields = array(); + + if ( ! empty( $record['custom_fields'] ) && is_array( $record['custom_fields'] ) ) { + foreach ( $record['custom_fields'] as $field ) { + if ( ! is_array( $field ) ) { + continue; + } + $type = sanitize_key( $field['type'] ?? 'text' ); + if ( ! in_array( $type, array( 'text', 'textarea', 'number', 'url', 'email', 'date', 'checkbox', 'select', 'radio' ), true ) ) { + $type = 'text'; + } + $options = array(); + if ( ! empty( $field['options'] ) && is_array( $field['options'] ) ) { + foreach ( $field['options'] as $option ) { + $option = sanitize_text_field( $option ); + if ( '' !== $option ) { + $options[] = $option; + } + } + } + $custom_fields[] = array( + 'label' => sanitize_text_field( $field['label'] ?? '' ), + 'key' => sanitize_key( $field['key'] ?? '' ), + 'type' => $type, + 'description' => sanitize_text_field( $field['description'] ?? '' ), + 'options' => $options, + ); + } + } + + +$template_settings = $this->normalize_cpt_template_settings( $record['template_settings'] ?? array() ); + + +return array( + 'key' => sanitize_key( $record['key'] ?? '' ), + 'singular_label' => sanitize_text_field( $record['singular_label'] ?? '' ), + 'plural_label' => sanitize_text_field( $record['plural_label'] ?? '' ), + 'slug' => sanitize_title( $record['slug'] ?? '' ), + 'hierarchical' => ! empty( $record['hierarchical'] ) ? 1 : 0, + 'object_type' => ! empty( $record['object_type'] ) && is_array( $record['object_type'] ) ? array_values( array_unique( array_map( 'sanitize_key', $record['object_type'] ) ) ) : array(), + 'template_settings' => $this->normalize_taxonomy_template_settings( $record['template_settings'] ?? array() ), + 'is_override' => ! empty( $record['is_override'] ) ? 1 : 0, +); + } + + public function normalize_taxonomy_record( $record ) { + $record = is_array( $record ) ? $record : array(); + + +$template_settings = $this->normalize_cpt_template_settings( $record['template_settings'] ?? array() ); + + +return array( + 'key' => sanitize_key( $record['key'] ?? '' ), + 'singular_label' => sanitize_text_field( $record['singular_label'] ?? '' ), + 'plural_label' => sanitize_text_field( $record['plural_label'] ?? '' ), + 'slug' => sanitize_title( $record['slug'] ?? '' ), + 'hierarchical' => ! empty( $record['hierarchical'] ) ? 1 : 0, + 'object_type' => ! empty( $record['object_type'] ) && is_array( $record['object_type'] ) ? array_values( array_unique( array_map( 'sanitize_key', $record['object_type'] ) ) ) : array(), + 'template_settings' => $this->normalize_taxonomy_template_settings( $record['template_settings'] ?? array() ), + 'is_override' => ! empty( $record['is_override'] ) ? 1 : 0, +); + } + + +public function normalize_cpt_template_settings( $settings ) { + $settings = is_array( $settings ) ? $settings : array(); + $single_mode = isset( $settings['single_mode'] ) && in_array( $settings['single_mode'], array( 'plugin', 'theme' ), true ) ? $settings['single_mode'] : 'plugin'; + $archive_mode = isset( $settings['archive_mode'] ) && in_array( $settings['archive_mode'], array( 'plugin', 'theme' ), true ) ? $settings['archive_mode'] : 'plugin'; + $archive_columns = absint( $settings['archive_columns'] ?? 3 ); + if ( $archive_columns < 1 || $archive_columns > 4 ) { + $archive_columns = 3; + } + return array( + 'single_mode' => $single_mode, + 'archive_mode' => $archive_mode, + 'single_show_featured' => ! empty( $settings['single_show_featured'] ) ? 1 : 0, + 'single_show_meta' => ! empty( $settings['single_show_meta'] ) ? 1 : 0, + 'single_show_terms' => ! empty( $settings['single_show_terms'] ) ? 1 : 0, + 'single_show_custom_fields' => ! empty( $settings['single_show_custom_fields'] ) ? 1 : 0, + 'archive_show_featured' => ! empty( $settings['archive_show_featured'] ) ? 1 : 0, + 'archive_show_excerpt' => ! empty( $settings['archive_show_excerpt'] ) ? 1 : 0, + 'archive_show_meta' => ! empty( $settings['archive_show_meta'] ) ? 1 : 0, + 'archive_columns' => $archive_columns, + 'archive_intro' => sanitize_textarea_field( $settings['archive_intro'] ?? '' ), + ); +} + +public function normalize_taxonomy_template_settings( $settings ) { + $settings = is_array( $settings ) ? $settings : array(); + $archive_mode = isset( $settings['archive_mode'] ) && in_array( $settings['archive_mode'], array( 'plugin', 'theme' ), true ) ? $settings['archive_mode'] : 'plugin'; + $archive_columns = absint( $settings['archive_columns'] ?? 3 ); + if ( $archive_columns < 1 || $archive_columns > 4 ) { + $archive_columns = 3; + } + return array( + 'archive_mode' => $archive_mode, + 'archive_show_featured' => ! empty( $settings['archive_show_featured'] ) ? 1 : 0, + 'archive_show_excerpt' => ! empty( $settings['archive_show_excerpt'] ) ? 1 : 0, + 'archive_show_meta' => ! empty( $settings['archive_show_meta'] ) ? 1 : 0, + 'archive_columns' => $archive_columns, + 'archive_intro' => sanitize_textarea_field( $settings['archive_intro'] ?? '' ), + ); +} + +public function get_cpt_template_settings( $post_type ) { + $record = $this->get_any_managed_cpt_by_key( $post_type ); + return $record['template_settings'] ?? $this->normalize_cpt_template_settings( array() ); +} + +public function get_taxonomy_template_settings( $taxonomy ) { + $record = $this->get_taxonomy_by_key( $taxonomy ) ?: $this->get_taxonomy_override_by_key( $taxonomy ); + return $record['template_settings'] ?? $this->normalize_taxonomy_template_settings( array() ); +} + + public function get_export_payload() { + return array( + 'plugin' => 'ansico-cpt-and-taxonomies', + 'version' => ANSICO_CPTAX_VERSION, + 'exported_at_gmt' => gmdate( 'c' ), + 'cpts' => $this->get_cpts(), + 'taxonomies' => $this->get_taxonomies(), + 'cpt_overrides' => $this->get_cpt_overrides(), + 'taxonomy_overrides' => $this->get_taxonomy_overrides(), + ); + } + + public function register_taxonomies() { + foreach ( $this->get_taxonomies() as $taxonomy ) { + if ( empty( $taxonomy['key'] ) ) { + continue; + } + + $attached_post_types = isset( $taxonomy['object_type'] ) && is_array( $taxonomy['object_type'] ) ? array_filter( $taxonomy['object_type'] ) : array( 'post' ); + if ( empty( $attached_post_types ) ) { + $attached_post_types = array( 'post' ); + } + + register_taxonomy( $taxonomy['key'], $attached_post_types, $this->build_taxonomy_args( $taxonomy ) ); + } + } + + public function register_post_types() { + foreach ( $this->get_cpts() as $cpt ) { + if ( empty( $cpt['key'] ) ) { + continue; + } + + register_post_type( $cpt['key'], $this->build_post_type_args( $cpt ) ); + } + } + + public function build_taxonomy_args( $taxonomy, $existing_args = array() ) { + $singular = $taxonomy['singular_label'] ?? ucfirst( $taxonomy['key'] ); + $plural = $taxonomy['plural_label'] ?? $singular . 's'; + $slug = $taxonomy['slug'] ?? $taxonomy['key']; + $hierarchical = ! empty( $taxonomy['hierarchical'] ); + + $labels = array( + 'name' => $plural, + 'singular_name' => $singular, + 'search_items' => sprintf( __( 'Search %s', 'ansico-cpt-and-taxonomies' ), $plural ), + 'all_items' => sprintf( __( 'All %s', 'ansico-cpt-and-taxonomies' ), $plural ), + 'parent_item' => sprintf( __( 'Parent %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'parent_item_colon' => sprintf( __( 'Parent %s:', 'ansico-cpt-and-taxonomies' ), $singular ), + 'edit_item' => sprintf( __( 'Edit %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'update_item' => sprintf( __( 'Update %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'add_new_item' => sprintf( __( 'Add New %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'new_item_name' => sprintf( __( 'New %s Name', 'ansico-cpt-and-taxonomies' ), $singular ), + 'menu_name' => $plural, + ); + + return array_merge( + $existing_args, + array( + 'labels' => $labels, + 'public' => $existing_args['public'] ?? true, + 'show_ui' => $existing_args['show_ui'] ?? true, + 'show_admin_column' => $existing_args['show_admin_column'] ?? true, + 'show_in_nav_menus' => $existing_args['show_in_nav_menus'] ?? true, + 'show_tagcloud' => ! $hierarchical, + 'show_in_rest' => $existing_args['show_in_rest'] ?? true, + 'hierarchical' => $hierarchical, + 'rewrite' => array( 'slug' => sanitize_title( $slug ) ), + ) + ); + } + + public function build_post_type_args( $cpt, $existing_args = array() ) { + $singular = $cpt['singular_label'] ?? ucfirst( $cpt['key'] ); + $plural = $cpt['plural_label'] ?? $singular . 's'; + $slug = $cpt['slug'] ?? $cpt['key']; + $supports = isset( $cpt['supports'] ) && is_array( $cpt['supports'] ) ? array_values( array_filter( $cpt['supports'] ) ) : array( 'title', 'editor' ); + $taxonomies = isset( $cpt['taxonomies'] ) && is_array( $cpt['taxonomies'] ) ? array_values( array_filter( $cpt['taxonomies'] ) ) : array(); + + if ( ! in_array( 'custom-fields', $supports, true ) && ! empty( $cpt['custom_fields'] ) ) { + $supports[] = 'custom-fields'; + } + + $labels = array( + 'name' => $plural, + 'singular_name' => $singular, + 'menu_name' => $plural, + 'name_admin_bar' => $singular, + 'add_new' => __( 'Add New', 'ansico-cpt-and-taxonomies' ), + 'add_new_item' => sprintf( __( 'Add New %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'new_item' => sprintf( __( 'New %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'edit_item' => sprintf( __( 'Edit %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'view_item' => sprintf( __( 'View %s', 'ansico-cpt-and-taxonomies' ), $singular ), + 'all_items' => sprintf( __( 'All %s', 'ansico-cpt-and-taxonomies' ), $plural ), + 'search_items' => sprintf( __( 'Search %s', 'ansico-cpt-and-taxonomies' ), $plural ), + 'parent_item_colon' => sprintf( __( 'Parent %s:', 'ansico-cpt-and-taxonomies' ), $singular ), + 'not_found' => sprintf( __( 'No %s found.', 'ansico-cpt-and-taxonomies' ), strtolower( $plural ) ), + 'not_found_in_trash' => sprintf( __( 'No %s found in Trash.', 'ansico-cpt-and-taxonomies' ), strtolower( $plural ) ), + 'featured_image' => __( 'Featured image', 'ansico-cpt-and-taxonomies' ), + 'set_featured_image' => __( 'Set featured image', 'ansico-cpt-and-taxonomies' ), + 'remove_featured_image' => __( 'Remove featured image', 'ansico-cpt-and-taxonomies' ), + 'use_featured_image' => __( 'Use as featured image', 'ansico-cpt-and-taxonomies' ), + 'archives' => sprintf( __( '%s archives', 'ansico-cpt-and-taxonomies' ), $singular ), + ); + + return array_merge( + $existing_args, + array( + 'labels' => $labels, + 'public' => $existing_args['public'] ?? true, + 'show_ui' => $existing_args['show_ui'] ?? true, + 'show_in_menu' => $existing_args['show_in_menu'] ?? true, + 'show_in_rest' => $existing_args['show_in_rest'] ?? true, + 'has_archive' => $existing_args['has_archive'] ?? true, + 'rewrite' => array( 'slug' => sanitize_title( $slug ) ), + 'exclude_from_search' => ! empty( $cpt['exclude_from_search'] ), + 'can_export' => ! empty( $cpt['can_export'] ), + 'supports' => $supports, + 'taxonomies' => $taxonomies, + 'menu_position' => $existing_args['menu_position'] ?? 20, + 'menu_icon' => $existing_args['menu_icon'] ?? 'dashicons-admin-post', + ) + ); + } + + public function filter_post_type_args( $args, $post_type ) { + $override = $this->get_cpt_override_by_key( $post_type ); + if ( ! $override ) { + return $args; + } + + return $this->build_post_type_args( $override, $args ); + } + + public function filter_taxonomy_args( $args, $taxonomy ) { + $override = $this->get_taxonomy_override_by_key( $taxonomy ); + if ( ! $override ) { + return $args; + } + + return $this->build_taxonomy_args( $override, $args ); + } + + public function attach_overridden_taxonomies_to_objects() { + foreach ( $this->get_cpt_overrides() as $override ) { + if ( empty( $override['key'] ) || empty( $override['taxonomies'] ) || ! is_array( $override['taxonomies'] ) ) { + continue; + } + foreach ( $override['taxonomies'] as $taxonomy ) { + if ( taxonomy_exists( $taxonomy ) ) { + register_taxonomy_for_object_type( $taxonomy, $override['key'] ); + } + } + } + + foreach ( $this->get_taxonomy_overrides() as $override ) { + if ( empty( $override['key'] ) || empty( $override['object_type'] ) || ! is_array( $override['object_type'] ) ) { + continue; + } + foreach ( $override['object_type'] as $object_type ) { + if ( post_type_exists( $object_type ) ) { + register_taxonomy_for_object_type( $override['key'], $object_type ); + } + } + } + } + + public function register_meta_boxes() { + foreach ( $this->get_all_managed_cpts() as $cpt ) { + if ( empty( $cpt['key'] ) || empty( $cpt['custom_fields'] ) || ! is_array( $cpt['custom_fields'] ) ) { + continue; + } + + add_meta_box( + 'ansico_cptax_fields_' . $cpt['key'], + __( 'Custom Fields', 'ansico-cpt-and-taxonomies' ), + array( $this, 'render_meta_box' ), + $cpt['key'], + 'normal', + 'default', + array( 'fields' => $cpt['custom_fields'] ) + ); + } + } + + public function render_meta_box( $post, $box ) { + $fields = $box['args']['fields'] ?? array(); + wp_nonce_field( 'ansico_cptax_save_fields', 'ansico_cptax_nonce' ); + + echo ''; + foreach ( $fields as $field ) { + $key = $field['key'] ?? ''; + $label = $field['label'] ?? $key; + $type = $field['type'] ?? 'text'; + $description = $field['description'] ?? ''; + $options = ! empty( $field['options'] ) && is_array( $field['options'] ) ? $field['options'] : array(); + + if ( ! $key ) { + continue; + } + + $meta_key = 'ansico_' . $key; + $value = get_post_meta( $post->ID, $meta_key, true ); + + echo ''; + echo ''; + echo ''; + echo ''; + } + echo '
'; + + switch ( $type ) { + case 'textarea': + echo ''; + break; + case 'checkbox': + echo ''; + break; + case 'select': + echo ''; + break; + case 'radio': + foreach ( $options as $index => $option ) { + echo ''; + } + break; + default: + echo ''; + break; + } + + if ( $description ) { + echo '

' . esc_html( $description ) . '

'; + } + + echo '
'; + } + + public function save_custom_fields( $post_id ) { + if ( ! isset( $_POST['ansico_cptax_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['ansico_cptax_nonce'] ) ), 'ansico_cptax_save_fields' ) ) { + return; + } + + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return; + } + + $post_type = get_post_type( $post_id ); + $cpt = $this->get_any_managed_cpt_by_key( $post_type ); + if ( ! $cpt || empty( $cpt['custom_fields'] ) ) { + return; + } + + $submitted = isset( $_POST['ansico_fields'] ) && is_array( $_POST['ansico_fields'] ) ? wp_unslash( $_POST['ansico_fields'] ) : array(); + + foreach ( $cpt['custom_fields'] as $field ) { + $key = $field['key'] ?? ''; + $type = $field['type'] ?? 'text'; + $options = ! empty( $field['options'] ) && is_array( $field['options'] ) ? $field['options'] : array(); + + if ( ! $key ) { + continue; + } + + $meta_key = 'ansico_' . $key; + + if ( 'checkbox' === $type ) { + $value = isset( $submitted[ $key ] ) ? '1' : '0'; + } elseif ( 'textarea' === $type ) { + $value = sanitize_textarea_field( $submitted[ $key ] ?? '' ); + } elseif ( in_array( $type, array( 'select', 'radio' ), true ) ) { + $candidate = sanitize_text_field( $submitted[ $key ] ?? '' ); + $value = in_array( $candidate, $options, true ) ? $candidate : ''; + } else { + $value = sanitize_text_field( $submitted[ $key ] ?? '' ); + } + + update_post_meta( $post_id, $meta_key, $value ); + } + } + + public function get_cpt_by_key( $key ) { + foreach ( $this->get_cpts() as $cpt ) { + if ( isset( $cpt['key'] ) && $cpt['key'] === $key ) { + return $cpt; + } + } + return null; + } + + public function get_taxonomy_by_key( $key ) { + foreach ( $this->get_taxonomies() as $taxonomy ) { + if ( isset( $taxonomy['key'] ) && $taxonomy['key'] === $key ) { + return $taxonomy; + } + } + return null; + } + + public function get_cpt_override_by_key( $key ) { + foreach ( $this->get_cpt_overrides() as $cpt ) { + if ( isset( $cpt['key'] ) && $cpt['key'] === $key ) { + return $cpt; + } + } + return null; + } + + public function get_taxonomy_override_by_key( $key ) { + foreach ( $this->get_taxonomy_overrides() as $tax ) { + if ( isset( $tax['key'] ) && $tax['key'] === $key ) { + return $tax; + } + } + return null; + } + + public function get_any_managed_cpt_by_key( $key ) { + return $this->get_cpt_by_key( $key ) ?: $this->get_cpt_override_by_key( $key ); + } + + public function get_all_managed_cpts() { + return array_merge( $this->get_cpts(), $this->get_cpt_overrides() ); + } + + public function get_editable_post_types() { + $objects = get_post_types( array(), 'objects' ); + $skip = array( 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation', 'wp_font_family', 'wp_font_face' ); + foreach ( $skip as $post_type ) { + unset( $objects[ $post_type ] ); + } + ksort( $objects ); + return $objects; + } + + public function get_editable_taxonomies() { + $objects = get_taxonomies( array(), 'objects' ); + unset( $objects['nav_menu'] ); + ksort( $objects ); + return $objects; + } + + +public function template_include( $template ) { + if ( is_singular() ) { + $post_type = get_post_type(); + if ( $post_type && $this->get_any_managed_cpt_by_key( $post_type ) ) { + $settings = $this->get_cpt_template_settings( $post_type ); + if ( 'plugin' === ( $settings['single_mode'] ?? 'plugin' ) ) { + $plugin_template = ANSICO_CPTAX_PATH . 'templates/single-cpt.php'; + if ( file_exists( $plugin_template ) ) { + return $plugin_template; + } + } + } + } + + if ( is_post_type_archive() ) { + $post_type = get_query_var( 'post_type' ); + if ( is_array( $post_type ) ) { + $post_type = reset( $post_type ); + } + if ( $post_type && $this->get_any_managed_cpt_by_key( $post_type ) ) { + $settings = $this->get_cpt_template_settings( $post_type ); + if ( 'plugin' === ( $settings['archive_mode'] ?? 'plugin' ) ) { + $plugin_template = ANSICO_CPTAX_PATH . 'templates/archive-cpt.php'; + if ( file_exists( $plugin_template ) ) { + return $plugin_template; + } + } + } + } + + if ( is_tax() || is_category() || is_tag() ) { + $term = get_queried_object(); + if ( ! empty( $term->taxonomy ) && ( $this->get_taxonomy_by_key( $term->taxonomy ) || $this->get_taxonomy_override_by_key( $term->taxonomy ) ) ) { + $settings = $this->get_taxonomy_template_settings( $term->taxonomy ); + if ( 'plugin' === ( $settings['archive_mode'] ?? 'plugin' ) ) { + $plugin_template = ANSICO_CPTAX_PATH . 'templates/taxonomy-archive.php'; + if ( file_exists( $plugin_template ) ) { + return $plugin_template; + } + } + } + } + + return $template; +} +} diff --git a/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php:Zone.Identifier b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/includes/class-ansico-cptax-plugin.php:Zone.Identifier differ diff --git a/ansico-cpt-and-taxonomies/readme.txt b/ansico-cpt-and-taxonomies/readme.txt new file mode 100644 index 0000000..df52ec4 --- /dev/null +++ b/ansico-cpt-and-taxonomies/readme.txt @@ -0,0 +1,70 @@ +=== Ansico CPT and Taxonomies === +Contributors: openai +Tags: custom post type, taxonomy, custom fields +Requires at least: 6.0 +Tested up to: 6.8 +Requires PHP: 7.4 +Stable tag: 0.0.0.4 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Create and manage custom post types, taxonomies, overrides, and simple custom fields from the WordPress admin. + +== Description == + +Ansico CPT and Taxonomies lets administrators: + +* Create custom post types +* Create taxonomies +* Define singular and plural labels +* Change slugs +* Choose whether a post type is excluded from search +* Choose whether a post type can be exported +* Enable support for title, editor, featured image, author, and comments +* Attach existing taxonomies, including categories and tags +* Add simple custom fields to managed post types +* Use text, textarea, number, URL, email, date, checkbox, select, and radio custom field types +* Add field descriptions and options for select/radio fields +* Use plugin-provided single and archive templates for managed content +* Import existing registered post types and taxonomies into the plugin as overrides +* Export and import plugin-managed data as JSON + +Version 0.0.0.3 improves the admin interface, adds more custom field types, and introduces JSON export/import tools. + +== Notes == + +* This is an early version intended as a practical foundation. +* Imported existing registrations are overridden through WordPress registration hooks rather than being re-created from scratch. +* Custom fields added through the plugin are simple meta box fields and not a full field framework. +* Template handling is performed through the template_include filter. +* JSON import can merge records by key or replace all plugin data. + +== Installation == + +1. Upload the plugin zip in WordPress. +2. Activate the plugin. +3. Visit Ansico CPT in the admin menu. +4. Create a new post type or taxonomy, or import an existing one for override. +5. Use the Tools page for JSON export and import. + +== Changelog == + += 0.0.0.3 = +* Added a more structured admin UI with dedicated Tools page. +* Added JSON export and import for managed records and overrides. +* Added more custom field types: select and radio. +* Added field descriptions and options for select/radio fields. +* Added normalization for stored plugin records. + += 0.0.0.2 = +* Added import and override support for existing post types. +* Added import and override support for existing taxonomies. +* Added separate lists for plugin-created and imported/overridden registrations. +* Improved internal data model for managed registrations. + += 0.0.0.1 = +* Initial release. + +== Author == +Andreas Andersen (Ansico) +https://ansico.dk diff --git a/ansico-cpt-and-taxonomies/readme.txt:Zone.Identifier b/ansico-cpt-and-taxonomies/readme.txt:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/readme.txt:Zone.Identifier differ diff --git a/ansico-cpt-and-taxonomies/templates/archive-cpt.php b/ansico-cpt-and-taxonomies/templates/archive-cpt.php new file mode 100644 index 0000000..9600466 --- /dev/null +++ b/ansico-cpt-and-taxonomies/templates/archive-cpt.php @@ -0,0 +1,45 @@ +get_cpt_template_settings( $post_type ) : array(); +$columns = max( 1, min( 4, (int) ( $settings['archive_columns'] ?? 3 ) ) ); +$min_card = 100 / $columns; +?> +
+ + + +
+ +
style="border:1px solid #ddd;border-radius:8px;padding:20px;"> + +
+ +

+ +

+ + +
+ +
+ +
+
+ +

+ +
+ diff --git a/ansico-cpt-and-taxonomies/templates/archive-cpt.php:Zone.Identifier b/ansico-cpt-and-taxonomies/templates/archive-cpt.php:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/templates/archive-cpt.php:Zone.Identifier differ diff --git a/ansico-cpt-and-taxonomies/templates/single-cpt.php b/ansico-cpt-and-taxonomies/templates/single-cpt.php new file mode 100644 index 0000000..9fed6c9 --- /dev/null +++ b/ansico-cpt-and-taxonomies/templates/single-cpt.php @@ -0,0 +1,79 @@ +get_cpt_template_settings( $post_type ) : array(); +?> +
+ +
> +
+

+ + + + +
+ +
+ +
+ +
+ + + + +
+

+ $taxonomy_obj ) : ?> + + +

labels->name ?? $taxonomy_name ); ?>: + +

+ + +
+ + + + +
+

+
    + $values ) : ?> + +
  • :
  • + +
+
+ +
+ +
+ diff --git a/ansico-cpt-and-taxonomies/templates/single-cpt.php:Zone.Identifier b/ansico-cpt-and-taxonomies/templates/single-cpt.php:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/templates/single-cpt.php:Zone.Identifier differ diff --git a/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php new file mode 100644 index 0000000..7a6c304 --- /dev/null +++ b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php @@ -0,0 +1,43 @@ +taxonomy ) ? ansico_cptax()->get_taxonomy_template_settings( $term->taxonomy ) : array(); +$columns = max( 1, min( 4, (int) ( $settings['archive_columns'] ?? 3 ) ) ); +?> +
+ + + +
+ +
style="border:1px solid #ddd;border-radius:8px;padding:20px;"> + +
+ +

+ +

+ + +
+ +
+ +
+
+ +

+ +
+ diff --git a/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php:Zone.Identifier b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php:Zone.Identifier new file mode 100644 index 0000000..d6c1ec6 Binary files /dev/null and b/ansico-cpt-and-taxonomies/templates/taxonomy-archive.php:Zone.Identifier differ