Inventory_Presser_Admin_Photo_Arranger
Source Source
File: includes/admin/class-admin-photo-arranger.php
class Inventory_Presser_Admin_Photo_Arranger { const CSS_CLASS = 'invp-rearrange'; /** * Adds hooks that power the feature. * * @return void */ public function add_hooks() { if ( ! self::is_enabled() ) { return; } // Make sure all attachment IDs are stored in the Gallery block when photos are attached, detached, or deleted. add_action( 'add_attachment', array( $this, 'add_attachment_to_gallery' ), 11, 1 ); add_action( 'delete_attachment', array( $this, 'delete_attachment_handler' ), 10, 2 ); add_action( 'wp_media_attach_action', array( $this, 'maintain_gallery_during_attach_and_detach' ), 10, 3 ); // When a vehicle is saved, the gallery should be examined and... // - change their post_parent values to the vehicle post ID // - make sure they have sequence numbers and VINs // - update all the sequence numbers to match the gallery order. add_action( 'edit_post_' . INVP::POST_TYPE, array( $this, 'change_parents_and_sequence' ), 10, 2 ); // - unattach photos that are no longer in the gallery add_action( 'edit_post_' . INVP::POST_TYPE, array( $this, 'unattach_when_removed' ), 10, 2 ); /** * When the vehicle is opened in the block editor, make sure the * gallery block is there waiting for the user. */ add_action( 'the_post', array( $this, 'create_gallery' ), 10, 1 ); } /** * add_attachment_to_gallery * * @param mixed $post_id * @param mixed $parent_id * @return void */ public function add_attachment_to_gallery( $post_id, $parent_id = null ) { $attachment = get_post( $post_id ); // Is this attachment an image? if ( ! wp_attachment_is_image( $attachment ) ) { // No. return; } // Is this new attachment even attached to a post? $parent; if ( ! empty( $attachment->post_parent ) ) { $parent = get_post( $attachment->post_parent ); } elseif ( ! empty( $parent_id ) ) { $parent = get_post( $parent_id ); } else { return; } // Is the new attachment attached to a vehicle? if ( empty( $parent->post_type ) || INVP::POST_TYPE !== $parent->post_type ) { // Parent post isn't a vehicle. return; } // Update the photo's post_parent. if ( empty( $attachment->post_parent ) || $attachment->post_parent !== $parent->ID ) { $attachment->post_parent = $parent->ID; $this->safe_update_post( $attachment ); } // Loop over all the post's blocks in search of our Gallery. $blocks = parse_blocks( $parent->post_content ); foreach ( $blocks as $index => &$block ) { // Is this a core gallery block? With a specific CSS class? if ( ! $this->is_gallery_block_with_specific_css_class( $block ) ) { continue; } // Does the block already have this attachment? if ( ! $this->inner_blocks_contains_id( $block, $post_id ) ) { // Add the uploaded attachment to this gallery. $block['innerBlocks'][] = array( 'blockName' => 'core/image', 'attrs' => array( 'id' => $post_id, 'sizeSlug' => 'large', 'linkDestination' => 'none', ), 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array( 0 => '', ), ); } $photo_count = count( $block['innerBlocks'] ); // Change a CSS class to reflect the number of photos in the Gallery // Replace all 'columns-#'. $block['innerContent'][0] = preg_replace( '/ columns-[0-9]+/', ' columns-' . $photo_count ?? 1, $block['innerContent'][0] ); // Do it again with extra parameter to replace just the first with a max of 'columns-3'. $block['innerContent'][0] = preg_replace( '/ columns-[0-9]+/', ' columns-' . min( 3, $photo_count ?? 4 ), $block['innerContent'][0], 1 ); if ( false === strpos( $block['attrs']['className'] ?? '', 'columns-' ) ) { $block['attrs']['className'] = sprintf( 'columns-%s %s', $photo_count ?? 1, $block['attrs']['className'] ); } else { $block['attrs']['className'] = preg_replace( '/columns-[0-9]+/', 'columns-' . $photo_count ?? 1, $block['attrs']['className'], 1 ); } // Add HTML that renders the image in the gallery // Is this image already in the Gallery HTML though? if ( false === mb_strpos( $block['innerContent'][0], "class=\"wp-image-$post_id\"" ) ) { // No. $position_list_end = mb_strpos( $block['innerContent'][0], "</figure>\r\n<!-- /wp:gallery -->" ); $new_html = sprintf( '<!-- wp:image {"id":%1$d,"sizeSlug":"large","linkDestination":"none"} --><figure class="wp-block-image size-large"><img src="%2$s" alt="" class="wp-image-%1$d"/></figure><!-- /wp:image -->', $post_id, $attachment->guid ); $block['innerContent'][0] = substr( $block['innerContent'][0], 0, $position_list_end ) . $new_html . substr( $block['innerContent'][0], ( $position_list_end ) ); } // Update the block in the $blocks array. $blocks[ $index ] = $block; // and then update the post. $parent->post_content = serialize_blocks( $blocks ); $this->safe_update_post( $parent ); break; } } /** * Runs on edit_post hook. Changes the post_parent values on photos in our * magic Gallery block. * * @param mixed $post_id * @param mixed $post * @return void */ public function change_parents_and_sequence( $post_id, $post ) { // If this post was just trashed, who cares. if ( ! empty( $post->post_status ) && 'trash' === $post->post_status ) { return; } // Make sure the photos in the gallery block have the post_parent value. $block = $this->find_gallery_block( $post ); if ( false === $block ) { return; } if ( empty( $block['innerBlocks'] ) ) { // There are no photos in the gallery. return; } // Don't renumber right after we renumber. remove_action( 'save_post_' . INVP::POST_TYPE, array( 'Inventory_Presser_Photo_Numberer', 'renumber_photos' ), 10, 1 ); // Set a post_parent value on every photo in the gallery block. foreach ( $block['innerBlocks'] as $index => $image_block ) { if ( empty( $image_block['blockName'] ) || 'core/image' !== $image_block['blockName'] ) { continue; } if ( empty( $image_block['attrs']['id'] ) ) { continue; } $attachment = get_post( $image_block['attrs']['id'] ); if ( ! empty( $attachment ) ) { // Update the photo's post_parent. if ( empty( $attachment->post_parent ) || $attachment->post_parent !== $post_id ) { $attachment->post_parent = $post_id; $this->safe_update_post( $attachment ); } // Does the number match? if ( strval( $index + 1 ) !== INVP::get_meta( 'photo_number', $attachment->ID ) ) { // Save the VIN in the photo meta. Inventory_Presser_Photo_Numberer::save_meta_vin( $attachment->ID, $attachment->post_parent ); // Save a md5 hash checksum of the attachment in meta. Inventory_Presser_Photo_Numberer::save_meta_hash( $attachment->ID ); // Force save the sequence number. Inventory_Presser_Photo_Numberer::save_meta_photo_number( $attachment->ID, $post_id, strval( $index + 1 ) ); } } } Inventory_Presser_Photo_Numberer::delete_photo_transients( $post_id ); } /** * Makes sure the Gallery Block is waiting for the user when they open a * vehicle post in the block editor. * * @param WP_Post $post * @return void */ public function create_gallery( $post ) { global $pagenow; if ( ! is_admin() || get_post_type() !== INVP::POST_TYPE || 'post-new.php' !== $pagenow ) { return; } // Does the post content contain a Gallery with a specific CSS class? if ( false !== $this->find_gallery_block( $post ) ) { // Yes. return; } $blocks = parse_blocks( $post->post_content ); $blocks[] = array( 'blockName' => 'core/gallery', 'attrs' => array( 'linkTo' => 'none', 'className' => self::CSS_CLASS, ), 'innerBlocks' => array(), 'innerHTML' => '', 'innerContent' => array( 0 => '<figure class="wp-block-gallery has-nested-images columns-default is-cropped columns-0 ' . self::CSS_CLASS . '"></figure>', ), ); $post->post_content = serialize_blocks( $blocks ); $this->safe_update_post( $post ); // Add all this vehicle's photos. // $posts = get_children( // array( // 'meta_key' => apply_filters( 'invp_prefix_meta_key', 'photo_number' ), // 'order' => 'ASC', // 'orderby' => 'meta_value_num', // 'post_parent' => $post->ID, // 'post_type' => 'attachment', // ) // ); // foreach ( $posts as $post ) { // $this->add_attachment_to_gallery( $post->ID, $post->post_parent ); // } } /** * delete_attachment_handler * * @param mixed $post_id * @param mixed $post * @return void */ public function delete_attachment_handler( $post_id, $post ) { $attachment = get_post( $post_id ); if ( empty( $attachment->post_parent ) ) { return; } $this->remove_attachment_from_gallery( $post_id, $attachment->post_parent ); } /** * Look at the blocks in a given post for a gallery block with a specific * CSS class. * * @param WP_Post $post A post to examine for our gallery block. * @return array|false */ protected function find_gallery_block( $post ) { if ( is_int( $post ) ) { $post = get_post( $post ); } $blocks = parse_blocks( $post->post_content ); foreach ( $blocks as $block ) { // Is this a core gallery block? With a specific CSS class? if ( ! $this->is_gallery_block_with_specific_css_class( $block ) ) { continue; } return $block; } return false; } /** * Is the photo arranger via a Gallery Block feature enabled? * * @return bool */ public static function is_enabled() { if ( ! class_exists( 'INVP' ) ) { return false; } return INVP::settings()['use_arranger_gallery'] ?? false; } /** * is_gallery_block_with_specific_css_class * * @param mixed $block * @return bool */ protected function is_gallery_block_with_specific_css_class( $block ) { return ! empty( $block['blockName'] ) && 'core/gallery' === $block['blockName'] && ! empty( $block['attrs']['className'] ) && false !== mb_strpos( $block['attrs']['className'], self::CSS_CLASS ); } /** * maintain_gallery_during_attach_and_detach * * @param mixed $action * @param mixed $attachment_id * @param mixed $parent_id * @return void */ public function maintain_gallery_during_attach_and_detach( $action, $attachment_id, $parent_id ) { $parent_id = intval( $parent_id ); if ( 'detach' === $action ) { $this->remove_attachment_from_gallery( $attachment_id, $parent_id ); // Is this photo set as the Featured Image? if ( (int) get_post_meta( $parent_id, '_thumbnail_id', true ) === $attachment_id ) { // Yes. Remove it. delete_post_thumbnail( $parent_id ); } // Remove vehicle-specific meta values. delete_post_meta( $attachment_id, apply_filters( 'invp_prefix_meta_key', 'photo_number' ) ); delete_post_meta( $attachment_id, apply_filters( 'invp_prefix_meta_key', 'vin' ) ); Inventory_Presser_Photo_Numberer::renumber_photos( $parent_id ); return; } $this->add_attachment_to_gallery( $attachment_id, $parent_id ); } /** * remove_attachment_from_gallery * * @param mixed $post_id * @param mixed $parent_id * @return void */ protected function remove_attachment_from_gallery( $post_id, $parent_id ) { $attachment = get_post( $post_id ); // Is this attachment an image? if ( ! wp_attachment_is_image( $attachment ) ) { // No. return; } // Is this new attachment even attached to a post? $parent = get_post( $parent_id ); // Is the new attachment attached to a vehicle? if ( empty( $parent->post_type ) || INVP::POST_TYPE !== $parent->post_type ) { // Parent post isn't a vehicle. return; } // Does the post content of the vehicle have a Gallery with a specific CSS class? $blocks = parse_blocks( $parent->post_content ); foreach ( $blocks as $index => $block ) { // Is this a core gallery block? With a specific CSS class? if ( ! $this->is_gallery_block_with_specific_css_class( $block ) ) { continue; } // Does the block already have this attachment ID? Remove it. if ( ! empty( $block['innerBlocks'] ) && $this->inner_blocks_contains_id( $block, $post_id ) ) { $inner_block_count = count( $block['innerBlocks'] ); for ( $b = 0; $b < $inner_block_count; $b++ ) { if ( $post_id === $block['innerBlocks'][ $b ]['attrs']['id'] ) { unset( $block['innerBlocks'][ $b ] ); $block['innerBlocks'] = array_values( $block['innerBlocks'] ); break; } } } if ( null !== $block['innerContent'][0] ) { // Change a CSS class to reflect the number of photos in the Gallery. $block['innerContent'][0] = preg_replace( '/ columns-[0-9]+/', ' columns-' . count( $block['innerBlocks'] ) ?? 0, $block['innerContent'][0], 2 ); // Remove a list item HTML that renders the image in the gallery. $pattern = sprintf( '/<!-- wp:image {"id":%1$d,"sizeSlug":"large","linkDestination":"none"} -->' . "[\r\n]*" . '<figure class="wp-block-image size-large"><img src="[^"]+" alt="" class="wp-image-%1$d"\/><\/figure>' . "[\r\n]*" . '<!-- /wp:image -->/', $post_id ); $block['innerContent'][0] = preg_replace( $pattern, '', $block['innerContent'][0] ); } // Update the block in the $blocks array. $blocks[ $index ] = $block; // and then update the post. $parent->post_content = serialize_blocks( $blocks ); $this->safe_update_post( $parent ); break; } } /** * Removes our `edit_post_{post-type}` hooks, calls `wp_update_post()`, and * re-adds the hooks. * * @param WP_Post $post The post to update. * @return void */ protected function safe_update_post( $post ) { // Do not allow inserts! if ( 0 === $post->ID ) { return; } // Don't cause hooks to fire themselves. remove_action( 'edit_post_' . INVP::POST_TYPE, array( $this, 'change_parents_and_sequence' ), 10, 2 ); remove_action( 'edit_post_' . INVP::POST_TYPE, array( $this, 'unattach_when_removed' ), 10, 2 ); wp_update_post( $post ); // Re-add the hooks now that we're done making changes. add_action( 'edit_post_' . INVP::POST_TYPE, array( $this, 'change_parents_and_sequence' ), 10, 2 ); add_action( 'edit_post_' . INVP::POST_TYPE, array( $this, 'unattach_when_removed' ), 10, 2 ); } /** * unattach_when_removed * * @param mixed $post_id * @param mixed $post * @return void */ public function unattach_when_removed( $post_id, $post ) { // If this post was just trashed, who cares. if ( ! empty( $post->post_status ) && 'trash' === $post->post_status ) { return; } // Does the post have our gallery block? $block = $this->find_gallery_block( $post ); if ( false === $block ) { return; } /** * Are there attachments to this post that are no longer in the * gallery block? */ $attachment_ids = get_children( array( 'fields' => 'ids', 'post_parent' => $post_id, 'post_type' => 'attachment', 'posts_per_page' => 500, ) ); foreach ( $attachment_ids as $attachment_id ) { if ( $this->inner_blocks_contains_id( $block, $attachment_id ) ) { continue; } // Detach those from this vehicle. $attachment = get_post( $attachment_id ); $attachment->post_parent = 0; $this->safe_update_post( $attachment ); } } /** * inner_blocks_contains_id * * @param mixed $block A core/gallery block output by parse_blocks(). * @param mixed $id The attachment ID to search for. * @return bool */ protected function inner_blocks_contains_id( $block, $id ) { if ( empty( $block['innerBlocks'] ) ) { return false; } foreach ( $block['innerBlocks'] as $inner_block ) { if ( empty( $inner_block['attrs']['id'] ) || $id !== $inner_block['attrs']['id'] ) { continue; } return true; } return false; } }
Expand full source codeCollapse full source codeView on Github
Methods Methods
- add_attachment_to_gallery — add_attachment_to_gallery
- add_hooks — Adds hooks that power the feature.
- change_parents_and_sequence — Runs on edit_post hook. Changes the post_parent values on photos in our magic Gallery block.
- create_gallery — Makes sure the Gallery Block is waiting for the user when they open a vehicle post in the block editor.
- delete_attachment_handler — delete_attachment_handler
- find_gallery_block — Look at the blocks in a given post for a gallery block with a specific CSS class.
- inner_blocks_contains_id — inner_blocks_contains_id
- is_enabled — Is the photo arranger via a Gallery Block feature enabled?
- is_gallery_block_with_specific_css_class — is_gallery_block_with_specific_css_class
- maintain_gallery_during_attach_and_detach — maintain_gallery_during_attach_and_detach
- remove_attachment_from_gallery — remove_attachment_from_gallery
- safe_update_post — Removes our `edit_post_{post-type}` hooks, calls `wp_update_post()`, and re-adds the hooks.
- unattach_when_removed — unattach_when_removed