[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> media-new-upload.js (source)

   1  /**
   2   * Routes "Add New Media File" uploads through the client-side media pipeline.
   3   *
   4   * On wp-admin/media-new.php WordPress uploads via a raw plupload.Uploader
   5   * (created by plupload-handlers) posting to async-upload.php. When the
   6   * browser is cross-origin isolated and supports the client-side pipeline,
   7   * this script intercepts the uploader's FilesAdded handler and hands files
   8   * to wp.mediaUploadPipeline (media-upload-pipeline.js) instead: the
   9   * original image is uploaded via the REST API and thumbnails are generated
  10   * in the browser (wasm-vips), then sideloaded and finalized.
  11   *
  12   * The screen's existing UI helpers from plupload-handlers are reused:
  13   * fileQueued() builds the progress item, uploadSuccess() renders the
  14   * finished attachment row (via the async-upload.php markup endpoint), and
  15   * uploadComplete() runs when the queue drains. Failed uploads render the
  16   * same notice async-upload.php returns for a server-side failure, so the
  17   * screen looks and behaves unchanged.
  18   *
  19   * When client-side support is unavailable the script cleanly no-ops and
  20   * the classic plupload flow is left untouched.
  21   *
  22   * @output wp-admin/js/media-new-upload.js
  23   */
  24  
  25  /* global plupload, pluploadL10n, wpUploaderInit, fileQueued, uploadStart, uploadSuccess, uploadComplete */
  26  
  27  ( function () {
  28      // Guard against double execution (e.g. duplicate enqueues).
  29      if ( window.__wpMediaNewUpload ) {
  30          return;
  31      }
  32  
  33      const pipeline = window.wp && wp.mediaUploadPipeline;
  34  
  35      // Bail unless the browser actually supports client-side processing. This
  36      // is the clean no-op: when the isolation headers did not land, classic
  37      // plupload keeps handling uploads.
  38      if (
  39          ! pipeline ||
  40          typeof plupload === 'undefined' ||
  41          ! wp.a11y ||
  42          ! pipeline.configure()
  43      ) {
  44          return;
  45      }
  46  
  47      window.__wpMediaNewUpload = true;
  48  
  49      const __ = wp.i18n.__;
  50      const sprintf = wp.i18n.sprintf;
  51  
  52      // Number of pipeline uploads currently in flight, used to fire
  53      // uploadComplete() once both the pipeline and plupload are done.
  54      let inFlightCount = 0;
  55  
  56      /**
  57       * Marks one pipeline upload as finished, firing uploadComplete() when
  58       * nothing is left uploading.
  59       *
  60       * The built-in UploadComplete binding never fires for pipeline uploads
  61       * because every file is removed from plupload before its queue starts.
  62       * Files plupload is still uploading (a batch's classic share, a HEIC
  63       * handed back) end with plupload's own UploadComplete instead.
  64       *
  65       * @param {plupload.Uploader} up The plupload uploader.
  66       */
  67  	function finishUpload( up ) {
  68          inFlightCount--;
  69          if ( inFlightCount === 0 && up.state !== plupload.STARTED ) {
  70              uploadComplete();
  71          }
  72      }
  73  
  74      /**
  75       * Returns the post the screen attaches uploads to, or 0.
  76       *
  77       * media-new.php validates `post_id` (the post must exist and be
  78       * editable by the user) before printing it into the form, whereas the
  79       * raw request value is what plupload carries in its multipart params.
  80       * async-upload.php re-validates that raw value and silently uploads
  81       * unattached; the REST API rejects it instead, so read the validated one.
  82       *
  83       * @return {number} The parent post ID, or 0.
  84       */
  85  	function getParentPostId() {
  86          const input = /** @type {HTMLInputElement|null} */ (
  87              document.getElementById( 'post_id' )
  88          );
  89          const postId = input ? parseInt( input.value, 10 ) : 0;
  90          return postId > 0 ? postId : 0;
  91      }
  92  
  93      /**
  94       * Returns the screen's progress item for a plupload file.
  95       *
  96       * @param {plupload.File} file The plupload file.
  97       * @return {HTMLElement|null} The item fileQueued() built, if it is still on the page.
  98       */
  99  	function getItem( file ) {
 100          return document.getElementById( 'media-item-' + file.id );
 101      }
 102  
 103      /**
 104       * Renders a failed upload the way async-upload.php does for a
 105       * server-side failure: an error notice with a real Dismiss button
 106       * described by the notice, a screen reader announcement, and focus
 107       * returned to the browse button once dismissed.
 108       *
 109       * @param {plupload.File} file    The plupload file that failed.
 110       * @param {string}        message The reason the upload failed.
 111       */
 112  	function renderError( file, message ) {
 113          const item = getItem( file );
 114          if ( ! item ) {
 115              return;
 116          }
 117  
 118          const descriptionId = 'error-description-' + file.id;
 119  
 120          const button = document.createElement( 'button' );
 121          button.type = 'button';
 122          button.id = 'dismiss-' + file.id;
 123          button.className = 'dismiss button-link';
 124          button.setAttribute( 'aria-describedby', descriptionId );
 125          button.textContent = pluploadL10n.dismiss;
 126  
 127          const heading = document.createElement( 'strong' );
 128          heading.textContent = pluploadL10n.error_uploading.replace(
 129              '%s',
 130              file.name
 131          );
 132  
 133          const notice = document.createElement( 'div' );
 134          notice.id = descriptionId;
 135          notice.className = 'notice notice-error error-div error';
 136          notice.append(
 137              button,
 138              ' ',
 139              heading,
 140              document.createElement( 'br' ),
 141              message
 142          );
 143  
 144          item.replaceChildren( notice );
 145          // Read by itemAjaxError() in plupload-handlers so a later server
 146          // error for the same file is not rendered twice.
 147          item.dataset.lastErr = file.id;
 148  
 149          setTimeout( function () {
 150              wp.a11y.speak(
 151                  sprintf(
 152                      /* translators: %s: Name of the file that failed to upload. */
 153                      __( '%s has failed to upload.' ),
 154                      file.name
 155                  )
 156              );
 157          }, 1500 );
 158  
 159          button.addEventListener( 'click', function () {
 160              item.remove();
 161              wp.a11y.speak( __( 'Error dismissed.' ) );
 162              const browseButton = document.getElementById(
 163                  'plupload-browse-button'
 164              );
 165              if ( browseButton ) {
 166                  browseButton.focus();
 167              }
 168          } );
 169      }
 170  
 171      /**
 172       * Reflects pipeline progress onto the screen's progress bar for a file.
 173       *
 174       * The bar is 200px wide at 100%, matching uploadProgress() in
 175       * plupload-handlers.
 176       *
 177       * @param {plupload.File} file    The plupload file being uploaded.
 178       * @param {number}        percent Progress percentage.
 179       */
 180  	function renderProgress( file, percent ) {
 181          const item = getItem( file );
 182          if ( ! item ) {
 183              return;
 184          }
 185  
 186          const bar = /** @type {HTMLElement|null} */ (
 187              item.querySelector( '.bar' )
 188          );
 189          if ( bar ) {
 190              bar.style.width = 2 * percent + 'px';
 191          }
 192  
 193          const label = item.querySelector( '.percent' );
 194          if ( label ) {
 195              label.textContent = percent + '%';
 196          }
 197      }
 198  
 199      /**
 200       * Intercepts files added to the plupload uploader.
 201       *
 202       * Builds the screen's progress items for the files the pipeline takes
 203       * (see wp.mediaUploadPipeline.routeBatch()). Returns undefined to leave
 204       * the whole batch to plupload's built-in handler.
 205       *
 206       * @param {plupload.Uploader} up    The plupload uploader instance.
 207       * @param {plupload.File[]}   files Files added to the queue.
 208       * @return {boolean|undefined} False to suppress the built-in handler.
 209       */
 210  	function handleFilesAdded( up, files ) {
 211          return pipeline.routeBatch( up, files, {
 212              // Without the parent post a file uploaded from
 213              // media-new.php?post_id=N would land unattached even though the
 214              // screen counts it against that post.
 215              postId: getParentPostId(),
 216              onStart: function ( /** @type {plupload.File[]} */ queued ) {
 217                  // Parity with the built-in handler: clear stale queue errors
 218                  // and run the shared upload-start housekeeping.
 219                  const queueErrors = document.getElementById( 'media-upload-error' );
 220                  if ( queueErrors ) {
 221                      queueErrors.replaceChildren();
 222                  }
 223                  uploadStart();
 224  
 225                  // Count the whole batch first: addItems() can reject a file
 226                  // synchronously, which must not drain the count mid-batch.
 227                  inFlightCount += queued.length;
 228              },
 229              onQueue: function (
 230                  /** @type {plupload.File} */ file,
 231                  /** @type {File} */ nativeFile
 232              ) {
 233                  // Build the screen's progress item for this file.
 234                  fileQueued( file );
 235  
 236                  return {
 237                      onSuccess: function (
 238                          /** @type {PipelineAttachment} */ attachment
 239                      ) {
 240                          // uploadSuccess() renders the finished attachment row
 241                          // via the existing async-upload.php markup endpoint;
 242                          // the server normally returns the ID as a string.
 243                          uploadSuccess( file, String( attachment.id ) );
 244                          finishUpload( up );
 245                      },
 246                      onError: function ( /** @type {UploadError} */ error ) {
 247                          renderError(
 248                              file,
 249                              pipeline.getErrorText( error, nativeFile.name )
 250                          );
 251                          finishUpload( up );
 252                      },
 253                      // plupload builds its own progress item for the file it
 254                      // took over, so this one goes away.
 255                      onHandOff: function () {
 256                          const item = getItem( file );
 257                          if ( item ) {
 258                              item.remove();
 259                          }
 260                          finishUpload( up );
 261                      },
 262                      onProgress: function ( /** @type {number} */ percent ) {
 263                          renderProgress( file, percent );
 264                      },
 265                  };
 266              },
 267          } );
 268      }
 269  
 270      /**
 271       * Binds the interceptor to an uploader once it has initialized.
 272       *
 273       * plupload sorts handlers by priority (descending) and a `false` return
 274       * breaks the chain, so priority 100 runs before and suppresses the
 275       * built-in FilesAdded and UploadComplete handlers.
 276       *
 277       * @param {plupload.Uploader} up The uploader that initialized.
 278       */
 279  	function bindUploader( up ) {
 280          if ( up.__wpMediaNewUploadBound ) {
 281              return;
 282          }
 283          up.__wpMediaNewUploadBound = true;
 284  
 285          up.bind(
 286              'FilesAdded',
 287              function ( up, files ) {
 288                  return handleFilesAdded( up, files );
 289              },
 290              null,
 291              100
 292          );
 293  
 294          // Hold back the built-in uploadComplete() while pipeline uploads
 295          // are still running; finishUpload() fires it once they are done.
 296          up.bind(
 297              'UploadComplete',
 298              function () {
 299                  return inFlightCount > 0 ? false : undefined;
 300              },
 301              null,
 302              100
 303          );
 304      }
 305  
 306      // plupload-handlers creates its uploader from the `wpUploaderInit`
 307      // settings once the DOM is ready, after this script has run, and
 308      // plupload binds the handlers in a settings `init` map while the
 309      // uploader initializes. Hooking PostInit there reaches the uploader
 310      // without depending on ready-callback ordering. The settings are
 311      // missing when the browser uploader is disabled (the html-uploader
 312      // fallback), in which case there is nothing to intercept.
 313      if ( typeof wpUploaderInit !== 'object' || ! wpUploaderInit ) {
 314          return;
 315      }
 316  
 317      // Either shape may already hold another script's handlers, which keep
 318      // running ahead of the interceptor being bound.
 319      const init = wpUploaderInit.init;
 320      if ( typeof init === 'function' ) {
 321          wpUploaderInit.init = function ( up ) {
 322              init( up );
 323              bindUploader( up );
 324          };
 325      } else {
 326          const postInit = init && init.PostInit;
 327          wpUploaderInit.init = Object.assign( {}, init, {
 328              PostInit: function (
 329                  /** @type {plupload.Uploader} */ up,
 330                  /** @type {unknown[]} */ ...args
 331              ) {
 332                  if ( typeof postInit === 'function' ) {
 333                      postInit.call( this, up, ...args );
 334                  }
 335                  bindUploader( up );
 336              },
 337          } );
 338      }
 339  } )();


Generated : Tue Oct 20 08:20:34 2026 Cross-referenced by PHPXref