├── .dev-lib ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .gitmodules ├── .jscsrc ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── composer.json ├── core-media-widgets.php ├── package.json ├── php └── class-media-widgets-wp-cli-command.php ├── phpcs.xml.dist ├── phpunit.xml.dist ├── readme.md ├── readme.txt ├── tests ├── phpunit │ ├── data │ │ ├── small-audio.mp3 │ │ └── small-video.mp4 │ ├── test-class-wp-widget-media-audio.php │ ├── test-class-wp-widget-media-gallery.php │ ├── test-class-wp-widget-media-image.php │ ├── test-class-wp-widget-media-video.php │ └── test-class-wp-widget-media.php └── qunit │ ├── index.html │ ├── test-media-gallery-widget.js │ ├── test-media-image-widget.js │ ├── test-media-video-widget.js │ ├── test-media-widgets.js │ └── test-suite.template ├── wp-admin ├── css │ └── widgets │ │ ├── media-gallery-widget.css │ │ └── media-widgets.css └── js │ └── widgets │ ├── media-audio-widget.js │ ├── media-gallery-widget.js │ ├── media-image-widget.js │ ├── media-video-widget.js │ ├── media-widgets.js │ └── text-widgets.js └── wp-includes ├── js └── customize-selective-refresh-extras.js └── widgets ├── class-wp-widget-media-audio.php ├── class-wp-widget-media-gallery.php ├── class-wp-widget-media-image.php ├── class-wp-widget-media-video.php ├── class-wp-widget-media.php └── class-wp-widget-visual-text.php /.dev-lib: -------------------------------------------------------------------------------- 1 | CHECK_SCOPE=changed-files 2 | PHPCS_RULESET_FILE=phpcs.xml.dist 3 | WPCS_BRANCH=develop 4 | PHPCS_PHAR_URL=https://github.com/squizlabs/PHP_CodeSniffer/releases/download/2.9.0/phpcs.phar 5 | 6 | if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.2" ]]; then 7 | DEV_LIB_SKIP="$DEV_LIB_SKIP,phpcs" 8 | fi 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | dev-lib/.editorconfig -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dev-lib/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "globals": { 6 | "_": false, 7 | "Backbone": false, 8 | "jQuery": false, 9 | "wp": false 10 | }, 11 | "rules": { 12 | "accessor-pairs": [2], 13 | "block-scoped-var": [2], 14 | "callback-return": [2], 15 | "complexity": [2, 8], 16 | "consistent-return": [2], 17 | "consistent-this": [2, "self"], 18 | "constructor-super": [2], 19 | "default-case": [2], 20 | "eqeqeq": [2], 21 | "func-style": [0], 22 | "global-require": [2], 23 | "guard-for-in": [0], 24 | "handle-callback-err": [2, "^err(or)?$"], 25 | "id-length": [0], 26 | "id-match": [0], 27 | "indent": ["error", "tab"], 28 | "init-declarations": [0], 29 | "max-depth": [2, 3], 30 | "max-nested-callbacks": [2, 3], 31 | "max-params": [2, 4], 32 | "max-statements": [0], 33 | "new-parens": [0], 34 | "no-alert": [2], 35 | "no-array-constructor": [0], 36 | "no-bitwise": [0], 37 | "no-caller": [2], 38 | "no-case-declarations": [2], 39 | "no-catch-shadow": [2], 40 | "no-class-assign": [2], 41 | "no-cond-assign": [2], 42 | "no-console": [0], 43 | "no-const-assign": [2], 44 | "no-constant-condition": [0], 45 | "no-continue": [0], 46 | "no-control-regex": [2], 47 | "no-debugger": [2], 48 | "no-delete-var": [2], 49 | "no-div-regex": [0], 50 | "no-dupe-args": [2], 51 | "no-dupe-class-members": [2], 52 | "no-dupe-keys": [2], 53 | "no-duplicate-case": [2], 54 | "no-else-return": [0], 55 | "no-empty-character-class": [2], 56 | "no-empty-pattern": [2], 57 | "no-empty": [2], 58 | "no-eq-null": [2], 59 | "no-eval": [2], 60 | "no-ex-assign": [2], 61 | "no-extend-native": [0], 62 | "no-extra-bind": [2], 63 | "no-extra-boolean-cast": [2], 64 | "no-extra-parens": [2], 65 | "no-extra-semi": [2], 66 | "no-fallthrough": [2], 67 | "no-floating-decimal": [2], 68 | "no-func-assign": [2], 69 | "no-implicit-coercion": [2], 70 | "no-implicit-globals": [0], 71 | "no-implied-eval": [2], 72 | "no-inline-comments": [0], 73 | "no-inner-declarations": [2], 74 | "no-invalid-regexp": [2], 75 | "no-invalid-this": [0], 76 | "no-irregular-whitespace": [2], 77 | "no-iterator": [2], 78 | "no-label-var": [2], 79 | "no-labels": [0], 80 | "no-lone-blocks": [2], 81 | "no-lonely-if": [2], 82 | "no-loop-func": [2], 83 | "no-magic-numbers": [2, { "ignoreArrayIndexes": true, "ignore": [ -1, 0, 1 ] }], 84 | "no-mixed-requires": [0], 85 | "no-multi-str": [2], 86 | "no-native-reassign": [2], 87 | "no-negated-condition": [0], 88 | "no-negated-in-lhs": [2], 89 | "no-nested-ternary": [0], 90 | "no-new-func": [0], 91 | "no-new-object": [2], 92 | "no-new-require": [0], 93 | "no-new-wrappers": [2], 94 | "no-new": [2], 95 | "no-obj-calls": [2], 96 | "no-octal-escape": [2], 97 | "no-octal": [2], 98 | "no-param-reassign": [2], 99 | "no-path-concat": [2], 100 | "no-plusplus": [0], 101 | "no-process-env": [2], 102 | "no-process-exit": [0], 103 | "no-proto": [2], 104 | "no-redeclare": [2], 105 | "no-regex-spaces": [0], 106 | "no-restricted-imports": [0], 107 | "no-restricted-syntax": [0], 108 | "no-return-assign": [2], 109 | "no-script-url": [0], 110 | "no-self-compare": [2], 111 | "no-sequences": [2], 112 | "no-shadow-restricted-names": [2], 113 | "no-shadow": [2], 114 | "no-sparse-arrays": [2], 115 | "no-sync": [0], 116 | "no-ternary": [0], 117 | "no-trailing-spaces": [2], 118 | "no-this-before-super": [2], 119 | "no-throw-literal": [2], 120 | "no-undef-init": [0], 121 | "no-undef": [2], 122 | "no-undefined": [0], 123 | "no-unneeded-ternary": [2], 124 | "no-unreachable": [2], 125 | "no-unused-expressions": [2], 126 | "no-unused-vars": [2], 127 | "no-use-before-define": [0], 128 | "no-useless-call": [2], 129 | "no-useless-concat": [2], 130 | "no-var": [0], 131 | "no-void": [0], 132 | "no-with": [2], 133 | "object-shorthand": [0], 134 | "one-var": [ 2, "always" ], 135 | "operator-assignment": [2, "always"], 136 | "prefer-arrow-callback": [0], 137 | "prefer-const": [0], 138 | "prefer-reflect": [0], 139 | "prefer-rest-params": [0], 140 | "prefer-spread": [0], 141 | "prefer-template": [0], 142 | "quotes": [0], 143 | "radix": [2, "always"], 144 | "require-jsdoc": [2, { 145 | "require": { 146 | "FunctionDeclaration": true, 147 | "MethodDefinition": true, 148 | "ClassDeclaration": true, 149 | "ArrowFunctionExpression": false 150 | } 151 | } ], 152 | "require-yield": [0], 153 | "sort-imports": [0], 154 | "sort-vars": [0], 155 | "strict": [2, "function"], 156 | "use-isnan": [2], 157 | "valid-typeof": [2], 158 | "valid-jsdoc": [2, { 159 | "prefer": { 160 | "arg": "param", 161 | "argument": "param", 162 | "class": "constructor", 163 | "return": "returns", 164 | "virtual": "abstract" 165 | }, 166 | "requireParamDescription": true, 167 | "requireReturnDescription": true, 168 | "requireReturn": true, 169 | "requireReturnType": true, 170 | "preferType": { 171 | "Boolean": "boolean", 172 | "Number": "number", 173 | "object": "Object", 174 | "String": "string" 175 | } 176 | }], 177 | "vars-on-top": [0], 178 | "wrap-iife": [2, "inside"], 179 | "wrap-regex": [0], 180 | "yoda": [0] 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /vendor 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dev-lib"] 2 | path = dev-lib 3 | url = https://github.com/xwp/wp-dev-lib.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "wordpress", 3 | "jsDoc": { 4 | "checkAnnotations": "jsdoc3", 5 | "checkParamExistence": true, 6 | "checkParamNames": true, 7 | "requireParamTypes": true, 8 | "checkRedundantParams": true, 9 | "checkReturnTypes": true, 10 | "requireReturnTypes": true, 11 | "checkTypes": "strictNativeCase", 12 | "checkRedundantAccess": "enforceLeadingUnderscore", 13 | "leadingUnderscoreAccess": "private", 14 | "requireHyphenBeforeDescription": true, 15 | "requireNewlineAfterDescription": true, 16 | "requireParamDescription": true 17 | }, 18 | "excludeFiles": [ 19 | "**/*.min.js", 20 | "**/*.jsx", 21 | "**/node_modules/**", 22 | "**/vendor/**" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | **/*.min.js 2 | **/node_modules/** 3 | **/vendor/** 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | dev-lib/.jshintrc -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: precise 3 | 4 | notifications: 5 | email: 6 | on_success: never 7 | on_failure: change 8 | 9 | cache: 10 | directories: 11 | - node_modules 12 | - vendor 13 | 14 | language: 15 | - php 16 | - node_js 17 | 18 | php: 19 | - 5.2 20 | - 7.0 21 | 22 | env: 23 | - WP_VERSION=trunk WP_MULTISITE=0 24 | - WP_VERSION=trunk WP_MULTISITE=1 25 | 26 | install: 27 | - nvm install 6 && nvm use 6 28 | - export DEV_LIB_PATH=dev-lib 29 | - if [ ! -e "$DEV_LIB_PATH" ] && [ -L .travis.yml ]; then export DEV_LIB_PATH=$( dirname $( readlink .travis.yml ) ); fi 30 | - if [ ! -e "$DEV_LIB_PATH" ]; then git clone https://github.com/xwp/wp-dev-lib.git $DEV_LIB_PATH; fi 31 | - source $DEV_LIB_PATH/travis.install.sh 32 | 33 | script: 34 | - source $DEV_LIB_PATH/travis.script.sh 35 | 36 | after_script: 37 | - source $DEV_LIB_PATH/travis.after_script.sh 38 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global require */ 2 | 3 | var grunt = require( 'grunt' ); 4 | 5 | grunt.initConfig({ 6 | qunit: { 7 | all: [ 'tests/qunit/**/*.html' ] 8 | } 9 | }); 10 | 11 | grunt.loadNpmTasks( 'grunt-contrib-qunit' ); 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xwp/wp-core-media-widgets", 3 | "version": "0.2.0", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-2.0", 6 | "homepage": "https://github.com/xwp/wp-core-media-widgets", 7 | "repositories": [ 8 | { 9 | "type": "git", 10 | "url": "https://github.com/xwp/wp-core-media-widgets.git" 11 | } 12 | ], 13 | "dist": { 14 | "url": "https://downloads.wordpress.org/plugin/wp-core-media-widgets.zip", 15 | "type": "zip" 16 | }, 17 | "require": { 18 | "php": ">=5.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core-media-widgets.php: -------------------------------------------------------------------------------- 1 | add( 'text-widgets', plugin_dir_url( __FILE__ ) . 'wp-admin/js/widgets/text-widgets.js', array( 'jquery', 'backbone', 'editor', 'wp-util' ) ); 46 | $scripts->add_inline_script( 'text-widgets', 'wp.textWidgets.init();', 'after' ); 47 | } 48 | 49 | $handle = 'media-widgets'; 50 | $src = plugin_dir_url( __FILE__ ) . 'wp-admin/js/widgets/media-widgets.js'; 51 | if ( ! $scripts->query( $handle, 'registered' ) ) { 52 | $scripts->add( $handle, $src, array( 'jquery', 'media-models', 'media-views' ) ); 53 | $scripts->add_inline_script( 'media-widgets', 'wp.mediaWidgets.init();', 'after' ); 54 | } 55 | 56 | $handle = 'media-image-widget'; 57 | $src = plugin_dir_url( __FILE__ ) . 'wp-admin/js/widgets/media-image-widget.js'; 58 | if ( ! $scripts->query( $handle, 'registered' ) ) { 59 | $scripts->add( $handle, $src, array( 'media-widgets' ) ); 60 | } 61 | 62 | $handle = 'media-video-widget'; 63 | $src = plugin_dir_url( __FILE__ ) . 'wp-admin/js/widgets/media-video-widget.js'; 64 | if ( ! $scripts->query( $handle, 'registered' ) ) { 65 | $scripts->add( $handle, $src, array( 'media-widgets', 'media-audiovideo' ) ); 66 | } 67 | 68 | $handle = 'media-audio-widget'; 69 | $src = plugin_dir_url( __FILE__ ) . 'wp-admin/js/widgets/media-audio-widget.js'; 70 | if ( ! $scripts->query( $handle, 'registered' ) ) { 71 | $scripts->add( $handle, $src, array( 'media-widgets', 'media-audiovideo' ) ); 72 | } 73 | 74 | $scripts->add( 'media-gallery-widget', plugin_dir_url( __FILE__ ) . 'wp-admin/js/widgets/media-gallery-widget.js', array( 'media-widgets' ) ); 75 | 76 | if ( ! WP_CORE_MEDIA_WIDGETS_MERGED ) { 77 | $scripts->add_inline_script( 'customize-selective-refresh', file_get_contents( dirname( __FILE__ ) . '/wp-includes/js/customize-selective-refresh-extras.js' ) ); 78 | } 79 | } 80 | add_action( 'wp_default_scripts', 'wp32417_default_scripts' ); 81 | 82 | /** 83 | * Add filters that will eventually reside in default-filters.php 84 | */ 85 | function wp32417_add_default_filters() { 86 | add_filter( 'widget_text_content', 'capital_P_dangit', 11 ); 87 | add_filter( 'widget_text_content', 'wptexturize' ); 88 | add_filter( 'widget_text_content', 'convert_smilies', 20 ); 89 | add_filter( 'widget_text_content', 'wpautop' ); 90 | } 91 | if ( ! WP_CORE_VISUAL_TEXT_WIDGET_MERGED ) { 92 | add_action( 'plugins_loaded', 'wp32417_add_default_filters' ); 93 | } 94 | 95 | /** 96 | * Register widget styles. 97 | * 98 | * @codeCoverageIgnore 99 | * @param WP_Styles $styles Styles. 100 | */ 101 | function wp32417_default_styles( WP_Styles $styles ) { 102 | $handle = 'media-widgets'; 103 | if ( ! WP_CORE_MEDIA_WIDGETS_MERGED ) { 104 | $src = plugin_dir_url( __FILE__ ) . 'wp-admin/css/widgets/media-widgets.css'; 105 | $styles->add( $handle, $src, array( 'media-views' ) ); 106 | } 107 | 108 | $handle = 'media-gallery-widget'; 109 | if ( ! WP_CORE_GALLERY_WIDGET_MERGED ) { 110 | $src = plugin_dir_url( __FILE__ ) . 'wp-admin/css/widgets/media-gallery-widget.css'; 111 | $styles->add( $handle, $src, array( 'media-views' ) ); 112 | } 113 | } 114 | add_action( 'wp_default_styles', 'wp32417_default_styles' ); 115 | 116 | /** 117 | * Style fixes for default themes. 118 | * 119 | * @codeCoverageIgnore 120 | */ 121 | function wp32417_custom_theme_styles() { 122 | if ( 'twentyten' === get_template() ) { 123 | add_action( 'wp_head', 'wp32417_twentyten_styles' ); 124 | } 125 | } 126 | if ( ! WP_CORE_MEDIA_WIDGETS_MERGED ) { 127 | add_action( 'wp_enqueue_scripts', 'wp32417_custom_theme_styles', 11 ); 128 | } 129 | 130 | /** 131 | * Style fixes for Twenty Ten. 132 | * 133 | * @codeCoverageIgnore 134 | */ 135 | function wp32417_twentyten_styles() { 136 | echo ''; 137 | } 138 | 139 | /** 140 | * Register widget. 141 | * 142 | * @codeCoverageIgnore 143 | */ 144 | function wp32417_widgets_init() { 145 | $class_files = array( 146 | 'WP_Widget_Media' => dirname( __FILE__ ) . '/wp-includes/widgets/class-wp-widget-media.php', 147 | 'WP_Widget_Media_Image' => dirname( __FILE__ ) . '/wp-includes/widgets/class-wp-widget-media-image.php', 148 | 'WP_Widget_Media_Video' => dirname( __FILE__ ) . '/wp-includes/widgets/class-wp-widget-media-video.php', 149 | 'WP_Widget_Media_Audio' => dirname( __FILE__ ) . '/wp-includes/widgets/class-wp-widget-media-audio.php', 150 | 'WP_Widget_Media_Gallery' => dirname( __FILE__ ) . '/wp-includes/widgets/class-wp-widget-media-gallery.php', 151 | ); 152 | foreach ( $class_files as $class => $file ) { 153 | if ( ! class_exists( $class ) ) { 154 | require_once( $file ); 155 | 156 | if ( 'WP_Widget_Media' !== $class ) { 157 | register_widget( $class ); 158 | } 159 | } 160 | } 161 | 162 | if ( function_exists( 'wp_enqueue_editor' ) && ! WP_CORE_VISUAL_TEXT_WIDGET_MERGED ) { 163 | require_once( dirname( __FILE__ ) . '/wp-includes/widgets/class-wp-widget-visual-text.php' ); 164 | unregister_widget( 'WP_Widget_Text' ); 165 | register_widget( 'WP_Widget_Visual_Text' ); 166 | } 167 | } 168 | add_action( 'widgets_init', 'wp32417_widgets_init', 0 ); 169 | 170 | /** 171 | * Add align class name to the alignment container in .attachment-display-settings. 172 | * 173 | * @see wp_print_media_templates() 174 | * @todo For Core merge, this should be patched in \wp_print_media_templates(). 175 | */ 176 | function wp32417_add_classname_to_display_settings() { 177 | ?> 178 | 186 | 198 | 209 | query( $script_handle, 'registered' ) ) { 91 | WP_CLI::error( "Script handle not registered: $script_handle" ); 92 | } 93 | } 94 | 95 | add_filter( 'script_loader_tag', array( __CLASS__, 'filter_script_loader_tag' ), 10, 2 ); 96 | wp_enqueue_media(); 97 | 98 | ob_start(); 99 | echo "\n"; 111 | $output = ob_get_clean(); 112 | 113 | $output = preg_replace( '//', '', $output ); 114 | $output = preg_replace( '#]*>.+?#s', '', $output ); 115 | 116 | $test_suite = file_get_contents( $test_suite_template ); 117 | $test_suite = preg_replace( '/(?=\n", $test_suite ); 118 | $test_suite = str_replace( '{{dependencies}}', $output, $test_suite ); 119 | file_put_contents( $test_suite_file, $test_suite ); 120 | 121 | remove_filter( 'script_loader_tag', array( __CLASS__, 'filter_script_loader_tag' ), 10 ); 122 | 123 | WP_CLI::success( sprintf( 'Wrote test suite to %s', realpath( $test_suite_file ) ) ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | php/class-media-widgets-wp-cli-command.php 14 | 15 | 16 | 17 | 18 | php/class-media-widgets-wp-cli-command.php 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | */tests/* 36 | 37 | 38 | 39 | 40 | core-media-widgets.php 41 | 42 | 43 | */dev-lib/* 44 | */node_modules/* 45 | */vendor/* 46 | 47 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | dev-lib/phpunit-plugin.xml -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Core Media Widgets 3 | 4 | Feature plugin for introducing new core media widgets for images, audio, and video. 5 | 6 | **Contributors:** [wordpressdotorg](https://profiles.wordpress.org/wordpressdotorg) 7 | **Tags:** [sidebar](https://wordpress.org/plugins/tags/sidebar), [widget](https://wordpress.org/plugins/tags/widget), [images](https://wordpress.org/plugins/tags/images), [video](https://wordpress.org/plugins/tags/video), [audio](https://wordpress.org/plugins/tags/audio), [tinymce](https://wordpress.org/plugins/tags/tinymce) 8 | **Requires at least:** 4.8.0 9 | **Tested up to:** 4.9.0-alpha 10 | **Stable tag:** master 11 | **License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html) 12 | 13 | [![Build Status](https://travis-ci.org/xwp/wp-core-media-widgets.svg?branch=master)](https://travis-ci.org/xwp/wp-core-media-widgets) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com) 14 | 15 | ## Description ## 16 | 17 | See Trac tickets: 18 | 19 | * _NEW_ [#35243](https://core.trac.wordpress.org/ticket/41914): Add gallery widget. 20 | * [#32417](https://core.trac.wordpress.org/ticket/32417): Add new core media widget 21 | * [#39993](https://core.trac.wordpress.org/ticket/39993): Introduce Core Widget: Image 22 | * [#39994](https://core.trac.wordpress.org/ticket/39994): Introduce Core Widget: Video 23 | * [#39995](https://core.trac.wordpress.org/ticket/39995): Introduce Core Widget: Audio 24 | * [#35243](https://core.trac.wordpress.org/ticket/35243): Extending the text widget to also allow visual mode 25 | 26 | Media widgets from this plugin are planned to be copied into core and deactivated in this 27 | plugin in future releases. 28 | 29 | For contributing patches to this plugin and for following developments, please see its [GitHub project](https://github.com/xwp/wp-core-media-widgets). 30 | 31 | ## Changelog ## 32 | 33 | ### [0.2.0] - 2018-09-18 ### 34 | Introduce gallery widget. See [#120](https://github.com/xwp/wp-core-media-widgets/pull/120). Fixes [#62](https://github.com/xwp/wp-core-media-widgets/issues/62). 35 | 36 | ### [0.1.0] ### 37 | Work in progress. 38 | 39 | 40 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Core Media Widgets === 2 | Contributors: wordpressdotorg 3 | Tags: sidebar, widget, images, video, audio, tinymce 4 | Requires at least: 4.8.0 5 | Tested up to: 4.9.0-alpha 6 | Stable tag: master 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Feature plugin for introducing new core media widgets for images, audio, and video. 11 | 12 | == Description == 13 | 14 | See Trac tickets: 15 | 16 | * _NEW_ [#35243](https://core.trac.wordpress.org/ticket/41914): Add gallery widget. 17 | * [#32417](https://core.trac.wordpress.org/ticket/32417): Add new core media widget 18 | * [#39993](https://core.trac.wordpress.org/ticket/39993): Introduce Core Widget: Image 19 | * [#39994](https://core.trac.wordpress.org/ticket/39994): Introduce Core Widget: Video 20 | * [#39995](https://core.trac.wordpress.org/ticket/39995): Introduce Core Widget: Audio 21 | * [#35243](https://core.trac.wordpress.org/ticket/35243): Extending the text widget to also allow visual mode 22 | 23 | Media widgets from this plugin are planned to be copied into core and deactivated in this 24 | plugin in future releases. 25 | 26 | For contributing patches to this plugin and for following developments, please see its [GitHub project](https://github.com/xwp/wp-core-media-widgets). 27 | 28 | == Changelog == 29 | 30 | = [0.2.0] - 2018-09-18 = 31 | 32 | Introduce gallery widget. See [#120](https://github.com/xwp/wp-core-media-widgets/pull/120). Fixes [#62](https://github.com/xwp/wp-core-media-widgets/issues/62). 33 | 34 | = [0.1.0] = 35 | 36 | Work in progress. 37 | -------------------------------------------------------------------------------- /tests/phpunit/data/small-audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwp/wp-core-media-widgets/ec7c2788299ba9d84760931135d0d3e1c08ee325/tests/phpunit/data/small-audio.mp3 -------------------------------------------------------------------------------- /tests/phpunit/data/small-video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwp/wp-core-media-widgets/ec7c2788299ba9d84760931135d0d3e1c08ee325/tests/phpunit/data/small-video.mp4 -------------------------------------------------------------------------------- /tests/phpunit/test-class-wp-widget-media-audio.php: -------------------------------------------------------------------------------- 1 | get_instance_schema(); 24 | 25 | $this->assertEqualSets( 26 | array_merge( 27 | array( 28 | 'attachment_id', 29 | 'preload', 30 | 'loop', 31 | 'title', 32 | 'url', 33 | ), 34 | wp_get_audio_extensions() 35 | ), 36 | array_keys( $schema ) 37 | ); 38 | } 39 | 40 | /** 41 | * Test constructor. 42 | * 43 | * @covers WP_Widget_Media_Audio::__construct() 44 | */ 45 | function test_constructor() { 46 | $widget = new WP_Widget_Media_Audio(); 47 | 48 | $this->assertArrayHasKey( 'mime_type', $widget->widget_options ); 49 | $this->assertArrayHasKey( 'customize_selective_refresh', $widget->widget_options ); 50 | $this->assertArrayHasKey( 'description', $widget->widget_options ); 51 | $this->assertTrue( $widget->widget_options['customize_selective_refresh'] ); 52 | $this->assertEquals( 'audio', $widget->widget_options['mime_type'] ); 53 | $this->assertEqualSets( array( 54 | 'add_to_widget', 55 | 'replace_media', 56 | 'edit_media', 57 | 'media_library_state_multi', 58 | 'media_library_state_single', 59 | 'missing_attachment', 60 | 'no_media_selected', 61 | 'add_media', 62 | 'unsupported_file_type', 63 | ), array_keys( $widget->l10n ) ); 64 | } 65 | 66 | /** 67 | * Test get_instance_schema method. 68 | * 69 | * @covers WP_Widget_Media_Audio::update 70 | */ 71 | function test_update() { 72 | $widget = new WP_Widget_Media_Audio(); 73 | $instance = array(); 74 | 75 | // Should return valid attachment ID. 76 | $expected = array( 77 | 'attachment_id' => 1, 78 | ); 79 | $result = $widget->update( $expected, $instance ); 80 | $this->assertSame( $result, $expected ); 81 | 82 | // Should filter invalid attachment ID. 83 | $result = $widget->update( array( 84 | 'attachment_id' => 'media', 85 | ), $instance ); 86 | $this->assertSame( $result, $instance ); 87 | 88 | // Should return valid attachment url. 89 | $expected = array( 90 | 'url' => 'https://chickenandribs.org', 91 | ); 92 | $result = $widget->update( $expected, $instance ); 93 | $this->assertSame( $result, $expected ); 94 | 95 | // Should filter invalid attachment url. 96 | $result = $widget->update( array( 97 | 'url' => 'not_a_url', 98 | ), $instance ); 99 | $this->assertNotSame( $result, $instance ); 100 | $this->assertStringStartsWith( 'http://', $result['url'] ); 101 | 102 | // Should return loop setting. 103 | $expected = array( 104 | 'loop' => true, 105 | ); 106 | $result = $widget->update( $expected, $instance ); 107 | $this->assertSame( $result, $expected ); 108 | 109 | // Should filter invalid loop setting. 110 | $result = $widget->update( array( 111 | 'loop' => 'not-boolean', 112 | ), $instance ); 113 | $this->assertSame( $result, $instance ); 114 | 115 | // Should return valid attachment title. 116 | $expected = array( 117 | 'title' => 'An audio sample of parrots', 118 | ); 119 | $result = $widget->update( $expected, $instance ); 120 | $this->assertSame( $result, $expected ); 121 | 122 | // Should filter invalid attachment title. 123 | $result = $widget->update( array( 124 | 'title' => '

