├── .bin └── install-wp-tests.sh ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .stylelintignore ├── .stylelintrc ├── .tests ├── bootstrap.php └── test-sample.php ├── LICENSE ├── README.md ├── assets ├── dist │ ├── blocks.js │ └── editor.css └── src │ ├── components │ ├── color-palette-control │ │ ├── editor.scss │ │ └── index.js │ ├── figure │ │ └── index.js │ ├── file-control │ │ ├── editor.scss │ │ └── index.js │ ├── focal-point-picker-control │ │ ├── editor.scss │ │ └── index.js │ ├── gallery-control │ │ ├── editor.scss │ │ └── index.js │ ├── image-control │ │ ├── editor.scss │ │ └── index.js │ ├── img │ │ └── index.js │ ├── link-button │ │ └── index.js │ ├── link-control │ │ ├── editor.scss │ │ ├── index.js │ │ └── modal.js │ ├── multiselect-control │ │ ├── editor.scss │ │ └── index.js │ ├── post-relationship-control │ │ └── index.js │ ├── relationship-control │ │ └── index.js │ ├── relationship │ │ ├── editor.scss │ │ ├── index.js │ │ ├── search-items.js │ │ ├── selected-items.js │ │ └── selector.js │ ├── select-image │ │ ├── editor.scss │ │ ├── image-container.js │ │ └── index.js │ └── taxonomy-relationship-control │ │ └── index.js │ ├── data │ ├── media.js │ └── relationship.js │ └── index.js ├── composer.json ├── composer.lock ├── gumponents.php ├── inc ├── autoload.php ├── namespace.php └── rest-api │ ├── class-media-controller.php │ └── relationship │ ├── class-controller.php │ ├── class-posts-controller.php │ └── class-taxonomies-controller.php ├── package-lock.json ├── package.json ├── phpcs.xml ├── phpunit.xml.dist └── webpack.config.js /.bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 29 | WP_TESTS_TAG="branches/$WP_VERSION" 30 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 31 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 32 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 33 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 34 | else 35 | WP_TESTS_TAG="tags/$WP_VERSION" 36 | fi 37 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 38 | WP_TESTS_TAG="trunk" 39 | else 40 | # http serves a single offer, whereas https serves multiple. we only want one 41 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 42 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 43 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 44 | if [[ -z "$LATEST_VERSION" ]]; then 45 | echo "Latest WordPress version could not be found" 46 | exit 1 47 | fi 48 | WP_TESTS_TAG="tags/$LATEST_VERSION" 49 | fi 50 | 51 | set -ex 52 | 53 | install_wp() { 54 | 55 | if [ -d $WP_CORE_DIR ]; then 56 | return; 57 | fi 58 | 59 | mkdir -p $WP_CORE_DIR 60 | 61 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 62 | mkdir -p $TMPDIR/wordpress-nightly 63 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 64 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 65 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 66 | else 67 | if [ $WP_VERSION == 'latest' ]; then 68 | local ARCHIVE_NAME='latest' 69 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 70 | # https serves multiple offers, whereas http serves single. 71 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 72 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 73 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 74 | LATEST_VERSION=${WP_VERSION%??} 75 | else 76 | # otherwise, scan the releases and get the most up to date minor version of the major release 77 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 78 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 79 | fi 80 | if [[ -z "$LATEST_VERSION" ]]; then 81 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 82 | else 83 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 84 | fi 85 | else 86 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 87 | fi 88 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 89 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 90 | fi 91 | 92 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 93 | } 94 | 95 | install_test_suite() { 96 | # portable in-place argument for both GNU sed and Mac OSX sed 97 | if [[ $(uname -s) == 'Darwin' ]]; then 98 | local ioption='-i.bak' 99 | else 100 | local ioption='-i' 101 | fi 102 | 103 | # set up testing suite if it doesn't yet exist 104 | if [ ! -d $WP_TESTS_DIR ]; then 105 | # set up testing suite 106 | mkdir -p $WP_TESTS_DIR 107 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 108 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 109 | fi 110 | 111 | if [ ! -f wp-tests-config.php ]; then 112 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 113 | # remove all forward slashes in the end 114 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 115 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 116 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 117 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 118 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 119 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 120 | fi 121 | 122 | } 123 | 124 | install_wp 125 | install_test_suite 126 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # WordPress Coding Standards 2 | # https://make.wordpress.org/core/handbook/coding-standards/ 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = tab 12 | 13 | [{.jshintrc,*.json,*.yml}] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [{*.txt,wp-config-sample.php}] 18 | end_of_line = crlf 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | vendor 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "plugin:@wordpress/eslint-plugin/recommended-with-formatting" ], 3 | "rules": { 4 | "no-shadow": "off", 5 | "jsx-a11y/anchor-is-valid": "off", 6 | "prefer-const": "off", 7 | "@wordpress/dependency-group": "off", 8 | "@wordpress/no-base-control-with-label-without-id": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Coding Standards and Tests 2 | 3 | on: [ push ] 4 | 5 | env: 6 | WP_TESTS_DIR: /home/runner/wp-tests/wordpress-tests-lib 7 | WP_CORE_DIR: /home/runner/wp-tests/wordpress 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | services: 14 | mysql: 15 | image: mariadb:10.6 16 | env: 17 | MYSQL_ROOT_PASSWORD: root 18 | MYSQL_DATABASE: wordpress_test 19 | ports: 20 | - 3306:3306 21 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=5s --health-retries=3 22 | 23 | steps: 24 | - name: Set up PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: '8.1' 28 | coverage: none 29 | tools: composer, cs2pr 30 | 31 | - name: Install NodeJS 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: '13.x' 35 | 36 | - name: Checkout repository 37 | uses: actions/checkout@v3 38 | 39 | - name: Composer cache 40 | id: composer-cache 41 | uses: actions/cache@v3 42 | with: 43 | path: vendor 44 | key: composer-${{ hashFiles( '.github/workflows/main.yml' ) }}-${{ hashFiles( 'composer.lock' ) }} 45 | 46 | - name: NodeJS cache 47 | id: node-cache 48 | uses: actions/cache@v3 49 | with: 50 | path: node_modules 51 | key: node-${{ hashFiles( '.github/workflows/main.yml' ) }}-${{ hashFiles( 'package-lock.json' ) }} 52 | 53 | - name: WordPress test suite cache 54 | id: wp-test-suite 55 | uses: actions/cache@v3 56 | with: 57 | path: /github/home/wp-tests 58 | key: wp-tests-${{ hashFiles( '.github/workflows/main.yml' ) }} 59 | 60 | - name: Install Composer dependencies 61 | if: steps.composer-cache.outputs.cache-hit != 'true' 62 | run: | 63 | composer install 64 | composer set-coding-standards 65 | 66 | - name: Install NodeJS dependencies 67 | if: steps.node-cache.outputs.cache-hit != 'true' 68 | run: npm ci 69 | 70 | - name: Install WordPress test suite 71 | run: bash ./.bin/install-wp-tests.sh wordpress_test root root mysql latest 72 | 73 | - name: PHP Coding standards 74 | run: composer run lint 75 | 76 | - name: JavaScript Coding standards 77 | run: npm run lint-js 78 | 79 | - name: CSS Coding standards 80 | run: npm run lint-css 81 | 82 | - name: Tests 83 | run: composer run test 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows and Mac files 2 | [Tt]humbs.db 3 | .DS_Store 4 | node_modules 5 | 6 | # Root. 7 | /* 8 | !.bin 9 | !.github 10 | !.tests 11 | !/assets 12 | !/inc 13 | !.editorconfig 14 | !.eslintignore 15 | !.eslintrc.json 16 | !.gitignore 17 | !.stylelintignore 18 | !.stylelintrc 19 | !composer.json 20 | !composer.lock 21 | !gumponents.php 22 | !LICENSE 23 | !README.md 24 | !package-lock.json 25 | !package.json 26 | !phpcs.xml 27 | !phpunit.xml.dist 28 | !webpack.config.js 29 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | vendor 4 | *.* 5 | !*.css 6 | !*.scss 7 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-wordpress/scss" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertTrue( true ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Junaid Bhura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Actions](https://github.com/junaidbhura/gumponents/workflows/Coding%20Standards%20and%20Tests/badge.svg) 2 | 3 | Gumponents! 4 | 5 | # Essential Gutenberg components for WordPress. 6 | 7 | Gumponents offer some **crucial** missing Gutenberg components, essential to create advanced blocks. 🚀 8 | 9 | Individual Gumponents aim to be deprecated over time, when components similar or better land in WordPress core. 10 | 11 | They are not blocks, but rather, what you would use to build advanced blocks. 12 | 13 | ## Quick Links 14 | 15 | [Documentation](https://github.com/junaidbhura/gumponents/wiki) | [Roadmap](https://github.com/junaidbhura/gumponents/projects/1) 16 | 17 | ## Components 18 | 19 | ### PostRelationshipControl 20 | 21 | ![post-relationship-control](https://user-images.githubusercontent.com/2512525/52121336-368dd180-266f-11e9-9cdd-37317a83a7e3.gif) 22 | 23 | #### Example 24 | 25 | ```js 26 | const { PostRelationshipControl } = gumponents.components; 27 | 28 | person.ID ) } 34 | onSelect={ people => setAttributes( { people } ) } 35 | buttonLabel="Select People" 36 | filter="people_meta" 37 | max="1" 38 | /> 39 | ``` 40 | 41 | ### TaxonomyRelationshipControl 42 | 43 | ![taxonomy-relationship-control](https://user-images.githubusercontent.com/2512525/52122521-342d7680-2673-11e9-88d7-f15f33245d86.gif) 44 | 45 | #### Example 46 | 47 | ```js 48 | const { TaxonomyRelationshipControl } = gumponents.components; 49 | 50 | tax.term_id ) } 54 | onSelect={ taxonomy => setAttributes( { taxonomy } ) } 55 | buttonLabel="Select People Roles" 56 | filter="people_meta" 57 | max="1" 58 | /> 59 | ``` 60 | 61 | ### ColorPaletteControl 62 | 63 | ![color-palette-control](https://user-images.githubusercontent.com/2512525/101442823-31d38c00-3970-11eb-989f-83627e0d782b.gif) 64 | 65 | #### Example 66 | 67 | ```js 68 | const { ColorPaletteControl } = gumponents.components; 69 | 70 | ... 71 | 72 | attributes: { 73 | color: { 74 | type: 'object', 75 | }, 76 | }, 77 | ... 78 | 79 | setAttributes( { color } ) } 83 | /> 84 | ``` 85 | 86 | ### MultiSelectControl 87 | 88 | ![multi-select-control](https://user-images.githubusercontent.com/2512525/56482870-83ae7080-6505-11e9-9ec6-9815818b4d38.gif) 89 | 90 | #### Example 91 | 92 | ```js 93 | const { MultiSelectControl } = gumponents.components; 94 | 95 | ... 96 | 97 | attributes: { 98 | simpsons: { 99 | type: 'array', 100 | default: [], 101 | }, 102 | }, 103 | 104 | ... 105 | 106 | const options = [ 107 | { value: 'bart', label: 'Bart' }, 108 | { value: 'homer', label: 'Homer' }, 109 | { value: 'marge', label: 'Marge' }, 110 | ]; 111 | 112 | setAttributes( { simpsons } ) } 118 | placeholder="D'oh" 119 | /> 120 | ``` 121 | 122 | ### LinkControl 123 | 124 | ![link-control](https://user-images.githubusercontent.com/2512525/56483259-e99bf780-6507-11e9-86ce-d905f5bbc74b.gif) 125 | 126 | #### Example 127 | 128 | ```js 129 | const { LinkControl } = gumponents.components; 130 | 131 | ... 132 | 133 | attributes: { 134 | link: { 135 | type: 'object', 136 | default: {}, 137 | }, 138 | }, 139 | 140 | ... 141 | 142 | setAttributes( { link } ) } 146 | help="Enter a URL." 147 | /> 148 | ``` 149 | 150 | ### FileControl 151 | 152 | ![file-control](https://user-images.githubusercontent.com/2512525/52123616-9c318c00-2676-11e9-910e-15daf6e144da.gif) 153 | 154 | #### Example 155 | 156 | ```js 157 | const { FileControl } = gumponents.components; 158 | 159 | ... 160 | 161 | attributes: { 162 | file: { 163 | type: 'object', 164 | default: null, 165 | }, 166 | }, 167 | 168 | ... 169 | 170 | setAttributes( { file: file ? { id: file.id, name: file.filename } : null } ) } 175 | value={ file ? file.id : null } 176 | /> 177 | ``` 178 | 179 | ### ImageControl 180 | 181 | ![image-control](https://user-images.githubusercontent.com/2512525/52124187-583f8680-2678-11e9-8119-fbf842b88848.gif) 182 | 183 | #### Example 184 | 185 | ```js 186 | const { ImageControl } = gumponents.components; 187 | 188 | ... 189 | 190 | attributes: { 191 | image: { 192 | type: 'object', 193 | default: null, 194 | }, 195 | }, 196 | 197 | ... 198 | 199 | setAttributes( { image } ) } 206 | /> 207 | ``` 208 | 209 | ### FocalPointPickerControl 210 | 211 | ![focal-point-picker](https://user-images.githubusercontent.com/11497423/231909988-7b4fdcec-1015-4512-91de-c0455bb3ff00.gif) 212 | 213 | #### Example 214 | 215 | ```js 216 | const { FocalPointPickerControl } = gumponents.components; 217 | 218 | ... 219 | 220 | attributes: { 221 | image: { 222 | type: 'object', 223 | default: {}, 224 | }, 225 | focalPoint: { 226 | type: 'object', 227 | default: {}, 228 | }, 229 | }, 230 | 231 | ... 232 | 233 | setAttributes( { focalPoint } ) } 239 | /> 240 | ``` 241 | 242 | ### GalleryControl 243 | 244 | ![gallery-control](https://user-images.githubusercontent.com/2512525/58150817-2136c480-7cab-11e9-86b6-19c3a544d831.gif) 245 | 246 | #### Example 247 | 248 | ```js 249 | const { GalleryControl } = gumponents.components; 250 | 251 | ... 252 | 253 | attributes: { 254 | gallery: { 255 | type: 'array', 256 | default: [], 257 | }, 258 | }, 259 | 260 | ... 261 | 262 | { 265 | setAttributes( { gallery: null } ); // The block editor doesn't update arrays correctly? 🤷‍♂️ 266 | setAttributes( { gallery } ); 267 | } } 268 | value={ attributes.gallery } 269 | /> 270 | ``` 271 | 272 | ### LinkButton 273 | 274 | ![link-button](https://user-images.githubusercontent.com/2512525/132930192-ede06fae-e7fd-4b0c-8259-80b5702f145c.gif) 275 | 276 | #### Example 277 | 278 | ```js 279 | const { LinkButton } = gumponents.components; 280 | 281 | ... 282 | 283 | attributes: { 284 | link: { 285 | type: 'object', 286 | }, 287 | }, 288 | 289 | ... 290 | 291 | setAttributes( { link } ) } 296 | /> 297 | ``` 298 | 299 | ### SelectImage 300 | 301 | ![select-image](https://user-images.githubusercontent.com/2512525/53619432-5220d380-3c3f-11e9-8a93-d0504d9fc9ee.gif) 302 | 303 | #### Example 304 | 305 | ```js 306 | const { SelectImage } = gumponents.components; 307 | 308 | ... 309 | 310 | attributes: { 311 | image: { 312 | type: 'object', 313 | default: null, 314 | }, 315 | }, 316 | 317 | ... 318 | 319 | { 324 | setAttributes( { image: null } ); // The block editor doesn't update objects correctly? 🤷‍♂️ 325 | setAttributes( { image } ); 326 | } } 327 | showCaption={ false } 328 | /> 329 | ``` 330 | -------------------------------------------------------------------------------- /assets/dist/editor.css: -------------------------------------------------------------------------------- 1 | .gumponents-select-image{background-color:#f9f9f9;overflow:hidden;position:relative;height:100px}.gumponents-select-image--selected{background-color:rgba(0,0,0,0)}.gumponents-select-image__container{position:relative;display:block;width:100%;height:100%;left:0;top:0}.gumponents-select-image__image-container{height:100%;margin:0}.gumponents-select-image__button{width:100%;height:100%;position:absolute;left:0;top:0}.gumponents-select-image__button:hover{background-color:rgba(0,0,0,0) !important}.gumponents-select-image__img-container{display:block;height:100%}.gumponents-select-image__img{cursor:pointer;width:100% !important;height:100% !important;object-fit:cover}.gumponents-select-image .spinner{margin:0;position:absolute;top:50%;left:50%;margin-top:-10px;margin-left:-10px}.gumponents-select-image .components-placeholder{position:absolute;min-height:auto;top:0;left:0;right:0;bottom:0;text-align:center;margin-bottom:0}.gumponents-select-image .components-placeholder__label{margin-bottom:0}.gumponents-select-image--no-placeholder .components-placeholder__label .dashicon{margin-right:0 !important}.gumponents-select-image__inline-menu{position:absolute;top:0;right:0;display:inline-flex;z-index:20}.gumponents-select-image__inline-menu .components-button{padding:0 !important;color:#fff;background-color:#0085ba;height:auto;min-width:auto}.gumponents-select-image__inline-menu .components-button+.components-button{margin-left:5px}.gumponents-select-image__inline-menu .components-button .dashicon{margin:0} 2 | .gumponents-image-control{padding:0}.gumponents-image-control .components-base-control__label{display:block;margin-bottom:4px}.gumponents-image-control__preview{margin-bottom:8px;display:block;width:100%;padding:0;transition:all .1s ease-out}.gumponents-image-control--selected .gumponents-image-control__preview{height:auto}.gumponents-image-control .components-spinner{float:none;margin:0}.gumponents-image-control .components-responsive-wrapper__content{width:100% !important;height:100% !important;object-fit:contain;object-position:left center} 3 | .gumponent-relationship .components-base-control__label{display:block;margin-bottom:4px}.gumponent-relationship__modal__actions{text-align:right;padding:16px}.gumponent-relationship__modal .components-modal__header{margin:0;position:relative;padding:16px 16px 0 16px !important;height:auto}.gumponent-relationship__modal .components-modal__header-heading{font-size:1.3em}.gumponent-relationship__modal .components-modal__content{padding:0;margin-top:0;position:relative}@media screen and (min-width: 600px){.gumponent-relationship__modal{width:50%;transform:none;top:calc(50% - 222px);left:25%;animation:none}}.gumponent-relationship__search-container{padding:16px}.gumponent-relationship__search{width:100%;padding:11px 16px !important}.gumponent-relationship__panel{display:flex;font-size:.9em;border-top:1px solid #e2e4e7;border-bottom:1px solid #e2e4e7}.gumponent-relationship__panel__search-items,.gumponent-relationship__panel__selected-items{width:50%;height:250px;display:flex;overflow-y:auto}.gumponent-relationship__panel__search-items{border-right:1px solid #e2e4e7;background-color:#f9f9f9}.gumponent-relationship__panel__selected-items a{cursor:grab}.gumponent-relationship__items{position:relative;width:100%;height:100%;list-style-type:none;margin:0;overflow-y:auto}.gumponent-relationship__items .components-spinner{position:absolute;margin:-9px 0 0 -9px;top:50%;left:50%;z-index:5}.gumponent-relationship__items--loading,.gumponent-relationship__items--disabled{overflow:hidden}.gumponent-relationship__items--loading::before,.gumponent-relationship__items--disabled::before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background-color:#fff;opacity:.5;z-index:3}.gumponent-relationship__item{margin:0;padding:10px 43px 10px 10px;text-align:left;position:relative}.gumponent-relationship__item:hover{background-color:#0073aa;color:#fff}.gumponent-relationship__item>button{width:100%;height:100%;margin:-10px}.gumponent-relationship__item--selected{opacity:.35;cursor:default !important}.gumponent-relationship__items--search .gumponent-relationship__item{cursor:pointer}.gumponent-relationship__item-action{opacity:0;position:absolute;right:10px;top:50%;margin-top:-13px}.gumponent-relationship__item-action button{padding:0 !important;height:auto;background-color:rgba(0,0,0,0) !important;box-shadow:none !important;min-width:auto !important}.gumponent-relationship__item-action .dashicon{margin:3px !important;color:#fff}.gumponent-relationship__item:hover .gumponent-relationship__item-action{opacity:1}.gumponent-relationship__item--selected:hover .gumponent-relationship__item-action{opacity:0 !important}.gumponent-relationship__selected-items{list-style-type:none !important;font-size:.9em;position:relative;padding:10px !important;background-color:#f3f3f4;border-radius:3px;color:#000;box-shadow:inset 0 0 10px rgba(0,0,0,.05);margin:10px 0 0 0 !important}.gumponent-relationship__selected-items li{margin:0}.gumponent-relationship__selected-items .components-spinner{float:none;margin:0} 4 | .gumponent-focal-point-picker .components-base-control__label{display:block;margin-bottom:4px}.gumponent-focal-point-picker__modal__actions{text-align:right;padding:16px}.gumponent-focal-point-picker__modal .components-modal__header{margin:0;position:relative;padding:0 16px !important}.gumponent-focal-point-picker__modal .components-modal__header-heading{font-size:1.3em}.gumponent-focal-point-picker__modal .components-modal__content{padding:0;margin-top:0;position:relative}@media screen and (min-width: 600px){.gumponent-focal-point-picker__modal{width:50%;transform:none;top:calc(50% - 222px);left:25%;animation:none}}.gumponent-focal-point-picker__modal .components-focal-point-picker-control{padding:0 16px 16px 16px} 5 | .gumponents-file-control .components-base-control__label{display:block;margin-bottom:4px}.gumponents-file-control__details{margin:8px 0;background-color:#f3f3f4;border-radius:3px;padding:10px;color:#000;box-shadow:inset 0 0 10px rgba(0,0,0,.05)}.gumponents-file-control .components-spinner{float:none;margin:0}.gumponents-file-control__details-container{display:flex}.gumponents-file-control__icon{margin-right:8px}.gumponents-file-control__icon img{width:24px;max-width:none}.gumponents-file-control p{margin:0} 6 | .gumponents-multi-select-control .components-base-control__label{display:block;margin-bottom:4px} 7 | .gumponents-link-control .components-base-control__label{display:block;margin-bottom:4px}.gumponents-link-control__preview{background-color:#f3f3f4;border-radius:3px;padding:10px;color:#000;box-shadow:inset 0 0 10px rgba(0,0,0,.05);word-break:break-all;margin:8px 0 12px 0}.gumponents-link-control__preview a{display:flex;align-items:center}.gumponents-link-control__preview svg{margin-left:5px;flex-shrink:0}.gumponents-url-control input[type=text]{width:100%}body.modal-open .components-popover:not(.is-mobile).is-bottom{z-index:999999} 8 | .gumponents-gallery-control .components-base-control__label{display:block;margin-bottom:4px}.gumponents-gallery-control__total{padding:10px;background-color:#f3f3f4;border-radius:3px;color:#000;box-shadow:inset 0 0 10px rgba(0,0,0,.05);margin:10px 0} 9 | .gumponents-color-palette-control .components-base-control__label{display:block;margin-bottom:4px} 10 | -------------------------------------------------------------------------------- /assets/src/components/color-palette-control/editor.scss: -------------------------------------------------------------------------------- 1 | .gumponents-color-palette-control { 2 | 3 | .components-base-control__label { 4 | display: block; 5 | margin-bottom: 4px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /assets/src/components/color-palette-control/index.js: -------------------------------------------------------------------------------- 1 | import './editor.scss'; 2 | 3 | import wp from 'wp'; 4 | const { 5 | BaseControl, 6 | ColorPalette, 7 | } = wp.components; 8 | const { select } = wp.data; 9 | 10 | export default function ColorPaletteControl( { label, help, value, colors = null, onChange, disableCustomColors = null } ) { 11 | if ( null === colors ) { 12 | colors = select( 'core/block-editor' ).getSettings().colors || []; 13 | } 14 | if ( null === disableCustomColors ) { 15 | disableCustomColors = select( 'core/block-editor' ).getSettings().disableCustomColors; 16 | } 17 | 18 | const onColorChange = ( color ) => { 19 | if ( ! onChange || 0 === colors.length ) { 20 | return; 21 | } 22 | 23 | let colorObject = null; 24 | if ( 'undefined' !== typeof color ) { 25 | colorObject = { 26 | color, 27 | }; 28 | colors.some( ( item ) => { 29 | if ( 'slug' in item && 'color' in item && color === item.color ) { 30 | colorObject.slug = item.slug; 31 | return true; 32 | } 33 | return false; 34 | } ); 35 | } 36 | 37 | onChange( colorObject ); 38 | }; 39 | 40 | return ( 41 | 46 | onColorChange( color ) } 50 | disableCustomColors={ disableCustomColors } 51 | /> 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /assets/src/components/figure/index.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/isEmpty'; 2 | import isString from 'lodash/isString'; 3 | import Img from '../img'; 4 | 5 | export default function Figure( { value, className } ) { 6 | if ( isEmpty( value ) ) { 7 | return ''; 8 | } 9 | 10 | let image = value; 11 | if ( isString( value ) ) { 12 | image = JSON.parse( image ); 13 | } 14 | 15 | return ( 16 |
17 | 20 | { ! isEmpty( image.caption ) && 21 |
{ image.caption }
22 | } 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /assets/src/components/file-control/editor.scss: -------------------------------------------------------------------------------- 1 | .gumponents-file-control { 2 | 3 | .components-base-control__label { 4 | display: block; 5 | margin-bottom: 4px; 6 | } 7 | 8 | &__details { 9 | margin: 8px 0; 10 | background-color: #f3f3f4; 11 | border-radius: 3px; 12 | padding: 10px; 13 | color: #000; 14 | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); 15 | } 16 | 17 | .components-spinner { 18 | float: none; 19 | margin: 0; 20 | } 21 | 22 | &__details-container { 23 | display: flex; 24 | } 25 | 26 | &__icon { 27 | margin-right: 8px; 28 | 29 | img { 30 | width: 24px; 31 | max-width: none; 32 | } 33 | } 34 | 35 | p { 36 | margin: 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/src/components/file-control/index.js: -------------------------------------------------------------------------------- 1 | import './editor.scss'; 2 | 3 | import wp from 'wp'; 4 | 5 | const { __ } = wp.i18n; 6 | const { 7 | Button, 8 | BaseControl, 9 | Spinner, 10 | } = wp.components; 11 | const { MediaUpload } = wp.blockEditor; 12 | const { 13 | useState, 14 | useEffect, 15 | } = wp.element; 16 | const { 17 | withSelect, 18 | withDispatch, 19 | } = wp.data; 20 | const { compose } = wp.compose; 21 | 22 | function FileControl( { value, file, help, allowedTypes, label = __( 'Select file' ), selectLabel = __( 'Select file' ), removeLabel = __( 'Remove file' ), onSetFile, onChange } ) { 23 | const [ id, setId ] = useState( null ); 24 | 25 | useEffect( 26 | () => setId( value ), 27 | [ value ] 28 | ); 29 | 30 | const onSelectFile = ( media ) => { 31 | setId( media.id ); 32 | onSetFile( media ); 33 | 34 | if ( onChange ) { 35 | onChange( media ); 36 | } 37 | }; 38 | 39 | const onRemoveFile = () => { 40 | setId( null ); 41 | 42 | if ( onChange ) { 43 | onChange( null ); 44 | } 45 | }; 46 | 47 | return ( 48 | 53 | ( 58 | 65 | ) } 66 | /> 67 | { id && 68 |
69 | { file && 70 |
71 |
72 | 76 |
77 |
78 |

{ file.filename }

79 |

{ file.filesizeHumanReadable }

80 |
81 |
82 | } 83 | { ! file && 84 | 85 | } 86 |
87 | } 88 | { id && file && 89 | 92 | } 93 |
94 | ); 95 | } 96 | 97 | export default compose( 98 | withSelect( ( select, ownProps ) => { 99 | const { getMedia } = select( 'gumponents/media' ); 100 | const { value } = ownProps; 101 | 102 | return { 103 | file: value ? getMedia( value ) : null, 104 | }; 105 | } ), 106 | withDispatch( ( dispatch ) => { 107 | return { 108 | onSetFile( media ) { 109 | dispatch( 'gumponents/media' ).setMedia( media ); 110 | }, 111 | }; 112 | } ), 113 | )( FileControl ); 114 | 115 | -------------------------------------------------------------------------------- /assets/src/components/focal-point-picker-control/editor.scss: -------------------------------------------------------------------------------- 1 | $gumponent-focal-point-picker__modal-gutter: 16px; 2 | 3 | .gumponent-focal-point-picker { 4 | 5 | .components-base-control__label { 6 | display: block; 7 | margin-bottom: 4px; 8 | } 9 | 10 | &__modal { 11 | 12 | &__actions { 13 | text-align: right; 14 | padding: $gumponent-focal-point-picker__modal-gutter; 15 | } 16 | 17 | .components-modal { 18 | 19 | &__header { 20 | margin: 0; 21 | position: relative; 22 | padding: 0 16px !important; 23 | 24 | &-heading { 25 | font-size: 1.3em; 26 | } 27 | } 28 | 29 | &__content { 30 | padding: 0; 31 | margin-top: 0; 32 | position: relative; 33 | } 34 | } 35 | 36 | @media screen and (min-width: 600px) { 37 | width: 50%; 38 | transform: none; 39 | top: calc(50% - 222px); 40 | left: calc(25%); 41 | animation: none; 42 | } 43 | 44 | .components-focal-point-picker-control { 45 | padding: 0 $gumponent-focal-point-picker__modal-gutter $gumponent-focal-point-picker__modal-gutter $gumponent-focal-point-picker__modal-gutter; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/src/components/focal-point-picker-control/index.js: -------------------------------------------------------------------------------- 1 | import './editor.scss'; 2 | 3 | import wp from 'wp'; 4 | 5 | const { __ } = wp.i18n; 6 | const { 7 | Button, 8 | Modal, 9 | BaseControl, 10 | FocalPointPicker, 11 | } = wp.components; 12 | const { 13 | useState, 14 | } = wp.element; 15 | 16 | function FocalPointPickerControl( { label = '', value = {}, onChange = () => {}, imageUrl = '', help = '', buttonLabel = __( 'Select', 'gumponents' ), modalTitle = __( 'Select', 'gumponents' ) } ) { 17 | // Initialize State. 18 | const initialFocalPointValue = ( value.x && value.y ) ? value : { x: 0.5, y: 0.5 }; 19 | const [ modalOpen, setModalOpen ] = useState( false ); 20 | const [ focalPoint, setFocalPoint ] = useState( initialFocalPointValue ); 21 | 22 | if ( ! imageUrl ) { 23 | return null; 24 | } 25 | 26 | /** 27 | * Handle Set Focal Point. 28 | * 29 | * @param {Object} focalPointData Focal Point Data. 30 | */ 31 | const handleSetFocalPoint = ( focalPointData ) => { 32 | setFocalPoint( focalPointData ); 33 | onChange( focalPoint ); 34 | }; 35 | 36 | /** 37 | * Open modal. 38 | */ 39 | const openModal = () => { 40 | setModalOpen( true ); 41 | }; 42 | 43 | return ( 44 | 49 | 55 | 56 | { modalOpen && 57 | setModalOpen( false ) }> 61 | 68 | 69 | } 70 | 71 | ); 72 | } 73 | 74 | export default FocalPointPickerControl; 75 | -------------------------------------------------------------------------------- /assets/src/components/gallery-control/editor.scss: -------------------------------------------------------------------------------- 1 | .gumponents-gallery-control { 2 | 3 | .components-base-control__label { 4 | display: block; 5 | margin-bottom: 4px; 6 | } 7 | 8 | &__total { 9 | padding: 10px; 10 | background-color: #f3f3f4; 11 | border-radius: 3px; 12 | color: #000; 13 | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); 14 | margin: 10px 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /assets/src/components/gallery-control/index.js: -------------------------------------------------------------------------------- 1 | import './editor.scss'; 2 | 3 | import wp from 'wp'; 4 | import isObject from 'lodash/isObject'; 5 | import { getImageDetails } from '../image-control'; 6 | 7 | const { __ } = wp.i18n; 8 | const { 9 | Button, 10 | BaseControl, 11 | } = wp.components; 12 | const { 13 | MediaUpload, 14 | } = wp.blockEditor; 15 | const { 16 | Fragment, 17 | useState, 18 | useEffect, 19 | } = wp.element; 20 | 21 | export default function GalleryControl( { value = [], size = 'full', help, onSelect, label = __( 'Select images' ), selectLabel = __( 'Select images' ), updateLabel = __( 'Update images' ), removeLabel = __( 'Remove images' ) } ) { 22 | const [ images, setImages ] = useState( [] ); 23 | 24 | useEffect( 25 | () => { 26 | if ( 0 === value.length ) { 27 | setImages( [] ); 28 | } else if ( isObject( value[ 0 ] ) ) { 29 | setImages( value.map( ( val ) => val.id ) ); 30 | } else { 31 | setImages( value ); 32 | } 33 | }, 34 | [ value ] 35 | ); 36 | 37 | const hasImages = 0 !== images.length; 38 | 39 | const imagesSelected = ( selectedImages ) => { 40 | if ( onSelect ) { 41 | onSelect( selectedImages.map( ( image ) => getImageDetails( image, size ) ), selectedImages ); 42 | } 43 | }; 44 | 45 | const removeImages = () => { 46 | if ( onSelect ) { 47 | onSelect( [], [] ); 48 | } 49 | }; 50 | 51 | return ( 52 | 57 | ( 64 | 71 | ) } 72 | /> 73 | { hasImages && 74 | 75 |
76 | { images.length } { __( 'images selected' ) } 77 |
78 | 81 |
82 | } 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /assets/src/components/image-control/editor.scss: -------------------------------------------------------------------------------- 1 | .gumponents-image-control { 2 | padding: 0; 3 | 4 | .components-base-control__label { 5 | display: block; 6 | margin-bottom: 4px; 7 | } 8 | 9 | &__preview { 10 | margin-bottom: 8px; 11 | display: block; 12 | width: 100%; 13 | padding: 0; 14 | transition: all 0.1s ease-out; 15 | } 16 | 17 | &--selected &__preview { 18 | height: auto; 19 | } 20 | 21 | .components-spinner { 22 | float: none; 23 | margin: 0; 24 | } 25 | 26 | .components-responsive-wrapper__content { 27 | width: 100% !important; 28 | height: 100% !important; 29 | object-fit: contain; 30 | object-position: left center; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /assets/src/components/image-control/index.js: -------------------------------------------------------------------------------- 1 | import './editor.scss'; 2 | 3 | import wp from 'wp'; 4 | import has from 'lodash/has'; 5 | import isObject from 'lodash/isObject'; 6 | import classnames from 'classnames'; 7 | 8 | const { Fragment } = wp.element; 9 | const { 10 | withSelect, 11 | withDispatch, 12 | } = wp.data; 13 | const { 14 | useState, 15 | useEffect, 16 | } = wp.element; 17 | const { __ } = wp.i18n; 18 | const { 19 | Button, 20 | Spinner, 21 | BaseControl, 22 | ResponsiveWrapper, 23 | } = wp.components; 24 | const { MediaUpload } = wp.blockEditor; 25 | const { compose } = wp.compose; 26 | 27 | /** 28 | * Get formatted image details from a media object. 29 | * 30 | * @param {Object} media Media object. 31 | * @param {string} thumbnailSize Thumbnail size. 32 | * @return {Object} Formatted image details. 33 | */ 34 | export function getImageDetails( media, thumbnailSize = 'full' ) { 35 | if ( ! media ) { 36 | return {}; 37 | } 38 | 39 | let src, width, height; 40 | 41 | if ( has( media, 'sizes' ) ) { 42 | if ( ! has( media.sizes, thumbnailSize ) ) { 43 | thumbnailSize = 'full'; 44 | } 45 | width = media.sizes[ thumbnailSize ].width; 46 | height = media.sizes[ thumbnailSize ].height; 47 | src = media.sizes[ thumbnailSize ].url; 48 | } 49 | 50 | return { 51 | id: media.id, 52 | src, 53 | width, 54 | height, 55 | alt: media.alt, 56 | caption: media.caption, 57 | title: media.title, 58 | size: thumbnailSize, 59 | }; 60 | } 61 | 62 | function ImageControl( { label, help, value, size, selectLabel = __( 'Select image' ), removeLabel = __( 'Remove image' ), onChange, onSetMedia, selectedMedia } ) { 63 | const [ id, setId ] = useState( null ); 64 | const [ controlValue, setControlValue ] = useState( null ); 65 | 66 | useEffect( 67 | () => { 68 | if ( ! isObject( value ) ) { 69 | setId( value ); 70 | setControlValue( null ); 71 | } else { 72 | setId( value.id ); 73 | setControlValue( value ); 74 | } 75 | }, 76 | [ value ] 77 | ); 78 | 79 | useEffect( 80 | () => { 81 | if ( isObject( selectedMedia ) ) { 82 | setId( selectedMedia.id ); 83 | setControlValue( getImageDetails( selectedMedia, size ) ); 84 | } 85 | }, 86 | [ selectedMedia ] 87 | ); 88 | 89 | const onSelectImage = ( media ) => { 90 | const image = getImageDetails( media, size ); 91 | 92 | setId( image.id ); 93 | setControlValue( image ); 94 | 95 | onSetMedia( media ); 96 | 97 | if ( onChange ) { 98 | onChange( image, media ); 99 | } 100 | }; 101 | 102 | const onRemoveImage = () => { 103 | setId( null ); 104 | setControlValue( null ); 105 | 106 | if ( onChange ) { 107 | onChange( null, null ); 108 | } 109 | }; 110 | 111 | return ( 112 | 119 | { id && 120 |
121 | { ! controlValue && 122 | 123 | } 124 | { controlValue && 125 | onSelectImage( media ) } 128 | type="image" 129 | value={ id } 130 | render={ ( { open } ) => ( 131 | 132 | 140 | 143 | 144 | ) } 145 | /> 146 | } 147 |
148 | } 149 | { controlValue && 150 | 153 | } 154 | { ! controlValue && ! id && 155 | onSelectImage( media ) } 158 | allowedTypes={ [ 'image' ] } 159 | render={ ( { open } ) => ( 160 |
161 | 167 |
168 | ) } 169 | /> 170 | } 171 |
172 | ); 173 | } 174 | 175 | export default compose( 176 | withSelect( ( select, ownProps ) => { 177 | const { getMedia } = select( 'gumponents/media' ); 178 | const { value } = ownProps; 179 | 180 | return { 181 | selectedMedia: value ? getMedia( value ) : null, 182 | }; 183 | } ), 184 | withDispatch( ( dispatch ) => { 185 | return { 186 | onSetMedia( media ) { 187 | dispatch( 'gumponents/media' ).setMedia( media ); 188 | }, 189 | }; 190 | } ), 191 | )( ImageControl ); 192 | -------------------------------------------------------------------------------- /assets/src/components/img/index.js: -------------------------------------------------------------------------------- 1 | import classnames from 'classnames'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | import isString from 'lodash/isString'; 4 | 5 | export default function Img( { value, className } ) { 6 | if ( isEmpty( value ) ) { 7 | return ''; 8 | } 9 | 10 | let image = value; 11 | if ( isString( value ) ) { 12 | image = JSON.parse( image ); 13 | } 14 | 15 | return ( 16 | { 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /assets/src/components/link-button/index.js: -------------------------------------------------------------------------------- 1 | import { UrlModal } from '../link-control/modal'; 2 | 3 | import wp from 'wp'; 4 | import React from 'react'; 5 | import classnames from 'classnames'; 6 | 7 | const { __ } = wp.i18n; 8 | const { useState } = wp.element; 9 | const { decodeEntities } = wp.htmlEntities; 10 | 11 | export default function LinkButton( { 12 | tagName = 'button', 13 | value, 14 | className = 'wp-block-button', 15 | placeholder = __( 'Button' ), 16 | modalTitle = __( 'URL' ), 17 | onChange, 18 | } ) { 19 | const [ modalOpen, setModalOpen ] = useState( false ); 20 | const { text } = value ?? {}; 21 | const Tag = tagName; 22 | let label = placeholder; 23 | 24 | if ( text && '' !== text ) { 25 | label = text; 26 | } 27 | 28 | return ( 29 | <> 30 | setModalOpen( true ) } 36 | > 37 | { decodeEntities( label ) } 38 | 39 | { modalOpen && 40 | setModalOpen( false ) } 44 | value={ value } 45 | onChange={ ( value ) => onChange( value ) } 46 | /> 47 | } 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /assets/src/components/link-control/editor.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * URL Input. 3 | */ 4 | 5 | .gumponents-link-control { 6 | 7 | .components-base-control__label { 8 | display: block; 9 | margin-bottom: 4px; 10 | } 11 | 12 | &__preview { 13 | background-color: #f3f3f4; 14 | border-radius: 3px; 15 | padding: 10px; 16 | color: #000; 17 | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05); 18 | word-break: break-all; 19 | margin: 8px 0 12px 0; 20 | 21 | a { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | svg { 27 | margin-left: 5px; 28 | flex-shrink: 0; 29 | } 30 | } 31 | } 32 | 33 | .gumponents-url-control { 34 | 35 | input[type="text"] { 36 | width: 100%; 37 | } 38 | } 39 | 40 | body.modal-open .components-popover:not(.is-mobile).is-bottom { 41 | z-index: 999999; 42 | } 43 | -------------------------------------------------------------------------------- /assets/src/components/link-control/index.js: -------------------------------------------------------------------------------- 1 | import './editor.scss'; 2 | import { UrlModal } from './modal'; 3 | 4 | import wp from 'wp'; 5 | import isEmpty from 'lodash/isEmpty'; 6 | 7 | const { __ } = wp.i18n; 8 | const { 9 | BaseControl, 10 | Icon, 11 | } = wp.components; 12 | const { 13 | Button, 14 | } = wp.components; 15 | const { 16 | useState, 17 | } = wp.element; 18 | 19 | export default function LinkControl( { value, label, help, onUrl, onChange, buttonLabel = __( 'Select link' ), modalTitle = __( 'URL' ) } ) { 20 | const [ modalOpen, setModalOpen ] = useState( false ); 21 | const { url, text, newWindow } = value ?? {}; 22 | 23 | return ( 24 | 29 | 35 | { ! isEmpty( value ) && '' !== url && 36 | 52 | } 53 | { modalOpen && 54 | setModalOpen( false ) } 58 | value={ value } 59 | onChange={ ( value ) => onChange( value ) } 60 | onUrl={ onUrl } 61 | /> 62 | } 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /assets/src/components/link-control/modal.js: -------------------------------------------------------------------------------- 1 | import wp from 'wp'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | const { __ } = wp.i18n; 5 | const { 6 | BaseControl, 7 | } = wp.components; 8 | const { URLInput } = wp.blockEditor; 9 | const { 10 | Modal, 11 | TextControl, 12 | ToggleControl, 13 | } = wp.components; 14 | const { 15 | useState, 16 | useEffect, 17 | } = wp.element; 18 | 19 | export function UrlModal( { className = '', onRequestClose, title, value, onChange, onUrl } ) { 20 | const [ url, setUrl ] = useState( '' ); 21 | const [ text, setText ] = useState( '' ); 22 | const [ newWindow, setNewWindow ] = useState( false ); 23 | 24 | useEffect( 25 | () => { 26 | if ( ! isEmpty( value ) ) { 27 | setUrl( value.url ); 28 | setText( value.text ); 29 | setNewWindow( value.newWindow ); 30 | } 31 | }, 32 | [ value ] 33 | ); 34 | 35 | return ( 36 | 42 | 46 | { 49 | if ( onUrl ) { 50 | onUrl( newUrl, post ); 51 | } 52 | 53 | let changes = { 54 | url: newUrl, 55 | text, 56 | newWindow, 57 | }; 58 | if ( post && '' === text ) { 59 | changes.text = post.title; 60 | } else if ( '' === newUrl ) { 61 | changes.text = ''; 62 | changes.newWindow = false; 63 | } 64 | 65 | setUrl( changes.url ); 66 | onChange( changes ); 67 | } } 68 | /> 69 | 70 | { 74 | setText( text ); 75 | onChange( { url, text, newWindow } ); 76 | } } 77 | /> 78 | onChange( { url, text, newWindow: ! newWindow } ) } 83 | /> 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /assets/src/components/multiselect-control/editor.scss: -------------------------------------------------------------------------------- 1 | .gumponents-multi-select-control { 2 | 3 | .components-base-control__label { 4 | display: block; 5 | margin-bottom: 4px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /assets/src/components/multiselect-control/index.js: -------------------------------------------------------------------------------- 1 | import './editor.scss'; 2 | 3 | import wp from 'wp'; 4 | import Select from 'react-select'; 5 | 6 | const { BaseControl } = wp.components; 7 | 8 | export default function MultiSelectControl( { value = [], options = [], label, help, placeholder, onChange, ...reactSelectProps } ) { 9 | // Filter out any values that are not in the options list and map them to the option object. 10 | const values = value 11 | .filter( ( token ) => options.some( ( option ) => option.value === token ) ) 12 | .map( ( token ) => options.find( ( option ) => option.value === token ) ); 13 | 14 | const valuesUpdated = ( values ) => { 15 | if ( ! onChange ) { 16 | return; 17 | } 18 | 19 | if ( null === values ) { 20 | onChange( [] ); 21 | return; 22 | } 23 | 24 | // Only return the values that are in the options list. 25 | onChange( 26 | values 27 | .filter( ( token ) => options.some( ( option ) => option.value === token.value ) ) 28 | .map( ( token ) => token.value ) 29 | ); 30 | }; 31 | 32 | return ( 33 | 38 | 42 | 43 |
44 |
45 | 0 && items.length >= maxItems } 47 | items={ results } 48 | loading={ searching } 49 | selected={ items } 50 | onSelected={ ( item ) => onSelect( Array.prototype.concat( items, [ item ] ) ) } 51 | /> 52 |
53 |
54 | onSelect( newItems ) } 57 | onUnselected={ ( item ) => onSelect( items.filter( ( thing ) => thing.value !== item.value ) ) } 58 | /> 59 |
60 |
61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /assets/src/components/select-image/editor.scss: -------------------------------------------------------------------------------- 1 | .gumponents-select-image { 2 | background-color: #f9f9f9; 3 | overflow: hidden; 4 | position: relative; 5 | height: 100px; 6 | 7 | &--selected { 8 | background-color: transparent; 9 | } 10 | 11 | &__container { 12 | position: relative; 13 | display: block; 14 | width: 100%; 15 | height: 100%; 16 | left: 0; 17 | top: 0; 18 | } 19 | 20 | &__image-container { 21 | height: 100%; 22 | margin: 0; 23 | } 24 | 25 | &__button { 26 | width: 100%; 27 | height: 100%; 28 | position: absolute; 29 | left: 0; 30 | top: 0; 31 | 32 | &:hover { 33 | background-color: transparent !important; 34 | } 35 | } 36 | 37 | &__img-container { 38 | display: block; 39 | height: 100%; 40 | } 41 | 42 | &__img { 43 | cursor: pointer; 44 | width: 100% !important; 45 | height: 100% !important; 46 | object-fit: cover; 47 | } 48 | 49 | .spinner { 50 | margin: 0; 51 | position: absolute; 52 | top: 50%; 53 | left: 50%; 54 | margin-top: -10px; 55 | margin-left: -10px; 56 | } 57 | 58 | .components-placeholder { 59 | position: absolute; 60 | min-height: auto; 61 | top: 0; 62 | left: 0; 63 | right: 0; 64 | bottom: 0; 65 | text-align: center; 66 | margin-bottom: 0; 67 | 68 | &__label { 69 | margin-bottom: 0; 70 | } 71 | } 72 | 73 | &--no-placeholder .components-placeholder__label .dashicon { 74 | margin-right: 0 !important; 75 | } 76 | 77 | &__inline-menu { 78 | position: absolute; 79 | top: 0; 80 | right: 0; 81 | display: inline-flex; 82 | z-index: 20; 83 | 84 | .components-button { 85 | padding: 0 !important; 86 | color: #fff; 87 | background-color: #0085ba; 88 | height: auto; 89 | min-width: auto; 90 | 91 | + .components-button { 92 | margin-left: 5px; 93 | } 94 | 95 | .dashicon { 96 | margin: 0; 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /assets/src/components/select-image/image-container.js: -------------------------------------------------------------------------------- 1 | import wp from 'wp'; 2 | import isEmpty from 'lodash/isEmpty'; 3 | 4 | const { 5 | Button, 6 | Placeholder, 7 | } = wp.components; 8 | const { RichText } = wp.blockEditor; 9 | const { __ } = wp.i18n; 10 | 11 | export default function ImageContainer( { image, open, placeholder, showCaption, onRemove, onEdit, onCaptionEdit } ) { 12 | return ( 13 | 14 | { isEmpty( image ) && 15 | 18 |