Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
87.30% |
715 / 819 |
|
61.90% |
13 / 21 |
CRAP | n/a |
0 / 0 |
|
| wpcom_write_asset_url | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| wpcom_write_url | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| wpcom_write_wpcom_blog_id | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| wpcom_write_resolve_back_url | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| wpcom_write_get_editor_strings | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
1 | |||
| wpcom_write_prompt_block_editor_url | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
12 | |||
| wpcom_write_divert_prompt_to_block_editor | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
8.83 | |||
| wpcom_write_remove_admin_notices | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| wpcom_write_convert_video_embeds | |
96.77% |
30 / 31 |
|
0.00% |
0 / 1 |
8 | |||
| wpcom_write_allowed_block_attrs | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
| wpcom_write_detect_unsupported_content | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
6 | |||
| wpcom_write_is_transparent_background | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| wpcom_write_has_unsupported_color_class | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| wpcom_write_has_unsupported_mark | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 | |||
| wpcom_write_has_unsupported_blocks | |
91.89% |
34 / 37 |
|
0.00% |
0 / 1 |
25.33 | |||
| wpcom_write_alignment_classes_to_inline | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
4 | |||
| wpcom_write_inline_color_marks_to_spans | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
2 | |||
| wpcom_write_prompt_quote_markup | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
| wpcom_write_get_recent_drafts | |
51.52% |
17 / 33 |
|
0.00% |
0 / 1 |
12.58 | |||
| wpcom_write_render_admin_page | |
85.79% |
169 / 197 |
|
0.00% |
0 / 1 |
54.62 | |||
| wpcom_write_template | |
92.97% |
238 / 256 |
|
0.00% |
0 / 1 |
18.11 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Write — A distraction-free front-end writing experience for WordPress.com. |
| 4 | * |
| 5 | * Based on Jamie Marsland's Write plugin (https://github.com/jamiemarsland/Write). |
| 6 | * Registers a wp-admin page that serves a clean, full-screen writing surface. |
| 7 | * Posts are saved as proper Gutenberg block markup via the REST API. |
| 8 | * |
| 9 | * @package automattic/jetpack-mu-wpcom |
| 10 | */ |
| 11 | |
| 12 | if ( ! defined( 'ABSPATH' ) ) { |
| 13 | exit; |
| 14 | } |
| 15 | |
| 16 | use Automattic\Jetpack\Connection\Manager as Connection_Manager; |
| 17 | use Automattic\Jetpack\Jetpack_Mu_Wpcom\Common; |
| 18 | use Automattic\Jetpack\Status\Host; |
| 19 | |
| 20 | if ( ! defined( 'WPCOM_WRITE_VERSION' ) ) { |
| 21 | // Use file modification time to bust CDN caches when files change. |
| 22 | define( 'WPCOM_WRITE_VERSION', (string) max( filemtime( __DIR__ . '/view.js' ), filemtime( __DIR__ . '/style.css' ), filemtime( __DIR__ . '/undo-history.js' ), filemtime( __DIR__ . '/image-format.js' ), filemtime( __DIR__ . '/text-helpers.js' ), filemtime( __DIR__ . '/quote-editing.js' ), filemtime( __DIR__ . '/post-publish-checklist.js' ), filemtime( __DIR__ . '/post-publish-checklist.css' ), filemtime( __DIR__ . '/post-publish-survey.js' ), filemtime( __DIR__ . '/post-publish-survey.css' ) ) ); |
| 23 | } |
| 24 | |
| 25 | if ( ! defined( 'WPCOM_WRITE_BLOCK_EDITOR_PREFERRED_COOKIE' ) ) { |
| 26 | // Marks a browser as having left Write for the Block editor. See view.js's |
| 27 | // markBlockEditorPreferred() for who writes it and who else reads it. |
| 28 | define( 'WPCOM_WRITE_BLOCK_EDITOR_PREFERRED_COOKIE', 'wpcom-write-block-editor-preferred' ); |
| 29 | } |
| 30 | |
| 31 | // Inline SVG icons used by the top bar and the formatting toolbar. |
| 32 | require_once __DIR__ . '/icons.php'; |
| 33 | |
| 34 | // Post-publish next-steps checklist, shown on the published post after a |
| 35 | // Write-editor publish on a Coming Soon site. |
| 36 | require_once __DIR__ . '/post-publish-checklist.php'; |
| 37 | |
| 38 | // Email-verification launch gate backing the checklist's inline confirm-email step. |
| 39 | require_once __DIR__ . '/email-verification.php'; |
| 40 | |
| 41 | // One-question survey shown on the published post after a writer's first Write publish. |
| 42 | require_once __DIR__ . '/post-publish-survey.php'; |
| 43 | |
| 44 | /** |
| 45 | * Get the URL for a Write feature asset file. |
| 46 | * |
| 47 | * Uses plugins_url() when loaded via a plugin (production). Falls back to |
| 48 | * a computed URL for local development where plugins_url() can't resolve |
| 49 | * the monorepo path. |
| 50 | * |
| 51 | * @param string $file The asset filename (e.g. 'view.js', 'style.css'). |
| 52 | * @return string The full URL to the asset. |
| 53 | */ |
| 54 | function wpcom_write_asset_url( $file ) { |
| 55 | $url = plugins_url( $file, __FILE__ ); |
| 56 | |
| 57 | // In local dev (Docker), __FILE__ resolves to an absolute monorepo path that |
| 58 | // plugins_url() can't map to a web-accessible URL. Detect this and fall back |
| 59 | // to the known plugin symlink path. |
| 60 | if ( strpos( $url, '/plugins/usr/' ) !== false || strpos( $url, '/plugins/Users/' ) !== false ) { |
| 61 | $url = content_url( 'plugins/jetpack-mu-wpcom-plugin/jetpack_vendor/automattic/jetpack-mu-wpcom/src/features/write/' . $file ); |
| 62 | } |
| 63 | |
| 64 | return $url; |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Get the URL to the Write editor. |
| 69 | * |
| 70 | * @return string The admin URL for the Write page. |
| 71 | */ |
| 72 | function wpcom_write_url() { |
| 73 | return admin_url( 'admin.php?page=write' ); |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * The wpcom blog ID, which Atomic's local `get_current_blog_id()` is not. |
| 78 | * |
| 79 | * @return int Blog ID, or 0 when the site has no wpcom identity. |
| 80 | */ |
| 81 | function wpcom_write_wpcom_blog_id() { |
| 82 | if ( ! class_exists( Connection_Manager::class ) ) { |
| 83 | return 0; |
| 84 | } |
| 85 | |
| 86 | return (int) Connection_Manager::get_site_id( true ); |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * Resolve the editor's back/close destination from the source the user arrived from. |
| 91 | * |
| 92 | * Maps a short allowlist of known source tokens to known internal destinations. |
| 93 | * Anything not in the allowlist (including an empty or inferred source) falls back |
| 94 | * to the default destination — the site dashboard — so behavior is unchanged. |
| 95 | * |
| 96 | * This is the single lookup point for back-button destinations: never echo an |
| 97 | * arbitrary return URL from the query string, only map to vetted destinations. |
| 98 | * |
| 99 | * @param string $source Sanitized source token (see wpcom_write_render_admin_page()). |
| 100 | * @return string The destination URL for the back/close button. |
| 101 | */ |
| 102 | function wpcom_write_resolve_back_url( $source ) { |
| 103 | // The prompt cards send a per-surface token rather than a bare |
| 104 | // `writing_prompt`: that one comes from the wp-admin Daily Writing Prompt |
| 105 | // widget, which belongs back on the dashboard the default below already gives. |
| 106 | if ( 'writing_prompt_home' === $source ) { |
| 107 | // A bare /home resolves to whichever landing page the account prefers, |
| 108 | // which need not be this site, so the id has to be in the URL. |
| 109 | $blog_id = wpcom_write_wpcom_blog_id(); |
| 110 | return $blog_id ? 'https://wordpress.com/home/' . $blog_id : admin_url(); |
| 111 | } |
| 112 | |
| 113 | $destinations = array( |
| 114 | 'reader' => 'https://wordpress.com/reader', |
| 115 | 'writing_prompt_reader' => 'https://wordpress.com/reader', |
| 116 | ); |
| 117 | |
| 118 | return $destinations[ $source ] ?? admin_url(); |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Translated UI strings consumed by view.js as `window.wpcomWriteStrings`. |
| 123 | * |
| 124 | * Exposed as a helper so callers that render the Write editor outside the |
| 125 | * wp-admin page lifecycle (and therefore never hit the admin_enqueue_scripts |
| 126 | * hook below) can print the same strings without duplicating the list. |
| 127 | * |
| 128 | * @return array<string, string> Map of i18n key -> translated string. |
| 129 | */ |
| 130 | function wpcom_write_get_editor_strings() { |
| 131 | return array( |
| 132 | 'caption' => __( 'Caption', 'jetpack-mu-wpcom' ), |
| 133 | 'editImage' => __( 'Edit image', 'jetpack-mu-wpcom' ), |
| 134 | 'writeCaption' => __( 'Write a caption...', 'jetpack-mu-wpcom' ), |
| 135 | // translators: %s is the error message from the upload failure. |
| 136 | 'uploadFailed' => __( 'Upload failed: %s', 'jetpack-mu-wpcom' ), |
| 137 | 'uploadingImage' => __( 'Uploading image…', 'jetpack-mu-wpcom' ), |
| 138 | 'libraryLoading' => __( 'Loading your library…', 'jetpack-mu-wpcom' ), |
| 139 | 'libraryEmpty' => __( 'No images in your library yet.', 'jetpack-mu-wpcom' ), |
| 140 | 'libraryNoResults' => __( 'No matching images.', 'jetpack-mu-wpcom' ), |
| 141 | 'libraryLoadFailed' => __( "Couldn't load your library.", 'jetpack-mu-wpcom' ), |
| 142 | // translators: %s is the alt text or filename of the selected library image. |
| 143 | 'librarySelected' => __( 'Selected %s', 'jetpack-mu-wpcom' ), |
| 144 | 'invalidVideoUrl' => __( 'Please paste a valid YouTube or Vimeo URL', 'jetpack-mu-wpcom' ), |
| 145 | 'pleaseAddTitle' => __( 'Please add a title', 'jetpack-mu-wpcom' ), |
| 146 | 'pleaseWriteSomething' => __( 'Please write something', 'jetpack-mu-wpcom' ), |
| 147 | 'savingDraft' => __( 'Saving draft...', 'jetpack-mu-wpcom' ), |
| 148 | 'updating' => __( 'Updating...', 'jetpack-mu-wpcom' ), |
| 149 | 'publishing' => __( 'Publishing...', 'jetpack-mu-wpcom' ), |
| 150 | 'updated' => __( 'Updated!', 'jetpack-mu-wpcom' ), |
| 151 | 'published' => __( 'Published!', 'jetpack-mu-wpcom' ), |
| 152 | 'draftSaved' => __( 'Draft saved', 'jetpack-mu-wpcom' ), |
| 153 | 'draftAutosaved' => __( 'Draft saved', 'jetpack-mu-wpcom' ), |
| 154 | // translators: %s is the error message. |
| 155 | 'error' => __( 'Error: %s', 'jetpack-mu-wpcom' ), |
| 156 | 'couldNotSave' => __( 'Could not save. Please try again.', 'jetpack-mu-wpcom' ), |
| 157 | 'saveTimedOut' => __( 'Saving timed out. Please check your connection and try again.', 'jetpack-mu-wpcom' ), |
| 158 | 'normal' => __( 'Normal', 'jetpack-mu-wpcom' ), |
| 159 | 'heading2' => __( 'Heading 2', 'jetpack-mu-wpcom' ), |
| 160 | 'heading3' => __( 'Heading 3', 'jetpack-mu-wpcom' ), |
| 161 | 'preview' => __( 'Preview', 'jetpack-mu-wpcom' ), |
| 162 | // translators: %s is a comma-separated list of category names, e.g. "Travel, Food". |
| 163 | 'writingIn' => __( 'Writing in %s', 'jetpack-mu-wpcom' ), |
| 164 | 'untitled' => __( 'Untitled', 'jetpack-mu-wpcom' ), |
| 165 | 'addCitation' => __( 'Add citation…', 'jetpack-mu-wpcom' ), |
| 166 | 'citation' => __( 'Citation', 'jetpack-mu-wpcom' ), |
| 167 | 'postNotFound' => __( 'Post not found. Check the URL or ID and try again.', 'jetpack-mu-wpcom' ), |
| 168 | 'postNoPermission' => __( 'You don\'t have permission to edit this post.', 'jetpack-mu-wpcom' ), |
| 169 | // Labels used only when the editor is rendered for a logged-out |
| 170 | // visitor (window.wpcomWriteIsAnon). "WordPress.com" is a product |
| 171 | // mark and stays untranslated; only the feature name is localised. |
| 172 | 'anonBrand' => 'WordPress.com · ' . _x( 'Write', 'editor name in the anonymous brand label', 'jetpack-mu-wpcom' ), |
| 173 | 'anonStatus' => __( 'Not signed in', 'jetpack-mu-wpcom' ), |
| 174 | ); |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * Register the script module on init. |
| 179 | */ |
| 180 | add_action( |
| 181 | 'init', |
| 182 | function () { |
| 183 | wp_register_script_module( |
| 184 | 'wpcom-write/undo-history', |
| 185 | wpcom_write_asset_url( 'undo-history.js' ), |
| 186 | array(), |
| 187 | WPCOM_WRITE_VERSION |
| 188 | ); |
| 189 | wp_register_script_module( |
| 190 | 'wpcom-write/image-format', |
| 191 | wpcom_write_asset_url( 'image-format.js' ), |
| 192 | array(), |
| 193 | WPCOM_WRITE_VERSION |
| 194 | ); |
| 195 | wp_register_script_module( |
| 196 | 'wpcom-write/text-helpers', |
| 197 | wpcom_write_asset_url( 'text-helpers.js' ), |
| 198 | array(), |
| 199 | WPCOM_WRITE_VERSION |
| 200 | ); |
| 201 | wp_register_script_module( |
| 202 | 'wpcom-write/quote-editing', |
| 203 | wpcom_write_asset_url( 'quote-editing.js' ), |
| 204 | array(), |
| 205 | WPCOM_WRITE_VERSION |
| 206 | ); |
| 207 | wp_register_script_module( |
| 208 | 'wpcom-write/view', |
| 209 | wpcom_write_asset_url( 'view.js' ), |
| 210 | array( |
| 211 | '@wordpress/interactivity', |
| 212 | 'wpcom-write/undo-history', |
| 213 | 'wpcom-write/image-format', |
| 214 | 'wpcom-write/text-helpers', |
| 215 | 'wpcom-write/quote-editing', |
| 216 | ), |
| 217 | WPCOM_WRITE_VERSION |
| 218 | ); |
| 219 | } |
| 220 | ); |
| 221 | |
| 222 | // Hidden submenu pages (empty parent slug) are not found by get_admin_page_title(), |
| 223 | // so the browser tab shows only the site name. Fix via the admin_title filter. |
| 224 | add_filter( |
| 225 | 'admin_title', |
| 226 | function ( $admin_title, $title ) { |
| 227 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page routing. |
| 228 | if ( empty( $title ) && isset( $_GET['page'] ) && 'write' === $_GET['page'] ) { |
| 229 | return __( 'Write editor', 'jetpack-mu-wpcom' ) . $admin_title; |
| 230 | } |
| 231 | return $admin_title; |
| 232 | }, |
| 233 | 10, |
| 234 | 2 |
| 235 | ); |
| 236 | |
| 237 | /** |
| 238 | * Register the Write admin page. |
| 239 | * |
| 240 | * Uses an empty parent to create a hidden page (no menu entry) — access is |
| 241 | * via the admin bar "Write" link. Renders inside wp-admin's normal page |
| 242 | * lifecycle so that wp.apiFetch and its middleware are fully configured. |
| 243 | */ |
| 244 | add_action( |
| 245 | 'admin_menu', |
| 246 | function () { |
| 247 | add_submenu_page( |
| 248 | '', // Hidden — no parent menu. |
| 249 | __( 'Write', 'jetpack-mu-wpcom' ), |
| 250 | __( 'Write', 'jetpack-mu-wpcom' ), |
| 251 | 'publish_posts', |
| 252 | 'write', |
| 253 | 'wpcom_write_render_admin_page' |
| 254 | ); |
| 255 | } |
| 256 | ); |
| 257 | |
| 258 | /** |
| 259 | * Where a daily-prompt answer should go instead of Write, if anywhere. |
| 260 | * |
| 261 | * The prompt surfaces on wordpress.com — the My Home and Reader cards, the |
| 262 | * notification and the email — cannot read the opt-out, so it is asked again on |
| 263 | * arrival. See view.js's markBlockEditorPreferred() for who writes it. |
| 264 | * |
| 265 | * @return string Block editor URL to divert to, or '' to let Write render. |
| 266 | */ |
| 267 | function wpcom_write_prompt_block_editor_url() { |
| 268 | // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only GET routing, no state change. |
| 269 | if ( ! isset( $_GET['page'] ) || 'write' !== $_GET['page'] ) { |
| 270 | return ''; |
| 271 | } |
| 272 | |
| 273 | // Anything naming a post to open is left alone; only a blank new post is diverted. |
| 274 | if ( ! empty( $_GET['post'] ) || ! empty( $_GET['url'] ) ) { |
| 275 | return ''; |
| 276 | } |
| 277 | |
| 278 | // The opt-out means "I answer prompts in blocks", not "never open Write", so |
| 279 | // the Reader's Write button and the /write-editor picker are untouched. |
| 280 | $answer_prompt_id = isset( $_GET['answer_prompt'] ) && is_scalar( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : 0; |
| 281 | // phpcs:enable WordPress.Security.NonceVerification.Recommended |
| 282 | if ( ! $answer_prompt_id ) { |
| 283 | return ''; |
| 284 | } |
| 285 | |
| 286 | // Exactly the value view.js writes, so all three readers agree on what counts. |
| 287 | if ( ! isset( $_COOKIE[ WPCOM_WRITE_BLOCK_EDITOR_PREFERRED_COOKIE ] ) || '1' !== $_COOKIE[ WPCOM_WRITE_BLOCK_EDITOR_PREFERRED_COOKIE ] ) { |
| 288 | return ''; |
| 289 | } |
| 290 | |
| 291 | // Refused either way — leave the arrival on the path it was already taking. |
| 292 | if ( ! current_user_can( 'publish_posts' ) ) { |
| 293 | return ''; |
| 294 | } |
| 295 | |
| 296 | $host = new Host(); |
| 297 | // Simple runs no editor script to insert the prompt block, so post-new.php |
| 298 | // would open empty there. Same reason the widget's link sends Simple to Calypso. |
| 299 | if ( $host->is_wpcom_simple() ) { |
| 300 | return 'https://wordpress.com/post/' . $host->get_wpcom_site_id() . '?answer_prompt=' . $answer_prompt_id; |
| 301 | } |
| 302 | |
| 303 | return admin_url( 'post-new.php?answer_prompt=' . $answer_prompt_id ); |
| 304 | } |
| 305 | |
| 306 | /** |
| 307 | * Divert a prompt answer to the Block editor before Write renders. |
| 308 | * |
| 309 | * Must run before wpcom_write_render_admin_page(), which sends headers and |
| 310 | * fires `wpcom_write_editor_open`. |
| 311 | */ |
| 312 | function wpcom_write_divert_prompt_to_block_editor() { |
| 313 | // admin_init also fires on admin-ajax.php and admin-post.php, where this |
| 314 | // would replace a response body with a redirect. |
| 315 | if ( wp_doing_ajax() || ( isset( $GLOBALS['pagenow'] ) && 'admin.php' !== $GLOBALS['pagenow'] ) ) { |
| 316 | return; |
| 317 | } |
| 318 | |
| 319 | $url = wpcom_write_prompt_block_editor_url(); |
| 320 | if ( ! $url ) { |
| 321 | return; |
| 322 | } |
| 323 | |
| 324 | // Simple's destination is off-host, so wp_safe_redirect() needs it allowed. |
| 325 | // A rejected host falls back to the dashboard rather than failing, which |
| 326 | // would drop the answer silently, so confirm it survives validation first. |
| 327 | add_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_environments' ) ); |
| 328 | if ( $url !== wp_validate_redirect( $url, '' ) ) { |
| 329 | remove_filter( 'allowed_redirect_hosts', array( Host::class, 'allow_wpcom_environments' ) ); |
| 330 | return; |
| 331 | } |
| 332 | |
| 333 | if ( function_exists( '\Automattic\Jetpack\Jetpack_Mu_Wpcom\Common\wpcom_record_tracks_event' ) ) { |
| 334 | // Names the surface the answer came from, once wordpress.com sends it. |
| 335 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET parameter, no state change. |
| 336 | $source = isset( $_GET['source'] ) ? sanitize_key( $_GET['source'] ) : ''; |
| 337 | Common\wpcom_record_tracks_event( |
| 338 | 'wpcom_write_prompt_diverted_to_block_editor', |
| 339 | array( 'source' => $source ) |
| 340 | ); |
| 341 | } |
| 342 | |
| 343 | wp_safe_redirect( $url ); |
| 344 | exit; // @codeCoverageIgnore -- the tests unwind from the wp_redirect filter before this line. |
| 345 | } |
| 346 | add_action( 'admin_init', 'wpcom_write_divert_prompt_to_block_editor' ); // @codeCoverageIgnore |
| 347 | |
| 348 | /** |
| 349 | * Enqueue Write assets only on the Write admin page. |
| 350 | */ |
| 351 | add_action( |
| 352 | 'admin_enqueue_scripts', |
| 353 | function () { |
| 354 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page routing. |
| 355 | if ( ! isset( $_GET['page'] ) || 'write' !== $_GET['page'] ) { |
| 356 | return; |
| 357 | } |
| 358 | |
| 359 | wp_enqueue_script_module( 'wpcom-write/view' ); |
| 360 | |
| 361 | // Pass translated strings to JavaScript for dynamic messages. |
| 362 | wp_print_inline_script_tag( |
| 363 | 'window.wpcomWriteStrings = ' . wp_json_encode( wpcom_write_get_editor_strings(), JSON_HEX_TAG | JSON_HEX_AMP ) . ';' |
| 364 | ); |
| 365 | |
| 366 | // No style dependencies: every icon is an inline SVG (see icons.php), |
| 367 | // so the editor no longer pulls in the dashicons font. |
| 368 | wp_enqueue_style( |
| 369 | 'wpcom-write', |
| 370 | wpcom_write_asset_url( 'style.css' ), |
| 371 | array(), |
| 372 | WPCOM_WRITE_VERSION |
| 373 | ); |
| 374 | |
| 375 | // Add CSS to hide wp-admin chrome for a full-screen writing experience. |
| 376 | // This works with the platform rather than against it — wp-admin's scripts |
| 377 | // (including wp-api-fetch middleware) are fully loaded and configured. |
| 378 | $hide_chrome_css = ' |
| 379 | #wpadminbar, |
| 380 | #adminmenuwrap, |
| 381 | #adminmenuback, |
| 382 | #adminmenumain, |
| 383 | #wpfooter, |
| 384 | .wp-admin-bar-fix { display: none !important; } |
| 385 | #wpcontent, |
| 386 | #wpbody, |
| 387 | #wpbody-content { margin-left: 0 !important; padding: 0 !important; } |
| 388 | .wrap { margin: 0 !important; padding: 0 !important; max-width: none !important; } |
| 389 | html.wp-toolbar { padding-top: 0 !important; } |
| 390 | body.admin-bar { padding-top: 0 !important; margin-top: 0 !important; } |
| 391 | html { margin-top: 0 !important; } |
| 392 | '; |
| 393 | wp_add_inline_style( 'wpcom-write', $hide_chrome_css ); |
| 394 | } |
| 395 | ); |
| 396 | |
| 397 | /** |
| 398 | * Drop wp-admin notices on the Write page, where the fixed editor header |
| 399 | * covers them and leaves only a clipped strip visible. |
| 400 | * |
| 401 | * Runs on in_admin_header, the last hook before admin-header.php prints notices, |
| 402 | * so notices registered late are caught too. |
| 403 | */ |
| 404 | function wpcom_write_remove_admin_notices() { |
| 405 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only page routing. |
| 406 | if ( ! isset( $_GET['page'] ) || 'write' !== $_GET['page'] ) { |
| 407 | return; |
| 408 | } |
| 409 | remove_all_actions( 'admin_notices' ); |
| 410 | remove_all_actions( 'all_admin_notices' ); |
| 411 | remove_all_actions( 'network_admin_notices' ); |
| 412 | remove_all_actions( 'user_admin_notices' ); |
| 413 | } |
| 414 | add_action( 'in_admin_header', 'wpcom_write_remove_admin_notices', PHP_INT_MAX ); |
| 415 | |
| 416 | /** |
| 417 | * Convert wp:embed video blocks to placeholder tokens for the Write editor. |
| 418 | * |
| 419 | * Video embed blocks are replaced with inert HTML comment tokens that survive |
| 420 | * the_content filters, wp_kses_post(), and the pre_kses filter chain (which |
| 421 | * converts YouTube iframes to shortcodes on non-frontend requests). The |
| 422 | * returned placeholders map is used to swap tokens for real iframe HTML after |
| 423 | * all sanitization is complete. |
| 424 | * |
| 425 | * @param string $content Raw post_content (block markup). |
| 426 | * @return array { content: string, placeholders: array<string,string> } |
| 427 | */ |
| 428 | function wpcom_write_convert_video_embeds( $content ) { |
| 429 | $placeholders = array(); |
| 430 | |
| 431 | $replaced = preg_replace_callback( |
| 432 | '/<!-- wp:embed (\{[^}]*"type"\s*:\s*"video"[^}]*\}) -->.+?<!-- \/wp:embed -->/s', |
| 433 | static function ( $matches ) use ( &$placeholders ) { |
| 434 | $attrs = json_decode( $matches[1], true ); |
| 435 | if ( ! is_array( $attrs ) || empty( $attrs['url'] ) ) { |
| 436 | return $matches[0]; |
| 437 | } |
| 438 | $url = $attrs['url']; |
| 439 | $embed_url = ''; |
| 440 | |
| 441 | // YouTube. |
| 442 | if ( preg_match( '/(?:youtube\.com\/watch\?(?:[^&]*&)*v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/', $url, $yt ) ) { |
| 443 | $embed_url = 'https://www.youtube.com/embed/' . $yt[1]; |
| 444 | } |
| 445 | // Vimeo. |
| 446 | if ( ! $embed_url && preg_match( '/vimeo\.com\/(\d+)/', $url, $vim ) ) { |
| 447 | $embed_url = 'https://player.vimeo.com/video/' . $vim[1]; |
| 448 | } |
| 449 | |
| 450 | if ( ! $embed_url ) { |
| 451 | return $matches[0]; |
| 452 | } |
| 453 | |
| 454 | $title = $yt ? __( 'YouTube video', 'jetpack-mu-wpcom' ) : __( 'Vimeo video', 'jetpack-mu-wpcom' ); |
| 455 | $token = '<!--WRITE_VIDEO_' . md5( $embed_url ) . '-->'; |
| 456 | $html = sprintf( |
| 457 | '<figure class="bw-video-figure"><div class="bw-video-wrap"><iframe src="%s" title="%s" frameborder="0" allowfullscreen></iframe></div></figure>', |
| 458 | esc_url( $embed_url ), |
| 459 | esc_attr( $title ) |
| 460 | ); |
| 461 | |
| 462 | $placeholders[ $token ] = $html; |
| 463 | return $token; |
| 464 | }, |
| 465 | $content |
| 466 | ); |
| 467 | |
| 468 | return array( |
| 469 | 'content' => $replaced ?? $content, |
| 470 | 'placeholders' => $placeholders, |
| 471 | ); |
| 472 | } |
| 473 | |
| 474 | /** |
| 475 | * Return the per-block-type attribute allowlist for Write. |
| 476 | * |
| 477 | * Attributes listed here are safe for Write to handle — either preserved by |
| 478 | * convertToBlocks() in view.js or metadata that doesn't affect visible |
| 479 | * content. Keep this in sync with convertToBlocks(); a PHPUnit test |
| 480 | * cross-checks that both sides list the same block types. |
| 481 | * |
| 482 | * @return array<string, string[]> Block type → allowed attribute keys. |
| 483 | */ |
| 484 | function wpcom_write_allowed_block_attrs() { |
| 485 | return array( |
| 486 | 'paragraph' => array( 'align' ), |
| 487 | 'heading' => array( 'level', 'align' ), |
| 488 | // id: media-library metadata, not visible formatting. |
| 489 | // alt: preserved via HTML element, not block JSON. |
| 490 | // sizeSlug: thumbnail/medium/large/full size presets. |
| 491 | // align: left/center/right via the image properties panel. Wide and |
| 492 | // full alignment values are rejected by wpcom_write_has_unsupported_blocks |
| 493 | // further down (the same value check used for paragraph/heading), |
| 494 | // so posts using those bounce to the block editor. |
| 495 | 'image' => array( 'id', 'sizeSlug', 'alt', 'align' ), |
| 496 | 'embed' => array( 'url', 'type', 'providerNameSlug', 'responsive' ), |
| 497 | 'quote' => array( 'align', 'citation' ), |
| 498 | 'list' => array( 'ordered' ), |
| 499 | 'list-item' => array(), |
| 500 | 'separator' => array(), |
| 501 | ); |
| 502 | } |
| 503 | |
| 504 | /** |
| 505 | * Detect whether a post contains features the Write editor cannot preserve. |
| 506 | * |
| 507 | * Uses parse_blocks() to inspect block types and attributes against an |
| 508 | * allowlist derived from what convertToBlocks() in view.js can round-trip. |
| 509 | * Any block type or attribute not in the allowlist will trigger a warning. |
| 510 | * |
| 511 | * @param string $content Raw post_content (block markup). |
| 512 | * @return string|false 'classic-editor' or 'block-editor' indicating the warning |
| 513 | * type, or false when no warning is needed. |
| 514 | */ |
| 515 | function wpcom_write_detect_unsupported_content( $content ) { |
| 516 | if ( empty( $content ) ) { |
| 517 | return false; |
| 518 | } |
| 519 | |
| 520 | if ( ! has_blocks( $content ) ) { |
| 521 | return 'classic-editor'; |
| 522 | } |
| 523 | |
| 524 | $allowed_attrs = wpcom_write_allowed_block_attrs(); |
| 525 | |
| 526 | $blocks = parse_blocks( $content ); |
| 527 | |
| 528 | if ( wpcom_write_has_unsupported_blocks( $blocks, $allowed_attrs ) ) { |
| 529 | return 'block-editor'; |
| 530 | } |
| 531 | |
| 532 | // Write supports inline custom text colors only. |
| 533 | // Palette colors and highlights stay block-editor-only. |
| 534 | // has-text-align-* is not checked — it's converted to inline styles on load. |
| 535 | if ( wpcom_write_has_unsupported_color_class( $content ) ) { |
| 536 | return 'block-editor'; |
| 537 | } |
| 538 | |
| 539 | if ( wpcom_write_has_unsupported_mark( $content ) ) { |
| 540 | return 'block-editor'; |
| 541 | } |
| 542 | |
| 543 | return false; |
| 544 | } |
| 545 | |
| 546 | /** |
| 547 | * Check whether a CSS background-color value is transparent. |
| 548 | * |
| 549 | * Matches "transparent", "rgba(0, 0, 0, 0)", and whitespace/case variants. |
| 550 | * |
| 551 | * @param string $value A CSS background-color value. |
| 552 | * @return bool True if the value is transparent. |
| 553 | */ |
| 554 | function wpcom_write_is_transparent_background( $value ) { |
| 555 | $value = strtolower( trim( $value ) ); |
| 556 | if ( 'transparent' === $value ) { |
| 557 | return true; |
| 558 | } |
| 559 | // Normalize whitespace so rgba(0,0,0,0) and rgba( 0, 0, 0, 0 ) both match. |
| 560 | $value = preg_replace( '/\s+/', '', $value ); |
| 561 | return 'rgba(0,0,0,0)' === $value; |
| 562 | } |
| 563 | |
| 564 | /** |
| 565 | * Check whether content has color classes that Write can't preserve. |
| 566 | * |
| 567 | * Palette classes (has-vivid-red-color, etc.) and has-text-color indicate |
| 568 | * block editor content. has-inline-color alone is fine — Write produces it. |
| 569 | * Only matches inside class attributes to avoid false positives from plain text. |
| 570 | * |
| 571 | * @param string $content Raw post_content. |
| 572 | * @return bool True if unsupported color classes are found. |
| 573 | */ |
| 574 | function wpcom_write_has_unsupported_color_class( $content ) { |
| 575 | return preg_match( '/class="[^"]*\bhas-(?!inline-color\b)[\w-]+-color\b[^"]*"/', $content ) || |
| 576 | preg_match( "/class='[^']*\bhas-(?!inline-color\b)[\w-]+-color\b[^']*'/", $content ); |
| 577 | } |
| 578 | |
| 579 | /** |
| 580 | * Check whether content has <mark> elements that Write can't round-trip. |
| 581 | * |
| 582 | * Catches two cases: |
| 583 | * - <mark> without has-inline-color (block editor highlights). |
| 584 | * - <mark> with has-inline-color AND a non-transparent background |
| 585 | * (combined text color + highlight from the block editor). |
| 586 | * |
| 587 | * @param string $content Raw post_content. |
| 588 | * @return bool True if unsupported marks are found. |
| 589 | */ |
| 590 | function wpcom_write_has_unsupported_mark( $content ) { |
| 591 | // Highlights: <mark> without has-inline-color. |
| 592 | if ( preg_match( '/<mark\b(?![^>]*\bclass="[^"]*has-inline-color)/', $content ) ) { |
| 593 | return true; |
| 594 | } |
| 595 | |
| 596 | // Combined text color + highlight: has-inline-color with a real background. |
| 597 | if ( preg_match_all( '/<mark\b[^>]*\bclass="[^"]*has-inline-color[^"]*"[^>]*>/', $content, $marks ) ) { |
| 598 | foreach ( $marks[0] as $tag ) { |
| 599 | if ( preg_match( '/background-color\s*:\s*([^;"]+)/', $tag, $bg ) ) { |
| 600 | if ( ! wpcom_write_is_transparent_background( $bg[1] ) ) { |
| 601 | return true; |
| 602 | } |
| 603 | } |
| 604 | } |
| 605 | } |
| 606 | |
| 607 | return false; |
| 608 | } |
| 609 | |
| 610 | /** |
| 611 | * Check whether any block in the tree has an unsupported type or attributes. |
| 612 | * |
| 613 | * @param array $blocks Parsed block array from parse_blocks(). |
| 614 | * @param array $allowed_attrs Per-block-type allowed attribute keys. |
| 615 | * @return bool True if any block is unsupported. |
| 616 | */ |
| 617 | function wpcom_write_has_unsupported_blocks( $blocks, $allowed_attrs ) { |
| 618 | foreach ( $blocks as $block ) { |
| 619 | // Skip freeform HTML / whitespace between blocks. |
| 620 | if ( empty( $block['blockName'] ) ) { |
| 621 | continue; |
| 622 | } |
| 623 | |
| 624 | // Normalize: strip 'core/' namespace prefix. |
| 625 | $name = $block['blockName']; |
| 626 | if ( strpos( $name, 'core/' ) === 0 ) { |
| 627 | $name = substr( $name, 5 ); |
| 628 | } |
| 629 | |
| 630 | // Unknown block type (including third-party namespaced blocks). |
| 631 | if ( ! isset( $allowed_attrs[ $name ] ) ) { |
| 632 | return true; |
| 633 | } |
| 634 | |
| 635 | // Attributes beyond what Write handles for this block type. |
| 636 | $attrs = $block['attrs'] ?? array(); |
| 637 | |
| 638 | // The block editor stores alignment in two ways: |
| 639 | // 1. Top-level: {"align":"center"} |
| 640 | // 2. Nested style: {"style":{"typography":{"textAlign":"center"}}} |
| 641 | // Both are converted to inline styles on load, so they round-trip. |
| 642 | // Strip the style attr if it only contains typography.textAlign |
| 643 | // with a supported value, so it doesn't trigger the extra-attrs check. |
| 644 | // Justify is paragraph-only. |
| 645 | $supported_text_aligns = 'paragraph' === $name |
| 646 | ? array( 'left', 'center', 'right', 'justify' ) |
| 647 | : array( 'left', 'center', 'right' ); |
| 648 | $style_align = $attrs['style']['typography']['textAlign'] ?? ''; |
| 649 | if ( |
| 650 | isset( $attrs['style'] ) && |
| 651 | $style_align && |
| 652 | in_array( $style_align, $supported_text_aligns, true ) && |
| 653 | array( 'typography' => array( 'textAlign' => $style_align ) ) === $attrs['style'] |
| 654 | ) { |
| 655 | unset( $attrs['style'] ); |
| 656 | } |
| 657 | |
| 658 | // Justify uses the canonical class "has-text-align-justify" on paragraph. |
| 659 | // Strip it before the extra-attrs check so the paragraph round-trips. |
| 660 | if ( 'paragraph' === $name && ( $attrs['className'] ?? '' ) === 'has-text-align-justify' ) { |
| 661 | unset( $attrs['className'] ); |
| 662 | } |
| 663 | |
| 664 | // The block editor stamps `linkDestination: "none"` onto image |
| 665 | // blocks by default, even when the user never set a link. Strip |
| 666 | // that no-op value so block-editor-created images round-trip into |
| 667 | // Write. Actual link configurations (media / attachment / custom) |
| 668 | // still flag as unsupported below since Write has no image-link UI. |
| 669 | if ( 'image' === $name && ( $attrs['linkDestination'] ?? '' ) === 'none' ) { |
| 670 | unset( $attrs['linkDestination'] ); |
| 671 | } |
| 672 | |
| 673 | $extra = array_diff( array_keys( $attrs ), $allowed_attrs[ $name ] ); |
| 674 | if ( ! empty( $extra ) ) { |
| 675 | return true; |
| 676 | } |
| 677 | |
| 678 | // Alignment: convertToBlocks() only preserves center and right |
| 679 | // (read from style.textAlign). Block-level values like wide/full |
| 680 | // are CSS-class-based and silently lost. Left is the default and |
| 681 | // renders identically to no alignment, so allow it too. Justify |
| 682 | // is paragraph-only (display type and narrow quotes look poor). |
| 683 | if ( isset( $attrs['align'] ) && ! in_array( $attrs['align'], $supported_text_aligns, true ) ) { |
| 684 | return true; |
| 685 | } |
| 686 | |
| 687 | // Image sizeSlug: convertToBlocks() only emits the four standard |
| 688 | // presets. Custom or theme-registered slugs (e.g. "hero") would be |
| 689 | // silently stripped on save, so bounce them to the block editor. |
| 690 | if ( 'image' === $name && isset( $attrs['sizeSlug'] ) && ! in_array( $attrs['sizeSlug'], array( 'thumbnail', 'medium', 'large', 'full' ), true ) ) { |
| 691 | return true; |
| 692 | } |
| 693 | |
| 694 | // Embed-specific: only YouTube and Vimeo video embeds round-trip. |
| 695 | if ( 'embed' === $name ) { |
| 696 | $type = $attrs['type'] ?? ''; |
| 697 | $provider = $attrs['providerNameSlug'] ?? ''; |
| 698 | if ( 'video' !== $type || ! in_array( $provider, array( 'youtube', 'vimeo' ), true ) ) { |
| 699 | return true; |
| 700 | } |
| 701 | } |
| 702 | |
| 703 | // Recurse into inner blocks (e.g. list → list-item). |
| 704 | if ( ! empty( $block['innerBlocks'] ) ) { |
| 705 | if ( wpcom_write_has_unsupported_blocks( $block['innerBlocks'], $allowed_attrs ) ) { |
| 706 | return true; |
| 707 | } |
| 708 | } |
| 709 | } |
| 710 | |
| 711 | return false; |
| 712 | } |
| 713 | |
| 714 | /** |
| 715 | * Convert block-editor alignment CSS classes to inline styles. |
| 716 | * |
| 717 | * The block editor uses `has-text-align-{left,center,right}` classes for |
| 718 | * alignment, but Write's convertToBlocks() reads alignment from |
| 719 | * node.style.textAlign. This converts the classes to inline styles so |
| 720 | * alignment round-trips through Write. |
| 721 | * |
| 722 | * @param string $html Rendered HTML content. |
| 723 | * @return string HTML with alignment classes replaced by inline styles. |
| 724 | */ |
| 725 | function wpcom_write_alignment_classes_to_inline( $html ) { |
| 726 | return preg_replace_callback( |
| 727 | '/(<(?:p|h[1-6]|blockquote)\b[^>]*?)class="([^"]*has-text-align-(left|center|right|justify)[^"]*)"([^>]*?>)/', |
| 728 | function ( $m ) { |
| 729 | $before = $m[1]; |
| 730 | $classes = $m[2]; |
| 731 | $align = $m[3]; |
| 732 | $after = $m[4]; |
| 733 | |
| 734 | // Remove the has-text-align-* class. |
| 735 | $classes = trim( preg_replace( '/\bhas-text-align-(?:left|center|right|justify)\b/', '', $classes ) ); |
| 736 | $classes = preg_replace( '/ +/', ' ', $classes ); |
| 737 | |
| 738 | // Build the new tag, merging alignment into any existing style |
| 739 | // attribute to avoid producing invalid duplicate style attrs. |
| 740 | $class_attr = $classes ? ' class="' . $classes . '"' : ''; |
| 741 | $tag = rtrim( $before ) . $class_attr . $after; |
| 742 | |
| 743 | if ( preg_match( '/style="([^"]*)"/', $tag, $sm ) ) { |
| 744 | // Skip prepending if the existing style attr already declares |
| 745 | // the same text-align (avoids duplicate text-align declarations |
| 746 | // when the saved HTML carries both class and inline style). |
| 747 | if ( strpos( $sm[1], 'text-align:' . $align ) === false ) { |
| 748 | $tag = preg_replace( '/style="/', 'style="text-align:' . $align . ';', $tag, 1 ); |
| 749 | } |
| 750 | } else { |
| 751 | $tag = preg_replace( '/>$/', ' style="text-align:' . $align . '">', $tag ); |
| 752 | } |
| 753 | |
| 754 | return $tag; |
| 755 | }, |
| 756 | $html |
| 757 | ); |
| 758 | } |
| 759 | |
| 760 | /** |
| 761 | * Convert Gutenberg inline-color marks to spans for contentEditable. |
| 762 | * |
| 763 | * Write edits colors as spans, saves colors as Gutenberg marks. |
| 764 | * On load, marks are converted back to spans so foreColor can interact |
| 765 | * with existing colors. normalizeColorMarkup() in view.js reverses this. |
| 766 | * |
| 767 | * @param string $html Rendered HTML content. |
| 768 | * @return string HTML with inline-color marks replaced by color spans. |
| 769 | */ |
| 770 | function wpcom_write_inline_color_marks_to_spans( $html ) { |
| 771 | return preg_replace_callback( |
| 772 | '/<mark\b([^>]*\bclass="[^"]*has-inline-color[^"]*"[^>]*)>(.*?)<\/mark>/s', |
| 773 | function ( $m ) { |
| 774 | $attrs = $m[1]; |
| 775 | $inner = $m[2]; |
| 776 | // Extract the color value from the style attribute. |
| 777 | // Use (?<!-) to match the `color` property but not `background-color`. |
| 778 | if ( preg_match( '/(?<!-)color\s*:\s*([^;"]+)/', $attrs, $cm ) ) { |
| 779 | return '<span style="color:' . $cm[1] . '">' . $inner . '</span>'; |
| 780 | } |
| 781 | // No color found — just unwrap to a plain span. |
| 782 | return '<span>' . $inner . '</span>'; |
| 783 | }, |
| 784 | $html |
| 785 | ); |
| 786 | } |
| 787 | |
| 788 | /** |
| 789 | * Build the seed content for a post that answers a daily writing prompt. |
| 790 | * |
| 791 | * Write can't hold the jetpack/blogging-prompt block — it round-trips only a |
| 792 | * fixed block vocabulary (see wpcom_write_allowed_block_attrs()), so the prompt |
| 793 | * is seeded as a quote block. convertToBlocks() in view.js turns a top-level |
| 794 | * <blockquote class="wp-block-quote"> back into a wp:quote block on save, and |
| 795 | * preserves a <cite> as the quote's citation. When a responses link is |
| 796 | * available it becomes the citation, so the published quote points readers at |
| 797 | * the other answers — the one piece of the blogging-prompt block worth keeping. |
| 798 | * |
| 799 | * @since 6.11.0 |
| 800 | * |
| 801 | * @param string $prompt_text The prompt text from the blogging-prompts endpoint. |
| 802 | * @param string $citation_text Optional citation text (e.g. the responses link text). |
| 803 | * @param string $citation_url Optional URL to link the citation to (e.g. the responses page). |
| 804 | * @return string Quote block markup, or '' when there is no usable text. |
| 805 | */ |
| 806 | function wpcom_write_prompt_quote_markup( $prompt_text, $citation_text = '', $citation_url = '' ) { |
| 807 | $prompt_text = trim( wp_strip_all_tags( html_entity_decode( (string) $prompt_text, ENT_QUOTES, 'UTF-8' ) ) ); |
| 808 | if ( '' === $prompt_text ) { |
| 809 | return ''; |
| 810 | } |
| 811 | |
| 812 | $markup = '<blockquote class="wp-block-quote"><p>' . esc_html( $prompt_text ) . '</p>'; |
| 813 | |
| 814 | $citation_text = trim( wp_strip_all_tags( html_entity_decode( (string) $citation_text, ENT_QUOTES, 'UTF-8' ) ) ); |
| 815 | if ( '' !== $citation_text ) { |
| 816 | $cite = esc_html( $citation_text ); |
| 817 | if ( '' !== $citation_url ) { |
| 818 | $cite = '<a href="' . esc_url( $citation_url ) . '" target="_blank" rel="noreferrer noopener">' . $cite . '</a>'; |
| 819 | } |
| 820 | $markup .= '<cite>' . $cite . '</cite>'; |
| 821 | } |
| 822 | |
| 823 | return $markup . '</blockquote>'; |
| 824 | } |
| 825 | |
| 826 | /** |
| 827 | * Get the current user's recent Write-compatible drafts. |
| 828 | * |
| 829 | * Queries up to 20 drafts by post_modified desc, filters out posts with |
| 830 | * unsupported content, and returns the first 5 that pass. |
| 831 | * |
| 832 | * @param int $exclude_post_id Post ID to exclude (the currently-edited post), or 0. |
| 833 | * @return array Array of { id: int, title: string, modified: string } objects. |
| 834 | */ |
| 835 | function wpcom_write_get_recent_drafts( $exclude_post_id = 0 ) { |
| 836 | // Drafts always belong to the current user; without one there is nothing to |
| 837 | // return, and querying with author=0 would otherwise match orphaned drafts. |
| 838 | if ( ! is_user_logged_in() ) { |
| 839 | return array(); |
| 840 | } |
| 841 | |
| 842 | $args = array( |
| 843 | 'post_type' => 'post', |
| 844 | 'post_status' => 'draft', |
| 845 | 'author' => get_current_user_id(), |
| 846 | 'orderby' => 'modified', |
| 847 | 'order' => 'DESC', |
| 848 | 'posts_per_page' => 20, |
| 849 | 'no_found_rows' => true, |
| 850 | ); |
| 851 | |
| 852 | if ( $exclude_post_id ) { |
| 853 | $args['post__not_in'] = array( $exclude_post_id ); |
| 854 | } |
| 855 | |
| 856 | $query = new WP_Query( $args ); |
| 857 | $drafts = array(); |
| 858 | |
| 859 | foreach ( $query->posts as $post ) { |
| 860 | if ( count( $drafts ) >= 5 ) { |
| 861 | break; |
| 862 | } |
| 863 | |
| 864 | if ( wpcom_write_detect_unsupported_content( $post->post_content ) ) { |
| 865 | continue; |
| 866 | } |
| 867 | |
| 868 | $gmt = $post->post_modified_gmt; |
| 869 | if ( '0000-00-00 00:00:00' !== $gmt ) { |
| 870 | $mod = str_replace( ' ', 'T', $gmt ) . 'Z'; |
| 871 | } else { |
| 872 | // Convert local time to UTC using the site's timezone setting |
| 873 | // so the browser can compute an accurate relative time. |
| 874 | $tz = wp_timezone(); |
| 875 | $dt = new \DateTime( $post->post_modified, $tz ); |
| 876 | $dt->setTimezone( new \DateTimeZone( 'UTC' ) ); |
| 877 | $mod = $dt->format( 'Y-m-d\TH:i:s' ) . 'Z'; |
| 878 | } |
| 879 | |
| 880 | $drafts[] = array( |
| 881 | 'id' => $post->ID, |
| 882 | 'title' => $post->post_title, |
| 883 | 'modified' => $mod, |
| 884 | ); |
| 885 | } |
| 886 | |
| 887 | return $drafts; |
| 888 | } |
| 889 | |
| 890 | /** |
| 891 | * Render the Write admin page. |
| 892 | * |
| 893 | * Called by add_submenu_page as the page callback. Runs inside wp-admin's |
| 894 | * normal page lifecycle, so wp.apiFetch is fully configured with the correct |
| 895 | * REST root URL and auth middleware. |
| 896 | */ |
| 897 | function wpcom_write_render_admin_page() { |
| 898 | // Check if editing an existing post. |
| 899 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET parameter, gated by capability check via add_submenu_page. |
| 900 | $edit_post_id = isset( $_GET['post'] ) && is_scalar( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; |
| 901 | $edit_title = ''; |
| 902 | $edit_content = ''; |
| 903 | $post_status = 'new'; |
| 904 | $edit_featured_id = 0; |
| 905 | $video_placeholders = array(); |
| 906 | $unsupported_type = false; |
| 907 | $open_post_error = ''; |
| 908 | |
| 909 | // Resolve a ?url= param to a post ID. |
| 910 | // For same-host URLs, extract ?p= or ?post= query params directly |
| 911 | // (covers admin URLs and shortlinks like /?p=123). Then fall back |
| 912 | // to url_to_postid() for pretty permalinks — core checks the host |
| 913 | // against home_url() internally. |
| 914 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET parameter for permalink resolution. |
| 915 | if ( ! $edit_post_id && ! empty( $_GET['url'] ) && is_scalar( $_GET['url'] ) ) { |
| 916 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 917 | $input_url = esc_url_raw( wp_unslash( $_GET['url'] ) ); |
| 918 | $url_host = wp_parse_url( $input_url, PHP_URL_HOST ); |
| 919 | $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); |
| 920 | |
| 921 | // Extract ?p= / ?post= only from same-host URLs to avoid |
| 922 | // treating foreign post IDs as local ones. |
| 923 | if ( $url_host && $url_host === $home_host ) { |
| 924 | $query_str = wp_parse_url( $input_url, PHP_URL_QUERY ); |
| 925 | if ( $query_str ) { |
| 926 | parse_str( $query_str, $query_params ); |
| 927 | $param_id = 0; |
| 928 | if ( ! empty( $query_params['p'] ) && is_scalar( $query_params['p'] ) ) { |
| 929 | $param_id = absint( $query_params['p'] ); |
| 930 | } elseif ( ! empty( $query_params['post'] ) && is_scalar( $query_params['post'] ) ) { |
| 931 | $param_id = absint( $query_params['post'] ); |
| 932 | } |
| 933 | if ( $param_id ) { |
| 934 | $edit_post_id = $param_id; |
| 935 | } |
| 936 | } |
| 937 | } |
| 938 | if ( ! $edit_post_id ) { |
| 939 | $resolved_id = url_to_postid( $input_url ); |
| 940 | if ( $resolved_id ) { |
| 941 | $edit_post_id = $resolved_id; |
| 942 | } else { |
| 943 | $open_post_error = __( 'Post not found. Check the URL or ID and try again.', 'jetpack-mu-wpcom' ); |
| 944 | } |
| 945 | } |
| 946 | } |
| 947 | |
| 948 | if ( $edit_post_id ) { |
| 949 | $edit_post = get_post( $edit_post_id ); |
| 950 | if ( ! $edit_post ) { |
| 951 | $open_post_error = __( 'Post not found. Check the URL or ID and try again.', 'jetpack-mu-wpcom' ); |
| 952 | $edit_post_id = 0; |
| 953 | } elseif ( ! current_user_can( 'edit_post', $edit_post_id ) ) { |
| 954 | $open_post_error = __( 'You don\'t have permission to edit this post.', 'jetpack-mu-wpcom' ); |
| 955 | $edit_post_id = 0; |
| 956 | } elseif ( 'post' !== $edit_post->post_type ) { |
| 957 | $open_post_error = __( 'Only posts can be opened in Write.', 'jetpack-mu-wpcom' ); |
| 958 | $edit_post_id = 0; |
| 959 | } else { |
| 960 | $edit_title = $edit_post->post_title; |
| 961 | // Convert video embed blocks to inert placeholder tokens before the |
| 962 | // the_content + wp_kses_post pipeline runs. Tokens survive all filters; |
| 963 | // real iframe HTML is swapped in after sanitization in the template. |
| 964 | $video_result = wpcom_write_convert_video_embeds( $edit_post->post_content ); |
| 965 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core filter needed to render blocks. |
| 966 | $edit_content = apply_filters( 'the_content', $video_result['content'] ); |
| 967 | $video_placeholders = $video_result['placeholders']; |
| 968 | $post_status = $edit_post->post_status; |
| 969 | $edit_featured_id = (int) get_post_thumbnail_id( $edit_post_id ); |
| 970 | $unsupported_type = wpcom_write_detect_unsupported_content( $edit_post->post_content ); |
| 971 | |
| 972 | // Only track the last editor when the post is actually editable |
| 973 | // in Write. Posts with unsupported content show a warning modal |
| 974 | // and are never editable, so recording Write as the last editor |
| 975 | // would be misleading. |
| 976 | if ( ! $unsupported_type ) { |
| 977 | \Automattic\Jetpack\Jetpack_Mu_Wpcom\WPCOM_Block_Editor\EditorType\remember_editor( $edit_post_id, 'write-editor' ); |
| 978 | } |
| 979 | } |
| 980 | } |
| 981 | |
| 982 | // Seed a brand-new post that's answering a daily writing prompt. The id |
| 983 | // arrives as ?answer_prompt=<id> — forwarded by the wp-admin Daily Writing |
| 984 | // Prompt widget and (later) by wpcom's /write-editor redirect. Only new |
| 985 | // posts are seeded; opening an existing post ignores the param. The param is |
| 986 | // an integer id only (never rendered text) and the prompt text comes from |
| 987 | // the trusted blogging-prompts endpoint, mirroring the block-editor flow in |
| 988 | // plugins/jetpack (_inc/blogging-prompts.php). |
| 989 | $answer_prompt_id = 0; |
| 990 | if ( 0 === $edit_post_id ) { |
| 991 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET param; seeds a new draft only, gated by the page's publish_posts capability. Mirrors _inc/blogging-prompts.php. |
| 992 | $answer_prompt_id = isset( $_GET['answer_prompt'] ) ? absint( $_GET['answer_prompt'] ) : 0; |
| 993 | if ( $answer_prompt_id && function_exists( 'jetpack_get_blogging_prompt_by_id' ) ) { |
| 994 | // @phan-suppress-next-line PhanUndeclaredFunction -- Defined in the Jetpack plugin (_inc/blogging-prompts.php); present at runtime when Jetpack is active, guarded by the function_exists() check above. |
| 995 | $prompt = (array) jetpack_get_blogging_prompt_by_id( $answer_prompt_id ); |
| 996 | if ( ! empty( $prompt['text'] ) ) { |
| 997 | $edit_content = wpcom_write_prompt_quote_markup( |
| 998 | $prompt['text'], |
| 999 | $prompt['answered_link_text'] ?? '', |
| 1000 | $prompt['answered_link'] ?? '' |
| 1001 | ); |
| 1002 | } |
| 1003 | } |
| 1004 | } |
| 1005 | |
| 1006 | if ( 'classic-editor' === $unsupported_type ) { |
| 1007 | $editor_url = admin_url( 'post.php?post=' . $edit_post_id . '&action=edit&classic-editor' ); |
| 1008 | } elseif ( 'block-editor' === $unsupported_type ) { |
| 1009 | $editor_url = admin_url( 'post.php?post=' . $edit_post_id . '&action=edit&classic-editor__forget' ); |
| 1010 | } else { |
| 1011 | $editor_url = ''; |
| 1012 | } |
| 1013 | |
| 1014 | // URLs for the topbar "more" menu (Open in block editor / Preview). |
| 1015 | // Always available when editing an existing post — independent of the |
| 1016 | // unsupported-content warning's $editor_url which only triggers on load. |
| 1017 | $block_editor_url = $edit_post_id |
| 1018 | ? admin_url( 'post.php?post=' . $edit_post_id . '&action=edit&classic-editor__forget' ) |
| 1019 | : ''; |
| 1020 | $preview_url = ''; |
| 1021 | if ( $edit_post_id ) { |
| 1022 | $preview_url = 'publish' === $post_status |
| 1023 | ? (string) get_permalink( $edit_post_id ) |
| 1024 | : (string) get_preview_post_link( $edit_post_id ); |
| 1025 | } |
| 1026 | |
| 1027 | // Determine how the user arrived at the Write editor. |
| 1028 | // 1. Explicit query param (highest priority). |
| 1029 | // 2. Infer from HTTP referer. |
| 1030 | // 3. Fall back to 'direct' (bookmarks, typed URLs, stripped referers). |
| 1031 | // The /write-editor redirect forwards this param into wp-admin, so an explicit |
| 1032 | // source (e.g. 'reader') survives the hop and drives the back-button destination. |
| 1033 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only GET parameter, no state change. |
| 1034 | $source = isset( $_GET['source'] ) ? sanitize_key( $_GET['source'] ) : ''; |
| 1035 | if ( ! $source ) { |
| 1036 | $referer = wp_get_referer(); |
| 1037 | if ( $referer ) { |
| 1038 | $path = wp_parse_url( $referer, PHP_URL_PATH ); |
| 1039 | if ( $path && str_contains( $path, '/wp-admin/index.php' ) ) { |
| 1040 | $source = 'dashboard'; |
| 1041 | } elseif ( $path && str_contains( $path, '/wp-admin/edit.php' ) ) { |
| 1042 | $source = 'posts_list'; |
| 1043 | } else { |
| 1044 | $source = 'referrer'; |
| 1045 | } |
| 1046 | } else { |
| 1047 | $source = 'direct'; |
| 1048 | } |
| 1049 | } |
| 1050 | |
| 1051 | // Resolve where the back/close button should return the user, based on source. |
| 1052 | $back_url = wpcom_write_resolve_back_url( $source ); |
| 1053 | |
| 1054 | if ( function_exists( '\Automattic\Jetpack\Jetpack_Mu_Wpcom\Common\wpcom_record_tracks_event' ) ) { |
| 1055 | $event_props = array( |
| 1056 | 'is_new_post' => (int) ( 0 === $edit_post_id ), |
| 1057 | 'source' => $source, |
| 1058 | // Anon entry is the only logged-out render of this editor (the wp-admin |
| 1059 | // page requires auth), so logged-out is a reliable proxy for the anon |
| 1060 | // fake-door funnel. Lets the funnel scope its top-of-funnel denominator |
| 1061 | // to anon traffic without depending on the client-only wpcomWriteIsAnon flag. |
| 1062 | 'is_anon' => (int) ! is_user_logged_in(), |
| 1063 | ); |
| 1064 | |
| 1065 | if ( $edit_post_id > 0 ) { |
| 1066 | $event_props['post_id'] = $edit_post_id; |
| 1067 | } |
| 1068 | |
| 1069 | Common\wpcom_record_tracks_event( |
| 1070 | 'wpcom_write_editor_open', |
| 1071 | $event_props |
| 1072 | ); |
| 1073 | } |
| 1074 | |
| 1075 | // Fetch existing tag IDs so JS can merge them with any #hashtag-extracted tags on save. |
| 1076 | $existing_tag_ids = $edit_post_id ? wp_get_post_tags( $edit_post_id, array( 'fields' => 'ids' ) ) : array(); |
| 1077 | |
| 1078 | // Build categories list for the UI (only categories that have posts). |
| 1079 | $all_cats = get_categories( array( 'hide_empty' => true ) ); |
| 1080 | $selected_cats = $edit_post_id ? wp_get_post_categories( $edit_post_id ) : array(); |
| 1081 | $categories_data = array(); |
| 1082 | foreach ( $all_cats as $cat ) { |
| 1083 | $categories_data[] = array( |
| 1084 | 'id' => $cat->term_id, |
| 1085 | 'name' => $cat->name, |
| 1086 | 'selected' => in_array( $cat->term_id, $selected_cats, true ), |
| 1087 | ); |
| 1088 | } |
| 1089 | |
| 1090 | // For new posts, pre-select the default category if it's in the used-categories list, |
| 1091 | // otherwise the first available used category, so the label is never empty. |
| 1092 | if ( ! $edit_post_id && ! empty( $categories_data ) ) { |
| 1093 | $has_selection = ! empty( array_filter( $categories_data, fn( $c ) => $c['selected'] ) ); |
| 1094 | if ( ! $has_selection ) { |
| 1095 | $default_id = (int) get_option( 'default_category' ); |
| 1096 | $default_idx = array_search( $default_id, array_column( $categories_data, 'id' ), true ); |
| 1097 | $select_idx = false !== $default_idx ? $default_idx : 0; |
| 1098 | $categories_data[ $select_idx ]['selected'] = true; |
| 1099 | } |
| 1100 | } |
| 1101 | |
| 1102 | // Show the category row only when the site has 2+ used categories. |
| 1103 | $show_cat_row = count( $categories_data ) >= 2; |
| 1104 | |
| 1105 | // Compute the initial "Writing in …" label from any pre-selected categories. |
| 1106 | $selected_cat_names = array_values( |
| 1107 | array_map( |
| 1108 | fn( $c ) => $c['name'], |
| 1109 | array_filter( $categories_data, fn( $c ) => $c['selected'] ) |
| 1110 | ) |
| 1111 | ); |
| 1112 | // translators: %s is a comma-separated list of category names, e.g. "Travel, Food". |
| 1113 | $writing_in_fmt = __( 'Writing in %s', 'jetpack-mu-wpcom' ); |
| 1114 | $cat_label = empty( $selected_cat_names ) |
| 1115 | ? '' |
| 1116 | : sprintf( $writing_in_fmt, implode( ', ', $selected_cat_names ) ); |
| 1117 | |
| 1118 | // Query recent compatible drafts for the post picker. |
| 1119 | $recent_drafts = wpcom_write_get_recent_drafts( $edit_post_id ); |
| 1120 | |
| 1121 | // Seed Interactivity API state. |
| 1122 | wp_interactivity_state( |
| 1123 | 'wpcom-write', |
| 1124 | array( |
| 1125 | 'postsPath' => '/wp/v2/posts', |
| 1126 | 'mediaPath' => '/wp/v2/media', |
| 1127 | 'homeUrl' => home_url( '/' ), |
| 1128 | 'adminUrl' => admin_url(), |
| 1129 | 'backUrl' => $back_url, |
| 1130 | 'writeUrl' => wpcom_write_url(), |
| 1131 | 'editPostId' => $edit_post_id, |
| 1132 | 'postStatus' => $post_status, |
| 1133 | 'isPublishedPost' => 'publish' === $post_status, |
| 1134 | // The query arg the redirect tags onto the post URL, kept in sync with |
| 1135 | // the server-side gate by sharing WPCOM_WRITE_PUBLISHED_MARKER (defined |
| 1136 | // in post-publish-checklist.php) rather than hardcoding it in view.js. |
| 1137 | 'publishedMarker' => WPCOM_WRITE_PUBLISHED_MARKER, |
| 1138 | // The entry point this editor session was opened from, forwarded onto |
| 1139 | // the post-publish redirect so survey responses segment by the same |
| 1140 | // `source` the funnel's wpcom_write_editor_open event records. |
| 1141 | 'source' => $source, |
| 1142 | 'title' => $edit_title, |
| 1143 | 'isSaving' => false, |
| 1144 | 'isPublished' => false, |
| 1145 | 'message' => '', |
| 1146 | 'showLinkInput' => false, |
| 1147 | 'linkUrl' => '', |
| 1148 | 'showImageModal' => false, |
| 1149 | 'showVideoModal' => false, |
| 1150 | 'videoUrl' => '', |
| 1151 | 'imageAlt' => '', |
| 1152 | 'setAsFeatured' => false, |
| 1153 | 'featuredMediaId' => $edit_featured_id, |
| 1154 | 'isEditMode' => false, |
| 1155 | 'editingImageAlign' => 'center', |
| 1156 | 'editingImageSize' => '', |
| 1157 | 'editingImageHasMediaId' => false, |
| 1158 | 'isUploading' => false, |
| 1159 | 'showLibraryPicker' => false, |
| 1160 | 'showUrlInput' => false, |
| 1161 | 'librarySearch' => '', |
| 1162 | 'libraryStatus' => '', |
| 1163 | 'categories' => $categories_data, |
| 1164 | 'catLabel' => $cat_label, |
| 1165 | 'existingTagIds' => $existing_tag_ids, |
| 1166 | 'answerPromptId' => $answer_prompt_id, |
| 1167 | 'showCatDropdown' => false, |
| 1168 | 'showHelp' => false, |
| 1169 | 'showSlashMenu' => false, |
| 1170 | 'slashActiveId' => '', |
| 1171 | 'slashFilter' => '', |
| 1172 | 'showLeaveConfirm' => false, |
| 1173 | 'showHeadingMenu' => false, |
| 1174 | 'showTextColorMenu' => false, |
| 1175 | 'formatStrikethrough' => false, |
| 1176 | 'formatUnderline' => false, |
| 1177 | 'formatAlignLeft' => false, |
| 1178 | 'formatAlignCenter' => false, |
| 1179 | 'formatAlignRight' => false, |
| 1180 | 'formatAlignJustify' => false, |
| 1181 | 'cannotJustify' => false, |
| 1182 | 'formatOList' => false, |
| 1183 | 'formatUList' => false, |
| 1184 | 'insideList' => false, |
| 1185 | 'showRecoveryBanner' => false, |
| 1186 | // The first-visit note is opened by view.js, which owns the |
| 1187 | // per-browser "already seen" flag; the server always renders it hidden. |
| 1188 | 'showEditorNote' => false, |
| 1189 | 'unsupportedWarning' => $unsupported_type, |
| 1190 | 'editorUrl' => $editor_url, |
| 1191 | 'blockEditorUrl' => $block_editor_url, |
| 1192 | 'previewUrl' => $preview_url, |
| 1193 | 'showMoreMenu' => false, |
| 1194 | 'recentDrafts' => $recent_drafts, |
| 1195 | 'openPostError' => $open_post_error, |
| 1196 | 'showPostPicker' => '' !== $open_post_error, |
| 1197 | 'postPickerUrl' => '', |
| 1198 | 'pendingOpenPost' => false, |
| 1199 | ) |
| 1200 | ); |
| 1201 | |
| 1202 | // Output the editor UI inside wp-admin's wrapper. |
| 1203 | wpcom_write_template( $edit_title, $edit_content, $edit_post_id, $categories_data, $post_status, $video_placeholders, $show_cat_row, $cat_label, $recent_drafts, $open_post_error, $back_url ); |
| 1204 | } |
| 1205 | |
| 1206 | /** |
| 1207 | * Render the distraction-free writing UI. |
| 1208 | * |
| 1209 | * This outputs HTML inside wp-admin's page wrapper (not a standalone page). |
| 1210 | * The wp-admin chrome is hidden via CSS added in admin_enqueue_scripts. |
| 1211 | * |
| 1212 | * @param string $edit_title The post title when editing. |
| 1213 | * @param string $edit_content The post content when editing. |
| 1214 | * @param int $edit_post_id The post ID when editing, 0 for new posts. |
| 1215 | * @param array $categories_data Array of category data for the picker. |
| 1216 | * @param string $post_status The post status ('new', 'draft', 'publish', etc.). |
| 1217 | * @param array $video_placeholders Map of comment tokens to iframe HTML for video embeds. |
| 1218 | * @param bool $show_cat_row Whether to show the category row (2+ used categories). |
| 1219 | * @param string $cat_label Full "Writing in X, Y" label text; empty string if none selected. |
| 1220 | * @param array $recent_drafts Array of recent draft objects for the post picker. |
| 1221 | * @param string $open_post_error Error message for post picker, empty if no error. |
| 1222 | * @param string $back_url Destination for the back/close button; defaults to the dashboard. |
| 1223 | */ |
| 1224 | function wpcom_write_template( $edit_title = '', $edit_content = '', $edit_post_id = 0, $categories_data = array(), $post_status = 'new', $video_placeholders = array(), $show_cat_row = false, $cat_label = '', $recent_drafts = array(), $open_post_error = '', $back_url = '' ) { |
| 1225 | if ( '' === $back_url ) { |
| 1226 | $back_url = admin_url(); |
| 1227 | } |
| 1228 | ?> |
| 1229 | <div data-wp-interactive="wpcom-write" class="bw-app"> |
| 1230 | |
| 1231 | <!-- Top bar --> |
| 1232 | <header class="bw-topbar" data-wp-class--has-topbar-message="state.hasMessage"> |
| 1233 | <a href="<?php echo esc_url( $back_url ); ?>" class="bw-back" title="<?php esc_attr_e( 'Back', 'jetpack-mu-wpcom' ); ?>" aria-label="<?php esc_attr_e( 'Back', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.handleBack"><?php wpcom_write_icon( 'back' ); ?></a> |
| 1234 | <div class="bw-help-wrap" data-wp-on--keydown="actions.handleHelpKeyDown" data-wp-on--focusout="actions.handleHelpFocusOut"> |
| 1235 | <button class="bw-help-toggle" data-wp-on--click="actions.toggleHelp" title="<?php esc_attr_e( 'Tips', 'jetpack-mu-wpcom' ); ?>" aria-label="<?php esc_attr_e( 'Tips', 'jetpack-mu-wpcom' ); ?>"><span class="bw-help-i" aria-hidden="true">i</span></button> |
| 1236 | <div class="bw-help-popover" hidden data-wp-bind--hidden="!state.showHelp"> |
| 1237 | <div class="bw-help-note"> |
| 1238 | <p class="bw-help-note-text"> |
| 1239 | <?php |
| 1240 | echo wp_kses( |
| 1241 | sprintf( |
| 1242 | /* translators: %1$s and %2$s are the opening and closing tags of a link to the Write editor guide. */ |
| 1243 | __( 'You’re using %1$sWrite%2$s, a simple editor for writing.', 'jetpack-mu-wpcom' ), |
| 1244 | '<a class="bw-help-note-guide" data-target="wpcom-help-center" href="https://wordpress.com/support/editors/write-editor/" target="_blank" rel="noopener noreferrer">', |
| 1245 | '</a>' |
| 1246 | ), |
| 1247 | array( |
| 1248 | 'a' => array( |
| 1249 | 'class' => array(), |
| 1250 | 'data-target' => array(), |
| 1251 | 'href' => array(), |
| 1252 | 'target' => array(), |
| 1253 | 'rel' => array(), |
| 1254 | ), |
| 1255 | ) |
| 1256 | ); |
| 1257 | ?> |
| 1258 | </p> |
| 1259 | <p class="bw-help-note-switch"> |
| 1260 | <?php |
| 1261 | echo wp_kses( |
| 1262 | sprintf( |
| 1263 | /* translators: %1$s and %2$s are the opening and closing tags of a button reading "Block editor". */ |
| 1264 | __( 'Use the %1$sBlock editor%2$s instead.', 'jetpack-mu-wpcom' ), |
| 1265 | '<button type="button" class="bw-help-note-button" data-wp-on--click="actions.switchToBlockEditor">', |
| 1266 | '</button>' |
| 1267 | ), |
| 1268 | array( |
| 1269 | 'button' => array( |
| 1270 | 'type' => array(), |
| 1271 | 'class' => array(), |
| 1272 | 'data-wp-on--click' => array(), |
| 1273 | ), |
| 1274 | ) |
| 1275 | ); |
| 1276 | ?> |
| 1277 | </p> |
| 1278 | </div> |
| 1279 | <div class="bw-help-title"><?php esc_html_e( 'Tips', 'jetpack-mu-wpcom' ); ?></div> |
| 1280 | <div class="bw-help-row"><kbd>/</kbd><span><?php esc_html_e( 'Insert a heading, image, video, list, quote or divider', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1281 | <div class="bw-help-row"><kbd>Ctrl+B</kbd><span><?php esc_html_e( 'Bold', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1282 | <div class="bw-help-row"><kbd>Ctrl+I</kbd><span><?php esc_html_e( 'Italic', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1283 | <div class="bw-help-row"><kbd>Ctrl+K</kbd><span><?php esc_html_e( 'Insert link', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1284 | <div class="bw-help-row"><kbd>Ctrl+S</kbd><span><?php esc_html_e( 'Save', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1285 | <div class="bw-help-row"><kbd>Tab</kbd><span><?php esc_html_e( 'Navigate slash menu options', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1286 | <div class="bw-help-row"><kbd>Shift+Tab</kbd><span><?php esc_html_e( 'Focus formatting toolbar', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1287 | <div class="bw-help-row"><kbd>#tag</kbd><span><?php esc_html_e( 'A line containing only #tags assigns them to the post on save', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1288 | <a class="bw-help-link" data-target="wpcom-help-center" href="https://wordpress.com/support/editors/write-editor/" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Read the Write editor guide', 'jetpack-mu-wpcom' ); ?> <span aria-hidden="true">↗</span></a> |
| 1289 | </div> |
| 1290 | </div><!-- /.bw-help-wrap --> |
| 1291 | <span class="bw-status" data-wp-text="state.displayStatus"></span> |
| 1292 | <?php |
| 1293 | // Screen-reader announcement for transient status (saving/publishing, |
| 1294 | // "Please write something" validation, save/publish errors). Bound to |
| 1295 | // state.message only — not state.displayStatus — so the title mirror that |
| 1296 | // the visible .bw-status also carries isn't re-announced on every keystroke. |
| 1297 | ?> |
| 1298 | <span class="bw-visually-hidden" role="status" aria-live="polite" data-wp-text="state.message"></span> |
| 1299 | <div class="bw-topbar-actions"> |
| 1300 | <button |
| 1301 | class="bw-btn bw-btn-draft" |
| 1302 | data-wp-on--click="actions.saveDraft" |
| 1303 | data-wp-bind--disabled="state.isSaving" |
| 1304 | data-wp-bind--hidden="state.isPublishedPost" |
| 1305 | <?php echo 'publish' === $post_status ? 'hidden' : ''; ?> |
| 1306 | ><?php esc_html_e( 'Save draft', 'jetpack-mu-wpcom' ); ?></button> |
| 1307 | <button |
| 1308 | class="bw-btn bw-btn-publish" |
| 1309 | data-wp-on--click="actions.publish" |
| 1310 | data-wp-bind--disabled="state.isSaving" |
| 1311 | ><?php echo 'publish' === $post_status ? esc_html__( 'Update', 'jetpack-mu-wpcom' ) : esc_html__( 'Publish', 'jetpack-mu-wpcom' ); ?></button> |
| 1312 | <div class="bw-more-wrap" data-wp-on--keydown="actions.handleMoreMenuKeyDown" data-wp-on--focusout="actions.handleMoreMenuFocusOut"> |
| 1313 | <button |
| 1314 | class="bw-more-toggle" |
| 1315 | aria-haspopup="menu" |
| 1316 | aria-expanded="false" |
| 1317 | data-wp-bind--aria-expanded="state.showMoreMenu" |
| 1318 | data-wp-on--click="actions.toggleMoreMenu" |
| 1319 | title="<?php esc_attr_e( 'More options', 'jetpack-mu-wpcom' ); ?>" |
| 1320 | aria-label="<?php esc_attr_e( 'More options', 'jetpack-mu-wpcom' ); ?>" |
| 1321 | ><span class="bw-more-dots"><?php wpcom_write_icon( 'kebab' ); ?></span></button> |
| 1322 | <div class="bw-more-menu" role="menu" aria-label="<?php esc_attr_e( 'More options', 'jetpack-mu-wpcom' ); ?>" hidden data-wp-bind--hidden="!state.showMoreMenu"> |
| 1323 | <button |
| 1324 | class="bw-more-menu-item bw-more-save-draft" |
| 1325 | role="menuitem" |
| 1326 | tabindex="-1" |
| 1327 | data-wp-on--click="actions.saveDraftFromMenu" |
| 1328 | data-wp-bind--hidden="state.isPublishedPost" |
| 1329 | <?php echo 'publish' === $post_status ? 'hidden' : ''; ?> |
| 1330 | ><?php esc_html_e( 'Save draft', 'jetpack-mu-wpcom' ); ?></button> |
| 1331 | <button |
| 1332 | class="bw-more-menu-item" |
| 1333 | role="menuitem" |
| 1334 | tabindex="-1" |
| 1335 | data-wp-on--click="actions.openInBlockEditor" |
| 1336 | ><?php esc_html_e( 'Open in Block editor', 'jetpack-mu-wpcom' ); ?></button> |
| 1337 | <button |
| 1338 | class="bw-more-menu-item" |
| 1339 | role="menuitem" |
| 1340 | tabindex="-1" |
| 1341 | data-wp-on--click="actions.previewPost" |
| 1342 | ><?php esc_html_e( 'Preview', 'jetpack-mu-wpcom' ); ?></button> |
| 1343 | <button |
| 1344 | class="bw-more-menu-item" |
| 1345 | role="menuitem" |
| 1346 | tabindex="-1" |
| 1347 | data-wp-on--click="actions.openPostPicker" |
| 1348 | ><?php esc_html_e( 'Open post', 'jetpack-mu-wpcom' ); ?></button> |
| 1349 | </div> |
| 1350 | </div> |
| 1351 | </div> |
| 1352 | </header> |
| 1353 | |
| 1354 | <!-- Recovery banner --> |
| 1355 | <div class="bw-recovery-banner" hidden data-wp-bind--hidden="!state.showRecoveryBanner"> |
| 1356 | <span class="bw-recovery-text"><?php esc_html_e( 'You have a recent draft — continue editing?', 'jetpack-mu-wpcom' ); ?></span> |
| 1357 | <button class="bw-recovery-btn" data-wp-on--click="actions.resumeDraft"><?php esc_html_e( 'Resume editing', 'jetpack-mu-wpcom' ); ?></button> |
| 1358 | <button class="bw-recovery-dismiss" data-wp-on--click="actions.dismissRecovery" aria-label="<?php esc_attr_e( 'Dismiss draft recovery notice', 'jetpack-mu-wpcom' ); ?>">×</button> |
| 1359 | </div> |
| 1360 | |
| 1361 | <!-- First-visit note. Sits after the recovery banner, which pushes it down. --> |
| 1362 | <div |
| 1363 | class="bw-editor-note" |
| 1364 | role="dialog" |
| 1365 | aria-label="<?php esc_attr_e( 'About the Write editor', 'jetpack-mu-wpcom' ); ?>" |
| 1366 | aria-describedby="bw-editor-note-text" |
| 1367 | tabindex="-1" |
| 1368 | hidden |
| 1369 | data-wp-bind--hidden="!state.showEditorNote" |
| 1370 | data-wp-on--keydown="actions.handleEditorNoteKeyDown" |
| 1371 | > |
| 1372 | <p class="bw-editor-note-text" id="bw-editor-note-text"> |
| 1373 | <?php |
| 1374 | echo wp_kses( |
| 1375 | sprintf( |
| 1376 | /* translators: %1$s and %2$s are the opening and closing tags of a link to the Write editor guide. */ |
| 1377 | __( 'You’re using %1$sWrite%2$s, a simple editor for writing.', 'jetpack-mu-wpcom' ), |
| 1378 | '<a class="bw-editor-note-guide" data-target="wpcom-help-center" href="https://wordpress.com/support/editors/write-editor/" target="_blank" rel="noopener noreferrer">', |
| 1379 | '</a>' |
| 1380 | ), |
| 1381 | array( |
| 1382 | 'a' => array( |
| 1383 | 'class' => array(), |
| 1384 | 'data-target' => array(), |
| 1385 | 'href' => array(), |
| 1386 | 'target' => array(), |
| 1387 | 'rel' => array(), |
| 1388 | ), |
| 1389 | ) |
| 1390 | ); |
| 1391 | ?> |
| 1392 | </p> |
| 1393 | <div class="bw-editor-note-actions"> |
| 1394 | <button class="bw-editor-note-secondary" data-wp-on--click="actions.openInBlockEditorFromNote"><?php esc_html_e( 'Use the Block editor', 'jetpack-mu-wpcom' ); ?></button> |
| 1395 | <button class="bw-editor-note-ok" data-wp-on--click="actions.dismissEditorNote"><?php esc_html_e( 'Got it', 'jetpack-mu-wpcom' ); ?></button> |
| 1396 | </div> |
| 1397 | </div> |
| 1398 | |
| 1399 | <!-- Persistent formatting toolbar --> |
| 1400 | <div |
| 1401 | class="bw-toolbar" |
| 1402 | role="toolbar" |
| 1403 | aria-label="<?php esc_attr_e( 'Formatting', 'jetpack-mu-wpcom' ); ?>" |
| 1404 | data-wp-on--mousedown="actions.preventToolbarBlur" |
| 1405 | data-wp-on--keydown="actions.handleToolbarKeyDown" |
| 1406 | > |
| 1407 | <div class="bw-toolbar-scroll"> |
| 1408 | <!-- Undo / Redo --> |
| 1409 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Undo', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.undo" data-wp-bind--disabled="!state.canUndo" title="<?php esc_attr_e( 'Undo', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'undo' ); ?></button> |
| 1410 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Redo', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.redo" data-wp-bind--disabled="!state.canRedo" title="<?php esc_attr_e( 'Redo', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'redo' ); ?></button> |
| 1411 | <span class="bw-tool-divider"></span> |
| 1412 | <!-- Heading dropdown --> |
| 1413 | <div class="bw-tool-dropdown-wrap"> |
| 1414 | <button class="bw-tool bw-tool-heading-toggle" aria-label="<?php esc_attr_e( 'Text style', 'jetpack-mu-wpcom' ); ?>" aria-haspopup="menu" aria-expanded="false" tabindex="0" data-wp-bind--aria-expanded="state.showHeadingMenu" data-wp-on--click="actions.toggleHeadingMenu" data-wp-class--bw-tool-active="state.formatHeading" title="<?php esc_attr_e( 'Text style', 'jetpack-mu-wpcom' ); ?>"> |
| 1415 | <span class="bw-tool-label" data-wp-text="state.headingLabel"><?php esc_html_e( 'Normal', 'jetpack-mu-wpcom' ); ?></span> |
| 1416 | <span class="bw-tool-caret"><?php wpcom_write_icon( 'chevron-down' ); ?></span> |
| 1417 | </button> |
| 1418 | <div class="bw-heading-menu" role="menu" aria-label="<?php esc_attr_e( 'Text style', 'jetpack-mu-wpcom' ); ?>" hidden data-wp-bind--hidden="!state.showHeadingMenu" data-wp-on--keydown="actions.handleSubmenuKeyDown"> |
| 1419 | <button class="bw-heading-option" role="menuitem" tabindex="-1" data-wp-on--click="actions.setHeadingNormal" data-wp-on--mousedown="actions.preventToolbarBlur"><span><?php esc_html_e( 'Normal', 'jetpack-mu-wpcom' ); ?></span></button> |
| 1420 | <button class="bw-heading-option bw-heading-option-h2" role="menuitem" tabindex="-1" data-wp-on--click="actions.setHeadingH2" data-wp-on--mousedown="actions.preventToolbarBlur"><span><?php esc_html_e( 'Heading 2', 'jetpack-mu-wpcom' ); ?></span></button> |
| 1421 | <button class="bw-heading-option bw-heading-option-h3" role="menuitem" tabindex="-1" data-wp-on--click="actions.setHeadingH3" data-wp-on--mousedown="actions.preventToolbarBlur"><span><?php esc_html_e( 'Heading 3', 'jetpack-mu-wpcom' ); ?></span></button> |
| 1422 | </div> |
| 1423 | </div> |
| 1424 | <span class="bw-tool-divider"></span> |
| 1425 | <!-- Inline formatting --> |
| 1426 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Bold', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.formatBold" data-wp-class--bw-tool-active="state.formatBold" title="<?php esc_attr_e( 'Bold', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'bold' ); ?></button> |
| 1427 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Italic', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.formatItalic" data-wp-class--bw-tool-active="state.formatItalic" title="<?php esc_attr_e( 'Italic', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'italic' ); ?></button> |
| 1428 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Underline', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.formatUnderline" data-wp-class--bw-tool-active="state.formatUnderline" title="<?php esc_attr_e( 'Underline', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'underline' ); ?></button> |
| 1429 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Strikethrough', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.formatStrikethrough" data-wp-class--bw-tool-active="state.formatStrikethrough" title="<?php esc_attr_e( 'Strikethrough', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'strikethrough' ); ?></button> |
| 1430 | <!-- Text color --> |
| 1431 | <div class="bw-tool-dropdown-wrap"> |
| 1432 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Text color', 'jetpack-mu-wpcom' ); ?>" aria-haspopup="menu" aria-expanded="false" tabindex="-1" data-wp-bind--aria-expanded="state.showTextColorMenu" data-wp-on--click="actions.toggleTextColorMenu" title="<?php esc_attr_e( 'Text color', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'text-color' ); ?></button> |
| 1433 | <div class="bw-color-menu" role="menu" aria-label="<?php esc_attr_e( 'Text color', 'jetpack-mu-wpcom' ); ?>" hidden data-wp-bind--hidden="!state.showTextColorMenu" data-wp-on--mousedown="actions.preventToolbarBlur" data-wp-on--keydown="actions.handleSubmenuKeyDown"> |
| 1434 | <button class="bw-color-swatch" role="menuitem" tabindex="-1" style="background:#1a1a1a;" aria-label="<?php esc_attr_e( 'Default', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.setTextColorDefault" title="<?php esc_attr_e( 'Default', 'jetpack-mu-wpcom' ); ?>"></button> |
| 1435 | <button class="bw-color-swatch" role="menuitem" tabindex="-1" style="background:#d63638;" aria-label="<?php esc_attr_e( 'Red', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.setTextColorRed" title="<?php esc_attr_e( 'Red', 'jetpack-mu-wpcom' ); ?>"></button> |
| 1436 | <button class="bw-color-swatch" role="menuitem" tabindex="-1" style="background:#2171b1;" aria-label="<?php esc_attr_e( 'Blue', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.setTextColorBlue" title="<?php esc_attr_e( 'Blue', 'jetpack-mu-wpcom' ); ?>"></button> |
| 1437 | <button class="bw-color-swatch" role="menuitem" tabindex="-1" style="background:#00a32a;" aria-label="<?php esc_attr_e( 'Green', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.setTextColorGreen" title="<?php esc_attr_e( 'Green', 'jetpack-mu-wpcom' ); ?>"></button> |
| 1438 | <button class="bw-color-swatch" role="menuitem" tabindex="-1" style="background:#dba617;" aria-label="<?php esc_attr_e( 'Yellow', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.setTextColorYellow" title="<?php esc_attr_e( 'Yellow', 'jetpack-mu-wpcom' ); ?>"></button> |
| 1439 | <button class="bw-color-swatch" role="menuitem" tabindex="-1" style="background:#8c5db0;" aria-label="<?php esc_attr_e( 'Purple', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.setTextColorPurple" title="<?php esc_attr_e( 'Purple', 'jetpack-mu-wpcom' ); ?>"></button> |
| 1440 | </div> |
| 1441 | </div> |
| 1442 | <span class="bw-tool-divider"></span> |
| 1443 | <!-- Alignment --> |
| 1444 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Align left', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.alignLeft" data-wp-class--bw-tool-active="state.formatAlignLeft" data-wp-bind--disabled="state.insideList" title="<?php esc_attr_e( 'Align left', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'align-left' ); ?></button> |
| 1445 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Align center', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.alignCenter" data-wp-class--bw-tool-active="state.formatAlignCenter" data-wp-bind--disabled="state.insideList" title="<?php esc_attr_e( 'Align center', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'align-center' ); ?></button> |
| 1446 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Align right', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.alignRight" data-wp-class--bw-tool-active="state.formatAlignRight" data-wp-bind--disabled="state.insideList" title="<?php esc_attr_e( 'Align right', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'align-right' ); ?></button> |
| 1447 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Justify', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.alignJustify" data-wp-class--bw-tool-active="state.formatAlignJustify" data-wp-bind--disabled="state.cannotJustify" title="<?php esc_attr_e( 'Justify', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'align-justify' ); ?></button> |
| 1448 | <span class="bw-tool-divider"></span> |
| 1449 | <!-- Lists --> |
| 1450 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Bulleted list', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.formatUList" data-wp-class--bw-tool-active="state.formatUList" title="<?php esc_attr_e( 'Bulleted list', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'list-bullets' ); ?></button> |
| 1451 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Numbered list', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.formatOList" data-wp-class--bw-tool-active="state.formatOList" title="<?php esc_attr_e( 'Numbered list', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'list-numbered' ); ?></button> |
| 1452 | <span class="bw-tool-divider"></span> |
| 1453 | <!-- Block-level --> |
| 1454 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Link', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.toggleLinkInput" data-wp-class--bw-tool-active="state.showLinkInput" title="<?php esc_attr_e( 'Link', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'link' ); ?></button> |
| 1455 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Quote', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.formatQuote" data-wp-class--bw-tool-active="state.formatQuote" title="<?php esc_attr_e( 'Quote', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'quote' ); ?></button> |
| 1456 | <button class="bw-tool" aria-label="<?php esc_attr_e( 'Image', 'jetpack-mu-wpcom' ); ?>" tabindex="-1" data-wp-on--click="actions.openImageModal" data-wp-bind--disabled="state.insideList" title="<?php esc_attr_e( 'Image', 'jetpack-mu-wpcom' ); ?>"><?php wpcom_write_icon( 'image' ); ?></button> |
| 1457 | </div> |
| 1458 | </div> |
| 1459 | |
| 1460 | <!-- Link input popover --> |
| 1461 | <div class="bw-link-popover" hidden data-wp-bind--hidden="!state.showLinkInput" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1462 | <input |
| 1463 | type="url" |
| 1464 | class="bw-link-input" |
| 1465 | placeholder="<?php esc_attr_e( 'Paste or type a link...', 'jetpack-mu-wpcom' ); ?>" |
| 1466 | data-wp-bind--value="state.linkUrl" |
| 1467 | data-wp-on--input="actions.updateLinkUrl" |
| 1468 | data-wp-on--keydown="actions.handleLinkKeyDown" |
| 1469 | /> |
| 1470 | <button class="bw-link-apply" data-wp-on--click="actions.applyLink"><?php esc_html_e( 'Apply', 'jetpack-mu-wpcom' ); ?></button> |
| 1471 | <button class="bw-link-remove" data-wp-on--click="actions.removeLink">×</button> |
| 1472 | </div> |
| 1473 | |
| 1474 | <!-- Writing area --> |
| 1475 | <main class="bw-main" data-wp-on--click="actions.handleMainClick"> |
| 1476 | <div class="bw-editor" data-wp-bind--inert="state.unsupportedWarning"> |
| 1477 | |
| 1478 | <?php if ( $show_cat_row ) : ?> |
| 1479 | <!-- Category selector — only shown when site has 2+ used categories --> |
| 1480 | <div class="bw-meta" data-wp-on--focusout="actions.handleCatFocusOut"> |
| 1481 | <button |
| 1482 | class="bw-meta-cat-btn" |
| 1483 | aria-haspopup="listbox" |
| 1484 | aria-expanded="false" |
| 1485 | aria-controls="bw-cat-dropdown" |
| 1486 | data-wp-bind--aria-expanded="state.showCatDropdown" |
| 1487 | data-wp-on--click="actions.toggleCatDropdown" |
| 1488 | data-wp-on--keydown="actions.handleCatBtnKeyDown" |
| 1489 | > |
| 1490 | <span |
| 1491 | class="bw-meta-cat-label" |
| 1492 | data-placeholder="<?php esc_attr_e( 'Select a category', 'jetpack-mu-wpcom' ); ?>" |
| 1493 | data-wp-text="state.catLabel" |
| 1494 | ><?php echo esc_html( $cat_label ); ?></span> |
| 1495 | <span class="bw-meta-cat-caret" aria-hidden="true">▾</span> |
| 1496 | </button> |
| 1497 | <div |
| 1498 | class="bw-meta-dropdown" |
| 1499 | id="bw-cat-dropdown" |
| 1500 | role="listbox" |
| 1501 | aria-multiselectable="true" |
| 1502 | hidden |
| 1503 | data-wp-bind--hidden="!state.showCatDropdown" |
| 1504 | data-wp-on--keydown="actions.handleCatDropdownKeyDown" |
| 1505 | > |
| 1506 | <?php foreach ( $categories_data as $i => $cat ) : ?> |
| 1507 | <button |
| 1508 | class="bw-meta-dropdown-item<?php echo $cat['selected'] ? ' bw-meta-dropdown-item--selected' : ''; ?>" |
| 1509 | role="option" |
| 1510 | aria-selected="<?php echo $cat['selected'] ? 'true' : 'false'; ?>" |
| 1511 | tabindex="-1" |
| 1512 | data-cat-index="<?php echo (int) $i; ?>" |
| 1513 | data-cat-name="<?php echo esc_attr( $cat['name'] ); ?>" |
| 1514 | data-wp-on--click="actions.handleCatDropdownClick" |
| 1515 | ><?php echo esc_html( $cat['name'] ); ?></button> |
| 1516 | <?php endforeach; ?> |
| 1517 | </div> |
| 1518 | </div><!-- /.bw-meta --> |
| 1519 | <?php endif; ?> |
| 1520 | |
| 1521 | <textarea |
| 1522 | class="bw-title" |
| 1523 | placeholder="<?php esc_attr_e( 'Title', 'jetpack-mu-wpcom' ); ?>" |
| 1524 | rows="1" |
| 1525 | data-wp-on--input="actions.updateTitle" |
| 1526 | data-wp-on--keydown="actions.handleTitleKeyDown" |
| 1527 | autocomplete="off" |
| 1528 | ><?php echo esc_textarea( $edit_title ); ?></textarea> |
| 1529 | <div class="bw-separator"></div> |
| 1530 | <div |
| 1531 | class="bw-content<?php echo $edit_content ? '' : ' bw-is-empty'; ?>" |
| 1532 | data-wp-watch="callbacks.syncComboboxAria" |
| 1533 | data-wp-on--mouseup="actions.checkFormatting" |
| 1534 | data-wp-on--keyup="actions.checkFormatting" |
| 1535 | data-wp-on--click="actions.handleContentClick" |
| 1536 | data-wp-on--input="actions.repairStructure" |
| 1537 | data-wp-on--keydown="actions.handleKeyDown" |
| 1538 | data-wp-on--beforeinput="actions.handleBeforeInput" |
| 1539 | data-wp-on--dragstart="actions.handleEditorDragStart" |
| 1540 | data-wp-on--dragover="actions.handleEditorDragOver" |
| 1541 | data-wp-on--dragleave="actions.handleEditorDragLeave" |
| 1542 | data-wp-on--drop="actions.handleEditorDrop" |
| 1543 | data-wp-on--dragend="actions.handleEditorDragEnd" |
| 1544 | data-placeholder="<?php esc_attr_e( 'Tell your story...', 'jetpack-mu-wpcom' ); ?>" |
| 1545 | ><div |
| 1546 | class="bw-content-inner" |
| 1547 | contenteditable="true" |
| 1548 | role="combobox" |
| 1549 | aria-label="<?php esc_attr_e( 'Post content', 'jetpack-mu-wpcom' ); ?>" |
| 1550 | aria-autocomplete="list" |
| 1551 | aria-haspopup="listbox" |
| 1552 | aria-expanded="false" |
| 1553 | aria-controls="bw-slash-menu" |
| 1554 | data-wp-ignore |
| 1555 | > |
| 1556 | <?php |
| 1557 | if ( $edit_content ) { |
| 1558 | // Convert block-editor alignment classes to inline styles so |
| 1559 | // convertToBlocks() can read them via node.style.textAlign. |
| 1560 | $edit_content = wpcom_write_alignment_classes_to_inline( $edit_content ); |
| 1561 | // Convert Gutenberg <mark class="has-inline-color"> to |
| 1562 | // <span style="color:#hex"> so foreColor can interact with |
| 1563 | // existing colors. Done before kses so the simpler span+hex |
| 1564 | // format passes through without needing a custom kses filter. |
| 1565 | $edit_content = wpcom_write_inline_color_marks_to_spans( $edit_content ); |
| 1566 | // Sanitize through wp_kses_post — video embed tokens (HTML comments) |
| 1567 | // pass through untouched, then we swap them for the real iframe HTML. |
| 1568 | // This avoids the pre_kses filter that converts iframes to shortcodes. |
| 1569 | $sanitized = wp_kses_post( $edit_content ); |
| 1570 | if ( $video_placeholders ) { |
| 1571 | $sanitized = str_replace( array_keys( $video_placeholders ), array_values( $video_placeholders ), $sanitized ); |
| 1572 | } |
| 1573 | echo $sanitized; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Sanitized by wp_kses_post; iframe HTML is self-constructed with esc_url/esc_attr. |
| 1574 | } else { |
| 1575 | echo '<p><br></p>'; |
| 1576 | } |
| 1577 | ?> |
| 1578 | </div></div> |
| 1579 | </div> |
| 1580 | </main> |
| 1581 | |
| 1582 | <!-- Image insert modal: centered, blocking overlay for picking a new image. --> |
| 1583 | <div class="bw-image-overlay" hidden data-wp-bind--hidden="!state.showImageInsertOverlay" data-wp-on--pointerdown="actions.handleOverlayPointerDown" data-wp-on--keydown="actions.handleImageModalKeyDown" data-wp-on--dragover="actions.handleOverlayDragOver" data-wp-on--drop="actions.handleOverlayDrop"> |
| 1584 | <div class="bw-image-modal" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e( 'Add an image', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.stopPropagation" data-wp-on--dragover="actions.handleOverlayDragOver" data-wp-on--drop="actions.handleOverlayDrop"> |
| 1585 | <h3><?php esc_html_e( 'Add an image', 'jetpack-mu-wpcom' ); ?></h3> |
| 1586 | <label class="bw-upload-zone" id="bw-upload-zone" data-wp-on--dragover="actions.handleDragOver" data-wp-on--dragleave="actions.handleDragLeave" data-wp-on--drop="actions.handleDrop"> |
| 1587 | <span class="bw-upload-label"><?php esc_html_e( 'Drop a file or click to upload', 'jetpack-mu-wpcom' ); ?></span> |
| 1588 | <span class="bw-upload-saving" style="display:none;"><?php esc_html_e( 'Uploading...', 'jetpack-mu-wpcom' ); ?></span> |
| 1589 | <input type="file" accept="image/*" data-wp-on--change="actions.uploadImage" class="bw-visually-hidden" /> |
| 1590 | </label> |
| 1591 | <input |
| 1592 | type="text" |
| 1593 | class="bw-image-url-input bw-image-alt-input" |
| 1594 | placeholder="<?php esc_attr_e( 'Alt text (describe the image)...', 'jetpack-mu-wpcom' ); ?>" |
| 1595 | data-wp-on--input="actions.updateImageAlt" |
| 1596 | data-wp-bind--value="state.imageAlt" |
| 1597 | /> |
| 1598 | <label class="bw-featured-toggle"> |
| 1599 | <input type="checkbox" data-wp-on--change="actions.toggleFeaturedImage" data-wp-bind--checked="state.setAsFeatured" /> |
| 1600 | <span><?php esc_html_e( 'Set as featured image', 'jetpack-mu-wpcom' ); ?></span> |
| 1601 | </label> |
| 1602 | <button class="bw-btn bw-btn-publish bw-insert-image-btn" data-wp-on--click="actions.insertImageFromUrl"><?php esc_html_e( 'Insert image', 'jetpack-mu-wpcom' ); ?></button> |
| 1603 | |
| 1604 | <!-- Secondary sources: collapsed by default. --> |
| 1605 | <div class="bw-source-expanders"> |
| 1606 | <div class="bw-source-expander"> |
| 1607 | <button |
| 1608 | type="button" |
| 1609 | class="bw-source-trigger" |
| 1610 | aria-controls="bw-library-section" |
| 1611 | data-wp-bind--aria-expanded="state.showLibraryPicker" |
| 1612 | data-wp-on--click="actions.toggleLibraryPicker" |
| 1613 | > |
| 1614 | <span class="bw-source-chevron" aria-hidden="true"></span> |
| 1615 | <?php esc_html_e( 'From your library', 'jetpack-mu-wpcom' ); ?> |
| 1616 | </button> |
| 1617 | <div id="bw-library-section" class="bw-library-section" hidden data-wp-bind--hidden="!state.showLibraryPicker"> |
| 1618 | <label class="bw-visually-hidden" for="bw-library-search"><?php esc_html_e( 'Search your media library', 'jetpack-mu-wpcom' ); ?></label> |
| 1619 | <input |
| 1620 | id="bw-library-search" |
| 1621 | type="search" |
| 1622 | class="bw-library-search" |
| 1623 | placeholder="<?php esc_attr_e( 'Search your library…', 'jetpack-mu-wpcom' ); ?>" |
| 1624 | autocomplete="off" |
| 1625 | data-wp-on--input="actions.searchLibrary" |
| 1626 | data-wp-bind--value="state.librarySearch" |
| 1627 | /> |
| 1628 | <div |
| 1629 | id="bw-library-grid" |
| 1630 | class="bw-library-strip" |
| 1631 | role="group" |
| 1632 | aria-label="<?php esc_attr_e( 'Your media library', 'jetpack-mu-wpcom' ); ?>" |
| 1633 | data-wp-on--click="actions.selectLibraryImage" |
| 1634 | ></div> |
| 1635 | <div class="bw-library-status" role="status" aria-live="polite" data-wp-text="state.libraryStatus"></div> |
| 1636 | </div> |
| 1637 | </div> |
| 1638 | <div class="bw-source-expander"> |
| 1639 | <button |
| 1640 | type="button" |
| 1641 | class="bw-source-trigger" |
| 1642 | aria-controls="bw-url-section" |
| 1643 | data-wp-bind--aria-expanded="state.showUrlInput" |
| 1644 | data-wp-on--click="actions.toggleUrlInput" |
| 1645 | > |
| 1646 | <span class="bw-source-chevron" aria-hidden="true"></span> |
| 1647 | <?php esc_html_e( 'Paste an image URL', 'jetpack-mu-wpcom' ); ?> |
| 1648 | </button> |
| 1649 | <div id="bw-url-section" class="bw-url-section" hidden data-wp-bind--hidden="!state.showUrlInput"> |
| 1650 | <input |
| 1651 | type="url" |
| 1652 | class="bw-image-url-input" |
| 1653 | placeholder="<?php esc_attr_e( 'https://…', 'jetpack-mu-wpcom' ); ?>" |
| 1654 | data-wp-on--input="actions.updateImageUrl" |
| 1655 | data-wp-bind--value="state.imageUrl" |
| 1656 | /> |
| 1657 | </div> |
| 1658 | </div> |
| 1659 | </div> |
| 1660 | |
| 1661 | </div> |
| 1662 | </div> |
| 1663 | |
| 1664 | <!-- Image edit panel: non-modal, docked bottom-right. Changes apply |
| 1665 | live to the figure so the rest of the editor stays visible. --> |
| 1666 | <div |
| 1667 | class="bw-image-edit-panel" |
| 1668 | hidden |
| 1669 | data-wp-bind--hidden="!state.isEditMode" |
| 1670 | role="dialog" |
| 1671 | aria-modal="false" |
| 1672 | aria-labelledby="bw-edit-panel-title" |
| 1673 | data-wp-watch="callbacks.syncEditImageModalRadios" |
| 1674 | > |
| 1675 | <div class="bw-edit-panel-header"> |
| 1676 | <h3 id="bw-edit-panel-title" class="bw-edit-panel-title"><?php esc_html_e( 'Edit image', 'jetpack-mu-wpcom' ); ?></h3> |
| 1677 | <button class="bw-edit-panel-close" type="button" data-wp-on--click="actions.closeImageModal" aria-label="<?php esc_attr_e( 'Close', 'jetpack-mu-wpcom' ); ?>" title="<?php esc_attr_e( 'Close', 'jetpack-mu-wpcom' ); ?>">×</button> |
| 1678 | </div> |
| 1679 | |
| 1680 | <label class="bw-edit-label" for="bw-edit-alt"><?php esc_html_e( 'Alt text', 'jetpack-mu-wpcom' ); ?></label> |
| 1681 | <input |
| 1682 | id="bw-edit-alt" |
| 1683 | type="text" |
| 1684 | class="bw-image-url-input" |
| 1685 | placeholder="<?php esc_attr_e( 'Describe this image…', 'jetpack-mu-wpcom' ); ?>" |
| 1686 | data-wp-on--input="actions.updateImageAlt" |
| 1687 | data-wp-bind--value="state.imageAlt" |
| 1688 | /> |
| 1689 | |
| 1690 | <div class="bw-edit-section" hidden data-wp-bind--hidden="!state.editingImageHasMediaId"> |
| 1691 | <div class="bw-edit-label"><?php esc_html_e( 'Size', 'jetpack-mu-wpcom' ); ?></div> |
| 1692 | <div class="bw-edit-radios" role="radiogroup" aria-label="<?php esc_attr_e( 'Image size', 'jetpack-mu-wpcom' ); ?>" data-wp-on--keydown="actions.handleEditRadiogroupKeyDown"> |
| 1693 | <button type="button" class="bw-edit-size-option" role="radio" aria-checked="false" tabindex="-1" value="thumbnail" data-wp-on--click="actions.setEditImageSize"><?php esc_html_e( 'Thumbnail', 'jetpack-mu-wpcom' ); ?></button> |
| 1694 | <button type="button" class="bw-edit-size-option" role="radio" aria-checked="false" tabindex="-1" value="medium" data-wp-on--click="actions.setEditImageSize"><?php esc_html_e( 'Medium', 'jetpack-mu-wpcom' ); ?></button> |
| 1695 | <button type="button" class="bw-edit-size-option" role="radio" aria-checked="false" tabindex="-1" value="large" data-wp-on--click="actions.setEditImageSize"><?php esc_html_e( 'Large', 'jetpack-mu-wpcom' ); ?></button> |
| 1696 | <button type="button" class="bw-edit-size-option" role="radio" aria-checked="false" tabindex="-1" value="full" data-wp-on--click="actions.setEditImageSize"><?php esc_html_e( 'Full', 'jetpack-mu-wpcom' ); ?></button> |
| 1697 | </div> |
| 1698 | </div> |
| 1699 | |
| 1700 | <div class="bw-edit-section"> |
| 1701 | <div class="bw-edit-label"><?php esc_html_e( 'Alignment', 'jetpack-mu-wpcom' ); ?></div> |
| 1702 | <div class="bw-edit-radios" role="radiogroup" aria-label="<?php esc_attr_e( 'Image alignment', 'jetpack-mu-wpcom' ); ?>" data-wp-on--keydown="actions.handleEditRadiogroupKeyDown"> |
| 1703 | <button type="button" class="bw-edit-align-option" role="radio" aria-checked="false" tabindex="-1" value="left" data-wp-on--click="actions.setEditImageAlign"><?php esc_html_e( 'Left', 'jetpack-mu-wpcom' ); ?></button> |
| 1704 | <button type="button" class="bw-edit-align-option" role="radio" aria-checked="false" tabindex="-1" value="center" data-wp-on--click="actions.setEditImageAlign"><?php esc_html_e( 'Center', 'jetpack-mu-wpcom' ); ?></button> |
| 1705 | <button type="button" class="bw-edit-align-option" role="radio" aria-checked="false" tabindex="-1" value="right" data-wp-on--click="actions.setEditImageAlign"><?php esc_html_e( 'Right', 'jetpack-mu-wpcom' ); ?></button> |
| 1706 | </div> |
| 1707 | </div> |
| 1708 | |
| 1709 | <label class="bw-featured-toggle" hidden data-wp-bind--hidden="!state.editingImageHasMediaId"> |
| 1710 | <input type="checkbox" data-wp-on--change="actions.toggleFeaturedImage" data-wp-bind--checked="state.setAsFeatured" /> |
| 1711 | <span><?php esc_html_e( 'Set as featured image', 'jetpack-mu-wpcom' ); ?></span> |
| 1712 | </label> |
| 1713 | |
| 1714 | <button class="bw-btn bw-btn-publish bw-edit-panel-done" type="button" data-wp-on--click="actions.closeImageModal"><?php esc_html_e( 'Done', 'jetpack-mu-wpcom' ); ?></button> |
| 1715 | </div> |
| 1716 | |
| 1717 | <!-- Leave confirmation — matches @wordpress/components ConfirmDialog --> |
| 1718 | <div class="bw-leave-overlay" hidden data-wp-bind--hidden="!state.showLeaveConfirm" data-wp-on--click="actions.cancelLeave" data-wp-on--keydown="actions.handleLeaveModalKeyDown"> |
| 1719 | <div class="bw-leave-modal" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e( 'Unsaved changes', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.stopPropagation"> |
| 1720 | <p><?php esc_html_e( 'Do you want to save your changes?', 'jetpack-mu-wpcom' ); ?></p> |
| 1721 | <div class="bw-leave-actions"> |
| 1722 | <button class="bw-leave-cancel" data-wp-on--click="actions.cancelLeave"><?php esc_html_e( 'Cancel', 'jetpack-mu-wpcom' ); ?></button> |
| 1723 | <button class="bw-leave-confirm" data-wp-on--click="actions.confirmLeave"><?php esc_html_e( "Don't save", 'jetpack-mu-wpcom' ); ?></button> |
| 1724 | <button class="bw-leave-save" data-wp-on--click="actions.saveAndLeave"><?php esc_html_e( 'Save', 'jetpack-mu-wpcom' ); ?></button> |
| 1725 | </div> |
| 1726 | </div> |
| 1727 | </div> |
| 1728 | |
| 1729 | <!-- Unsupported content warning --> |
| 1730 | <div class="bw-unsupported-overlay" hidden data-wp-bind--hidden="!state.unsupportedWarning" data-wp-on--keydown="actions.handleUnsupportedKeyDown"> |
| 1731 | <div class="bw-unsupported-modal" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e( 'Unsupported formatting', 'jetpack-mu-wpcom' ); ?>" data-wp-bind--aria-describedby="state.unsupportedDescId" data-wp-on--click="actions.stopPropagation"> |
| 1732 | <p class="bw-unsupported-title"><?php esc_html_e( 'This post has formatting that Write can\'t preserve', 'jetpack-mu-wpcom' ); ?></p> |
| 1733 | <p class="bw-unsupported-desc" id="bw-unsupported-desc-classic" hidden data-wp-bind--hidden="!state.isClassicWarning"><?php esc_html_e( 'This post was created in the classic editor. Editing in Write would convert it to the block format, which may change some formatting.', 'jetpack-mu-wpcom' ); ?></p> |
| 1734 | <p class="bw-unsupported-desc" id="bw-unsupported-desc-block" hidden data-wp-bind--hidden="!state.isBlockEditorWarning"><?php esc_html_e( 'This post uses block editor features that the Write editor doesn\'t support. Editing here could lose some of that formatting.', 'jetpack-mu-wpcom' ); ?></p> |
| 1735 | <div class="bw-unsupported-actions"> |
| 1736 | <button class="bw-unsupported-back" data-wp-on--click="actions.goBack"><?php esc_html_e( 'Go back', 'jetpack-mu-wpcom' ); ?></button> |
| 1737 | <button class="bw-unsupported-open-editor" hidden data-wp-bind--hidden="!state.isClassicWarning" data-wp-on--click="actions.openEditor"><?php esc_html_e( 'Open in Classic Editor', 'jetpack-mu-wpcom' ); ?></button> |
| 1738 | <button class="bw-unsupported-open-editor" hidden data-wp-bind--hidden="!state.isBlockEditorWarning" data-wp-on--click="actions.openEditor"><?php esc_html_e( 'Open in Block editor', 'jetpack-mu-wpcom' ); ?></button> |
| 1739 | </div> |
| 1740 | </div> |
| 1741 | </div> |
| 1742 | |
| 1743 | <!-- Slash command menu --> |
| 1744 | <div class="bw-slash-menu" id="bw-slash-menu" role="listbox" aria-label="<?php esc_attr_e( 'Insert block', 'jetpack-mu-wpcom' ); ?>" hidden data-wp-bind--hidden="!state.showSlashMenu"> |
| 1745 | <div class="bw-slash-item" id="bw-slash-opt-heading" role="option" aria-selected="false" data-action="heading" data-wp-on--click="actions.insertHeading" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1746 | <span class="bw-slash-icon" aria-hidden="true">H</span> |
| 1747 | <div><strong><?php esc_html_e( 'Heading', 'jetpack-mu-wpcom' ); ?></strong><span class="bw-slash-desc"><?php esc_html_e( 'Large section heading', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1748 | </div> |
| 1749 | <div class="bw-slash-item" id="bw-slash-opt-image" role="option" aria-selected="false" data-action="image" data-wp-on--click="actions.insertImage" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1750 | <span class="bw-slash-icon" aria-hidden="true">▵</span> |
| 1751 | <div><strong><?php esc_html_e( 'Image', 'jetpack-mu-wpcom' ); ?></strong><span class="bw-slash-desc"><?php esc_html_e( 'Upload or embed an image', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1752 | </div> |
| 1753 | <div class="bw-slash-item" id="bw-slash-opt-quote" role="option" aria-selected="false" data-action="quote" data-wp-on--click="actions.insertQuote" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1754 | <span class="bw-slash-icon" aria-hidden="true">“</span> |
| 1755 | <div><strong><?php esc_html_e( 'Quote', 'jetpack-mu-wpcom' ); ?></strong><span class="bw-slash-desc"><?php esc_html_e( 'Highlight a quote', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1756 | </div> |
| 1757 | <div class="bw-slash-item" id="bw-slash-opt-bulleted-list" role="option" aria-selected="false" data-action="bulleted-list" data-wp-on--click="actions.insertBulletedList" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1758 | <span class="bw-slash-icon" aria-hidden="true">•</span> |
| 1759 | <div><strong><?php esc_html_e( 'Bulleted list', 'jetpack-mu-wpcom' ); ?></strong><span class="bw-slash-desc"><?php esc_html_e( 'An unordered list', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1760 | </div> |
| 1761 | <div class="bw-slash-item" id="bw-slash-opt-numbered-list" role="option" aria-selected="false" data-action="numbered-list" data-wp-on--click="actions.insertNumberedList" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1762 | <span class="bw-slash-icon" aria-hidden="true">1.</span> |
| 1763 | <div><strong><?php esc_html_e( 'Numbered list', 'jetpack-mu-wpcom' ); ?></strong><span class="bw-slash-desc"><?php esc_html_e( 'An ordered list', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1764 | </div> |
| 1765 | <div class="bw-slash-item" id="bw-slash-opt-video" role="option" aria-selected="false" data-action="video" data-wp-on--click="actions.insertVideo" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1766 | <span class="bw-slash-icon" aria-hidden="true">▶</span> |
| 1767 | <div><strong><?php esc_html_e( 'Video', 'jetpack-mu-wpcom' ); ?></strong><span class="bw-slash-desc"><?php esc_html_e( 'Embed a YouTube or Vimeo video', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1768 | </div> |
| 1769 | <div class="bw-slash-item" id="bw-slash-opt-divider" role="option" aria-selected="false" data-action="divider" data-wp-on--click="actions.insertDivider" data-wp-on--mousedown="actions.preventToolbarBlur"> |
| 1770 | <span class="bw-slash-icon" aria-hidden="true">—</span> |
| 1771 | <div><strong><?php esc_html_e( 'Divider', 'jetpack-mu-wpcom' ); ?></strong><span class="bw-slash-desc"><?php esc_html_e( 'A horizontal separator', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1772 | </div> |
| 1773 | </div> |
| 1774 | |
| 1775 | <!-- Video modal --> |
| 1776 | <div class="bw-image-overlay" hidden data-wp-bind--hidden="!state.showVideoModal" data-wp-on--pointerdown="actions.handleOverlayPointerDown" data-wp-on--keydown="actions.handleVideoModalKeyDown"> |
| 1777 | <div class="bw-image-modal" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e( 'Embed a video', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.stopPropagation"> |
| 1778 | <h3><?php esc_html_e( 'Embed a video', 'jetpack-mu-wpcom' ); ?></h3> |
| 1779 | <input |
| 1780 | type="url" |
| 1781 | class="bw-image-url-input" |
| 1782 | placeholder="<?php esc_attr_e( 'Paste a YouTube or Vimeo URL...', 'jetpack-mu-wpcom' ); ?>" |
| 1783 | data-wp-on--input="actions.updateVideoUrl" |
| 1784 | data-wp-on--keydown="actions.handleVideoKeyDown" |
| 1785 | data-wp-bind--value="state.videoUrl" |
| 1786 | /> |
| 1787 | <button class="bw-btn bw-btn-publish" data-wp-on--click="actions.insertVideoEmbed" style="width:100%;margin-top:12px;"><?php esc_html_e( 'Embed video', 'jetpack-mu-wpcom' ); ?></button> |
| 1788 | </div> |
| 1789 | </div> |
| 1790 | |
| 1791 | <!-- Post picker modal --> |
| 1792 | <div class="bw-postpicker-overlay" hidden data-wp-bind--hidden="!state.showPostPicker" data-wp-on--pointerdown="actions.handlePostPickerOverlayClick" data-wp-on--keydown="actions.handlePostPickerKeyDown"> |
| 1793 | <div class="bw-postpicker-modal" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e( 'Open post', 'jetpack-mu-wpcom' ); ?>" data-wp-on--click="actions.stopPropagation"> |
| 1794 | <h3 class="bw-postpicker-title"><?php esc_html_e( 'Open post', 'jetpack-mu-wpcom' ); ?></h3> |
| 1795 | |
| 1796 | <p class="bw-postpicker-error" <?php echo $open_post_error ? '' : 'hidden'; ?> data-wp-bind--hidden="!state.openPostError" data-wp-text="state.openPostError"><?php echo esc_html( $open_post_error ); ?></p> |
| 1797 | |
| 1798 | <?php if ( ! empty( $recent_drafts ) ) : ?> |
| 1799 | <p class="bw-postpicker-label"><?php esc_html_e( 'Last edited', 'jetpack-mu-wpcom' ); ?></p> |
| 1800 | <div class="bw-postpicker-list" role="listbox" aria-label="<?php esc_attr_e( 'Recent drafts', 'jetpack-mu-wpcom' ); ?>"> |
| 1801 | <?php foreach ( $recent_drafts as $idx => $draft ) : ?> |
| 1802 | <button |
| 1803 | class="bw-postpicker-item" |
| 1804 | role="option" |
| 1805 | tabindex="<?php echo 0 === $idx ? '0' : '-1'; ?>" |
| 1806 | data-post-id="<?php echo (int) $draft['id']; ?>" |
| 1807 | data-wp-on--click="actions.openPickedPost" |
| 1808 | > |
| 1809 | <span class="bw-postpicker-item-title<?php echo empty( $draft['title'] ) ? ' bw-postpicker-item-untitled' : ''; ?>"><?php echo esc_html( $draft['title'] ? $draft['title'] : __( 'Untitled', 'jetpack-mu-wpcom' ) ); ?></span> |
| 1810 | <span class="bw-postpicker-item-date" data-modified="<?php echo esc_attr( $draft['modified'] ); ?>"></span> |
| 1811 | </button> |
| 1812 | <?php endforeach; ?> |
| 1813 | </div> |
| 1814 | <div class="bw-image-divider"><span><?php esc_html_e( 'or', 'jetpack-mu-wpcom' ); ?></span></div> |
| 1815 | <?php else : ?> |
| 1816 | <p class="bw-postpicker-empty"><?php esc_html_e( 'No recent drafts', 'jetpack-mu-wpcom' ); ?></p> |
| 1817 | <?php endif; ?> |
| 1818 | |
| 1819 | <div class="bw-postpicker-input-row"> |
| 1820 | <label for="bw-postpicker-url-input" class="bw-visually-hidden"><?php esc_html_e( 'Post URL or ID', 'jetpack-mu-wpcom' ); ?></label> |
| 1821 | <input |
| 1822 | id="bw-postpicker-url-input" |
| 1823 | type="text" |
| 1824 | class="bw-image-url-input" |
| 1825 | placeholder="<?php esc_attr_e( 'Paste a post URL or enter a post ID', 'jetpack-mu-wpcom' ); ?>" |
| 1826 | data-wp-bind--value="state.postPickerUrl" |
| 1827 | data-wp-on--input="actions.updatePostPickerUrl" |
| 1828 | data-wp-on--keydown="actions.handlePostPickerInputKeyDown" |
| 1829 | /> |
| 1830 | <button class="bw-btn bw-btn-publish bw-postpicker-go" data-wp-on--click="actions.submitPostPickerUrl"><?php esc_html_e( 'Go', 'jetpack-mu-wpcom' ); ?></button> |
| 1831 | </div> |
| 1832 | </div> |
| 1833 | </div> |
| 1834 | |
| 1835 | </div> |
| 1836 | <?php |
| 1837 | } |