| @@ -9,25 +9,40 @@ | ||
| 9 | 9 | use FluentCommunity\Modules\Course\Model\CourseLesson; |
| 10 | 10 | |
| 11 | 11 | class FluentBlockEditorHandler |
| 12 | 12 | { |
| 13 | + /** | |
| 14 | + * Extra fcomEditorVars entries contributed by whoever answered the | |
| 15 | + * fluent_community/block_editor_context filter. Stashed in renderCustomEditor() | |
| 16 | + * and merged in gutenberg_editor_scripts_and_styles(), which runs later in the | |
| 17 | + * same request off the wp_enqueue_scripts callback registered below. | |
| 18 | + * | |
| 19 | + * @var array | |
| 20 | + */ | |
| 21 | + private $contextEditorVars = []; | |
| 22 | + | |
| 13 | 23 | public function register() |
| 14 | 24 | { |
| 15 | 25 | add_action('init', function () { |
| 16 | 26 | |
| 17 | - register_post_type('fcom-dummy', [ | |
| 18 | - 'label' => 'Lesson', | |
| 19 | - 'public' => false, | |
| 20 | - 'show_in_rest' => true, | |
| 21 | - 'supports' => ['title', 'editor', 'thumbnail'], | |
| 27 | + $editorPostTypes = apply_filters('fluent_community/block_editor_post_types', [ | |
| 28 | + 'fcom-dummy' => [ | |
| 29 | + 'label' => __('Lesson', 'fluent-community'), | |
| 30 | + 'public' => false, | |
| 31 | + 'show_in_rest' => true, | |
| 32 | + 'supports' => ['title', 'editor', 'thumbnail'], | |
| 33 | + ], | |
| 34 | + 'fcom-lockscreen' => [ | |
| 35 | + 'label' => __('Lock Screen', 'fluent-community'), | |
| 36 | + 'public' => false, | |
| 37 | + 'show_in_rest' => true, | |
| 38 | + 'supports' => ['editor'], | |
| 39 | + ], | |
| 22 | 40 | ]); |
| 23 | 41 | |
| 24 | - register_post_type('fcom-lockscreen', [ | |
| 25 | - 'label' => 'Lockscreen', | |
| 26 | - 'public' => false, | |
| 27 | - 'show_in_rest' => true, | |
| 28 | - 'supports' => ['editor'], | |
| 29 | - ]); | |
| 42 | + foreach ($editorPostTypes as $editorPostType => $editorPostTypeArgs) { | |
| 43 | + register_post_type($editorPostType, $editorPostTypeArgs); | |
| 44 | + } | |
| 30 | 45 | |
| 31 | 46 | if (!isset($_REQUEST['fluent_community_block_editor'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 32 | 47 | return; |
| 33 | 48 | } |
| @@ -50,8 +65,10 @@ | ||
| 50 | 65 | media="screen"/> |
| 51 | 66 | <?php // phpcs:enable WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet ?> |
| 52 | 67 | |
| 53 | 68 | <style> |
| 69 | + /* No admin bar here; WP 7.1 offsets the editor by this var, leaving a gap. */ | |
| 70 | + html { --wp-admin--admin-bar--height: 0; } | |
| 54 | 71 | <?php echo wp_strip_all_tags($this->getColorSchemaCss()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- generated CSS, no user input ?> |
| 55 | 72 | </style> |
| 56 | 73 | |
| 57 | 74 | <?php |
| @@ -99,10 +116,32 @@ | ||
| 99 | 116 | $hasAccess = $space && $space->isAdmin(get_current_user_id(), true); |
| 100 | 117 | } |
| 101 | 118 | } |
| 102 | 119 | |
| 120 | + /* | |
| 121 | + * Extension point for editor contexts registered outside this plugin (the pro | |
| 122 | + * Space Pages module uses it). Handlers matching $context return their own | |
| 123 | + * access decision, post type and the content to seed the simulated post with. | |
| 124 | + */ | |
| 125 | + $contextConfig = apply_filters('fluent_community/block_editor_context', [ | |
| 126 | + 'has_access' => $hasAccess, | |
| 127 | + 'post_type' => $postType, | |
| 128 | + 'has_content' => false, | |
| 129 | + 'title' => '', | |
| 130 | + 'content' => '', | |
| 131 | + ], $context, $data); | |
| 132 | + | |
| 133 | + $hasAccess = !empty($contextConfig['has_access']); | |
| 134 | + | |
| 135 | + $this->contextEditorVars = (array)Arr::get($contextConfig, 'editor_vars', []); | |
| 136 | + | |
| 137 | + $contextPostType = Arr::get($contextConfig, 'post_type'); | |
| 138 | + if ($contextPostType) { | |
| 139 | + $postType = $contextPostType; | |
| 140 | + } | |
| 141 | + | |
| 103 | 142 | if (!$hasAccess) { |
| 104 | - echo '<h3 style="padding: 100px; text-align: center;">Sorry, you do not have access to this page.</h3>'; | |
| 143 | + echo '<h3 style="padding: 100px; text-align: center;">' . esc_html__('Sorry, you do not have access to this page.', 'fluent-community') . '</h3>'; | |
| 105 | 144 | exit(200); |
| 106 | 145 | } |
| 107 | 146 | |
| 108 | 147 | add_filter('should_load_separate_core_block_assets', '__return_false', 20); |
| @@ -131,17 +170,20 @@ | ||
| 131 | 170 | |
| 132 | 171 | if ($lesson) { |
| 133 | 172 | $post->post_title = $lesson->title; |
| 134 | 173 | $post->post_content = $lesson->message ?: '<!-- wp:paragraph --><p> </p><!-- /wp:paragraph -->'; |
| 174 | + } elseif (!empty($contextConfig['has_content'])) { | |
| 175 | + $contextContent = Arr::get($contextConfig, 'content'); | |
| 176 | + $post->post_title = Arr::get($contextConfig, 'title', ''); | |
| 177 | + $post->post_content = $contextContent ? $contextContent : '<!-- wp:paragraph --><p> </p><!-- /wp:paragraph -->'; | |
| 135 | 178 | } |
| 136 | 179 | |
| 137 | - $enqueueHook = 'wp_enqueue_scripts'; | |
| 138 | - | |
| 139 | - if(is_admin()) { | |
| 140 | - $enqueueHook = 'admin_enqueue_scripts'; | |
| 141 | - } | |
| 142 | - | |
| 143 | - add_action($enqueueHook, function () use ($post) { | |
| 180 | + // renderPage() exits during admin_init and manually fires wp_enqueue_scripts in | |
| 181 | + // both the admin and frontend branches. admin_enqueue_scripts never runs here, so | |
| 182 | + // the apiFetch preload must ride wp_enqueue_scripts or the editor falls back to a | |
| 183 | + // live GET /wp/v2/fcom-dummy/{id}?context=edit — which 403s for users without | |
| 184 | + // edit_others_posts (e.g. contributors) on the shared, admin-authored dummy post. | |
| 185 | + add_action('wp_enqueue_scripts', function () use ($post) { | |
| 144 | 186 | wp_enqueue_script('postbox', admin_url('js/postbox.min.js'), array('jquery-ui-sortable'), FLUENT_COMMUNITY_PLUGIN_VERSION, true); |
| 145 | 187 | wp_enqueue_style('dashicons'); |
| 146 | 188 | wp_enqueue_style('media'); |
| 147 | 189 | wp_enqueue_style('admin-menu'); |
| @@ -249,13 +291,9 @@ | ||
| 249 | 291 | |
| 250 | 292 | /** |
| 251 | 293 | * Scripts |
| 252 | 294 | */ |
| 253 | - wp_enqueue_media( | |
| 254 | - array( | |
| 255 | - 'post' => null | |
| 256 | - ) | |
| 257 | - ); | |
| 295 | + wp_enqueue_media(); | |
| 258 | 296 | |
| 259 | 297 | add_filter('user_can_richedit', '__return_true'); |
| 260 | 298 | wp_tinymce_inline_scripts(); |
| 261 | 299 | wp_enqueue_editor(); |
| @@ -278,12 +316,18 @@ | ||
| 278 | 316 | wp_enqueue_style('wp-block-cover'); |
| 279 | 317 | wp_enqueue_style('global-styles-css-custom-properties'); |
| 280 | 318 | wp_enqueue_style('wp-block-spacer'); |
| 281 | 319 | |
| 282 | - wp_register_style('fluent_com_editor_styles', FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/style.css', false, FLUENT_COMMUNITY_PLUGIN_VERSION, 'all'); | |
| 320 | + wp_register_style('fluent_com_editor_styles', FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/style.css', [], FLUENT_COMMUNITY_PLUGIN_VERSION, 'all'); | |
| 283 | 321 | |
| 284 | 322 | add_action('fluent_enqueue_block_editor_assets', 'wp_enqueue_editor_format_library_assets'); |
| 285 | 323 | |
| 324 | + // WP 6.9+ loads the LaTeX-to-MathML converter as a script module; core wires this on | |
| 325 | + // 'enqueue_block_editor_assets', which this standalone editor page never fires. | |
| 326 | + if (function_exists('wp_enqueue_block_editor_script_modules')) { | |
| 327 | + add_action('fluent_enqueue_block_editor_assets', 'wp_enqueue_block_editor_script_modules'); | |
| 328 | + } | |
| 329 | + | |
| 286 | 330 | /** |
| 287 | 331 | * Fires after block assets have been enqueued for the editing interface. |
| 288 | 332 | * |
| 289 | 333 | * Call `add_action` on any hook before 'admin_enqueue_scripts'. |
| @@ -296,8 +340,12 @@ | ||
| 296 | 340 | do_action('fluent_enqueue_block_editor_assets'); |
| 297 | 341 | |
| 298 | 342 | wp_enqueue_script('fcom_editor_custom', FLUENT_COMMUNITY_PLUGIN_URL . 'Modules/Gutenberg/editor/index.js', ['react', 'wp-components', 'wp-compose', 'wp-data', 'wp-edit-post', 'wp-i18n', 'wp-plugins'], FLUENT_COMMUNITY_PLUGIN_VERSION . time(), true); |
| 299 | 343 | wp_localize_script('fcom_editor_custom', 'fcomEditorI18n', $this->getEditorI18nStrings()); |
| 344 | + wp_localize_script('fcom_editor_custom', 'fcomEditorVars', array_merge([ | |
| 345 | + 'video_gate_default_threshold' => \FluentCommunity\Modules\Course\Services\LessonVideoGateService::getDefaultThreshold(), | |
| 346 | + 'can_unfiltered_html' => current_user_can('unfiltered_html') ? 'yes' : 'no', | |
| 347 | + ], $this->contextEditorVars)); | |
| 300 | 348 | } |
| 301 | 349 | |
| 302 | 350 | private function getEditorI18nStrings() |
| 303 | 351 | { |
| @@ -316,16 +364,37 @@ | ||
| 316 | 364 | "User's Profile Link:" => __("User's Profile Link:", 'fluent-community'), |
| 317 | 365 | 'Failed to fetch embed. Please check the URL.' => __('Failed to fetch embed. Please check the URL.', 'fluent-community'), |
| 318 | 366 | 'Video thumbnail' => __('Video thumbnail', 'fluent-community'), |
| 319 | 367 | 'Media embedded successfully' => __('Media embedded successfully', 'fluent-community'), |
| 320 | - 'Edit media' => __('Edit media', 'fluent-community'), | |
| 368 | + '+ Add Media' => __('+ Add Media', 'fluent-community'), | |
| 369 | + 'Add Media' => __('Add Media', 'fluent-community'), | |
| 370 | + 'Edit Media' => __('Edit Media', 'fluent-community'), | |
| 371 | + 'Oembed URL' => __('Oembed URL', 'fluent-community'), | |
| 372 | + 'Video URL' => __('Video URL', 'fluent-community'), | |
| 373 | + 'Supports Vimeo, YouTube, Wistia and more' => __('Supports Vimeo, YouTube, Wistia and more', 'fluent-community'), | |
| 374 | + 'Embedding…' => __('Embedding…', 'fluent-community'), | |
| 375 | + 'Cancel' => __('Cancel', 'fluent-community'), | |
| 376 | + 'Remove' => __('Remove', 'fluent-community'), | |
| 321 | 377 | 'Oembed' => __('Oembed', 'fluent-community'), |
| 322 | 378 | 'Custom HTML' => __('Custom HTML', 'fluent-community'), |
| 323 | 379 | 'Custom HTML Code' => __('Custom HTML Code', 'fluent-community'), |
| 380 | + 'Your account cannot save custom HTML. Scripts, iframes and similar tags are removed on save.' => __('Your account cannot save custom HTML. Scripts, iframes and similar tags are removed on save.', 'fluent-community'), | |
| 324 | 381 | 'Paste an iframe code' => __('Paste an iframe code', 'fluent-community'), |
| 325 | 382 | 'Embed' => __('Embed', 'fluent-community'), |
| 326 | 383 | 'Paste a URL to embed' => __('Paste a URL to embed', 'fluent-community'), |
| 327 | 384 | 'Embed from Vimeo, YouTube, Wistia and more' => __('Embed from Vimeo, YouTube, Wistia and more', 'fluent-community'), |
| 385 | + 'FluentPlayer' => __('FluentPlayer', 'fluent-community'), | |
| 386 | + 'Select Player Block' => __('Select Player Block', 'fluent-community'), | |
| 387 | + 'Use FluentPlayer Block' => __('Use FluentPlayer Block', 'fluent-community'), | |
| 388 | + 'The feature video is managed by the FluentPlayer block in the editor.' => __('The feature video is managed by the FluentPlayer block in the editor.', 'fluent-community'), | |
| 389 | + 'A FluentPlayer block will be added at the top of the lesson content. Add or edit the video directly from that block in the editor.' => __('A FluentPlayer block will be added at the top of the lesson content. Add or edit the video directly from that block in the editor.', 'fluent-community'), | |
| 390 | + 'Video Completion' => __('Video Completion', 'fluent-community'), | |
| 391 | + 'Require video watch to complete' => __('Require video watch to complete', 'fluent-community'), | |
| 392 | + 'Students must watch the video before they can mark this lesson as completed.' => __('Students must watch the video before they can mark this lesson as completed.', 'fluent-community'), | |
| 393 | + 'Auto-complete lesson when the video ends' => __('Auto-complete lesson when the video ends', 'fluent-community'), | |
| 394 | + 'Marks the lesson complete a few seconds after the student watches the video to the end.' => __('Marks the lesson complete a few seconds after the student watches the video to the end.', 'fluent-community'), | |
| 395 | + 'Required watch percentage' => __('Required watch percentage', 'fluent-community'), | |
| 396 | + '100% means the video must be watched to the end.' => __('100% means the video must be watched to the end.', 'fluent-community'), | |
| 328 | 397 | 'Lesson Duration' => __('Lesson Duration', 'fluent-community'), |
| 329 | 398 | 'Minutes' => __('Minutes', 'fluent-community'), |
| 330 | 399 | 'Seconds' => __('Seconds', 'fluent-community'), |
| 331 | 400 | 'View' => __('View', 'fluent-community'), |
| @@ -334,8 +403,32 @@ | ||
| 334 | 403 | "User's Name" => __("User's Name", 'fluent-community'), |
| 335 | 404 | "User's Email" => __("User's Email", 'fluent-community'), |
| 336 | 405 | "User's photo HTML" => __("User's photo HTML", 'fluent-community'), |
| 337 | 406 | 'Profile Link' => __('Profile Link', 'fluent-community'), |
| 407 | + // Space page settings panel | |
| 408 | + 'Who can view this page' => __('Who can view this page', 'fluent-community'), | |
| 409 | + 'Everyone who can view the space' => __('Everyone who can view the space', 'fluent-community'), | |
| 410 | + 'Space members only' => __('Space members only', 'fluent-community'), | |
| 411 | + 'URL Slug' => __('URL Slug', 'fluent-community'), | |
| 412 | + 'Changing the slug will break existing links to this page.' => __('Changing the slug will break existing links to this page.', 'fluent-community'), | |
| 413 | + 'Show in space menu' => __('Show in space menu', 'fluent-community'), | |
| 414 | + 'Menu Label' => __('Menu Label', 'fluent-community'), | |
| 415 | + 'Leave empty to use the page title.' => __('Leave empty to use the page title.', 'fluent-community'), | |
| 416 | + 'SEO' => __('SEO', 'fluent-community'), | |
| 417 | + 'SEO Description' => __('SEO Description', 'fluent-community'), | |
| 418 | + 'Used for search engines and link previews' => __('Used for search engines and link previews', 'fluent-community'), | |
| 419 | + // Space page layout picker. The template labels and descriptions are | |
| 420 | + // localized by whoever registers them (SpacePageHelper::getLayoutTemplates | |
| 421 | + // in pro); only the fallback names used when pro is absent live here. | |
| 422 | + 'Page Layout' => __('Page Layout', 'fluent-community'), | |
| 423 | + 'Show the page title' => __('Show the page title', 'fluent-community'), | |
| 424 | + 'Show the featured image' => __('Show the featured image', 'fluent-community'), | |
| 425 | + 'The title stays editable here and still names the page in the menu.' => __('The title stays editable here and still names the page in the menu.', 'fluent-community'), | |
| 426 | + 'The image still appears in search results and link previews.' => __('The image still appears in search results and link previews.', 'fluent-community'), | |
| 427 | + 'Standard' => __('Standard', 'fluent-community'), | |
| 428 | + 'Classic' => __('Classic', 'fluent-community'), | |
| 429 | + 'Full Width' => __('Full Width', 'fluent-community'), | |
| 430 | + 'Unified' => __('Unified', 'fluent-community'), | |
| 338 | 431 | ]; |
| 339 | 432 | |
| 340 | 433 | return apply_filters('fluent_community/editor_i18n_strings', $strings); |
| 341 | 434 | } |
| @@ -368,11 +461,14 @@ | ||
| 368 | 461 | |
| 369 | 462 | add_action('fluent_community/block_editor_footer', function () { |
| 370 | 463 | wp_underscore_playlist_templates(); |
| 371 | 464 | wp_print_footer_scripts(); |
| 465 | + $this->printScriptModules(); | |
| 372 | 466 | wp_print_media_templates(); |
| 373 | 467 | wp_enqueue_global_styles(); |
| 374 | - wp_enqueue_stored_styles(); | |
| 468 | + if (function_exists('wp_enqueue_stored_styles')) { | |
| 469 | + wp_enqueue_stored_styles(); | |
| 470 | + } | |
| 375 | 471 | wp_maybe_inline_styles(); |
| 376 | 472 | }); |
| 377 | 473 | |
| 378 | 474 | add_action('fluent_block_editor/head', 'wp_enqueue_scripts', 1); |
| @@ -406,8 +502,30 @@ | ||
| 406 | 502 | </html> |
| 407 | 503 | <?php |
| 408 | 504 | } |
| 409 | 505 | |
| 506 | + /** | |
| 507 | + * Core prints script modules on 'wp_footer' / 'admin_print_footer_scripts'. This editor page | |
| 508 | + * renders its own document and fires neither, so blocks relying on script modules (core/math | |
| 509 | + * and its LaTeX-to-MathML converter on WP 6.9+) would never load. | |
| 510 | + */ | |
| 511 | + protected function printScriptModules() | |
| 512 | + { | |
| 513 | + if (!function_exists('wp_script_modules')) { | |
| 514 | + return; | |
| 515 | + } | |
| 516 | + | |
| 517 | + $scriptModules = wp_script_modules(); | |
| 518 | + | |
| 519 | + $scriptModules->print_import_map(); | |
| 520 | + $scriptModules->print_enqueued_script_modules(); | |
| 521 | + $scriptModules->print_script_module_preloads(); | |
| 522 | + | |
| 523 | + if (method_exists($scriptModules, 'print_script_module_translations')) { | |
| 524 | + $scriptModules->print_script_module_translations(); | |
| 525 | + } | |
| 526 | + } | |
| 527 | + | |
| 410 | 528 | private function shouldBlockAsset(string $src, string $pluginUrl, string $themesUrl, string $approvedPattern): bool |
| 411 | 529 | { |
| 412 | 530 | $isPlugin = strpos($src, $pluginUrl) !== false; |
| 413 | 531 | $isTheme = strpos($src, $themesUrl) !== false; |
| @@ -420,24 +538,19 @@ | ||
| 420 | 538 | } |
| 421 | 539 | |
| 422 | 540 | private function unloadOtherScripts() |
| 423 | 541 | { |
| 542 | + // skips scripts and styles both; fluent_community/skip_no_conflict is styles only | |
| 424 | 543 | $isSkip = apply_filters('fluent_com_editor/skip_no_conflict', false); |
| 425 | 544 | if ($isSkip) { |
| 426 | 545 | return; |
| 427 | 546 | } |
| 428 | 547 | |
| 429 | - /** | |
| 430 | - * Define the list of approved slugs for FluentCRM assets. | |
| 431 | - * | |
| 432 | - * This filter allows modification of the list of slugs that are approved for FluentCRM assets. | |
| 433 | - * | |
| 434 | - * @param array $approvedSlugs An array of approved slugs for FluentCRM assets. | |
| 435 | - */ | |
| 548 | + // scripts only; styles use fluent_community/asset_listed_slugs | |
| 436 | 549 | $approvedSlugs = apply_filters('fluent_com_editor/asset_listed_slugs', [ |
| 437 | 550 | '\/gutenberg\/' |
| 438 | 551 | ]); |
| 439 | - $approvedSlugs[] = 'fluent-community'; | |
| 552 | + $approvedSlugs[] = '\/fluent-community(-pro)?\/'; | |
| 440 | 553 | $approvedSlugs = array_unique($approvedSlugs); |
| 441 | 554 | $approvedSlugs = implode('|', $approvedSlugs); |
| 442 | 555 | |
| 443 | 556 | $pluginUrl = str_replace(['http:', 'https:'], '', plugins_url()); |
| @@ -485,13 +598,14 @@ | ||
| 485 | 598 | if (!$wp_styles) { |
| 486 | 599 | return; |
| 487 | 600 | } |
| 488 | 601 | |
| 602 | + // styles only; scripts use fluent_com_editor/asset_listed_slugs | |
| 489 | 603 | $approvedSlugs = apply_filters('fluent_community/asset_listed_slugs', [ |
| 490 | 604 | '\/gutenberg\/', |
| 491 | 605 | ]); |
| 492 | 606 | |
| 493 | - $approvedSlugs[] = '\/fluent-community\/'; | |
| 607 | + $approvedSlugs[] = '\/fluent-community(-pro)?\/'; | |
| 494 | 608 | |
| 495 | 609 | $approvedSlugs = array_unique($approvedSlugs); |
| 496 | 610 | $approvedSlugs = implode('|', $approvedSlugs); |
| 497 | 611 | |
| @@ -612,8 +726,9 @@ | ||
| 612 | 726 | 'core/code', |
| 613 | 727 | 'core/columns', |
| 614 | 728 | 'core/column', |
| 615 | 729 | 'core/cover', |
| 730 | + 'core/details', | |
| 616 | 731 | 'core/embed', |
| 617 | 732 | 'core/footnotes', |
| 618 | 733 | 'core/freeform', |
| 619 | 734 | 'core/gallery', |
| @@ -620,11 +735,11 @@ | ||
| 620 | 735 | 'core/group', |
| 621 | 736 | 'core/heading', |
| 622 | 737 | 'core/html', |
| 623 | 738 | 'core/image', |
| 624 | - 'core/latest-posts', | |
| 625 | 739 | 'core/list', |
| 626 | 740 | 'core/list-item', |
| 741 | + 'core/math', | |
| 627 | 742 | 'core/media-text', |
| 628 | 743 | 'core/missing', |
| 629 | 744 | 'core/paragraph', |
| 630 | 745 | 'core/preformatted', |
| @@ -629,17 +744,14 @@ | ||
| 629 | 744 | 'core/paragraph', |
| 630 | 745 | 'core/preformatted', |
| 631 | 746 | 'core/pullquote', |
| 632 | 747 | 'core/quote', |
| 633 | - 'core/rss', | |
| 634 | 748 | 'core/separator', |
| 635 | 749 | 'core/social-link', |
| 636 | 750 | 'core/social-links', |
| 637 | 751 | 'core/spacer', |
| 638 | 752 | 'core/table', |
| 639 | - 'core/text-columns', | |
| 640 | - 'core/verse', | |
| 641 | - 'core/freeform' | |
| 753 | + 'core/verse' | |
| 642 | 754 | ]), |
| 643 | 755 | 'gradients' => [], |
| 644 | 756 | 'imageDefaultSize' => 'large', |
| 645 | 757 | 'imageEditing' => true, |