From 319afb38244e2b21d261ac82dbdb5552e3f01b1d Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Tue, 21 Jul 2026 13:08:32 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20Public=20JS=20API=20=E2=80=94=20wind?= =?UTF-8?q?ow.cimo.optimizeFiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/admin/js/index.js | 1 + src/admin/js/media-manager/drop-zone.js | 39 +------ src/admin/js/media-manager/select-files.js | 39 +------ src/admin/js/optimize-files.js | 128 +++++++++++++++++++++ src/admin/js/public-api.js | 7 ++ 5 files changed, 140 insertions(+), 74 deletions(-) create mode 100644 src/admin/js/optimize-files.js create mode 100644 src/admin/js/public-api.js diff --git a/src/admin/js/index.js b/src/admin/js/index.js index fb75ac4..05d3c4c 100644 --- a/src/admin/js/index.js +++ b/src/admin/js/index.js @@ -1,3 +1,4 @@ +import './public-api' import './media-manager/drop-zone' import './media-manager/select-files' import './media-manager/sidebar-info' diff --git a/src/admin/js/media-manager/drop-zone.js b/src/admin/js/media-manager/drop-zone.js index deee381..3883514 100644 --- a/src/admin/js/media-manager/drop-zone.js +++ b/src/admin/js/media-manager/drop-zone.js @@ -8,9 +8,7 @@ import { domReady } from '~cimo/shared/dom-ready' import { getFileConverter, requiresFileConversion } from '~cimo/shared/converters' import { watchForEditorIframe } from '~cimo/shared/util' -import { saveMetadata } from '~cimo/shared/metadata-saver' -import { cacheConverterNotice } from '~cimo/shared/upload-notice-cache' -import { ProgressModal } from './progress-modal' +import { optimizeFileConverters } from '../optimize-files' import { applyFilters } from '@wordpress/hooks' /** @@ -99,39 +97,12 @@ function addDropZoneListenerToMediaManager( targetDocument ) { uploaderWindow.style.display = 'none' } - // Cancel the conversion if the user closes the progress modal. - const onCancel = () => { - // This is enough to cancel the entire process since the promises below will finish resolving. - fileConverters.forEach( converter => converter.cancel() ) - } - - // Show the progress modal - const progressModal = new ProgressModal( fileConverters, onCancel ) - progressModal.open() - // Process and optimize each media file here, // e.g. converting to webp, resizing, compressing, etc. - const optimizedResults = await Promise.all( - fileConverters.map( async converter => { - try { - const result = await converter.optimize() - cacheConverterNotice( result ) - if ( result.error ) { - // eslint-disable-next-line no-console - console.warn( result.error ) - } - return result - } catch ( error ) { - // eslint-disable-next-line no-console - console.warn( error ) - return { file: converter.file, metadata: null } - } - } ) - ) + const optimizedResults = await optimizeFileConverters( fileConverters ) // Extract files and metadata from results const optimizedFiles = optimizedResults.map( result => result.file ) - const conversionMetadata = optimizedResults.map( result => result.metadata ) // Create a DataTransfer to hold the optimized file const dataTransfer = new DataTransfer() @@ -139,9 +110,6 @@ function addDropZoneListenerToMediaManager( targetDocument ) { dataTransfer.items.add( file ) } ) - // Save the metadata to the server. - await saveMetadata( conversionMetadata ) - // Find the correct target to dispatch the event to let target = event.target.closest( '.components-drop-zone, [data-is-drop-zone="true"]' ) || event.target // This specifically handles the WooCommerce/classic description editor @@ -218,9 +186,6 @@ function addDropZoneListenerToMediaManager( targetDocument ) { target.dispatchEvent( dropEvent ) } } - - // Close when optimization finishes, including when we fall back to the original file after an error. - progressModal.close() } // Add our custom drop listener diff --git a/src/admin/js/media-manager/select-files.js b/src/admin/js/media-manager/select-files.js index cf5db4e..17d4acd 100644 --- a/src/admin/js/media-manager/select-files.js +++ b/src/admin/js/media-manager/select-files.js @@ -8,9 +8,7 @@ import { domReady } from '~cimo/shared/dom-ready' import { getFileConverter, requiresFileConversion } from '~cimo/shared/converters' import { watchForEditorIframe } from '~cimo/shared/util' -import { saveMetadata } from '~cimo/shared/metadata-saver' -import { cacheConverterNotice } from '~cimo/shared/upload-notice-cache' -import { ProgressModal } from './progress-modal' +import { optimizeFileConverters } from '../optimize-files' import { applyFilters } from '@wordpress/hooks' // Allowed locations to be able to select files. @@ -77,39 +75,12 @@ function addSelectFilesListenerToFileUploads( targetDocument ) { event.stopPropagation() event.stopImmediatePropagation() - // Cancel the conversion if the user closes the progress modal. - const onCancel = () => { - // This is enough to cancel the entire process since the promises below will finish resolving. - fileConverters.forEach( converter => converter.cancel() ) - } - - // Show the progress modal - const progressModal = new ProgressModal( fileConverters, onCancel ) - progressModal.open() - // Process and optimize each media file here, // e.g. converting to webp, resizing, compressing, etc. - const optimizedResults = await Promise.all( - fileConverters.map( async converter => { - try { - const result = await converter.optimize() - cacheConverterNotice( result ) - if ( result.error ) { - // eslint-disable-next-line no-console - console.warn( result.error ) - } - return result - } catch ( error ) { - // eslint-disable-next-line no-console - console.warn( error ) - return { file: converter.file, metadata: null } - } - } ) - ) + const optimizedResults = await optimizeFileConverters( fileConverters ) // Extract files from results const optimizedFiles = optimizedResults.map( result => result.file ) - const conversionMetadata = optimizedResults.map( result => result.metadata ) // Create a DataTransfer to hold the optimized file const dataTransfer = new DataTransfer() @@ -117,9 +88,6 @@ function addSelectFilesListenerToFileUploads( targetDocument ) { dataTransfer.items.add( file ) } ) - // Save the metadata to the server. - await saveMetadata( conversionMetadata ) - // Assign the files to the input element event.target.files = dataTransfer.files @@ -128,9 +96,6 @@ function addSelectFilesListenerToFileUploads( targetDocument ) { // Mark this event so we know conversion is already done changeEvent.__cimo_converted = true // eslint-disable-line camelcase event.target.dispatchEvent( changeEvent ) - - // Close when optimization finishes, including when we fall back to the original file after an error. - progressModal.close() } if ( ! targetDocument.body.__cimo_selectfiles_listener_attached ) { diff --git a/src/admin/js/optimize-files.js b/src/admin/js/optimize-files.js new file mode 100644 index 0000000..aabd013 --- /dev/null +++ b/src/admin/js/optimize-files.js @@ -0,0 +1,128 @@ +import { getFileConverter, requiresFileConversion } from '~cimo/shared/converters' +import { saveMetadata } from '~cimo/shared/metadata-saver' +import { cacheConverterNotice } from '~cimo/shared/upload-notice-cache' +import { ProgressModal } from './media-manager/progress-modal' + +/** + * Normalize the public API input shape. Integrators may pass a single File, + * FileList, DataTransfer file list, or plain array of File objects. + * + * @param {File|FileList|Array} files Files to optimize. + * @return {Array} Normalized file array. + */ +const normalizeFiles = files => { + if ( ! files ) { + return [] + } + + if ( typeof files.length === 'number' ) { + return Array.from( files ) + } + + return [ files ] +} + +/** + * Optimize an existing converter list and save Cimo metadata before the + * caller's upload flow continues. + * + * Interceptors use this after they have already checked whether conversion is + * required. The public API usually calls optimizeFiles() instead. + * + * @param {Array} fileConverters Converter instances. + * @param {Object} options Options for the optimization run. + * @param {boolean} [options.showProgress=true] Whether to show Cimo's progress modal. + * @return {Promise>} Optimized files and metadata. + */ +export const optimizeFileConverters = async ( fileConverters, options = {} ) => { + const converters = Array.isArray( fileConverters ) ? fileConverters : [] + + if ( window.cimoSettings?.disableOptimization ) { + return converters.map( converter => ( { + file: converter.file, + metadata: null, + } ) ) + } + + const showProgress = options.showProgress !== false + + // Cancel the conversion if the user closes the progress modal. + const onCancel = () => { + // This is enough to cancel the entire process since the promises below will finish resolving. + converters.forEach( converter => converter.cancel() ) + } + + // Show the progress modal. + const progressModal = showProgress + ? new ProgressModal( converters, onCancel ) + : null + + progressModal?.open() + + try { + // Process and optimize each media file here, + // e.g. converting to webp, resizing, compressing, etc. + const optimizedResults = await Promise.all( + converters.map( async converter => { + try { + const result = await converter.optimize() + cacheConverterNotice( result ) + if ( result.error ) { + // eslint-disable-next-line no-console + console.warn( result.error ) + } + return result + } catch ( error ) { + // eslint-disable-next-line no-console + console.warn( error ) + return { file: converter.file, metadata: null } + } + } ) + ) + + // Save the metadata to the server. + await saveMetadata( optimizedResults.map( result => result.metadata ?? null ) ) + + return optimizedResults.map( result => ( { + file: result.file, + metadata: result.metadata ?? null, + } ) ) + } finally { + // Close when optimization finishes, including when we fall back to the original file after an error. + progressModal?.close() + } +} + +/** + * Public optimization entry point exposed as window.cimo.optimizeFiles(). + * + * Cimo only handles pre-upload optimization here. The integrator remains + * responsible for uploading results.map( result => result.file ) through their + * normal upload flow. + * + * @param {File|FileList|Array} files Files to optimize before upload. + * @param {Object} options Options for the optimization run. + * @param {boolean} [options.showProgress=true] Whether to show Cimo's progress modal. + * @return {Promise>} Files for the caller to upload. + */ +export const optimizeFiles = async ( files, options = {} ) => { + if ( window.cimoSettings?.disableOptimization ) { + return normalizeFiles( files ).map( file => ( { + file, + metadata: null, + } ) ) + } + + // Use Cimo's shared converter resolver so free and premium converters follow + // the same path as Media Library uploads. + const fileConverters = normalizeFiles( files ).map( file => getFileConverter( file ) ) + + if ( ! requiresFileConversion( fileConverters ) ) { + return fileConverters.map( converter => ( { + file: converter.file, + metadata: null, + } ) ) + } + + return optimizeFileConverters( fileConverters, options ) +} diff --git a/src/admin/js/public-api.js b/src/admin/js/public-api.js new file mode 100644 index 0000000..a9efd38 --- /dev/null +++ b/src/admin/js/public-api.js @@ -0,0 +1,7 @@ +import { optimizeFiles } from './optimize-files' + +window.cimo = window.cimo || {} + +// Stable public API for plugins/themes that need pre-upload optimization. +// Cimo returns optimized File objects. +window.cimo.optimizeFiles = optimizeFiles From da41fb784797f1687d779437a88aa546b9a147b9 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Tue, 21 Jul 2026 15:07:45 +0800 Subject: [PATCH 2/5] fix: PHP filters for upload selectors --- src/admin/class-script-loader.php | 64 +++++++++++++++++++ .../js/media-manager/allowed-locations.js | 56 ++++++++++++++++ src/admin/js/media-manager/drop-zone.js | 25 +------- src/admin/js/media-manager/select-files.js | 16 +---- 4 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 src/admin/js/media-manager/allowed-locations.js diff --git a/src/admin/class-script-loader.php b/src/admin/class-script-loader.php index 76e80ed..9418fed 100644 --- a/src/admin/class-script-loader.php +++ b/src/admin/class-script-loader.php @@ -48,6 +48,67 @@ public function maybe_enqueue_for_bricks_builder() { } } + /** + * Get selectors where Cimo should intercept file input selections. + * + * @return array + */ + public static function get_select_files_allowed_locations() { + $locations = [ + '.components-form-file-upload', // Allow uploads to the image block. + '.media-frame', // Allow uploads from the Media Manager. + '.media-upload-form', // Allow uploads from the admin Media > Add Media File. + '.moxie-shim', // Allow uploads from the admin Media > Library grid view. + ]; + + // Public integration point for plugins/themes that want Cimo to auto-intercept + // file inputs inside their own upload UI. + return self::sanitize_allowed_locations( + apply_filters( 'cimo/select_files/allowed_locations', $locations ) + ); + } + + /** + * Get selectors where Cimo should intercept file drops. + * + * @return array + */ + public static function get_drop_zone_allowed_locations() { + $locations = [ + '.media-frame-uploader', // Allowed to drop in the Media Manager. + '.media-upload-form', // Allowed to drop in the admin Media > Add Media File. + '.editor-post-featured-image', // Allowed to drop in the featured image drop zone. + '.editor-styles-wrapper', // Allowed to drop in the block editor when adding new image blocks. + '.uploader-window', // Allowed to drop in the admin Media > Library grid view. + '.uploader-editor', // Allowed to drop in the WooCommerce description editor. + '.block-library-utils__media-control', // Allowed to drop in the block editor image block media control. + ]; + + // Public integration point for plugins/themes that want Cimo to auto-intercept + // dropped files inside their own upload UI. + return self::sanitize_allowed_locations( + apply_filters( 'cimo/drop_zone/allowed_locations', $locations ) + ); + } + + /** + * Keep localized selector settings as a clean list of strings. + * + * @param mixed $locations Selectors from defaults and filters. + * @return array + */ + private static function sanitize_allowed_locations( $locations ) { + if ( ! is_array( $locations ) ) { + return []; + } + + $locations = array_filter( $locations, 'is_string' ); + $locations = array_map( 'trim', $locations ); + $locations = array_filter( $locations ); + + return array_values( array_unique( $locations ) ); + } + public static function enqueue_cimo_assets() { // If cimo-script is already enqueued, don't enqueue again. if ( wp_script_is( 'cimo-script', 'enqueued' ) ) { @@ -112,6 +173,9 @@ public static function enqueue_cimo_assets() { 'svgOptimizationEnabled' => isset( $settings['svg_optimization_enabled'] ) ? (int) $settings['svg_optimization_enabled'] : 1, 'stealthModeEnabled' => isset( $settings['stealth_mode_enabled'] ) ? (int) $settings['stealth_mode_enabled'] : 0, 'wpScalingThreshold' => $threshold, + // Public PHP selector filters are localized for the browser-side interceptors. + 'selectFilesAllowedLocations' => self::get_select_files_allowed_locations(), + 'dropZoneAllowedLocations' => self::get_drop_zone_allowed_locations(), ] ); diff --git a/src/admin/js/media-manager/allowed-locations.js b/src/admin/js/media-manager/allowed-locations.js new file mode 100644 index 0000000..d0c839f --- /dev/null +++ b/src/admin/js/media-manager/allowed-locations.js @@ -0,0 +1,56 @@ +import { applyFilters } from '@wordpress/hooks' + +const normalizeLocations = locations => { + if ( ! Array.isArray( locations ) ) { + return [] + } + + return locations.filter( location => typeof location === 'string' && location.length > 0 ) +} + +/** + * Get current select file selectors from PHP settings and internal JS filters. + * + * @return {Array} Selectors where file input changes should be intercepted. + */ +export const getSelectFilesAllowedLocations = () => { + const locations = normalizeLocations( window.cimoSettings?.selectFilesAllowedLocations ) + + return normalizeLocations( applyFilters( 'cimo.selectFiles.allowedLocations', locations ) ) +} + +/** + * Get current drop zone selectors from PHP settings and internal JS filters. + * + * @return {Array} Selectors where file drops should be intercepted. + */ +export const getDropZoneAllowedLocations = () => { + const locations = normalizeLocations( window.cimoSettings?.dropZoneAllowedLocations ) + + return normalizeLocations( applyFilters( 'cimo.dropZone.allowedLocations', locations ) ) +} + +/** + * Find the closest ancestor matching one of Cimo's allowed upload locations. + * + * @param {Element} element Element where the upload event started. + * @param {Array} locations CSS selectors to test. + * @return {Element|null} Matching element, if one exists. + */ +export const closestAllowedLocation = ( element, locations ) => { + for ( const location of locations ) { + try { + // Invalid selectors should not break uploads; ignore them and keep checking. + const matchedElement = element.closest( location ) + + if ( matchedElement ) { + return matchedElement + } + } catch ( error ) { + // eslint-disable-next-line no-console + console.warn( `[Cimo] Ignoring invalid selector: ${ location }`, error ) + } + } + + return null +} diff --git a/src/admin/js/media-manager/drop-zone.js b/src/admin/js/media-manager/drop-zone.js index 3883514..46af5b5 100644 --- a/src/admin/js/media-manager/drop-zone.js +++ b/src/admin/js/media-manager/drop-zone.js @@ -9,24 +9,13 @@ import { domReady } from '~cimo/shared/dom-ready' import { getFileConverter, requiresFileConversion } from '~cimo/shared/converters' import { watchForEditorIframe } from '~cimo/shared/util' import { optimizeFileConverters } from '../optimize-files' -import { applyFilters } from '@wordpress/hooks' +import { closestAllowedLocation, getDropZoneAllowedLocations } from './allowed-locations' /** * Intercept editor media uploads and convert images to WebP on the client * before uploading to WordPress. This affects the block editor only. */ -// Allowed locations to be able to select files. -const ALLOWED_LOCATIONS = applyFilters( 'cimo.dropZone.allowedLocations', [ - '.media-frame-uploader', // Allowed to drop in the Media Manager - '.media-upload-form', // Allowed to drop in the admin Media > Add Media File - '.editor-post-featured-image', // Allowed to drop in the featured image drop zone - '.editor-styles-wrapper', // Allowed to drop in the block editor when adding new image blocks - '.uploader-window', // Allowed to drop in the admin Media > Library grid view - '.uploader-editor', // Allowed to drop in the WooCommerce description editor - '.block-library-utils__media-control', // Allowed to drop in the block editor image block media control -] ) - // Add event listener to the Media Manager's drop zone function addDropZoneListenerToMediaManager( targetDocument ) { if ( ! targetDocument ) { @@ -57,18 +46,8 @@ function addDropZoneListenerToMediaManager( targetDocument ) { return } - // TODO: We also want to filter out the target so we can set when this - // is triggered. We might break other funcitonality that we don't have - // the conversion to happen. - // Find the matched element based on the locations - let matchedElement - for ( const location of ALLOWED_LOCATIONS ) { - matchedElement = event.target.closest( location ) - if ( matchedElement ) { - break - } - } + let matchedElement = closestAllowedLocation( event.target, getDropZoneAllowedLocations() ) // Allowed a fallback to drop in the Media Manager matchedElement = matchedElement || event.target.closest( '.supports-drag-drop' ) diff --git a/src/admin/js/media-manager/select-files.js b/src/admin/js/media-manager/select-files.js index 17d4acd..b3a5d45 100644 --- a/src/admin/js/media-manager/select-files.js +++ b/src/admin/js/media-manager/select-files.js @@ -9,15 +9,7 @@ import { domReady } from '~cimo/shared/dom-ready' import { getFileConverter, requiresFileConversion } from '~cimo/shared/converters' import { watchForEditorIframe } from '~cimo/shared/util' import { optimizeFileConverters } from '../optimize-files' -import { applyFilters } from '@wordpress/hooks' - -// Allowed locations to be able to select files. -const ALLOWED_LOCATIONS = applyFilters( 'cimo.selectFiles.allowedLocations', [ - '.components-form-file-upload', // Allow uploads to the image block - '.media-frame', // Allow uploads from the Media Manager - '.media-upload-form', // Allow uploads from the admin Media > Add Media File - '.moxie-shim', // Allow uploads from the admin Media > Library grid view -] ) +import { closestAllowedLocation, getSelectFilesAllowedLocations } from './allowed-locations' // Add event listener to the Media Manager's drop zone function addSelectFilesListenerToFileUploads( targetDocument ) { @@ -54,12 +46,8 @@ function addSelectFilesListenerToFileUploads( targetDocument ) { return } - // TODO: We need filter this so that we will only override this on file - // selects that we want to, like the media manager picker or the image - // block uploader. // Allow these locations to be able to select files. - const isAllowed = ALLOWED_LOCATIONS.some( selector => event.target.closest( selector ) ) - if ( ! isAllowed ) { + if ( ! closestAllowedLocation( event.target, getSelectFilesAllowedLocations() ) ) { return } From 0185b3dce1d2a6acfa4b8dd86b59e6407a756b56 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Tue, 21 Jul 2026 15:52:06 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20Public=20enqueue=20helper=20?= =?UTF-8?q?=E2=80=94=20cimo=5Fenqueue=5Fassets()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cimo.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cimo.php b/cimo.php index 9e6f403..da58cb0 100644 --- a/cimo.php +++ b/cimo.php @@ -18,10 +18,22 @@ } defined( 'CIMO_FILE' ) || define( 'CIMO_FILE', __FILE__ ); -defined( 'CIMO_BUILD' ) || define( 'CIMO_BUILD', 'free' ); +defined( 'CIMO_BUILD' ) || define( 'CIMO_BUILD', 'premium' ); defined( 'CIMO_SETTINGS_SLUG' ) || define( 'CIMO_SETTINGS_SLUG', 'cimo-settings' ); require_once __DIR__ . '/src/admin/class-script-loader.php'; + +if ( ! function_exists( 'cimo_enqueue_assets' ) ) { + /** + * Public helper for integrators to enqueue Cimo on custom upload screens. + */ + function cimo_enqueue_assets() { + if ( class_exists( 'Cimo_Script_Loader' ) ) { + Cimo_Script_Loader::enqueue_cimo_assets(); + } + } +} + require_once __DIR__ . '/src/admin/class-meta-box.php'; require_once __DIR__ . '/src/admin/class-metadata.php'; require_once __DIR__ . '/src/admin/class-admin-notices.php'; @@ -44,4 +56,4 @@ function cimo_activate() { if ( file_exists( plugin_dir_path( __FILE__ ) . 'pro__premium_only/index.php' ) ) { require_once( plugin_dir_path( __FILE__ ) . 'pro__premium_only/index.php' ); } -} \ No newline at end of file +} From 89e4b77a70594a5ec659d6f41bb73e5f511074d1 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Tue, 21 Jul 2026 16:03:56 +0800 Subject: [PATCH 4/5] chore: developer docs --- DEVELOPER.md | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 DEVELOPER.md diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 0000000..369378e --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,97 @@ +# Cimo Developer Integration + +Cimo can optimize media in the browser before your plugin or theme uploads it. + +## Enqueue Cimo + +Admin screens and the block editor already enqueue Cimo. For frontend upload +forms or custom screens where Cimo is not already loaded, call +`cimo_enqueue_assets()`. + +```php +add_action( 'wp_enqueue_scripts', function () { + if ( is_page( 'my-upload-form' ) && function_exists( 'cimo_enqueue_assets' ) ) { + cimo_enqueue_assets(); + } +} ); +``` + +## Optimize Files Directly + +Use `window.cimo.optimizeFiles()` when your code already controls the upload +process and can replace the selected files before uploading. + +```js +const results = await window.cimo.optimizeFiles( files, { showProgress: true } ) + +const filesToUpload = results.map( result => result.file ) +``` + +The API accepts a single `File`, a `FileList`, or an array of `File` objects. +It returns: + +```js +[ + { + file: File, + metadata: Object || null, + }, +] +``` + +`showProgress` defaults to `true`. Set it to `false` if your UI already shows +upload or optimization progress. + +If Cimo optimization is disabled or a file type is unsupported, the original +file is returned with `metadata: null`. + +## Automatic Selector Interception + +Use PHP selector filters when your markup uses normal file inputs or drop zones +and you want Cimo to intercept them automatically before your existing upload +handler runs. + +```php +add_filter( 'cimo/select_files/allowed_locations', function ( $locations ) { + $locations[] = '.my-plugin-uploader'; + return $locations; +} ); + +add_filter( 'cimo/drop_zone/allowed_locations', function ( $locations ) { + $locations[] = '.my-plugin-dropzone'; + return $locations; +} ); +``` + +For file inputs, register a selector for a wrapper around +``. + +```html +
+ +
+``` + +For drops, register a selector for the drop target. + +## Which Approach To Use + +Use `window.cimo.optimizeFiles()` when you can explicitly await optimization +before calling your uploader. + +Use PHP selector interception when your existing UI already reacts to file input +or drop events and you want Cimo to transparently replace files before that flow +continues. + +## Free And Premium Behavior + +The same JavaScript API is used in free and premium. If Cimo Premium is loaded, +premium converters are applied automatically through Cimo's normal converter +pipeline. There is no separate premium entry point. + +Cimo optimization is pre-upload only. It does not optimize by attachment ID, +bulk replace existing files, or run server-side Imagick/GD compression. + +On guest frontend uploads, files can still be optimized in the browser. Metadata +saving may be skipped when the visitor is not logged in or cannot access the +Cimo metadata endpoint. From cff199504a99eb442b853d3eaaba229720e5ce76 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Wed, 22 Jul 2026 10:55:02 +0800 Subject: [PATCH 5/5] chore: dev docs and clearer comment --- DEVELOPER.md | 73 +++++++++++++++++++++----------------- src/admin/js/public-api.js | 2 +- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/DEVELOPER.md b/DEVELOPER.md index 369378e..b0ef602 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -2,11 +2,45 @@ Cimo can optimize media in the browser before your plugin or theme uploads it. +## Automatic Selector Interception + +Use PHP selector filters when your markup uses normal file inputs or drop zones +and you want Cimo to intercept them automatically before your existing upload +handler runs. + +Register selector filters before calling `cimo_enqueue_assets()`. +`cimo_enqueue_assets()` localizes the filtered selector lists while enqueueing, +so filters added afterward will not apply to the current page load. + +```php +add_filter( 'cimo/select_files/allowed_locations', function ( $locations ) { + $locations[] = '.my-plugin-uploader'; + return $locations; +} ); + +add_filter( 'cimo/drop_zone/allowed_locations', function ( $locations ) { + $locations[] = '.my-plugin-dropzone'; + return $locations; +} ); +``` + +For file inputs, register a selector for a wrapper around +``. + +```html +
+ +
+``` + +For drops, register a selector for the drop target. + ## Enqueue Cimo Admin screens and the block editor already enqueue Cimo. For frontend upload forms or custom screens where Cimo is not already loaded, call -`cimo_enqueue_assets()`. +`cimo_enqueue_assets()` after registering any selector filters needed for that +page. ```php add_action( 'wp_enqueue_scripts', function () { @@ -22,9 +56,13 @@ Use `window.cimo.optimizeFiles()` when your code already controls the upload process and can replace the selected files before uploading. ```js -const results = await window.cimo.optimizeFiles( files, { showProgress: true } ) +async function handleFiles( files ) { + const results = await window.cimo.optimizeFiles( files, { showProgress: true } ) + const filesToUpload = results.map( result => result.file ) -const filesToUpload = results.map( result => result.file ) + // Continue with your plugin/theme upload flow. + uploadFiles( filesToUpload ) +} ``` The API accepts a single `File`, a `FileList`, or an array of `File` objects. @@ -45,35 +83,6 @@ upload or optimization progress. If Cimo optimization is disabled or a file type is unsupported, the original file is returned with `metadata: null`. -## Automatic Selector Interception - -Use PHP selector filters when your markup uses normal file inputs or drop zones -and you want Cimo to intercept them automatically before your existing upload -handler runs. - -```php -add_filter( 'cimo/select_files/allowed_locations', function ( $locations ) { - $locations[] = '.my-plugin-uploader'; - return $locations; -} ); - -add_filter( 'cimo/drop_zone/allowed_locations', function ( $locations ) { - $locations[] = '.my-plugin-dropzone'; - return $locations; -} ); -``` - -For file inputs, register a selector for a wrapper around -``. - -```html -
- -
-``` - -For drops, register a selector for the drop target. - ## Which Approach To Use Use `window.cimo.optimizeFiles()` when you can explicitly await optimization diff --git a/src/admin/js/public-api.js b/src/admin/js/public-api.js index a9efd38..3d520fe 100644 --- a/src/admin/js/public-api.js +++ b/src/admin/js/public-api.js @@ -3,5 +3,5 @@ import { optimizeFiles } from './optimize-files' window.cimo = window.cimo || {} // Stable public API for plugins/themes that need pre-upload optimization. -// Cimo returns optimized File objects. +// Returns { file, metadata } results. window.cimo.optimizeFiles = optimizeFiles