Cute Baby Goats

', 125 | ), $instance ); 126 | $this->assertNotSame( $result, $instance ); 127 | 128 | // Should return valid preload setting. 129 | $expected = array( 130 | 'preload' => 'none', 131 | ); 132 | $result = $widget->update( $expected, $instance ); 133 | $this->assertSame( $result, $expected ); 134 | 135 | // Should filter invalid preload setting. 136 | $result = $widget->update( array( 137 | 'preload' => 'nope', 138 | ), $instance ); 139 | $this->assertSame( $result, $instance ); 140 | 141 | // Should filter invalid key. 142 | $result = $widget->update( array( 143 | 'h4x' => 'value', 144 | ), $instance ); 145 | $this->assertSame( $result, $instance ); 146 | } 147 | 148 | /** 149 | * Test render_media method. 150 | * 151 | * @covers WP_Widget_Media_Audio::render_media 152 | */ 153 | function test_render_media() { 154 | $test_audio_file = __FILE__ . '../data/small-audio.mp3'; 155 | $widget = new WP_Widget_Media_Audio(); 156 | $attachment_id = self::factory()->attachment->create_object( array( 157 | 'file' => $test_audio_file, 158 | 'post_parent' => 0, 159 | 'post_mime_type' => 'audio/mp3', 160 | 'post_title' => 'Test Audio', 161 | ) ); 162 | wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $test_audio_file ) ); 163 | 164 | // Should be empty when there is no attachment_id. 165 | ob_start(); 166 | $widget->render_media( array() ); 167 | $output = ob_get_clean(); 168 | $this->assertEmpty( $output ); 169 | 170 | // Should be empty when there is an invalid attachment_id. 171 | ob_start(); 172 | $widget->render_media( array( 173 | 'attachment_id' => 777, 174 | ) ); 175 | $output = ob_get_clean(); 176 | $this->assertEmpty( $output ); 177 | 178 | // Tests with audio from library. 179 | ob_start(); 180 | $widget->render_media( array( 181 | 'attachment_id' => $attachment_id, 182 | ) ); 183 | $output = ob_get_clean(); 184 | 185 | // Check default outputs. 186 | $this->assertContains( 'preload="none"', $output ); 187 | $this->assertContains( 'class="wp-audio-shortcode"', $output ); 188 | $this->assertContains( 'small-audio.mp3', $output ); 189 | 190 | ob_start(); 191 | $widget->render_media( array( 192 | 'attachment_id' => $attachment_id, 193 | 'title' => 'Funny', 194 | 'preload' => 'auto', 195 | 'loop' => true, 196 | ) ); 197 | $output = ob_get_clean(); 198 | 199 | // Custom attributes. 200 | $this->assertContains( 'preload="auto"', $output ); 201 | $this->assertContains( 'loop="1"', $output ); 202 | } 203 | 204 | /** 205 | * Test enqueue_preview_scripts method. 206 | * 207 | * @global WP_Scripts $wp_scripts 208 | * @global WP_Styles $wp_styles 209 | * @covers WP_Widget_Media_Audio::enqueue_preview_scripts 210 | */ 211 | function test_enqueue_preview_scripts() { 212 | global $wp_scripts, $wp_styles; 213 | $wp_scripts = null; 214 | $wp_styles = null; 215 | $widget = new WP_Widget_Media_Audio(); 216 | 217 | $this->assertFalse( wp_script_is( 'wp-mediaelement' ) ); 218 | $this->assertFalse( wp_style_is( 'wp-mediaelement' ) ); 219 | 220 | $widget->enqueue_preview_scripts(); 221 | 222 | $this->assertTrue( wp_script_is( 'wp-mediaelement' ) ); 223 | $this->assertTrue( wp_style_is( 'wp-mediaelement' ) ); 224 | } 225 | 226 | /** 227 | * Test enqueue_admin_scripts method. 228 | * 229 | * @covers WP_Widget_Media_Audio::enqueue_admin_scripts 230 | */ 231 | function test_enqueue_admin_scripts() { 232 | $widget = new WP_Widget_Media_Audio(); 233 | $widget->enqueue_admin_scripts(); 234 | 235 | $this->assertTrue( wp_script_is( 'media-audio-widget' ) ); 236 | } 237 | 238 | /** 239 | * Test render_control_template_scripts method. 240 | * 241 | * @covers WP_Widget_Media_Audio::render_control_template_scripts 242 | */ 243 | function test_render_control_template_scripts() { 244 | $widget = new WP_Widget_Media_Audio(); 245 | 246 | ob_start(); 247 | $widget->render_control_template_scripts(); 248 | $output = ob_get_clean(); 249 | 250 | $this->assertContains( ' 14 | 15 | 16 | {{dependencies}} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /wp-admin/css/widgets/media-gallery-widget.css: -------------------------------------------------------------------------------- 1 | .media-widget-gallery-preview { 2 | display: flex; 3 | justify-content: flex-start; 4 | flex-wrap: wrap; 5 | } 6 | 7 | .media-widget-preview.gallery .placeholder, 8 | .media-widget-gallery-preview { 9 | cursor: pointer; 10 | } 11 | 12 | .media-widget-gallery-preview .gallery-item { 13 | box-sizing: border-box; 14 | width: 50%; 15 | margin: 0; 16 | padding: 1.79104477%; 17 | } 18 | 19 | /* 20 | * Use targetted nth-last-child selectors to control the size of each image 21 | * based on how many gallery items are present in the grid. 22 | * See: https://alistapart.com/article/quantity-queries-for-css 23 | */ 24 | .media-widget-gallery-preview .gallery-item:nth-last-child(3):first-child, 25 | .media-widget-gallery-preview .gallery-item:nth-last-child(3):first-child ~ .gallery-item, 26 | .media-widget-gallery-preview .gallery-item:nth-last-child(n+5), 27 | .media-widget-gallery-preview .gallery-item:nth-last-child(n+5) ~ .gallery-item, 28 | .media-widget-gallery-preview .gallery-item:nth-last-child(n+6), 29 | .media-widget-gallery-preview .gallery-item:nth-last-child(n+6) ~ .gallery-item { 30 | max-width: 33.33%; 31 | } 32 | 33 | .media-widget-gallery-preview .gallery-item img { 34 | height: auto; 35 | vertical-align: bottom; 36 | } 37 | 38 | .media-widget-gallery-preview .gallery-icon { 39 | position: relative; 40 | } 41 | 42 | .media-widget-gallery-preview .gallery-icon-placeholder { 43 | position: absolute; 44 | top: 0; 45 | bottom: 0; 46 | width: 100%; 47 | box-sizing: border-box; 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | background-color: rgba( 0, 0, 0, .5 ); 52 | } 53 | 54 | .media-widget-gallery-preview .gallery-icon-placeholder-text { 55 | font-weight: 600; 56 | font-size: 2em; 57 | color: white; 58 | } 59 | -------------------------------------------------------------------------------- /wp-admin/css/widgets/media-widgets.css: -------------------------------------------------------------------------------- 1 | .wp-core-ui .media-widget-control.selected .placeholder, 2 | .wp-core-ui .media-widget-control.selected .not-selected, 3 | .wp-core-ui .media-widget-control .selected { 4 | display: none; 5 | } 6 | 7 | .media-widget-control.selected .selected { 8 | display: inline-block; 9 | } 10 | 11 | .media-widget-buttons { 12 | text-align: left; 13 | margin-bottom: 10px; 14 | } 15 | 16 | .media-widget-control .media-widget-buttons .button { 17 | margin-left: 8px; 18 | width: auto; 19 | } 20 | .media-widget-control:not(.selected) .media-widget-buttons .button, 21 | .media-widget-buttons .button:first-child { 22 | margin-left: 0; 23 | } 24 | 25 | .media-widget-control .placeholder { 26 | border: 1px dashed #b4b9be; 27 | -webkit-box-sizing: border-box; 28 | -moz-box-sizing: border-box; 29 | box-sizing: border-box; 30 | cursor: default; 31 | line-height: 20px; 32 | padding: 9px 0; 33 | position: relative; 34 | text-align: center; 35 | width: 100%; 36 | } 37 | 38 | .media-widget-control .media-widget-preview { 39 | text-align: center; 40 | } 41 | .media-widget-control .media-widget-preview .notice { 42 | text-align: initial; 43 | } 44 | .media-frame .media-widget-embed-notice p code, 45 | .media-widget-control .notice p code { 46 | padding: 0 3px 0 0; 47 | } 48 | .media-frame .media-widget-embed-notice { 49 | margin-top: 16px; 50 | } 51 | .media-widget-control .media-widget-preview img { 52 | max-width: 100%; 53 | } 54 | .media-widget-control .media-widget-preview .wp-video-shortcode { 55 | background: #000; 56 | } 57 | 58 | .media-frame.media-widget .media-toolbar-secondary { 59 | min-width: 300px; 60 | } 61 | 62 | .media-frame.media-widget .image-details .embed-media-settings .setting.align, 63 | .media-frame.media-widget .attachment-display-settings .setting.align, 64 | .media-frame.media-widget .embed-media-settings .setting.align, 65 | .media-frame.media-widget .embed-link-settings .setting.link-text, 66 | .media-frame.media-widget .replace-attachment, 67 | .media-frame.media-widget .checkbox-setting.autoplay { 68 | display: none; 69 | } 70 | 71 | .media-widget-video-preview { 72 | width: 100%; 73 | } 74 | 75 | .media-widget-video-link { 76 | display: inline-block; 77 | min-height: 132px; 78 | width: 100%; 79 | background: black; 80 | } 81 | 82 | .media-widget-video-link .dashicons { 83 | font: normal 60px/1 'dashicons'; 84 | position: relative; 85 | width: 100%; 86 | top: -90px; 87 | color: white; 88 | text-decoration: none; 89 | } 90 | 91 | .media-widget-video-link.no-poster .dashicons { 92 | top: 30px; 93 | } 94 | 95 | .media-frame #embed-url-field.invalid { 96 | border: 1px solid #f00; 97 | } 98 | 99 | .wp-customizer div.mce-inline-toolbar-grp, 100 | .wp-customizer div.mce-tooltip { 101 | z-index: 500100 !important; 102 | } 103 | .wp-customizer .ui-autocomplete.wplink-autocomplete { 104 | z-index: 500110; /* originally 100110, but z-index of .wp-full-overlay is 500000 */ 105 | } 106 | .wp-customizer #wp-link-backdrop { 107 | z-index: 500100; /* originally 100100, but z-index of .wp-full-overlay is 500000 */ 108 | } 109 | .wp-customizer #wp-link-wrap { 110 | z-index: 500105; /* originally 100105, but z-index of .wp-full-overlay is 500000 */ 111 | } 112 | 113 | .wp-customizer .mejs-controls a:focus > .mejs-offscreen, 114 | .widgets-php .mejs-controls a:focus > .mejs-offscreen { 115 | z-index: 2; 116 | } 117 | -------------------------------------------------------------------------------- /wp-admin/js/widgets/media-audio-widget.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-this: [ "error", "control" ] */ 2 | (function( component ) { 3 | 'use strict'; 4 | 5 | var AudioWidgetModel, AudioWidgetControl, AudioDetailsMediaFrame; 6 | 7 | /** 8 | * Custom audio details frame that removes the replace-audio state. 9 | * 10 | * @class AudioDetailsMediaFrame 11 | * @constructor 12 | */ 13 | AudioDetailsMediaFrame = wp.media.view.MediaFrame.AudioDetails.extend({ 14 | 15 | /** 16 | * Create the default states. 17 | * 18 | * @returns {void} 19 | */ 20 | createStates: function createStates() { 21 | this.states.add([ 22 | new wp.media.controller.AudioDetails( { 23 | media: this.media 24 | } ), 25 | 26 | new wp.media.controller.MediaLibrary( { 27 | type: 'audio', 28 | id: 'add-audio-source', 29 | title: wp.media.view.l10n.audioAddSourceTitle, 30 | toolbar: 'add-audio-source', 31 | media: this.media, 32 | menu: false 33 | } ) 34 | ]); 35 | } 36 | }); 37 | 38 | /** 39 | * Audio widget model. 40 | * 41 | * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports. 42 | * 43 | * @class AudioWidgetModel 44 | * @constructor 45 | */ 46 | AudioWidgetModel = component.MediaWidgetModel.extend( {} ); 47 | 48 | /** 49 | * Audio widget control. 50 | * 51 | * See WP_Widget_Audio::enqueue_admin_scripts() for amending prototype from PHP exports. 52 | * 53 | * @class AudioWidgetModel 54 | * @constructor 55 | */ 56 | AudioWidgetControl = component.MediaWidgetControl.extend( { 57 | 58 | /** 59 | * Show display settings. 60 | * 61 | * @type {boolean} 62 | */ 63 | showDisplaySettings: false, 64 | 65 | /** 66 | * Map model props to media frame props. 67 | * 68 | * @param {Object} modelProps - Model props. 69 | * @returns {Object} Media frame props. 70 | */ 71 | mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) { 72 | var control = this, mediaFrameProps; 73 | mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps ); 74 | mediaFrameProps.link = 'embed'; 75 | return mediaFrameProps; 76 | }, 77 | 78 | /** 79 | * Render preview. 80 | * 81 | * @returns {void} 82 | */ 83 | renderPreview: function renderPreview() { 84 | var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl; 85 | attachmentId = control.model.get( 'attachment_id' ); 86 | attachmentUrl = control.model.get( 'url' ); 87 | 88 | if ( ! attachmentId && ! attachmentUrl ) { 89 | return; 90 | } 91 | 92 | previewContainer = control.$el.find( '.media-widget-preview' ); 93 | previewTemplate = wp.template( 'wp-media-widget-audio-preview' ); 94 | 95 | previewContainer.html( previewTemplate( { 96 | model: { 97 | attachment_id: control.model.get( 'attachment_id' ), 98 | src: attachmentUrl 99 | }, 100 | error: control.model.get( 'error' ) 101 | } ) ); 102 | wp.mediaelement.initialize(); 103 | }, 104 | 105 | /** 106 | * Open the media audio-edit frame to modify the selected item. 107 | * 108 | * @returns {void} 109 | */ 110 | editMedia: function editMedia() { 111 | var control = this, mediaFrame, metadata, updateCallback; 112 | 113 | metadata = control.mapModelToMediaFrameProps( control.model.toJSON() ); 114 | 115 | // Set up the media frame. 116 | mediaFrame = new AudioDetailsMediaFrame({ 117 | frame: 'audio', 118 | state: 'audio-details', 119 | metadata: metadata 120 | } ); 121 | wp.media.frame = mediaFrame; 122 | mediaFrame.$el.addClass( 'media-widget' ); 123 | 124 | updateCallback = function( mediaFrameProps ) { 125 | 126 | // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. 127 | control.selectedAttachment.set( mediaFrameProps ); 128 | 129 | control.model.set( _.extend( 130 | control.model.defaults(), 131 | control.mapMediaToModelProps( mediaFrameProps ), 132 | { error: false } 133 | ) ); 134 | }; 135 | 136 | mediaFrame.state( 'audio-details' ).on( 'update', updateCallback ); 137 | mediaFrame.state( 'replace-audio' ).on( 'replace', updateCallback ); 138 | mediaFrame.on( 'close', function() { 139 | mediaFrame.detach(); 140 | }); 141 | 142 | mediaFrame.open(); 143 | } 144 | } ); 145 | 146 | // Exports. 147 | component.controlConstructors.media_audio = AudioWidgetControl; 148 | component.modelConstructors.media_audio = AudioWidgetModel; 149 | 150 | })( wp.mediaWidgets ); 151 | -------------------------------------------------------------------------------- /wp-admin/js/widgets/media-gallery-widget.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-this: [ "error", "control" ] */ 2 | (function( component, $ ) { 3 | 'use strict'; 4 | 5 | var GalleryWidgetModel, GalleryWidgetControl, GalleryDetailsMediaFrame; 6 | 7 | /** 8 | * Custom gallery details frame. 9 | * 10 | * @class GalleryDetailsMediaFrame 11 | * @constructor 12 | */ 13 | GalleryDetailsMediaFrame = wp.media.view.MediaFrame.Post.extend( { 14 | 15 | /** 16 | * Create the default states. 17 | * 18 | * @returns {void} 19 | */ 20 | createStates: function createStates() { 21 | this.states.add([ 22 | new wp.media.controller.Library({ 23 | id: 'gallery', 24 | title: wp.media.view.l10n.createGalleryTitle, 25 | priority: 40, 26 | toolbar: 'main-gallery', 27 | filterable: 'uploaded', 28 | multiple: 'add', 29 | editable: true, 30 | 31 | library: wp.media.query( _.defaults({ 32 | type: 'image' 33 | }, this.options.library ) ) 34 | }), 35 | 36 | // Gallery states. 37 | new wp.media.controller.GalleryEdit({ 38 | library: this.options.selection, 39 | editing: this.options.editing, 40 | menu: 'gallery' 41 | }), 42 | 43 | new wp.media.controller.GalleryAdd() 44 | ]); 45 | } 46 | } ); 47 | 48 | /** 49 | * Gallery widget model. 50 | * 51 | * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports. 52 | * 53 | * @class GalleryWidgetModel 54 | * @constructor 55 | */ 56 | GalleryWidgetModel = component.MediaWidgetModel.extend( {} ); 57 | 58 | /** 59 | * Gallery widget control. 60 | * 61 | * See WP_Widget_Gallery::enqueue_admin_scripts() for amending prototype from PHP exports. 62 | * 63 | * @class GalleryWidgetControl 64 | * @constructor 65 | */ 66 | GalleryWidgetControl = component.MediaWidgetControl.extend( { 67 | 68 | events: _.extend( {}, component.MediaWidgetControl.prototype.events, { 69 | 'click .media-widget-preview': 'editMedia' 70 | } ), 71 | 72 | /** 73 | * Initialize. 74 | * 75 | * @param {Object} options - Options. 76 | * @param {Backbone.Model} options.model - Model. 77 | * @param {jQuery} options.el - Control field container element. 78 | * @param {jQuery} options.syncContainer - Container element where fields are synced for the server. 79 | * @returns {void} 80 | */ 81 | initialize: function initialize( options ) { 82 | var control = this; 83 | 84 | component.MediaWidgetControl.prototype.initialize.call( control, options ); 85 | 86 | _.bindAll( control, 'updateSelectedAttachments', 'handleAttachmentDestroy' ); 87 | control.selectedAttachments = new wp.media.model.Attachments(); 88 | control.model.on( 'change:ids', control.updateSelectedAttachments ); 89 | control.selectedAttachments.on( 'change', control.renderPreview ); 90 | control.selectedAttachments.on( 'reset', control.renderPreview ); 91 | control.updateSelectedAttachments(); 92 | }, 93 | 94 | /** 95 | * Update the selected attachments if necessary. 96 | * 97 | * @returns {void} 98 | */ 99 | updateSelectedAttachments: function updateSelectedAttachments() { 100 | var control = this, newIds, oldIds, removedIds, addedIds, addedQuery; 101 | 102 | newIds = control.parseIdList( control.model.get( 'ids' ) ); 103 | oldIds = _.pluck( control.selectedAttachments.models, 'id' ); 104 | 105 | removedIds = _.difference( oldIds, newIds ); 106 | _.each( removedIds, function( removedId ) { 107 | control.selectedAttachments.remove( control.selectedAttachments.get( removedId ) ); 108 | }); 109 | 110 | addedIds = _.difference( newIds, oldIds ); 111 | if ( addedIds.length ) { 112 | addedQuery = wp.media.query({ 113 | order: 'ASC', 114 | orderby: 'post__in', 115 | perPage: -1, 116 | post__in: newIds, 117 | query: true, 118 | type: 'image' 119 | }); 120 | addedQuery.more().done( function() { 121 | control.selectedAttachments.reset( addedQuery.models ); 122 | }); 123 | } 124 | }, 125 | 126 | /** 127 | * Parse ID list. 128 | * 129 | * @param {Array|string} ids - ID list. 130 | * @returns {Array} Valid integers. 131 | */ 132 | parseIdList: function( ids ) { 133 | var parsedIds; 134 | if ( 'string' === typeof ids ) { 135 | parsedIds = ids.split( ',' ); 136 | } else { 137 | parsedIds = ids; 138 | } 139 | return _.filter( 140 | _.map( parsedIds, function( id ) { 141 | return parseInt( id, 10 ); 142 | }, 143 | function( id ) { 144 | return 'number' === typeof id; 145 | } 146 | ) ); 147 | }, 148 | 149 | /** 150 | * Render preview. 151 | * 152 | * @returns {void} 153 | */ 154 | renderPreview: function renderPreview() { 155 | var control = this, previewContainer, previewTemplate, data; 156 | 157 | previewContainer = control.$el.find( '.media-widget-preview' ); 158 | previewTemplate = wp.template( 'wp-media-widget-gallery-preview' ); 159 | 160 | data = control.previewTemplateProps.toJSON(); 161 | data.ids = control.parseIdList( data.ids ); 162 | data.attachments = {}; 163 | control.selectedAttachments.each( function( attachment ) { 164 | data.attachments[ attachment.id ] = attachment.toJSON(); 165 | } ); 166 | 167 | previewContainer.html( previewTemplate( data ) ); 168 | previewContainer.addClass( 'gallery' ); 169 | }, 170 | 171 | /** 172 | * Determine whether there are selected attachments. 173 | * 174 | * @returns {boolean} Selected. 175 | */ 176 | isSelected: function isSelected() { 177 | var control = this, ids; 178 | 179 | if ( control.model.get( 'error' ) ) { 180 | return false; 181 | } 182 | 183 | ids = control.parseIdList( control.model.get( 'ids' ) ); 184 | return ids.length > 0; 185 | }, 186 | 187 | /** 188 | * Sync the model attributes to the hidden inputs, and update previewTemplateProps. 189 | * 190 | * @todo Patch this in core to eliminate need for override. 191 | * @returns {void} 192 | */ 193 | syncModelToInputs: function syncModelToInputs() { 194 | var control = this; 195 | control.syncContainer.find( '.media-widget-instance-property' ).each( function() { 196 | var input = $( this ), value, propertyName = input.data( 'property' ); 197 | value = control.model.get( propertyName ); 198 | if ( _.isUndefined( value ) ) { 199 | return; 200 | } 201 | 202 | // @todo Support comma-separated ID list arrays? This will depend on WP_Widget_Media::form() being updated to support serializing array to form field. 203 | if ( 'boolean' === control.model.schema[ propertyName ].type ) { 204 | value = value ? '1' : ''; // Because in PHP, strval( true ) === '1' && strval( false ) === ''. 205 | } else { 206 | value = String( value ); 207 | } 208 | 209 | if ( input.val() !== value ) { 210 | input.val( value ); 211 | input.trigger( 'change' ); 212 | } 213 | }); 214 | }, 215 | 216 | /** 217 | * Open the media select frame to edit images. 218 | * 219 | * @returns {void} 220 | */ 221 | editMedia: function editMedia() { 222 | var control = this, selection, mediaFrame, mediaFrameProps; 223 | 224 | // If no images are selected, open the select frame instead. 225 | if ( ! control.isSelected() ) { 226 | control.selectMedia(); 227 | return; 228 | } 229 | 230 | selection = new wp.media.model.Selection( control.selectedAttachments.models, { 231 | multiple: true 232 | }); 233 | 234 | mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() ); 235 | selection.gallery = new Backbone.Model( _.pick( mediaFrameProps, 'columns', 'link', 'size', '_orderbyRandom' ) ); 236 | if ( mediaFrameProps.size ) { 237 | control.displaySettings.set( 'size', mediaFrameProps.size ); 238 | } 239 | mediaFrame = new GalleryDetailsMediaFrame({ 240 | frame: 'manage', 241 | text: control.l10n.add_to_widget, 242 | selection: selection, 243 | mimeType: control.mime_type, 244 | selectedDisplaySettings: control.displaySettings, 245 | showDisplaySettings: control.showDisplaySettings, 246 | metadata: mediaFrameProps, 247 | editing: true, 248 | multiple: true, 249 | state: 'gallery-edit' 250 | }); 251 | wp.media.frame = mediaFrame; // See wp.media(). 252 | 253 | // Handle selection of a media item. 254 | mediaFrame.on( 'update', function onUpdate( newSelection ) { 255 | var state = mediaFrame.state(), resultSelection; 256 | 257 | resultSelection = newSelection || state.get( 'selection' ); 258 | if ( ! resultSelection ) { 259 | return; 260 | } 261 | 262 | // Copy orderby_random from gallery state. 263 | if ( resultSelection.gallery ) { 264 | control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) ); 265 | } 266 | 267 | // Directly update selectedAttachments to prevent needing to do additional request. 268 | control.selectedAttachments.reset( resultSelection.models ); 269 | 270 | // Update models in the widget instance. 271 | control.model.set( { 272 | ids: _.pluck( resultSelection.models, 'id' ).join( ',' ) // @todo Array. 273 | } ); 274 | } ); 275 | 276 | mediaFrame.$el.addClass( 'media-widget' ); 277 | mediaFrame.open(); 278 | 279 | if ( selection ) { 280 | selection.on( 'destroy', control.handleAttachmentDestroy ); 281 | } 282 | }, 283 | 284 | /** 285 | * Open the media select frame to chose an item. 286 | * 287 | * @returns {void} 288 | */ 289 | selectMedia: function selectMedia() { 290 | var control = this, selection, mediaFrame, mediaFrameProps; 291 | selection = new wp.media.model.Selection( control.selectedAttachments.models, { 292 | multiple: true 293 | }); 294 | 295 | mediaFrameProps = control.mapModelToMediaFrameProps( control.model.toJSON() ); 296 | if ( mediaFrameProps.size ) { 297 | control.displaySettings.set( 'size', mediaFrameProps.size ); 298 | } 299 | mediaFrame = new GalleryDetailsMediaFrame({ 300 | frame: 'select', 301 | text: control.l10n.add_to_widget, 302 | selection: selection, 303 | mimeType: control.mime_type, 304 | selectedDisplaySettings: control.displaySettings, 305 | showDisplaySettings: control.showDisplaySettings, 306 | metadata: mediaFrameProps, 307 | state: 'gallery' 308 | }); 309 | wp.media.frame = mediaFrame; // See wp.media(). 310 | 311 | // Handle selection of a media item. 312 | mediaFrame.on( 'update', function onUpdate( newSelection ) { 313 | var state = mediaFrame.state(), resultSelection; 314 | 315 | resultSelection = newSelection || state.get( 'selection' ); 316 | if ( ! resultSelection ) { 317 | return; 318 | } 319 | 320 | // Copy orderby_random from gallery state. 321 | if ( resultSelection.gallery ) { 322 | control.model.set( control.mapMediaToModelProps( resultSelection.gallery.toJSON() ) ); 323 | } 324 | 325 | // Directly update selectedAttachments to prevent needing to do additional request. 326 | control.selectedAttachments.reset( resultSelection.models ); 327 | 328 | // Update widget instance. 329 | control.model.set( { 330 | ids: _.pluck( resultSelection.models, 'id' ).join( ',' ) // @todo Allow array. 331 | } ); 332 | } ); 333 | 334 | mediaFrame.$el.addClass( 'media-widget' ); 335 | mediaFrame.open(); 336 | 337 | if ( selection ) { 338 | selection.on( 'destroy', control.handleAttachmentDestroy ); 339 | } 340 | 341 | /* 342 | * Make sure focus is set inside of modal so that hitting Esc will close 343 | * the modal and not inadvertently cause the widget to collapse in the customizer. 344 | */ 345 | mediaFrame.$el.find( ':focusable:first' ).focus(); 346 | }, 347 | 348 | /** 349 | * Clear the selected attachment when it is deleted in the media select frame. 350 | * 351 | * @param {wp.media.models.Attachment} attachment - Attachment. 352 | * @returns {void} 353 | */ 354 | handleAttachmentDestroy: function handleAttachmentDestroy( attachment ) { 355 | var control = this; 356 | control.model.set( { 357 | ids: _.difference( 358 | control.parseIdList( control.model.get( 'ids' ) ), 359 | [ attachment.id ] 360 | ).join( ',' ) // @todo Array. 361 | } ); 362 | } 363 | } ); 364 | 365 | // Exports. 366 | component.controlConstructors.media_gallery = GalleryWidgetControl; 367 | component.modelConstructors.media_gallery = GalleryWidgetModel; 368 | 369 | })( wp.mediaWidgets, jQuery ); 370 | -------------------------------------------------------------------------------- /wp-admin/js/widgets/media-image-widget.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-this: [ "error", "control" ] */ 2 | (function( component, $ ) { 3 | 'use strict'; 4 | 5 | var ImageWidgetModel, ImageWidgetControl; 6 | 7 | /** 8 | * Image widget model. 9 | * 10 | * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports. 11 | * 12 | * @class ImageWidgetModel 13 | * @constructor 14 | */ 15 | ImageWidgetModel = component.MediaWidgetModel.extend({}); 16 | 17 | /** 18 | * Image widget control. 19 | * 20 | * See WP_Widget_Media_Image::enqueue_admin_scripts() for amending prototype from PHP exports. 21 | * 22 | * @class ImageWidgetModel 23 | * @constructor 24 | */ 25 | ImageWidgetControl = component.MediaWidgetControl.extend({ 26 | 27 | /** 28 | * Render preview. 29 | * 30 | * @returns {void} 31 | */ 32 | renderPreview: function renderPreview() { 33 | var control = this, previewContainer, previewTemplate; 34 | if ( ! control.model.get( 'attachment_id' ) && ! control.model.get( 'url' ) ) { 35 | return; 36 | } 37 | 38 | previewContainer = control.$el.find( '.media-widget-preview' ); 39 | previewTemplate = wp.template( 'wp-media-widget-image-preview' ); 40 | previewContainer.html( previewTemplate( _.extend( control.previewTemplateProps.toJSON() ) ) ); 41 | }, 42 | 43 | /** 44 | * Open the media image-edit frame to modify the selected item. 45 | * 46 | * @returns {void} 47 | */ 48 | editMedia: function editMedia() { 49 | var control = this, mediaFrame, updateCallback, defaultSync, metadata; 50 | 51 | metadata = control.mapModelToMediaFrameProps( control.model.toJSON() ); 52 | 53 | // Needed or else none will not be selected if linkUrl is not also empty. 54 | if ( 'none' === metadata.link ) { 55 | metadata.linkUrl = ''; 56 | } 57 | 58 | // Set up the media frame. 59 | mediaFrame = wp.media({ 60 | frame: 'image', 61 | state: 'image-details', 62 | metadata: metadata 63 | }); 64 | mediaFrame.$el.addClass( 'media-widget' ); 65 | 66 | updateCallback = function() { 67 | var mediaProps; 68 | 69 | // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. 70 | mediaProps = mediaFrame.state().attributes.image.toJSON(); 71 | control.selectedAttachment.set( mediaProps ); 72 | 73 | control.model.set( _.extend( 74 | control.mapMediaToModelProps( mediaProps ), 75 | { error: false } 76 | ) ); 77 | }; 78 | 79 | mediaFrame.state( 'image-details' ).on( 'update', updateCallback ); 80 | mediaFrame.state( 'replace-image' ).on( 'replace', updateCallback ); 81 | 82 | // Disable syncing of attachment changes back to server. See . 83 | defaultSync = wp.media.model.Attachment.prototype.sync; 84 | wp.media.model.Attachment.prototype.sync = function rejectedSync() { 85 | return $.Deferred().rejectWith( this ).promise(); 86 | }; 87 | mediaFrame.on( 'close', function onClose() { 88 | mediaFrame.detach(); 89 | wp.media.model.Attachment.prototype.sync = defaultSync; 90 | }); 91 | 92 | mediaFrame.open(); 93 | }, 94 | 95 | /** 96 | * Get props which are merged on top of the model when an embed is chosen (as opposed to an attachment). 97 | * 98 | * @returns {Object} Reset/override props. 99 | */ 100 | getEmbedResetProps: function getEmbedResetProps() { 101 | return _.extend( 102 | component.MediaWidgetControl.prototype.getEmbedResetProps.call( this ), 103 | { 104 | size: 'full', 105 | width: 0, 106 | height: 0 107 | } 108 | ); 109 | }, 110 | 111 | /** 112 | * Map model props to preview template props. 113 | * 114 | * @returns {Object} Preview template props. 115 | */ 116 | mapModelToPreviewTemplateProps: function mapModelToPreviewTemplateProps() { 117 | var control = this, mediaFrameProps, url; 118 | url = control.model.get( 'url' ); 119 | mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToPreviewTemplateProps.call( control ); 120 | mediaFrameProps.currentFilename = url ? url.replace( /\?.*$/, '' ).replace( /^.+\//, '' ) : ''; 121 | return mediaFrameProps; 122 | } 123 | }); 124 | 125 | // Exports. 126 | component.controlConstructors.media_image = ImageWidgetControl; 127 | component.modelConstructors.media_image = ImageWidgetModel; 128 | 129 | })( wp.mediaWidgets, jQuery ); 130 | -------------------------------------------------------------------------------- /wp-admin/js/widgets/media-video-widget.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-this: [ "error", "control" ] */ 2 | (function( component ) { 3 | 'use strict'; 4 | 5 | var VideoWidgetModel, VideoWidgetControl, VideoDetailsMediaFrame; 6 | 7 | /** 8 | * Custom video details frame that removes the replace-video state. 9 | * 10 | * @class VideoDetailsMediaFrame 11 | * @constructor 12 | */ 13 | VideoDetailsMediaFrame = wp.media.view.MediaFrame.VideoDetails.extend({ 14 | 15 | /** 16 | * Create the default states. 17 | * 18 | * @returns {void} 19 | */ 20 | createStates: function createStates() { 21 | this.states.add([ 22 | new wp.media.controller.VideoDetails({ 23 | media: this.media 24 | }), 25 | 26 | new wp.media.controller.MediaLibrary( { 27 | type: 'video', 28 | id: 'add-video-source', 29 | title: wp.media.view.l10n.videoAddSourceTitle, 30 | toolbar: 'add-video-source', 31 | media: this.media, 32 | menu: false 33 | } ), 34 | 35 | new wp.media.controller.MediaLibrary( { 36 | type: 'text', 37 | id: 'add-track', 38 | title: wp.media.view.l10n.videoAddTrackTitle, 39 | toolbar: 'add-track', 40 | media: this.media, 41 | menu: 'video-details' 42 | } ) 43 | ]); 44 | } 45 | }); 46 | 47 | /** 48 | * Video widget model. 49 | * 50 | * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports. 51 | * 52 | * @class VideoWidgetModel 53 | * @constructor 54 | */ 55 | VideoWidgetModel = component.MediaWidgetModel.extend( {} ); 56 | 57 | /** 58 | * Video widget control. 59 | * 60 | * See WP_Widget_Video::enqueue_admin_scripts() for amending prototype from PHP exports. 61 | * 62 | * @class VideoWidgetControl 63 | * @constructor 64 | */ 65 | VideoWidgetControl = component.MediaWidgetControl.extend( { 66 | 67 | /** 68 | * Show display settings. 69 | * 70 | * @type {boolean} 71 | */ 72 | showDisplaySettings: false, 73 | 74 | /** 75 | * Cache of oembed responses. 76 | * 77 | * @type {Object} 78 | */ 79 | oembedResponses: {}, 80 | 81 | /** 82 | * Map model props to media frame props. 83 | * 84 | * @param {Object} modelProps - Model props. 85 | * @returns {Object} Media frame props. 86 | */ 87 | mapModelToMediaFrameProps: function mapModelToMediaFrameProps( modelProps ) { 88 | var control = this, mediaFrameProps; 89 | mediaFrameProps = component.MediaWidgetControl.prototype.mapModelToMediaFrameProps.call( control, modelProps ); 90 | mediaFrameProps.link = 'embed'; 91 | return mediaFrameProps; 92 | }, 93 | 94 | /** 95 | * Fetches embed data for external videos. 96 | * 97 | * @returns {void} 98 | */ 99 | fetchEmbed: function fetchEmbed() { 100 | var control = this, url; 101 | url = control.model.get( 'url' ); 102 | 103 | // If we already have a local cache of the embed response, return. 104 | if ( control.oembedResponses[ url ] ) { 105 | return; 106 | } 107 | 108 | // If there is an in-flight embed request, abort it. 109 | if ( control.fetchEmbedDfd && 'pending' === control.fetchEmbedDfd.state() ) { 110 | control.fetchEmbedDfd.abort(); 111 | } 112 | 113 | control.fetchEmbedDfd = jQuery.ajax({ 114 | url: 'https://noembed.com/embed', 115 | data: { 116 | url: control.model.get( 'url' ), 117 | maxwidth: control.model.get( 'width' ), 118 | maxheight: control.model.get( 'height' ) 119 | }, 120 | type: 'GET', 121 | crossDomain: true, 122 | dataType: 'json' 123 | }); 124 | 125 | control.fetchEmbedDfd.done( function( response ) { 126 | control.oembedResponses[ url ] = response; 127 | control.renderPreview(); 128 | }); 129 | 130 | control.fetchEmbedDfd.fail( function() { 131 | control.oembedResponses[ url ] = null; 132 | }); 133 | }, 134 | 135 | /** 136 | * Render preview. 137 | * 138 | * @returns {void} 139 | */ 140 | renderPreview: function renderPreview() { 141 | var control = this, previewContainer, previewTemplate, attachmentId, attachmentUrl, poster, isHostedEmbed = false, parsedUrl, mime, error; 142 | attachmentId = control.model.get( 'attachment_id' ); 143 | attachmentUrl = control.model.get( 'url' ); 144 | error = control.model.get( 'error' ); 145 | 146 | if ( ! attachmentId && ! attachmentUrl ) { 147 | return; 148 | } 149 | 150 | if ( ! attachmentId && attachmentUrl ) { 151 | parsedUrl = document.createElement( 'a' ); 152 | parsedUrl.href = attachmentUrl; 153 | isHostedEmbed = /vimeo|youtu\.?be/.test( parsedUrl.host ); 154 | } 155 | 156 | if ( isHostedEmbed ) { 157 | control.fetchEmbed(); 158 | poster = control.oembedResponses[ attachmentUrl ] ? control.oembedResponses[ attachmentUrl ].thumbnail_url : null; 159 | } 160 | 161 | // Verify the selected attachment mime is supported. 162 | mime = control.selectedAttachment.get( 'mime' ); 163 | if ( mime && attachmentId ) { 164 | if ( ! _.contains( _.values( wp.media.view.settings.embedMimes ), mime ) ) { 165 | error = 'unsupported_file_type'; 166 | } 167 | } 168 | 169 | previewContainer = control.$el.find( '.media-widget-preview' ); 170 | previewTemplate = wp.template( 'wp-media-widget-video-preview' ); 171 | 172 | previewContainer.html( previewTemplate( { 173 | model: { 174 | attachment_id: control.model.get( 'attachment_id' ), 175 | src: attachmentUrl, 176 | poster: poster 177 | }, 178 | is_hosted_embed: isHostedEmbed, 179 | error: error 180 | } ) ); 181 | }, 182 | 183 | /** 184 | * Open the media image-edit frame to modify the selected item. 185 | * 186 | * @returns {void} 187 | */ 188 | editMedia: function editMedia() { 189 | var control = this, mediaFrame, metadata, updateCallback; 190 | 191 | metadata = control.mapModelToMediaFrameProps( control.model.toJSON() ); 192 | 193 | // Set up the media frame. 194 | mediaFrame = new VideoDetailsMediaFrame({ 195 | frame: 'video', 196 | state: 'video-details', 197 | metadata: metadata 198 | }); 199 | wp.media.frame = mediaFrame; 200 | mediaFrame.$el.addClass( 'media-widget' ); 201 | 202 | updateCallback = function( mediaFrameProps ) { 203 | 204 | // Update cached attachment object to avoid having to re-fetch. This also triggers re-rendering of preview. 205 | control.selectedAttachment.set( mediaFrameProps ); 206 | 207 | control.model.set( _.extend( 208 | _.omit( control.model.defaults(), 'title' ), 209 | control.mapMediaToModelProps( mediaFrameProps ), 210 | { error: false } 211 | ) ); 212 | }; 213 | 214 | mediaFrame.state( 'video-details' ).on( 'update', updateCallback ); 215 | mediaFrame.state( 'replace-video' ).on( 'replace', updateCallback ); 216 | mediaFrame.on( 'close', function() { 217 | mediaFrame.detach(); 218 | }); 219 | 220 | mediaFrame.open(); 221 | } 222 | } ); 223 | 224 | // Exports. 225 | component.controlConstructors.media_video = VideoWidgetControl; 226 | component.modelConstructors.media_video = VideoWidgetModel; 227 | 228 | })( wp.mediaWidgets ); 229 | -------------------------------------------------------------------------------- /wp-admin/js/widgets/text-widgets.js: -------------------------------------------------------------------------------- 1 | /* global tinymce, switchEditors */ 2 | /* eslint consistent-this: [ "error", "control" ] */ 3 | wp.textWidgets = ( function( $ ) { 4 | 'use strict'; 5 | 6 | var component = {}; 7 | 8 | /** 9 | * Text widget control. 10 | * 11 | * @class TextWidgetControl 12 | * @constructor 13 | * @abstract 14 | */ 15 | component.TextWidgetControl = Backbone.View.extend({ 16 | 17 | /** 18 | * View events. 19 | * 20 | * @type {Object} 21 | */ 22 | events: {}, 23 | 24 | /** 25 | * Initialize. 26 | * 27 | * @param {Object} options - Options. 28 | * @param {Backbone.Model} options.model - Model. 29 | * @param {jQuery} options.el - Control container element. 30 | * @returns {void} 31 | */ 32 | initialize: function initialize( options ) { 33 | var control = this; 34 | 35 | if ( ! options.el ) { 36 | throw new Error( 'Missing options.el' ); 37 | } 38 | 39 | Backbone.View.prototype.initialize.call( control, options ); 40 | 41 | /* 42 | * Create a container element for the widget control fields. 43 | * This is inserted into the DOM immediately before the the .widget-content 44 | * element because the contents of this element are essentially "managed" 45 | * by PHP, where each widget update cause the entire element to be emptied 46 | * and replaced with the rendered output of WP_Widget::form() which is 47 | * sent back in Ajax request made to save/update the widget instance. 48 | * To prevent a "flash of replaced DOM elements and re-initialized JS 49 | * components", the JS template is rendered outside of the normal form 50 | * container. 51 | */ 52 | control.fieldContainer = $( '
' ); 53 | control.fieldContainer.html( wp.template( 'widget-text-control-fields' ) ); 54 | control.widgetContentContainer = control.$el.find( '.widget-content:first' ); 55 | control.widgetContentContainer.before( control.fieldContainer ); 56 | 57 | control.fields = { 58 | title: control.fieldContainer.find( '.title' ), 59 | text: control.fieldContainer.find( '.text' ) 60 | }; 61 | 62 | // Sync input fields to hidden sync fields which actually get sent to the server. 63 | _.each( control.fields, function( fieldInput, fieldName ) { 64 | fieldInput.on( 'input change', function updateSyncField() { 65 | var syncInput = control.widgetContentContainer.find( 'input[type=hidden].' + fieldName ); 66 | if ( syncInput.val() !== $( this ).val() ) { 67 | syncInput.val( $( this ).val() ); 68 | syncInput.trigger( 'change' ); 69 | } 70 | }); 71 | 72 | // Note that syncInput cannot be re-used because it will be destroyed with each widget-updated event. 73 | fieldInput.val( control.widgetContentContainer.find( 'input[type=hidden].' + fieldName ).val() ); 74 | }); 75 | }, 76 | 77 | /** 78 | * Update input fields from the sync fields. 79 | * 80 | * This function is called at the widget-updated and widget-synced events. 81 | * A field will only be updated if it is not currently focused, to avoid 82 | * overwriting content that the user is entering. 83 | * 84 | * @returns {void} 85 | */ 86 | updateFields: function updateFields() { 87 | var control = this, syncInput; 88 | 89 | if ( ! control.fields.title.is( document.activeElement ) ) { 90 | syncInput = control.widgetContentContainer.find( 'input[type=hidden].title' ); 91 | control.fields.title.val( syncInput.val() ); 92 | } 93 | 94 | syncInput = control.widgetContentContainer.find( 'input[type=hidden].text' ); 95 | if ( control.fields.text.is( ':visible' ) ) { 96 | if ( ! control.fields.text.is( document.activeElement ) ) { 97 | control.fields.text.val( syncInput.val() ); 98 | } 99 | } else if ( control.editor && ! control.editorFocused && syncInput.val() !== control.fields.text.val() ) { 100 | control.editor.setContent( wp.editor.autop( syncInput.val() ) ); 101 | } 102 | }, 103 | 104 | /** 105 | * Initialize editor. 106 | * 107 | * @returns {void} 108 | */ 109 | initializeEditor: function initializeEditor() { 110 | var control = this, changeDebounceDelay = 1000, id, textarea, restoreTextMode = false; 111 | textarea = control.fields.text; 112 | id = textarea.attr( 'id' ); 113 | 114 | /** 115 | * Build (or re-build) the visual editor. 116 | * 117 | * @returns {void} 118 | */ 119 | function buildEditor() { 120 | var editor, triggerChangeIfDirty, onInit; 121 | 122 | // Abort building if the textarea is gone, likely due to the widget having been deleted entirely. 123 | if ( ! document.getElementById( id ) ) { 124 | return; 125 | } 126 | 127 | // Destroy any existing editor so that it can be re-initialized after a widget-updated event. 128 | if ( tinymce.get( id ) ) { 129 | restoreTextMode = tinymce.get( id ).isHidden(); 130 | wp.editor.remove( id ); 131 | } 132 | 133 | wp.editor.initialize( id, { 134 | tinymce: { 135 | wpautop: true 136 | }, 137 | quicktags: true 138 | } ); 139 | 140 | editor = window.tinymce.get( id ); 141 | if ( ! editor ) { 142 | throw new Error( 'Failed to initialize editor' ); 143 | } 144 | onInit = function() { 145 | 146 | // When a widget is moved in the DOM the dynamically-created TinyMCE iframe will be destroyed and has to be re-built. 147 | $( editor.getWin() ).on( 'unload', function() { 148 | _.defer( buildEditor ); 149 | }); 150 | 151 | // If a prior mce instance was replaced, and it was in text mode, toggle to text mode. 152 | if ( restoreTextMode ) { 153 | switchEditors.go( id, 'toggle' ); 154 | } 155 | }; 156 | 157 | if ( editor.initialized ) { 158 | onInit(); 159 | } else { 160 | editor.on( 'init', onInit ); 161 | } 162 | 163 | control.editorFocused = false; 164 | triggerChangeIfDirty = function() { 165 | var updateWidgetBuffer = 300; // See wp.customize.Widgets.WidgetControl._setupUpdateUI() which uses 250ms for updateWidgetDebounced. 166 | if ( editor.isDirty() ) { 167 | 168 | /* 169 | * Account for race condition in customizer where user clicks Save & Publish while 170 | * focus was just previously given to to the editor. Since updates to the editor 171 | * are debounced at 1 second and since widget input changes are only synced to 172 | * settings after 250ms, the customizer needs to be put into the processing 173 | * state during the time between the change event is triggered and updateWidget 174 | * logic starts. Note that the debounced update-widget request should be able 175 | * to be removed with the removal of the update-widget request entirely once 176 | * widgets are able to mutate their own instance props directly in JS without 177 | * having to make server round-trips to call the respective WP_Widget::update() 178 | * callbacks. See . 179 | */ 180 | if ( wp.customize ) { 181 | wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() + 1 ); 182 | _.delay( function() { 183 | wp.customize.state( 'processing' ).set( wp.customize.state( 'processing' ).get() - 1 ); 184 | }, updateWidgetBuffer ); 185 | } 186 | 187 | editor.save(); 188 | textarea.trigger( 'change' ); 189 | } 190 | }; 191 | editor.on( 'focus', function() { 192 | control.editorFocused = true; 193 | } ); 194 | editor.on( 'NodeChange', _.debounce( triggerChangeIfDirty, changeDebounceDelay ) ); 195 | editor.on( 'blur', function() { 196 | control.editorFocused = false; 197 | triggerChangeIfDirty(); 198 | } ); 199 | 200 | control.editor = editor; 201 | } 202 | 203 | buildEditor(); 204 | } 205 | }); 206 | 207 | /** 208 | * Mapping of widget ID to instances of TextWidgetControl subclasses. 209 | * 210 | * @type {Object.} 211 | */ 212 | component.widgetControls = {}; 213 | 214 | /** 215 | * Handle widget being added or initialized for the first time at the widget-added event. 216 | * 217 | * @param {jQuery.Event} event - Event. 218 | * @param {jQuery} widgetContainer - Widget container element. 219 | * @returns {void} 220 | */ 221 | component.handleWidgetAdded = function handleWidgetAdded( event, widgetContainer ) { 222 | var widgetForm, idBase, widgetControl, widgetId, animatedCheckDelay = 50, widgetInside, renderWhenAnimationDone; 223 | widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); // Note: '.form' appears in the customizer, whereas 'form' on the widgets admin screen. 224 | 225 | idBase = widgetForm.find( '> .id_base' ).val(); 226 | if ( 'text' !== idBase ) { 227 | return; 228 | } 229 | 230 | // Prevent initializing already-added widgets. 231 | widgetId = widgetForm.find( '> .widget-id' ).val(); 232 | if ( component.widgetControls[ widgetId ] ) { 233 | return; 234 | } 235 | 236 | widgetControl = new component.TextWidgetControl({ 237 | el: widgetContainer 238 | }); 239 | 240 | component.widgetControls[ widgetId ] = widgetControl; 241 | 242 | /* 243 | * Render the widget once the widget parent's container finishes animating, 244 | * as the widget-added event fires with a slideDown of the container. 245 | * This ensures that the textarea is visible and an iframe can be embedded 246 | * with TinyMCE being able to set contenteditable on it. 247 | */ 248 | widgetInside = widgetContainer.parent(); 249 | renderWhenAnimationDone = function() { 250 | if ( widgetInside.is( ':animated' ) ) { 251 | setTimeout( renderWhenAnimationDone, animatedCheckDelay ); 252 | } else { 253 | widgetControl.initializeEditor(); 254 | } 255 | }; 256 | renderWhenAnimationDone(); 257 | }; 258 | 259 | /** 260 | * Sync widget instance data sanitized from server back onto widget model. 261 | * 262 | * This gets called via the 'widget-updated' event when saving a widget from 263 | * the widgets admin screen and also via the 'widget-synced' event when making 264 | * a change to a widget in the customizer. 265 | * 266 | * @param {jQuery.Event} event - Event. 267 | * @param {jQuery} widgetContainer - Widget container element. 268 | * @returns {void} 269 | */ 270 | component.handleWidgetUpdated = function handleWidgetUpdated( event, widgetContainer ) { 271 | var widgetForm, widgetId, widgetControl, idBase; 272 | widgetForm = widgetContainer.find( '> .widget-inside > .form, > .widget-inside > form' ); 273 | 274 | idBase = widgetForm.find( '> .id_base' ).val(); 275 | if ( 'text' !== idBase ) { 276 | return; 277 | } 278 | 279 | widgetId = widgetForm.find( '> .widget-id' ).val(); 280 | widgetControl = component.widgetControls[ widgetId ]; 281 | if ( ! widgetControl ) { 282 | return; 283 | } 284 | 285 | widgetControl.updateFields(); 286 | }; 287 | 288 | /** 289 | * Initialize functionality. 290 | * 291 | * This function exists to prevent the JS file from having to boot itself. 292 | * When WordPress enqueues this script, it should have an inline script 293 | * attached which calls wp.textWidgets.init(). 294 | * 295 | * @returns {void} 296 | */ 297 | component.init = function init() { 298 | var $document = $( document ); 299 | $document.on( 'widget-added', component.handleWidgetAdded ); 300 | $document.on( 'widget-synced widget-updated', component.handleWidgetUpdated ); 301 | 302 | /* 303 | * Manually trigger widget-added events for media widgets on the admin 304 | * screen once they are expanded. The widget-added event is not triggered 305 | * for each pre-existing widget on the widgets admin screen like it is 306 | * on the customizer. Likewise, the customizer only triggers widget-added 307 | * when the widget is expanded to just-in-time construct the widget form 308 | * when it is actually going to be displayed. So the following implements 309 | * the same for the widgets admin screen, to invoke the widget-added 310 | * handler when a pre-existing media widget is expanded. 311 | */ 312 | $( function initializeExistingWidgetContainers() { 313 | var widgetContainers; 314 | if ( 'widgets' !== window.pagenow ) { 315 | return; 316 | } 317 | widgetContainers = $( '.widgets-holder-wrap:not(#available-widgets)' ).find( 'div.widget' ); 318 | widgetContainers.one( 'click.toggle-widget-expanded', function toggleWidgetExpanded() { 319 | var widgetContainer = $( this ); 320 | component.handleWidgetAdded( new jQuery.Event( 'widget-added' ), widgetContainer ); 321 | }); 322 | }); 323 | }; 324 | 325 | return component; 326 | })( jQuery ); 327 | -------------------------------------------------------------------------------- /wp-includes/js/customize-selective-refresh-extras.js: -------------------------------------------------------------------------------- 1 | /* global wp */ 2 | (function( api ) { 3 | 'use strict'; 4 | 5 | /* 6 | * The following logic should be placed at https://github.com/WordPress/wordpress-develop/blob/4.7.2/src/wp-includes/js/customize-selective-refresh.js#L471 7 | */ 8 | api.selectiveRefresh.bind( 'partial-content-rendered', function initializeMediaElements() { 9 | 10 | /* 11 | * Note that the 'wp_audio_shortcode_library' and 'wp_video_shortcode_library' filters 12 | * will determine whether or not wp.mediaelement is loaded and whether it will 13 | * initialize audio and video respectively. See also https://core.trac.wordpress.org/ticket/40144 14 | */ 15 | if ( wp.mediaelement ) { 16 | wp.mediaelement.initialize(); 17 | } 18 | }); 19 | 20 | })( wp.customize ); 21 | -------------------------------------------------------------------------------- /wp-includes/widgets/class-wp-widget-media-audio.php: -------------------------------------------------------------------------------- 1 | __( 'Displays an audio player.' ), 28 | 'mime_type' => 'audio', 29 | ) ); 30 | 31 | $this->l10n = array_merge( $this->l10n, array( 32 | 'no_media_selected' => __( 'No audio selected' ), 33 | 'add_media' => _x( 'Add File', 'label for button in the audio widget; should not be longer than ~13 characters long' ), 34 | 'replace_media' => _x( 'Replace Audio', 'label for button in the audio widget; should not be longer than ~13 characters long' ), 35 | 'edit_media' => _x( 'Edit Audio', 'label for button in the audio widget; should not be longer than ~13 characters long' ), 36 | 'missing_attachment' => sprintf( 37 | /* translators: placeholder is URL to media library */ 38 | __( 'We can’t find that audio file. Check your media library and make sure it wasn’t deleted.' ), 39 | esc_url( admin_url( 'upload.php' ) ) 40 | ), 41 | /* translators: %d is widget count */ 42 | 'media_library_state_multi' => _n_noop( 'Audio Widget (%d)', 'Audio Widget (%d)' ), 43 | 'media_library_state_single' => __( 'Audio Widget' ), 44 | 'unsupported_file_type' => __( 'Looks like this isn’t the correct kind of file. Please link to an audio file instead.' ), 45 | ) ); 46 | } 47 | 48 | /** 49 | * Get schema for properties of a widget instance (item). 50 | * 51 | * @since 4.8.0 52 | * @access public 53 | * 54 | * @see WP_REST_Controller::get_item_schema() 55 | * @see WP_REST_Controller::get_additional_fields() 56 | * @link https://core.trac.wordpress.org/ticket/35574 57 | * @return array Schema for properties. 58 | */ 59 | public function get_instance_schema() { 60 | $schema = array_merge( 61 | parent::get_instance_schema(), 62 | array( 63 | 'preload' => array( 64 | 'type' => 'string', 65 | 'enum' => array( 'none', 'auto', 'metadata' ), 66 | 'default' => 'none', 67 | ), 68 | 'loop' => array( 69 | 'type' => 'boolean', 70 | 'default' => false, 71 | ), 72 | ) 73 | ); 74 | 75 | foreach ( wp_get_audio_extensions() as $audio_extension ) { 76 | $schema[ $audio_extension ] = array( 77 | 'type' => 'string', 78 | 'default' => '', 79 | 'format' => 'uri', 80 | /* translators: placeholder is audio extension */ 81 | 'description' => sprintf( __( 'URL to the %s audio source file' ), $audio_extension ), 82 | ); 83 | } 84 | 85 | return $schema; 86 | } 87 | 88 | /** 89 | * Render the media on the frontend. 90 | * 91 | * @since 4.8.0 92 | * @access public 93 | * 94 | * @param array $instance Widget instance props. 95 | * @return void 96 | */ 97 | public function render_media( $instance ) { 98 | $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance ); 99 | $attachment = null; 100 | 101 | if ( $this->is_attachment_with_mime_type( $instance['attachment_id'], $this->widget_options['mime_type'] ) ) { 102 | $attachment = get_post( $instance['attachment_id'] ); 103 | } 104 | 105 | if ( $attachment ) { 106 | $src = wp_get_attachment_url( $attachment->ID ); 107 | } else { 108 | $src = $instance['url']; 109 | } 110 | 111 | echo wp_audio_shortcode( 112 | array_merge( 113 | $instance, 114 | compact( 'src' ) 115 | ) 116 | ); 117 | } 118 | 119 | /** 120 | * Enqueue preview scripts. 121 | * 122 | * These scripts normally are enqueued just-in-time when an audio shortcode is used. 123 | * In the customizer, however, widgets can be dynamically added and rendered via 124 | * selective refresh, and so it is important to unconditionally enqueue them in 125 | * case a widget does get added. 126 | * 127 | * @since 4.8.0 128 | * @access public 129 | */ 130 | public function enqueue_preview_scripts() { 131 | /** This filter is documented in wp-includes/media.php */ 132 | if ( 'mediaelement' === apply_filters( 'wp_audio_shortcode_library', 'mediaelement' ) ) { 133 | wp_enqueue_style( 'wp-mediaelement' ); 134 | wp_enqueue_script( 'wp-mediaelement' ); 135 | } 136 | } 137 | 138 | /** 139 | * Loads the required media files for the media manager and scripts for media widgets. 140 | * 141 | * @since 4.8.0 142 | * @access public 143 | */ 144 | public function enqueue_admin_scripts() { 145 | parent::enqueue_admin_scripts(); 146 | 147 | wp_enqueue_style( 'wp-mediaelement' ); 148 | wp_enqueue_script( 'wp-mediaelement' ); 149 | 150 | $handle = 'media-audio-widget'; 151 | wp_enqueue_script( $handle ); 152 | 153 | $exported_schema = array(); 154 | foreach ( $this->get_instance_schema() as $field => $field_schema ) { 155 | $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) ); 156 | } 157 | wp_add_inline_script( 158 | $handle, 159 | sprintf( 160 | 'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;', 161 | wp_json_encode( $this->id_base ), 162 | wp_json_encode( $exported_schema ) 163 | ) 164 | ); 165 | 166 | wp_add_inline_script( 167 | $handle, 168 | sprintf( 169 | ' 170 | wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s; 171 | wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n = _.extend( {}, wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s ); 172 | ', 173 | wp_json_encode( $this->id_base ), 174 | wp_json_encode( $this->widget_options['mime_type'] ), 175 | wp_json_encode( $this->l10n ) 176 | ) 177 | ); 178 | } 179 | 180 | /** 181 | * Render form template scripts. 182 | * 183 | * @since 4.8.0 184 | * @access public 185 | */ 186 | public function render_control_template_scripts() { 187 | parent::render_control_template_scripts() 188 | ?> 189 | 202 | __( 'Displays an image gallery.' ), 27 | 'mime_type' => 'image', 28 | ) ); 29 | 30 | $this->l10n = array_merge( $this->l10n, array( 31 | 'no_media_selected' => __( 'No images selected' ), 32 | 'select_media' => _x( 'Select Images', 'label for button in the gallery widget; should not be longer than ~13 characters long' ), 33 | 'replace_media' => _x( 'Replace Gallery', 'label for button in the gallery widget; should not be longer than ~13 characters long' ), 34 | 'change_media' => _x( 'Add Image', 'label for button in the gallery widget; should not be longer than ~13 characters long' ), 35 | 'edit_media' => _x( 'Edit Gallery', 'label for button in the gallery widget; should not be longer than ~13 characters long' ), 36 | ) ); 37 | } 38 | 39 | /** 40 | * Get schema for properties of a widget instance (item). 41 | * 42 | * @since 4.9.0 43 | * 44 | * @see WP_REST_Controller::get_item_schema() 45 | * @see WP_REST_Controller::get_additional_fields() 46 | * @link https://core.trac.wordpress.org/ticket/35574 47 | * @return array Schema for properties. 48 | */ 49 | public function get_instance_schema() { 50 | return array( 51 | 'title' => array( 52 | 'type' => 'string', 53 | 'default' => '', 54 | 'sanitize_callback' => 'sanitize_text_field', 55 | 'description' => __( 'Title for the widget' ), 56 | 'should_preview_update' => false, 57 | ), 58 | 'ids' => array( 59 | 'type' => 'string', 60 | 'default' => '', 61 | ), 62 | 'columns' => array( 63 | 'type' => 'integer', 64 | 'default' => 3, 65 | ), 66 | 'size' => array( 67 | 'type' => 'string', 68 | 'enum' => array_merge( get_intermediate_image_sizes(), array( 'full', 'custom' ) ), 69 | 'default' => 'thumbnail', 70 | ), 71 | 'link_type' => array( 72 | 'type' => 'string', 73 | 'enum' => array( 'none', 'file', 'post' ), 74 | 'default' => 'none', 75 | 'media_prop' => 'link', 76 | 'should_preview_update' => false, 77 | ), 78 | 'orderby_random' => array( 79 | 'type' => 'boolean', 80 | 'default' => false, 81 | 'media_prop' => '_orderbyRandom', 82 | 'should_preview_update' => false, 83 | ), 84 | ); 85 | } 86 | 87 | /** 88 | * Sanitizes the widget form values as they are saved. 89 | * 90 | * @since 4.9.0 91 | * 92 | * @param array $new_instance Values just sent to be saved. 93 | * @param array $instance Previously saved values from database. 94 | * @return array Updated safe values to be saved. 95 | */ 96 | public function update( $new_instance, $instance ) { 97 | 98 | // Workaround for rest_validate_value_from_schema() due to the fact that rest_is_boolean( '' ) === false, while rest_is_boolean( '1' ) is true. 99 | if ( isset( $new_instance['orderby_random'] ) && '' === $new_instance['orderby_random'] ) { 100 | $new_instance['orderby_random'] = false; 101 | } 102 | 103 | return parent::update( $new_instance, $instance ); 104 | } 105 | 106 | /** 107 | * Render the media on the frontend. 108 | * 109 | * @since 4.9.0 110 | * 111 | * @param array $instance Widget instance props. 112 | * @return void 113 | */ 114 | public function render_media( $instance ) { 115 | $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance ); 116 | 117 | $shortcode_atts = array( 118 | 'ids' => $instance['ids'], 119 | 'columns' => $instance['columns'], 120 | 'link' => $instance['link_type'], 121 | 'size' => $instance['size'], 122 | ); 123 | 124 | // @codingStandardsIgnoreStart 125 | if ( $instance['orderby_random'] ) { 126 | $shortcode_atts['orderby'] = 'rand'; 127 | } 128 | // @codingStandardsIgnoreEnd 129 | 130 | echo gallery_shortcode( $shortcode_atts ); 131 | } 132 | 133 | /** 134 | * Loads the required media files for the media manager and scripts for media widgets. 135 | * 136 | * @since 4.9.0 137 | */ 138 | public function enqueue_admin_scripts() { 139 | parent::enqueue_admin_scripts(); 140 | 141 | $handle = 'media-gallery-widget'; 142 | wp_enqueue_script( $handle ); 143 | wp_enqueue_style( $handle ); 144 | 145 | $exported_schema = array(); 146 | foreach ( $this->get_instance_schema() as $field => $field_schema ) { 147 | $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) ); 148 | } 149 | wp_add_inline_script( 150 | $handle, 151 | sprintf( 152 | 'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;', 153 | wp_json_encode( $this->id_base ), 154 | wp_json_encode( $exported_schema ) 155 | ) 156 | ); 157 | 158 | wp_add_inline_script( 159 | $handle, 160 | sprintf( 161 | ' 162 | wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s; 163 | _.extend( wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s ); 164 | ', 165 | wp_json_encode( $this->id_base ), 166 | wp_json_encode( $this->widget_options['mime_type'] ), 167 | wp_json_encode( $this->l10n ) 168 | ) 169 | ); 170 | } 171 | 172 | /** 173 | * Render form template scripts. 174 | * 175 | * @since 4.9.0 176 | */ 177 | public function render_control_template_scripts() { 178 | parent::render_control_template_scripts(); 179 | ?> 180 | 215 | __( 'Displays an image.' ), 28 | 'mime_type' => 'image', 29 | ) ); 30 | 31 | $this->l10n = array_merge( $this->l10n, array( 32 | 'no_media_selected' => __( 'No image selected' ), 33 | 'add_media' => _x( 'Add Image', 'label for button in the image widget; should not be longer than ~13 characters long' ), 34 | 'replace_media' => _x( 'Replace Image', 'label for button in the image widget; should not be longer than ~13 characters long' ), 35 | 'edit_media' => _x( 'Edit Image', 'label for button in the image widget; should not be longer than ~13 characters long' ), 36 | 'missing_attachment' => sprintf( 37 | /* translators: placeholder is URL to media library */ 38 | __( 'We can’t find that image. Check your media library and make sure it wasn’t deleted.' ), 39 | esc_url( admin_url( 'upload.php' ) ) 40 | ), 41 | /* translators: %d is widget count */ 42 | 'media_library_state_multi' => _n_noop( 'Image Widget (%d)', 'Image Widget (%d)' ), 43 | 'media_library_state_single' => __( 'Image Widget' ), 44 | ) ); 45 | } 46 | 47 | /** 48 | * Get schema for properties of a widget instance (item). 49 | * 50 | * @since 4.8.0 51 | * @access public 52 | * 53 | * @see WP_REST_Controller::get_item_schema() 54 | * @see WP_REST_Controller::get_additional_fields() 55 | * @link https://core.trac.wordpress.org/ticket/35574 56 | * @return array Schema for properties. 57 | */ 58 | public function get_instance_schema() { 59 | return array_merge( 60 | parent::get_instance_schema(), 61 | array( 62 | 'size' => array( 63 | 'type' => 'string', 64 | 'enum' => array_merge( get_intermediate_image_sizes(), array( 'full', 'custom' ) ), 65 | 'default' => 'medium', 66 | ), 67 | 'width' => array( // Via 'customWidth', only when size=custom; otherwise via 'width'. 68 | 'type' => 'integer', 69 | 'minimum' => 0, 70 | 'default' => 0, 71 | ), 72 | 'height' => array( // Via 'customHeight', only when size=custom; otherwise via 'height'. 73 | 'type' => 'integer', 74 | 'minimum' => 0, 75 | 'default' => 0, 76 | ), 77 | 78 | 'caption' => array( 79 | 'type' => 'string', 80 | 'default' => '', 81 | 'sanitize_callback' => 'wp_kses_post', 82 | 'should_preview_update' => false, 83 | ), 84 | 'alt' => array( 85 | 'type' => 'string', 86 | 'default' => '', 87 | 'sanitize_callback' => 'sanitize_text_field', 88 | ), 89 | 'link_type' => array( 90 | 'type' => 'string', 91 | 'enum' => array( 'none', 'file', 'post', 'custom' ), 92 | 'default' => 'none', 93 | 'media_prop' => 'link', 94 | 'should_preview_update' => false, 95 | ), 96 | 'link_url' => array( 97 | 'type' => 'string', 98 | 'default' => '', 99 | 'format' => 'uri', 100 | 'media_prop' => 'linkUrl', 101 | 'should_preview_update' => false, 102 | ), 103 | 'image_classes' => array( 104 | 'type' => 'string', 105 | 'default' => '', 106 | 'sanitize_callback' => array( $this, 'sanitize_token_list' ), 107 | 'media_prop' => 'extraClasses', 108 | 'should_preview_update' => false, 109 | ), 110 | 'link_classes' => array( 111 | 'type' => 'string', 112 | 'default' => '', 113 | 'sanitize_callback' => array( $this, 'sanitize_token_list' ), 114 | 'media_prop' => 'linkClassName', 115 | 'should_preview_update' => false, 116 | ), 117 | 'link_rel' => array( 118 | 'type' => 'string', 119 | 'default' => '', 120 | 'sanitize_callback' => array( $this, 'sanitize_token_list' ), 121 | 'media_prop' => 'linkRel', 122 | 'should_preview_update' => false, 123 | ), 124 | 'link_target_blank' => array( // Via 'linkTargetBlank' property. 125 | 'type' => 'boolean', 126 | 'default' => false, 127 | 'media_prop' => 'linkTargetBlank', 128 | 'should_preview_update' => false, 129 | ), 130 | 'image_title' => array( 131 | 'type' => 'string', 132 | 'default' => '', 133 | 'sanitize_callback' => 'sanitize_text_field', 134 | 'media_prop' => 'title', 135 | 'should_preview_update' => false, 136 | ), 137 | 138 | /* 139 | * There are two additional properties exposed by the PostImage modal 140 | * that don't seem to be relevant, as they may only be derived read-only 141 | * values: 142 | * - originalUrl 143 | * - aspectRatio 144 | * - height (redundant when size is not custom) 145 | * - width (redundant when size is not custom) 146 | */ 147 | ) 148 | ); 149 | } 150 | 151 | /** 152 | * Render the media on the frontend. 153 | * 154 | * @since 4.8.0 155 | * @access public 156 | * 157 | * @param array $instance Widget instance props. 158 | * @return void 159 | */ 160 | public function render_media( $instance ) { 161 | $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance ); 162 | $instance = wp_parse_args( $instance, array( 163 | 'size' => 'thumbnail', 164 | ) ); 165 | 166 | $attachment = null; 167 | if ( $this->is_attachment_with_mime_type( $instance['attachment_id'], $this->widget_options['mime_type'] ) ) { 168 | $attachment = get_post( $instance['attachment_id'] ); 169 | } 170 | if ( $attachment ) { 171 | $caption = $attachment->post_excerpt; 172 | if ( $instance['caption'] ) { 173 | $caption = $instance['caption']; 174 | } 175 | 176 | $image_attributes = array( 177 | 'class' => sprintf( 'image wp-image-%d %s', $attachment->ID, $instance['image_classes'] ), 178 | 'style' => 'max-width: 100%; height: auto;', 179 | ); 180 | if ( ! empty( $instance['image_title'] ) ) { 181 | $image_attributes['title'] = $instance['image_title']; 182 | } 183 | 184 | if ( $instance['alt'] ) { 185 | $image_attributes['alt'] = $instance['alt']; 186 | } 187 | 188 | $size = $instance['size']; 189 | if ( 'custom' === $size || ! in_array( $size, array_merge( get_intermediate_image_sizes(), array( 'full' ) ), true ) ) { 190 | $size = array( $instance['width'], $instance['height'] ); 191 | } 192 | 193 | $image = wp_get_attachment_image( $attachment->ID, $size, false, $image_attributes ); 194 | 195 | $caption_size = _wp_get_image_size_from_meta( $instance['size'], wp_get_attachment_metadata( $attachment->ID ) ); 196 | $width = empty( $caption_size[0] ) ? 0 : $caption_size[0]; 197 | 198 | } else { 199 | if ( empty( $instance['url'] ) ) { 200 | return; 201 | } 202 | 203 | $instance['size'] = 'custom'; 204 | $caption = $instance['caption']; 205 | $width = $instance['width']; 206 | $classes = 'image ' . $instance['image_classes']; 207 | if ( 0 === $instance['width'] ) { 208 | $instance['width'] = ''; 209 | } 210 | if ( 0 === $instance['height'] ) { 211 | $instance['height'] = ''; 212 | } 213 | 214 | $image = sprintf( '%3$s', 215 | esc_attr( $classes ), 216 | esc_url( $instance['url'] ), 217 | esc_attr( $instance['alt'] ), 218 | esc_attr( $instance['width'] ), 219 | esc_attr( $instance['height'] ) 220 | ); 221 | } // End if(). 222 | 223 | $url = ''; 224 | if ( 'file' === $instance['link_type'] ) { 225 | $url = $attachment ? wp_get_attachment_url( $attachment->ID ) : $instance['url']; 226 | } elseif ( $attachment && 'post' === $instance['link_type'] ) { 227 | $url = get_attachment_link( $attachment->ID ); 228 | } elseif ( 'custom' === $instance['link_type'] && ! empty( $instance['link_url'] ) ) { 229 | $url = $instance['link_url']; 230 | } 231 | 232 | if ( $url ) { 233 | $image = sprintf( 234 | '%5$s', 235 | esc_url( $url ), 236 | esc_attr( $instance['link_classes'] ), 237 | esc_attr( $instance['link_rel'] ), 238 | ! empty( $instance['link_target_blank'] ) ? '_blank' : '', 239 | $image 240 | ); 241 | } 242 | 243 | if ( $caption ) { 244 | $image = img_caption_shortcode( array( 245 | 'width' => $width, 246 | 'caption' => $caption, 247 | ), $image ); 248 | } 249 | 250 | echo $image; 251 | } 252 | 253 | /** 254 | * Loads the required media files for the media manager and scripts for media widgets. 255 | * 256 | * @since 4.8.0 257 | * @access public 258 | */ 259 | public function enqueue_admin_scripts() { 260 | parent::enqueue_admin_scripts(); 261 | 262 | $handle = 'media-image-widget'; 263 | wp_enqueue_script( $handle ); 264 | 265 | $exported_schema = array(); 266 | foreach ( $this->get_instance_schema() as $field => $field_schema ) { 267 | $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) ); 268 | } 269 | wp_add_inline_script( 270 | $handle, 271 | sprintf( 272 | 'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;', 273 | wp_json_encode( $this->id_base ), 274 | wp_json_encode( $exported_schema ) 275 | ) 276 | ); 277 | 278 | wp_add_inline_script( 279 | $handle, 280 | sprintf( 281 | ' 282 | wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s; 283 | wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n = _.extend( {}, wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s ); 284 | ', 285 | wp_json_encode( $this->id_base ), 286 | wp_json_encode( $this->widget_options['mime_type'] ), 287 | wp_json_encode( $this->l10n ) 288 | ) 289 | ); 290 | } 291 | 292 | /** 293 | * Render form template scripts. 294 | * 295 | * @since 4.8.0 296 | * @access public 297 | */ 298 | public function render_control_template_scripts() { 299 | parent::render_control_template_scripts(); 300 | 301 | ?> 302 | 324 | __( 'Displays a video from the media library or from YouTube, Vimeo, or another provider.' ), 28 | 'mime_type' => 'video', 29 | ) ); 30 | 31 | $this->l10n = array_merge( $this->l10n, array( 32 | 'no_media_selected' => __( 'No video selected' ), 33 | 'add_media' => _x( 'Add Video', 'label for button in the video widget; should not be longer than ~13 characters long' ), 34 | 'replace_media' => _x( 'Replace Video', 'label for button in the video widget; should not be longer than ~13 characters long' ), 35 | 'edit_media' => _x( 'Edit Video', 'label for button in the video widget; should not be longer than ~13 characters long' ), 36 | 'missing_attachment' => sprintf( 37 | /* translators: placeholder is URL to media library */ 38 | __( 'We can’t find that video. Check your media library and make sure it wasn’t deleted.' ), 39 | esc_url( admin_url( 'upload.php' ) ) 40 | ), 41 | /* translators: %d is widget count */ 42 | 'media_library_state_multi' => _n_noop( 'Video Widget (%d)', 'Video Widget (%d)' ), 43 | 'media_library_state_single' => __( 'Video Widget' ), 44 | /* translators: placeholder is a list of valid video file extensions */ 45 | 'unsupported_file_type' => sprintf( __( 'Sorry, we can’t display the video file type selected. Please select a supported video file (%1$s) or stream (YouTube or Vimeo) instead.' ), '.' . implode( ', .', wp_get_video_extensions() ) . '' ), 46 | ) ); 47 | } 48 | 49 | /** 50 | * Get schema for properties of a widget instance (item). 51 | * 52 | * @since 4.8.0 53 | * @access public 54 | * 55 | * @see WP_REST_Controller::get_item_schema() 56 | * @see WP_REST_Controller::get_additional_fields() 57 | * @link https://core.trac.wordpress.org/ticket/35574 58 | * @return array Schema for properties. 59 | */ 60 | public function get_instance_schema() { 61 | $schema = array_merge( 62 | parent::get_instance_schema(), 63 | array( 64 | 'preload' => array( 65 | 'type' => 'string', 66 | 'enum' => array( 'none', 'auto', 'metadata' ), 67 | 'default' => 'metadata', 68 | 'should_preview_update' => false, 69 | ), 70 | 'loop' => array( 71 | 'type' => 'boolean', 72 | 'default' => false, 73 | 'should_preview_update' => false, 74 | ), 75 | 'content' => array( 76 | 'type' => 'string', 77 | 'default' => '', 78 | 'sanitize_callback' => 'wp_kses_post', 79 | 'description' => __( 'Tracks (subtitles, captions, descriptions, chapters, or metadata)' ), 80 | 'should_preview_update' => false, 81 | ), 82 | ) 83 | ); 84 | 85 | foreach ( wp_get_video_extensions() as $video_extension ) { 86 | $schema[ $video_extension ] = array( 87 | 'type' => 'string', 88 | 'default' => '', 89 | 'format' => 'uri', 90 | /* translators: placeholder is video extension */ 91 | 'description' => sprintf( __( 'URL to the %s video source file' ), $video_extension ), 92 | ); 93 | } 94 | 95 | // TODO: height and width? 96 | return $schema; 97 | } 98 | 99 | /** 100 | * Render the media on the frontend. 101 | * 102 | * @since 4.8.0 103 | * @access public 104 | * 105 | * @param array $instance Widget instance props. 106 | * 107 | * @return void 108 | */ 109 | public function render_media( $instance ) { 110 | $instance = array_merge( wp_list_pluck( $this->get_instance_schema(), 'default' ), $instance ); 111 | $attachment = null; 112 | 113 | if ( $this->is_attachment_with_mime_type( $instance['attachment_id'], $this->widget_options['mime_type'] ) ) { 114 | $attachment = get_post( $instance['attachment_id'] ); 115 | } 116 | 117 | if ( $attachment ) { 118 | $src = wp_get_attachment_url( $attachment->ID ); 119 | } else { 120 | 121 | // Manually add the loop query argument. 122 | $loop = $instance['loop'] ? '1' : '0'; 123 | $src = empty( $instance['url'] ) ? $instance['url'] : add_query_arg( 'loop', $loop, $instance['url'] ); 124 | } 125 | 126 | if ( empty( $src ) ) { 127 | return; 128 | } 129 | 130 | add_filter( 'wp_video_shortcode', array( $this, 'inject_video_max_width_style' ) ); 131 | 132 | echo wp_video_shortcode( 133 | array_merge( 134 | $instance, 135 | compact( 'src' ) 136 | ), 137 | $instance['content'] 138 | ); 139 | 140 | remove_filter( 'wp_video_shortcode', array( $this, 'inject_video_max_width_style' ) ); 141 | } 142 | 143 | /** 144 | * Inject max-width and remove height for videos too constrained to fit inside sidebars on frontend. 145 | * 146 | * @since 4.8.0 147 | * @access public 148 | * 149 | * @param string $html Video shortcode HTML output. 150 | * @return string HTML Output. 151 | */ 152 | public function inject_video_max_width_style( $html ) { 153 | $html = preg_replace( '/\sheight="\d+"/', '', $html ); 154 | $html = preg_replace( '/\swidth="\d+"/', '', $html ); 155 | $html = preg_replace( '/(?<=width:)\s*\d+px(?=;?)/', '100%', $html ); 156 | return $html; 157 | } 158 | 159 | /** 160 | * Enqueue preview scripts. 161 | * 162 | * These scripts normally are enqueued just-in-time when a video shortcode is used. 163 | * In the customizer, however, widgets can be dynamically added and rendered via 164 | * selective refresh, and so it is important to unconditionally enqueue them in 165 | * case a widget does get added. 166 | * 167 | * @since 4.8.0 168 | * @access public 169 | */ 170 | public function enqueue_preview_scripts() { 171 | /** This filter is documented in wp-includes/media.php */ 172 | if ( 'mediaelement' === apply_filters( 'wp_video_shortcode_library', 'mediaelement' ) ) { 173 | wp_enqueue_style( 'wp-mediaelement' ); 174 | wp_enqueue_script( 'wp-mediaelement' ); 175 | } 176 | 177 | // Enqueue script needed by Vimeo; see wp_video_shortcode(). 178 | wp_enqueue_script( 'froogaloop' ); 179 | } 180 | 181 | /** 182 | * Loads the required scripts and styles for the widget control. 183 | * 184 | * @since 4.8.0 185 | * @access public 186 | */ 187 | public function enqueue_admin_scripts() { 188 | parent::enqueue_admin_scripts(); 189 | 190 | $handle = 'media-video-widget'; 191 | wp_enqueue_script( $handle ); 192 | 193 | $exported_schema = array(); 194 | foreach ( $this->get_instance_schema() as $field => $field_schema ) { 195 | $exported_schema[ $field ] = wp_array_slice_assoc( $field_schema, array( 'type', 'default', 'enum', 'minimum', 'format', 'media_prop', 'should_preview_update' ) ); 196 | } 197 | wp_add_inline_script( 198 | $handle, 199 | sprintf( 200 | 'wp.mediaWidgets.modelConstructors[ %s ].prototype.schema = %s;', 201 | wp_json_encode( $this->id_base ), 202 | wp_json_encode( $exported_schema ) 203 | ) 204 | ); 205 | 206 | wp_add_inline_script( 207 | $handle, 208 | sprintf( 209 | ' 210 | wp.mediaWidgets.controlConstructors[ %1$s ].prototype.mime_type = %2$s; 211 | wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n = _.extend( {}, wp.mediaWidgets.controlConstructors[ %1$s ].prototype.l10n, %3$s ); 212 | ', 213 | wp_json_encode( $this->id_base ), 214 | wp_json_encode( $this->widget_options['mime_type'] ), 215 | wp_json_encode( $this->l10n ) 216 | ) 217 | ); 218 | } 219 | 220 | /** 221 | * Render form template scripts. 222 | * 223 | * @since 4.8.0 224 | * @access public 225 | */ 226 | public function render_control_template_scripts() { 227 | parent::render_control_template_scripts() 228 | ?> 229 | 254 | '', 27 | 'replace_media' => '', 28 | 'edit_media' => '', 29 | 'media_library_state_multi' => '', 30 | 'media_library_state_single' => '', 31 | 'missing_attachment' => '', 32 | 'no_media_selected' => '', 33 | 'add_media' => '', 34 | ); 35 | 36 | /** 37 | * Constructor. 38 | * 39 | * @since 4.8.0 40 | * @access public 41 | * 42 | * @param string $id_base Base ID for the widget, lowercase and unique. 43 | * @param string $name Name for the widget displayed on the configuration page. 44 | * @param array $widget_options Optional. Widget options. See wp_register_sidebar_widget() for 45 | * information on accepted arguments. Default empty array. 46 | * @param array $control_options Optional. Widget control options. See wp_register_widget_control() 47 | * for information on accepted arguments. Default empty array. 48 | */ 49 | public function __construct( $id_base, $name, $widget_options = array(), $control_options = array() ) { 50 | $widget_opts = wp_parse_args( $widget_options, array( 51 | 'description' => __( 'A media item.' ), 52 | 'customize_selective_refresh' => true, 53 | 'mime_type' => '', 54 | ) ); 55 | 56 | $control_opts = wp_parse_args( $control_options, array() ); 57 | 58 | $l10n_defaults = array( 59 | 'no_media_selected' => __( 'No media selected' ), 60 | 'add_media' => _x( 'Add Media', 'label for button in the media widget; should not be longer than ~13 characters long' ), 61 | 'replace_media' => _x( 'Replace Media', 'label for button in the media widget; should not be longer than ~13 characters long' ), 62 | 'edit_media' => _x( 'Edit Media', 'label for button in the media widget; should not be longer than ~13 characters long' ), 63 | 'add_to_widget' => __( 'Add to Widget' ), 64 | 'missing_attachment' => sprintf( 65 | /* translators: placeholder is URL to media library */ 66 | __( 'We can’t find that file. Check your media library and make sure it wasn’t deleted.' ), 67 | esc_url( admin_url( 'upload.php' ) ) 68 | ), 69 | /* translators: %d is widget count */ 70 | 'media_library_state_multi' => _n_noop( 'Media Widget (%d)', 'Media Widget (%d)' ), 71 | 'media_library_state_single' => __( 'Media Widget' ), 72 | 'unsupported_file_type' => __( 'Looks like this isn’t the correct kind of file. Please link to an appropriate file instead.' ), 73 | ); 74 | $this->l10n = array_merge( $l10n_defaults, array_filter( $this->l10n ) ); 75 | 76 | parent::__construct( 77 | $id_base, 78 | $name, 79 | $widget_opts, 80 | $control_opts 81 | ); 82 | } 83 | 84 | /** 85 | * Add hooks while registering all widget instances of this widget class. 86 | * 87 | * @since 4.8.0 88 | * @access public 89 | */ 90 | public function _register() { 91 | 92 | // Note that the widgets component in the customizer will also do the 'admin_print_scripts-widgets.php' action in WP_Customize_Widgets::print_scripts(). 93 | add_action( 'admin_print_scripts-widgets.php', array( $this, 'enqueue_admin_scripts' ) ); 94 | 95 | if ( $this->is_preview() ) { 96 | add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_preview_scripts' ) ); 97 | } 98 | 99 | // Note that the widgets component in the customizer will also do the 'admin_footer-widgets.php' action in WP_Customize_Widgets::print_footer_scripts(). 100 | add_action( 'admin_footer-widgets.php', array( $this, 'render_control_template_scripts' ) ); 101 | 102 | add_filter( 'display_media_states', array( $this, 'display_media_state' ), 10, 2 ); 103 | 104 | parent::_register(); 105 | } 106 | 107 | /** 108 | * Get schema for properties of a widget instance (item). 109 | * 110 | * @since 4.8.0 111 | * @access public 112 | * 113 | * @see WP_REST_Controller::get_item_schema() 114 | * @see WP_REST_Controller::get_additional_fields() 115 | * @link https://core.trac.wordpress.org/ticket/35574 116 | * @return array Schema for properties. 117 | */ 118 | public function get_instance_schema() { 119 | return array( 120 | 'attachment_id' => array( 121 | 'type' => 'integer', 122 | 'default' => 0, 123 | 'minimum' => 0, 124 | 'description' => __( 'Attachment post ID' ), 125 | 'media_prop' => 'id', 126 | ), 127 | 'url' => array( 128 | 'type' => 'string', 129 | 'default' => '', 130 | 'format' => 'uri', 131 | 'description' => __( 'URL to the media file' ), 132 | ), 133 | 'title' => array( 134 | 'type' => 'string', 135 | 'default' => '', 136 | 'sanitize_callback' => 'sanitize_text_field', 137 | 'description' => __( 'Title for the widget' ), 138 | 'should_preview_update' => false, 139 | ), 140 | ); 141 | } 142 | 143 | /** 144 | * Determine if the supplied attachment is for a valid attachment post with the specified MIME type. 145 | * 146 | * @since 4.8.0 147 | * @access public 148 | * 149 | * @param int|WP_Post $attachment Attachment post ID or object. 150 | * @param string $mime_type MIME type. 151 | * @return bool Is matching MIME type. 152 | */ 153 | public function is_attachment_with_mime_type( $attachment, $mime_type ) { 154 | if ( empty( $attachment ) ) { 155 | return false; 156 | } 157 | $attachment = get_post( $attachment ); 158 | if ( ! $attachment ) { 159 | return false; 160 | } 161 | if ( 'attachment' !== $attachment->post_type ) { 162 | return false; 163 | } 164 | return wp_attachment_is( $mime_type, $attachment ); 165 | } 166 | 167 | /** 168 | * Sanitize a token list string, such as used in HTML rel and class attributes. 169 | * 170 | * @since 4.8.0 171 | * @access public 172 | * 173 | * @link http://w3c.github.io/html/infrastructure.html#space-separated-tokens 174 | * @link https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList 175 | * @param string|array $tokens List of tokens separated by spaces, or an array of tokens. 176 | * @return string Sanitized token string list. 177 | */ 178 | public function sanitize_token_list( $tokens ) { 179 | if ( is_string( $tokens ) ) { 180 | $tokens = preg_split( '/\s+/', trim( $tokens ) ); 181 | } 182 | $tokens = array_map( 'sanitize_html_class', $tokens ); 183 | $tokens = array_filter( $tokens ); 184 | return join( ' ', $tokens ); 185 | } 186 | 187 | /** 188 | * Displays the widget on the front-end. 189 | * 190 | * @since 4.8.0 191 | * @access public 192 | * 193 | * @see WP_Widget::widget() 194 | * 195 | * @param array $args Display arguments including before_title, after_title, before_widget, and after_widget. 196 | * @param array $instance Saved setting from the database. 197 | */ 198 | public function widget( $args, $instance ) { 199 | $instance = wp_parse_args( $instance, wp_list_pluck( $this->get_instance_schema(), 'default' ) ); 200 | 201 | // Short-circuit if no media is selected. 202 | if ( ! $this->has_content( $instance ) ) { 203 | return; 204 | } 205 | 206 | echo $args['before_widget']; 207 | 208 | if ( $instance['title'] ) { 209 | 210 | /** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */ 211 | $title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base ); 212 | echo $args['before_title'] . $title . $args['after_title']; 213 | } 214 | 215 | /** 216 | * Filters the media widget instance prior to rendering the media. 217 | * 218 | * @since 4.8.0 219 | * 220 | * @param array $instance Instance data. 221 | * @param array $args Widget args. 222 | * @param WP_Widget_Media $this Widget object. 223 | */ 224 | $instance = apply_filters( "widget_{$this->id_base}_instance", $instance, $args, $this ); 225 | 226 | $this->render_media( $instance ); 227 | 228 | echo $args['after_widget']; 229 | } 230 | 231 | /** 232 | * Sanitizes the widget form values as they are saved. 233 | * 234 | * @since 4.8.0 235 | * @access public 236 | * 237 | * @see WP_Widget::update() 238 | * @see WP_REST_Request::has_valid_params() 239 | * @see WP_REST_Request::sanitize_params() 240 | * 241 | * @param array $new_instance Values just sent to be saved. 242 | * @param array $instance Previously saved values from database. 243 | * @return array Updated safe values to be saved. 244 | */ 245 | public function update( $new_instance, $instance ) { 246 | 247 | $schema = $this->get_instance_schema(); 248 | foreach ( $schema as $field => $field_schema ) { 249 | if ( ! array_key_exists( $field, $new_instance ) ) { 250 | continue; 251 | } 252 | $value = $new_instance[ $field ]; 253 | if ( true !== rest_validate_value_from_schema( $value, $field_schema, $field ) ) { 254 | continue; 255 | } 256 | 257 | $value = rest_sanitize_value_from_schema( $value, $field_schema ); 258 | 259 | // @codeCoverageIgnoreStart 260 | if ( is_wp_error( $value ) ) { 261 | continue; // Handle case when rest_sanitize_value_from_schema() ever returns WP_Error as its phpdoc @return tag indicates. 262 | } 263 | 264 | // @codeCoverageIgnoreEnd 265 | if ( isset( $field_schema['sanitize_callback'] ) ) { 266 | $value = call_user_func( $field_schema['sanitize_callback'], $value ); 267 | } 268 | if ( is_wp_error( $value ) ) { 269 | continue; 270 | } 271 | $instance[ $field ] = $value; 272 | } 273 | 274 | return $instance; 275 | } 276 | 277 | /** 278 | * Render the media on the frontend. 279 | * 280 | * @since 4.8.0 281 | * @access public 282 | * 283 | * @param array $instance Widget instance props. 284 | * @return string 285 | */ 286 | abstract public function render_media( $instance ); 287 | 288 | /** 289 | * Outputs the settings update form. 290 | * 291 | * Note that the widget UI itself is rendered with JavaScript via `MediaWidgetControl#render()`. 292 | * 293 | * @since 4.8.0 294 | * @access public 295 | * 296 | * @see \WP_Widget_Media::render_control_template_scripts() Where the JS template is located. 297 | * @param array $instance Current settings. 298 | * @return void 299 | */ 300 | final public function form( $instance ) { 301 | $instance_schema = $this->get_instance_schema(); 302 | $instance = wp_array_slice_assoc( 303 | wp_parse_args( (array) $instance, wp_list_pluck( $instance_schema, 'default' ) ), 304 | array_keys( $instance_schema ) 305 | ); 306 | 307 | foreach ( $instance as $name => $value ) : ?> 308 | 316 | get_settings() as $instance ) { 338 | if ( isset( $instance['attachment_id'] ) && $instance['attachment_id'] === $post->ID ) { 339 | $use_count++; 340 | } 341 | } 342 | 343 | if ( 1 === $use_count ) { 344 | $states[] = $this->l10n['media_library_state_single']; 345 | } elseif ( $use_count > 0 ) { 346 | $states[] = sprintf( translate_nooped_plural( $this->l10n['media_library_state_multi'], $use_count ), number_format_i18n( $use_count ) ); 347 | } 348 | 349 | return $states; 350 | } 351 | 352 | /** 353 | * Enqueue preview scripts. 354 | * 355 | * These scripts normally are enqueued just-in-time when a widget is rendered. 356 | * In the customizer, however, widgets can be dynamically added and rendered via 357 | * selective refresh, and so it is important to unconditionally enqueue them in 358 | * case a widget does get added. 359 | * 360 | * @since 4.8.0 361 | * @access public 362 | */ 363 | public function enqueue_preview_scripts() {} 364 | 365 | /** 366 | * Loads the required scripts and styles for the widget control. 367 | * 368 | * @since 4.8.0 369 | * @access public 370 | */ 371 | public function enqueue_admin_scripts() { 372 | wp_enqueue_media(); 373 | wp_enqueue_style( 'media-widgets' ); 374 | wp_enqueue_script( 'media-widgets' ); 375 | } 376 | 377 | /** 378 | * Render form template scripts. 379 | * 380 | * @since 4.8.0 381 | * @access public 382 | */ 383 | public function render_control_template_scripts() { 384 | ?> 385 | 408 | id_base ); 50 | 51 | $widget_text = ! empty( $instance['text'] ) ? $instance['text'] : ''; 52 | 53 | /** 54 | * Filters the content of the Text widget. 55 | * 56 | * @since 2.3.0 57 | * @since 4.4.0 Added the `$this` parameter. 58 | * 59 | * @param string $widget_text The widget content. 60 | * @param array $instance Array of settings for the current widget. 61 | * @param WP_Widget_Text $this Current Text widget instance. 62 | */ 63 | $text = apply_filters( 'widget_text', $widget_text, $instance, $this ); 64 | 65 | if ( isset( $instance['filter'] ) ) { 66 | if ( 'content' === $instance['filter'] ) { 67 | 68 | /** 69 | * Filters the content of the Text widget to apply changes expected from the visual (TinyMCE) editor. 70 | * 71 | * By default a subset of the_content filters are applied, including wpautop and wptexturize. 72 | * 73 | * @since 4.8.0 74 | * 75 | * @param string $widget_text The widget content. 76 | * @param array $instance Array of settings for the current widget. 77 | * @param WP_Widget_Text $this Current Text widget instance. 78 | */ 79 | $text = apply_filters( 'widget_text_content', $widget_text, $instance, $this ); 80 | 81 | } elseif ( $instance['filter'] ) { 82 | $text = wpautop( $text ); // Back-compat for instances prior to 4.8. 83 | } 84 | } 85 | 86 | echo $args['before_widget']; 87 | if ( ! empty( $title ) ) { 88 | echo $args['before_title'] . $title . $args['after_title']; 89 | } 90 | 91 | ?> 92 |
93 | '', 155 | 'text' => '', 156 | ) 157 | ); 158 | ?> 159 | 160 | 161 | 172 | 183 |