├── .distignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── run-test-and-deploy.yml │ └── run-test.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .nvmrc ├── .prettierrc.js ├── .stylelintrc.js ├── .wordpress-org ├── banner-1544x500.png ├── banner-772x250.png ├── blueprints │ └── blueprint.json ├── icon-128x128.png ├── icon-256x256.png ├── screenshot-1.jpg ├── screenshot-2.jpg ├── screenshot-3.jpg └── screenshot-4.jpg ├── .wp-env.json ├── LICENSE ├── README.md ├── classes ├── class-api.php ├── class-enqueue.php ├── class-helper.php ├── class-init.php └── class-settings.php ├── composer.json ├── composer.lock ├── flexible-table-block.php ├── package-lock.json ├── package.json ├── phpcs.ruleset.xml ├── playwright.config.ts ├── readme.txt ├── src ├── BlockAttributes.ts ├── block.json ├── constants.ts ├── controls │ ├── border-color-control.tsx │ ├── border-radius-control.tsx │ ├── border-spacing-control.tsx │ ├── border-style-control.tsx │ ├── border-width-control.tsx │ ├── color-control.tsx │ ├── color-indicator-button.tsx │ ├── index.ts │ ├── indicator-control.tsx │ ├── padding-control.tsx │ ├── style.scss │ └── styles.tsx ├── deprecated.tsx ├── edit.tsx ├── editor.scss ├── elements │ ├── index.ts │ ├── table-caption.tsx │ ├── table-placeholder.tsx │ └── table.tsx ├── example.ts ├── icons.tsx ├── index.js ├── save.tsx ├── settings │ ├── global-settings │ │ ├── help-modal.tsx │ │ ├── index.tsx │ │ ├── setting-modal.tsx │ │ └── style.scss │ ├── index.ts │ ├── table-caption-settings.tsx │ ├── table-cell-settings.tsx │ └── table-settings.tsx ├── store.ts ├── style.scss ├── transforms.tsx └── utils │ ├── helper.ts │ ├── style-converter.ts │ ├── style-picker.ts │ ├── style-updater.ts │ ├── table-state.ts │ └── test │ ├── helper.test.ts │ ├── style-converter.test.ts │ ├── style-picker.test.ts │ ├── style-updater.test.ts │ └── table-state.test.ts ├── test ├── e2e │ ├── specs │ │ ├── __snapshots__ │ │ │ ├── Block-Support-dimensions-settings-should-be-applied-1-chromium.txt │ │ │ ├── Block-Support-typography-settings-should-be-applied-1-chromium.txt │ │ │ ├── Flexible-table-allows-all-cells-in-a-vertical-line-to-be-merge-1-chromium.txt │ │ │ ├── Flexible-table-allows-all-cells-side-by-side-to-be-merge-1-chromium.txt │ │ │ ├── Flexible-table-allows-cells-in-a-vertical-line-to-be-merge-1-chromium.txt │ │ │ ├── Flexible-table-allows-cells-side-by-side-to-be-merge-1-chromium.txt │ │ │ ├── Flexible-table-allows-cells-to-be-merge-1-chromium.txt │ │ │ ├── Flexible-table-allows-merged-cells-to-be-split-1-chromium.txt │ │ │ ├── Flexible-table-allows-to-delete-column-even-if-they-contain-merged-cells-1-chromium.txt │ │ │ ├── Flexible-table-allows-to-delete-rows-even-if-they-contain-merged-cells-1-chromium.txt │ │ │ ├── Flexible-table-cell-allows-cell-movement-with-tab-key-1-chromium.txt │ │ │ ├── Flexible-table-cell-allows-keyboard-operation-within-the-link-popover-1-chromium.txt │ │ │ ├── Flexible-table-cell-allows-keyboard-operation-within-the-link-popover-2-chromium.txt │ │ │ ├── Flexible-table-should-be-inserted-1-chromium.txt │ │ │ ├── Flexible-table-should-create-block-1-chromium.txt │ │ │ ├── Flexible-table-should-create-block-with-option-1-chromium.txt │ │ │ ├── Styles-caption-styles-should-be-applied-1-chromium.txt │ │ │ ├── Styles-cell-styles-should-be-applied-1-chromium.txt │ │ │ ├── Styles-cell-styles-should-be-applied-to-multiple-cells-1-chromium.txt │ │ │ ├── Styles-table-styles-should-be-applied-1-chromium.txt │ │ │ ├── Transform-from-core-table-block-should-be-tran-aff73--block-keeping-Fixed-width-table-cells-option-1-chromium.txt │ │ │ ├── Transform-from-core-table-block-should-be-tran-de743-table-block-keeping-header-and-footer-section-1-chromium.txt │ │ │ ├── Transform-from-core-table-block-should-be-tran-e2ee4--block-with-no-Fixed-width-table-cells-option-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--0572b-le-block-with-no-unnecessary-attributes-cells-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--0f411-core-table-block-with-rowspan-colspan-cells-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--122cc-o-core-table-block-with-appropriate-tag-cells-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--2c3de--block-with-no-Fixed-width-table-cells-option-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--43933--core-table-block-with-no-option-caption-text-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--82299-ore-table-block-with-no-style-and-class-cells-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--86681--block-keeping-Fixed-width-table-cells-option-1-chromium.txt │ │ │ ├── Transform-from-flexible-table-block-should-be--8ea6a-ore-table-block-with-no-style-and-class-table-1-chromium.txt │ │ │ └── Transform-from-flexible-table-block-should-be-transformed-to-core-table-block-keeping-caption-text-1-chromium.txt │ │ ├── block-support.spec.ts │ │ ├── global-setting.spec.ts │ │ ├── table-cell.spec.ts │ │ ├── table-style.spec.ts │ │ ├── table.spec.ts │ │ ├── transform.spec.ts │ │ └── various.spec.ts │ └── util.ts └── unit │ └── jest.config.js └── tsconfig.json /.distignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.github 3 | /.husky 4 | /.wordpress-org 5 | /artifacts 6 | /node_modules 7 | /test 8 | /vendor 9 | .distignore 10 | .editorconfig 11 | .eslintignore 12 | .eslintrc.js 13 | .gitignore 14 | .npmrc 15 | .nvmrc 16 | .prettierrc.js 17 | .stylelintrc.js 18 | .wp-env.json 19 | composer.json 20 | composer.lock 21 | playwright.config.ts 22 | package-lock.json 23 | package.json 24 | phpcs.ruleset.xml 25 | README.md 26 | tsconfig.json 27 | *.tsbuildinfo 28 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = tab 9 | indent_size = 2 10 | 11 | [*.{yml,yaml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | vendor/ 3 | build/ 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'plugin:@wordpress/eslint-plugin/recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | ], 6 | plugins: [ '@typescript-eslint' ], 7 | parser: '@typescript-eslint/parser', 8 | rules: { 9 | 'react/jsx-boolean-value': 'error', 10 | 'react/jsx-curly-brace-presence': [ 'error', { props: 'never', children: 'never' } ], 11 | '@wordpress/dependency-group': 'error', 12 | '@wordpress/no-unsafe-wp-apis': 'off', 13 | '@wordpress/i18n-text-domain': [ 14 | 'error', 15 | { 16 | allowedTextDomain: 'flexible-table-block', 17 | }, 18 | ], 19 | 'prettier/prettier': [ 20 | 'error', 21 | { 22 | useTabs: true, 23 | tabWidth: 2, 24 | singleQuote: true, 25 | printWidth: 100, 26 | bracketSpacing: true, 27 | parenSpacing: true, 28 | bracketSameLine: false, 29 | }, 30 | ], 31 | }, 32 | overrides: [ 33 | { 34 | files: [ 35 | '**/test/**/*.ts', 36 | '**/test/**/*.js', 37 | '**/__tests__/**/*.ts', 38 | '**/__tests__/**/*.js', 39 | '**/*.spec.ts', 40 | '**/*.spec.js', 41 | ], 42 | extends: [ 'plugin:@wordpress/eslint-plugin/test-unit' ], 43 | settings: { 44 | jest: { 45 | version: 26, 46 | }, 47 | }, 48 | }, 49 | { 50 | files: [ 'test/e2e/**/*.js', 'test/e2e/**/*.ts' ], 51 | extends: [ 'plugin:@wordpress/eslint-plugin/test-e2e' ], 52 | rules: { 53 | 'jest/expect-expect': 'off', 54 | }, 55 | }, 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /.github/workflows/run-test-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test and Deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | - php: '7.4' 15 | wp: WordPress 16 | - php: '7.4' 17 | wp: WordPress#6.8.1 18 | - php: '7.4' 19 | wp: WordPress#6.7.2 20 | - php: '7.4' 21 | wp: WordPress#6.6.2 22 | - php: '8.0' 23 | wp: WordPress 24 | - php: '8.0' 25 | wp: WordPress#6.8.1 26 | - php: '8.0' 27 | wp: WordPress#6.7.2 28 | - php: '8.0' 29 | wp: WordPress#6.6.2 30 | - php: '8.2' 31 | wp: WordPress 32 | - php: '8.2' 33 | wp: WordPress#6.8.1 34 | - php: '8.2' 35 | wp: WordPress#6.7.2 36 | - php: '8.2' 37 | wp: WordPress#6.6.2 38 | - php: '8.4' 39 | wp: WordPress 40 | - php: '8.4' 41 | wp: WordPress#6.8.1 42 | - php: '8.4' 43 | wp: WordPress#6.7.2 44 | name: PHP ${{ matrix.php }} / ${{ matrix.wp }} Test 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | 50 | - name: Use desired version of Node.js 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version-file: '.nvmrc' 54 | 55 | - name: Npm install and build 56 | run: | 57 | npm ci 58 | npm run build 59 | 60 | - name: Composer install and set phpcs 61 | run: | 62 | composer install 63 | composer phpcs 64 | 65 | - name: Running lint check 66 | run: npm run lint 67 | 68 | - name: Running unit tests 69 | run: npm run test:unit 70 | 71 | - name: Install Playwright dependencies 72 | run: npx playwright install chromium firefox webkit --with-deps 73 | 74 | - name: Install WordPress 75 | run: | 76 | WP_ENV_CORE=WordPress/${{ matrix.wp }} WP_ENV_PHP_VERSION=${{ matrix.php }} npm run wp-env start 77 | npm run wp-env run cli wp core version 78 | npm run wp-env run cli wp cli info 79 | 80 | - name: Running e2e tests 81 | run: npm run test:e2e 82 | 83 | deploy: 84 | name: Deploy to WP.org 85 | runs-on: ubuntu-latest 86 | needs: [test] 87 | 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v4 91 | 92 | - name: Use desired version of Node.js 93 | uses: actions/setup-node@v4 94 | with: 95 | node-version-file: '.nvmrc' 96 | 97 | - name: Npm install and build 98 | run: | 99 | npm ci 100 | npm run build 101 | 102 | - name: WordPress Plugin Deploy 103 | id: deploy 104 | uses: 10up/action-wordpress-plugin-deploy@stable 105 | with: 106 | generate-zip: true 107 | env: 108 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 109 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 110 | SLUG: flexible-table-block 111 | 112 | - name: Create Release 113 | id: create_release 114 | uses: actions/create-release@v1 115 | env: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | with: 118 | tag_name: ${{ github.ref }} 119 | release_name: Release ${{ github.ref }} 120 | draft: false 121 | prerelease: false 122 | commitish: main 123 | 124 | - name: Upload release asset 125 | uses: actions/upload-release-asset@v1 126 | env: 127 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 128 | with: 129 | upload_url: ${{ steps.create_release.outputs.upload_url }} 130 | asset_path: ${{ steps.deploy.outputs.zip-path }} 131 | asset_name: ${{ github.event.repository.name }}.zip 132 | asset_content_type: application/zip 133 | -------------------------------------------------------------------------------- /.github/workflows/run-test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | # Cancels all previous workflow runs for pull requests that have not completed. 10 | concurrency: 11 | # The concurrency group contains the workflow name and the branch name for pull requests 12 | # or the commit hash for any other events. 13 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | include: 22 | - php: '7.4' 23 | wp: WordPress 24 | - php: '7.4' 25 | wp: WordPress#6.8.1 26 | - php: '7.4' 27 | wp: WordPress#6.7.2 28 | - php: '7.4' 29 | wp: WordPress#6.6.2 30 | - php: '8.0' 31 | wp: WordPress 32 | - php: '8.0' 33 | wp: WordPress#6.8.1 34 | - php: '8.0' 35 | wp: WordPress#6.7.2 36 | - php: '8.0' 37 | wp: WordPress#6.6.2 38 | - php: '8.2' 39 | wp: WordPress 40 | - php: '8.2' 41 | wp: WordPress#6.8.1 42 | - php: '8.2' 43 | wp: WordPress#6.7.2 44 | - php: '8.2' 45 | wp: WordPress#6.6.2 46 | - php: '8.4' 47 | wp: WordPress 48 | - php: '8.4' 49 | wp: WordPress#6.8.1 50 | - php: '8.4' 51 | wp: WordPress#6.7.2 52 | name: PHP ${{ matrix.php }} / ${{ matrix.wp }} Test 53 | 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | 58 | - name: Use desired version of Node.js 59 | uses: actions/setup-node@v4 60 | with: 61 | node-version-file: '.nvmrc' 62 | 63 | - name: Npm install and build 64 | run: | 65 | npm ci 66 | npm run build 67 | 68 | - name: Composer install and set phpcs 69 | run: | 70 | composer install 71 | composer phpcs 72 | 73 | - name: Running lint check 74 | run: npm run lint 75 | 76 | - name: Running unit tests 77 | run: npm run test:unit 78 | 79 | - name: Install Playwright dependencies 80 | run: npx playwright install chromium firefox webkit --with-deps 81 | 82 | - name: Install WordPress 83 | run: | 84 | WP_ENV_CORE=WordPress/${{ matrix.wp }} WP_ENV_PHP_VERSION=${{ matrix.php }} npm run wp-env start 85 | npm run wp-env run cli wp core version 86 | npm run wp-env run cli wp cli info 87 | 88 | - name: Running e2e tests 89 | run: npm run test:e2e 90 | 91 | - name: Archive debug artifacts 92 | uses: actions/upload-artifact@v4 93 | if: always() 94 | with: 95 | name: failures-artifacts-${{ matrix.php }}-${{ matrix.wp }} 96 | path: artifacts 97 | if-no-files-found: ignore 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | artifacts/ 3 | build/ 4 | node_modules/ 5 | vendor/ 6 | .wp-env.override.json 7 | *.tsbuildinfo 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | engine-strict = true 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | ...require( '@wordpress/prettier-config' ), 3 | semi: true, 4 | useTabs: true, 5 | tabWidth: 2, 6 | singleQuote: true, 7 | printWidth: 100, 8 | bracketSpacing: true, 9 | parenSpacing: true, 10 | bracketSameLine: false, 11 | }; 12 | 13 | module.exports = config; 14 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ '@wordpress/stylelint-config/scss' ], 3 | ignoreFiles: [ 4 | 'build/**/*.css', 5 | 'node_modules/**/*.css', 6 | 'vendor/**/*.css', 7 | '**/*.js', 8 | '**/*.ts', 9 | '**/*.tsx', 10 | ], 11 | rules: { 12 | 'no-descending-specificity': null, 13 | 'font-weight-notation': null, 14 | 'selector-class-pattern': null, 15 | 'value-keyword-case': [ 16 | 'lower', 17 | { 18 | camelCaseSvgKeywords: true, 19 | }, 20 | ], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/banner-1544x500.png -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/banner-772x250.png -------------------------------------------------------------------------------- /.wordpress-org/blueprints/blueprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://playground.wordpress.net/blueprint-schema.json", 3 | "landingPage": "/wp-admin/post.php?post=5&action=edit", 4 | "steps": [ 5 | { 6 | "step": "login", 7 | "username": "admin" 8 | }, 9 | { 10 | "step": "installPlugin", 11 | "pluginData": { 12 | "resource": "wordpress.org/plugins", 13 | "slug": "flexible-table-block" 14 | } 15 | }, 16 | { 17 | "step": "runPHP", 18 | "code": " 5,\n'post_title' => 'Flexible Table Block',\n'post_content' => '',\n'post_status' => 'publish',\n'post_author' => 1\n));" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/icon-128x128.png -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/icon-256x256.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/screenshot-1.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/screenshot-2.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/screenshot-3.jpg -------------------------------------------------------------------------------- /.wordpress-org/screenshot-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t-hamano/flexible-table-block/1d68726db0a8d0effe72c0381d2456a6faee4e0b/.wordpress-org/screenshot-4.jpg -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ".", 4 | "https://downloads.wordpress.org/plugin/wordpress-beta-tester.zip", 5 | "https://downloads.wordpress.org/plugin/wp-downgrade.zip" 6 | ], 7 | "env": { 8 | "development": { 9 | "phpmyadminPort": 9000 10 | }, 11 | "tests": { 12 | "phpmyadminPort": 9001, 13 | "config": { 14 | "WP_DEBUG": true, 15 | "SCRIPT_DEBUG": true 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flexible Table Block 2 | 3 | [![Tests](https://github.com/t-hamano/flexible-table-block/actions/workflows/run-test.yml/badge.svg)](https://github.com/t-hamano/flexible-table-block/actions/workflows/run-test.yml) 4 | [![Tests and Deploy](https://github.com/t-hamano/flexible-table-block/actions/workflows/run-test-and-deploy.yml/badge.svg)](https://github.com/t-hamano/flexible-table-block/actions/workflows/run-test-and-deploy.yml) 5 | 6 | ![Flexible Table Block](https://raw.githubusercontent.com/t-hamano/flexible-table-block/main/.wordpress-org/banner-1544x500.png) 7 | 8 | Flexible Table Block is a custom block plugin for the WordPress block editor that allows you to create tables in any configuration. 9 | 10 | ## Features 11 | 12 | ### Merge and Split Cells 13 | 14 | ![Merge and Split Cells](https://raw.githubusercontent.com/t-hamano/flexible-table-block/main/.wordpress-org/screenshot-1.jpg) 15 | 16 | You can merge or split cells from multiple selected cells. 17 | 18 | ### Flexible Styling 19 | 20 | ![Flexible Styling](https://raw.githubusercontent.com/t-hamano/flexible-table-block/main/.wordpress-org/screenshot-2.jpg) 21 | 22 | You can set various styles for each tag of table, cell, and caption individually. 23 | 24 | ### Advanced UI 25 | 26 | ![Advanced UI](https://raw.githubusercontent.com/t-hamano/flexible-table-block/main/.wordpress-org/screenshot-3.jpg) 27 | 28 | You can easily select a batch of cells in a section, or select, add, or delete rows and columns with the buttons. 29 | 30 | ### Responsive Support 31 | 32 | ![Responsive Support](https://raw.githubusercontent.com/t-hamano/flexible-table-block/main/.wordpress-org/screenshot-4.jpg) 33 | 34 | You can set the table to scroll horizontally on both Desktop and mobile, and arrange cells vertically on mobile. The breakpoints for switching between Desktop and mobile can be changed freely. 35 | 36 | ## How to build 37 | 38 | ```sh 39 | npm install 40 | npm run build 41 | ``` 42 | 43 | ## Author 44 | 45 | [Aki Hamano (Github)](https://github.com/t-hamano) 46 | -------------------------------------------------------------------------------- /classes/class-api.php: -------------------------------------------------------------------------------- 1 | 'GET', 34 | 'callback' => array( $this, 'get_options' ), 35 | 'permission_callback' => function () { 36 | return current_user_can( 'edit_posts' ); 37 | }, 38 | ), 39 | array( 40 | 'methods' => 'POST', 41 | 'callback' => array( $this, 'update_options' ), 42 | 'permission_callback' => function () { 43 | $show_global_setting = get_option( FTB_OPTION_PREFIX . '_show_global_setting', Settings::OPTIONS['show_global_setting']['default'] ); 44 | 45 | if ( $show_global_setting ) { 46 | return current_user_can( 'edit_posts' ); 47 | } else { 48 | return current_user_can( 'administrator' ); 49 | } 50 | }, 51 | ), 52 | array( 53 | 'methods' => 'DELETE', 54 | 'callback' => array( $this, 'delete_options' ), 55 | 'permission_callback' => function () { 56 | $show_global_setting = get_option( FTB_OPTION_PREFIX . '_show_global_setting', Settings::OPTIONS['show_global_setting']['default'] ); 57 | 58 | if ( $show_global_setting ) { 59 | return current_user_can( 'edit_posts' ); 60 | } else { 61 | return current_user_can( 'administrator' ); 62 | } 63 | }, 64 | ), 65 | ) 66 | ); 67 | } 68 | 69 | /** 70 | * Get options 71 | * 72 | * @return WP_REST_Response|WP_Error 73 | */ 74 | public function get_options() { 75 | $options = Settings::get_options(); 76 | $options['block_css'] = Helper::minify_css( Helper::get_block_css( '.editor-styles-wrapper ' ) ); 77 | return rest_ensure_response( $options ); 78 | } 79 | 80 | /** 81 | * Update options 82 | * 83 | * @return WP_REST_Response|WP_Error 84 | */ 85 | public function update_options( $request ) { 86 | $params = $request->get_json_params(); 87 | 88 | // Sanitize option values. 89 | foreach ( $params as $key => $value ) { 90 | if ( ! array_key_exists( $key, Settings::OPTIONS ) ) { 91 | continue; 92 | } 93 | 94 | if ( 'boolean' === Settings::OPTIONS[ $key ]['type'] ) { 95 | $value = $value ? 1 : 0; 96 | } 97 | 98 | if ( 'array' === Settings::OPTIONS[ $key ]['type'] ) { 99 | if ( ! is_array( $value ) ) { 100 | continue; 101 | } 102 | 103 | $new_value = array(); 104 | foreach ( $value as $array_key => $array_value ) { 105 | if ( isset( Settings::OPTIONS[ $key ]['default'][ $array_key ] ) ) { 106 | $new_value[ $array_key ] = $array_value; 107 | } 108 | } 109 | } 110 | 111 | if ( isset( Settings::OPTIONS[ $key ]['range'] ) ) { 112 | $min = Settings::OPTIONS[ $key ]['range']['min']; 113 | $max = Settings::OPTIONS[ $key ]['range']['max']; 114 | $value = min( max( $value, $min ), $max ); 115 | } 116 | 117 | if ( is_wp_error( $value ) ) { 118 | return rest_ensure_response( 119 | array( 120 | 'status' => 'error', 121 | 'message' => $value->get_error_message(), 122 | ) 123 | ); 124 | } else { 125 | update_option( FTB_OPTION_PREFIX . '_' . $key, $value ); 126 | } 127 | } 128 | 129 | return rest_ensure_response( 130 | array( 131 | 'status' => 'success', 132 | 'message' => __( 'Global setting saved.', 'flexible-table-block' ), 133 | 'block_css' => Helper::minify_css( Helper::get_block_css( '.editor-styles-wrapper ' ) ), 134 | ) 135 | ); 136 | } 137 | 138 | /** 139 | * Delete options 140 | * 141 | * @return WP_REST_Response|WP_Error 142 | */ 143 | public function delete_options() { 144 | foreach ( Settings::OPTIONS as $key => $value ) { 145 | delete_option( FTB_OPTION_PREFIX . '_' . $key ); 146 | } 147 | 148 | return rest_ensure_response( 149 | array( 150 | 'options' => Settings::get_options(), 151 | 'status' => 'success', 152 | 'message' => __( 'Global setting restored.', 'flexible-table-block' ), 153 | 'block_css' => Helper::minify_css( Helper::get_block_css( '.editor-styles-wrapper ' ) ), 154 | ) 155 | ); 156 | } 157 | } 158 | 159 | new Api(); 160 | -------------------------------------------------------------------------------- /classes/class-enqueue.php: -------------------------------------------------------------------------------- 1 | load_classes(); 24 | } 25 | 26 | /** 27 | * Uninstallation process 28 | */ 29 | public static function plugin_uninstall() { 30 | foreach ( Settings::OPTIONS as $option ) { 31 | delete_option( $option ); 32 | } 33 | } 34 | 35 | /** 36 | * Load classes 37 | */ 38 | public function load_classes() { 39 | require_once FTB_PATH . '/classes/class-helper.php'; 40 | require_once FTB_PATH . '/classes/class-settings.php'; 41 | require_once FTB_PATH . '/classes/class-enqueue.php'; 42 | require_once FTB_PATH . '/classes/class-api.php'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /classes/class-settings.php: -------------------------------------------------------------------------------- 1 | array( 17 | 'type' => 'boolean', 18 | 'default' => true, 19 | ), 20 | // Show insert row/column buttons. 21 | 'show_control_button' => array( 22 | 'type' => 'boolean', 23 | 'default' => true, 24 | ), 25 | // Focus insert/select buttons, select row/column buttons, section label from being focused when moving with the crosshairs. 26 | 'focus_control_button' => array( 27 | 'type' => 'boolean', 28 | 'default' => false, 29 | ), 30 | // Show dot on th tag in the editor. 31 | 'show_dot_on_th' => array( 32 | 'type' => 'boolean', 33 | 'default' => true, 34 | ), 35 | // Use the TAB key to move cells. 36 | 'tab_move' => array( 37 | 'type' => 'boolean', 38 | 'default' => false, 39 | ), 40 | // Keep the contents of all cells when merging cells. 41 | 'merge_content' => array( 42 | 'type' => 'boolean', 43 | 'default' => false, 44 | ), 45 | // Show Global setting button to non-administrative users. 46 | 'show_global_setting' => array( 47 | 'type' => 'boolean', 48 | 'default' => false, 49 | ), 50 | // Set the screen width (breakpoint) as the basis for switching between desktop and mobile devices. 51 | 'breakpoint' => array( 52 | 'type' => 'number', 53 | 'default' => 768, 54 | 'range' => array( 55 | 'min' => 200, 56 | 'max' => 1200, 57 | ), 58 | ), 59 | // Default table styles. 60 | 'block_style' => array( 61 | 'type' => 'array', 62 | 'default' => array( 63 | 'table_width' => '100%', 64 | 'table_max_width' => '100%', 65 | 'table_border_collapse' => 'collapse', 66 | 'row_odd_color' => '#f0f0f1', 67 | 'row_even_color' => '#ffffff', 68 | 'cell_text_color_th' => null, 69 | 'cell_text_color_td' => null, 70 | 'cell_background_color_th' => '#f0f0f1', 71 | 'cell_background_color_td' => '#ffffff', 72 | 'cell_padding' => array( 73 | 'top' => '0.5em', 74 | 'right' => '0.5em', 75 | 'bottom' => '0.5em', 76 | 'left' => '0.5em', 77 | ), 78 | 'cell_border_width' => '1px', 79 | 'cell_border_style' => 'solid', 80 | 'cell_border_color' => '#000000', 81 | 'cell_text_align' => 'left', 82 | 'cell_vertical_align' => 'middle', 83 | ), 84 | ), 85 | ); 86 | 87 | /** 88 | * Constructor 89 | */ 90 | public function __construct() { 91 | } 92 | 93 | /** 94 | * Get options 95 | * 96 | * @return array 97 | */ 98 | public static function get_options() { 99 | $options = array(); 100 | 101 | foreach ( self::OPTIONS as $key => $value ) { 102 | $options[ $key ] = get_option( FTB_OPTION_PREFIX . '_' . $key, self::OPTIONS[ $key ]['default'] ); 103 | 104 | if ( 'boolean' === self::OPTIONS[ $key ]['type'] ) { 105 | $options[ $key ] = $options[ $key ] ? true : false; 106 | } 107 | } 108 | 109 | // Convert cell padding of string values to array. 110 | if ( 'string' === gettype( $options['block_style']['cell_padding'] ) ) { 111 | $padding_value = $options['block_style']['cell_padding']; 112 | 113 | $options['block_style']['cell_padding'] = array( 114 | 'top' => $padding_value, 115 | 'right' => $padding_value, 116 | 'bottom' => $padding_value, 117 | 'left' => $padding_value, 118 | ); 119 | } 120 | 121 | return $options; 122 | } 123 | } 124 | 125 | new Settings(); 126 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t-hamano/flexible-table-block", 3 | "description": "WordPress Plugin", 4 | "license": "GPL-2.0-or-later", 5 | "authors": [ 6 | { 7 | "name": "Aki Hamano" 8 | } 9 | ], 10 | "require-dev": { 11 | "squizlabs/php_codesniffer": "*", 12 | "wp-coding-standards/wpcs": "^3.1" 13 | }, 14 | "scripts": { 15 | "phpcs": "phpcs --config-set installed_paths vendor/wp-coding-standards/wpcs,vendor/phpcsstandards/phpcsextra,vendor/phpcsstandards/phpcsutils", 16 | "lint": "phpcs ./ --standard=./phpcs.ruleset.xml" 17 | }, 18 | "config": { 19 | "allow-plugins": { 20 | "dealerdirect/phpcodesniffer-composer-installer": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /flexible-table-block.php: -------------------------------------------------------------------------------- 1 | =22.0.0", 19 | "npm": ">=10.9.2" 20 | }, 21 | "volta": { 22 | "node": "22.16.0", 23 | "npm": "10.9.2" 24 | }, 25 | "dependencies": { 26 | "@emotion/react": "^11.14.0", 27 | "@emotion/styled": "^11.14.0", 28 | "@wordpress/icons": "^10.24.0" 29 | }, 30 | "devDependencies": { 31 | "@types/jest": "^29.5.14", 32 | "@types/wordpress__block-editor": "^11.5.16", 33 | "@wordpress/api-fetch": "7.24.0", 34 | "@wordpress/env": "^10.24.0", 35 | "@wordpress/eslint-plugin": "22.10.0", 36 | "@wordpress/notices": "5.24.0", 37 | "@wordpress/scripts": "^30.17.0", 38 | "clsx": "2.1.1", 39 | "husky": "^9.1.7", 40 | "lint-staged": "16.1.0", 41 | "prettier": "npm:wp-prettier@3.0.3", 42 | "typescript": "^5.8.3" 43 | }, 44 | "scripts": { 45 | "wp-env": "wp-env", 46 | "stop": "wp-env stop", 47 | "start": "wp-scripts start", 48 | "build": "wp-scripts build", 49 | "check-licenses": "wp-scripts check-licenses", 50 | "lint": "npm run lint:css && npm run lint:js && npm run lint:types && npm run lint:php && npm run lint:md-docs && npm run lint:pkg-json", 51 | "lint:css": "wp-scripts lint-style", 52 | "lint:js": "wp-scripts lint-js", 53 | "lint:types": "tsc", 54 | "lint:php": "composer lint", 55 | "lint:md-docs": "wp-scripts lint-md-docs", 56 | "lint:pkg-json": "wp-scripts lint-pkg-json", 57 | "format": "wp-scripts format", 58 | "test": "npm run lint:js && npm run test:e2e && npm run test:unit", 59 | "test:unit": "wp-scripts test-unit-js --config test/unit/jest.config.js", 60 | "test:e2e": "wp-scripts test-playwright", 61 | "test:e2e:debug": "wp-scripts test-playwright --debug", 62 | "prepare": "husky" 63 | }, 64 | "lint-staged": { 65 | "*.{js,json,ts,tsx,yml,yaml}": [ 66 | "wp-scripts format" 67 | ], 68 | "*.{js,ts,tsx}": [ 69 | "wp-scripts lint-js" 70 | ], 71 | "*.scss": [ 72 | "wp-scripts lint-style" 73 | ], 74 | "*.md": [ 75 | "wp-scripts lint-md-docs" 76 | ], 77 | "package.json": [ 78 | "wp-scripts lint-pkg-json" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /phpcs.ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | A custom set of code standard rules to check for WordPress themes. 4 | 5 | */node_modules/* 6 | */vendor/* 7 | */build/* 8 | */*.js 9 | */*.css 10 | 11 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | const config = require( '@wordpress/scripts/config/playwright.config.js' ); 5 | 6 | export default { 7 | ...config, 8 | testDir: './test/e2e', 9 | }; 10 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Flexible Table Block === 2 | Contributors: wildworks, Toro_Unit 3 | Tags: gutenberg, block, table 4 | Donate link: https://www.paypal.me/thamanoJP 5 | Requires at least: 6.6 6 | Tested up to: 6.7 7 | Stable tag: 3.5.0 8 | Requires PHP: 7.4 9 | License: GPLv2 or later 10 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 | 12 | Flexible Table Block is a custom block plugin for the WordPress block editor that allows you to create tables in any configuration. 13 | 14 | == Description == 15 | 16 | **Merge and Split Cells** 17 | 18 | You can merge or split cells from multiple selected cells. 19 | 20 | **Flexible Styling** 21 | 22 | You can set various styles for each tag of table, cell, and caption individually. 23 | 24 | **Advanced UI** 25 | 26 | You can easily select a batch of cells in a section, or select, add, or delete rows and columns with the buttons. 27 | 28 | **Responsive Support** 29 | 30 | You can set the table to scroll horizontally on both Desktop and mobile, and arrange cells vertically on mobile. 31 | The breakpoints for switching between Desktop and mobile can be changed freely. 32 | 33 | == Installation == 34 | 1. Upload the `flexible-table-block` folder to the `/wp-content/plugins/` directory. 35 | 2. Activate the plugin through the \'Plugins\' menu in WordPress. 36 | 37 | == Screenshots == 38 | 1. Merge and Split Cells 39 | 2. Flexible Styling 40 | 3. Advanced UI 41 | 4. Responsive Support 42 | 43 | == Changelog == 44 | 45 | = 3.5.0 = 46 | * Tested to WordPress 6.8 47 | * Drop support for WP 6.5 48 | * Enhancement: Replace deprecated UI with recommended UI 49 | 50 | = 3.4.0 = 51 | * Tested to WordPress 6.7 52 | * Drop support for WordPress 6.4 53 | * Add: Caption toolbar button 54 | * Enhancement: Update icons 55 | * Enhancement: Improve accessibility 56 | * Enhancement: Improve hint text 57 | * Enhancement: Add box-sizing 58 | 59 | = 3.3.0 = 60 | * Tested to WordPress 6.6 61 | * Drop support for WordPress 6.3 62 | * Enhancement: Support content only mode 63 | * Enhancement: Update API version from 2 to 3 64 | 65 | = 3.2.0 = 66 | * Tested to WordPress 6.5 67 | * Drop support for WordPress 6.2 68 | * Enhancement: Polish block sidebar UI 69 | * Enhancement: Polish UI in Global Setting modal 70 | 71 | = 3.1.0 = 72 | * Tested to WordPress 6.4 73 | * Drop support for WordPress 6.1 74 | * Enhancement: Use Snackbar component instead of window.alert 75 | * Fix: Some block styles are not carried over when transforming the block 76 | 77 | = 3.0.1 = 78 | * Fix: Keyboard controls don't work within the link control popover 79 | * Fix: Tab key focus doesn't work when cell text contains footnote links 80 | * Enhancement: Release cell selection when the block is unselected 81 | 82 | = 3.0.0 = 83 | * Tested to WordPress 6.3 84 | * Fix: Missing top border in the block sidebar 85 | * Fix: Some grammatical errors 86 | * Fix: Incorrect pixel value in description about breakpoint 87 | * Fix: Popovers in Global Settings modal are not showing in the Site Editor 88 | * Fix: Cursor style when mousing over RichText in the cell 89 | * Clean: Remove link to wiki page in help modal 90 | * Drop support for WordPress 5.9, 6.0 91 | * Drop support for PHP7.3 92 | 93 | = 2.9.1 = 94 | * Enhancement: Adjust tab width in global setting modal 95 | 96 | = 2.9.0 = 97 | * Tested to WordPress 6.2 98 | * Enhancement: Redesign global setting modal 99 | * Enhancement: Keep rowspan and colspan attributes when converting to and from the core table block 100 | * Enhancement: Apply stripe colors to tbody only 101 | * Enhancement: Polish style for WordPress 6.2 102 | * Fix: Link color is not applied 103 | 104 | = 2.8.0 = 105 | * Tested to WordPress 6.1 106 | * Drop support for WordPress 5.8 107 | * Enhancement: Polish block sidebar UI 108 | * Enhancement: Polish UI in Global Setting modal 109 | * Doc: Use code tags in some text 110 | * Fix: Don't apply typography support styles to the placeholder 111 | * Fix: register_block_type path 112 | 113 | = 2.7.3 = 114 | * Add: Loading status to global settings button 115 | * Clean: Use code tags for text in the options section 116 | * Fix: Overflow of input field in placeholder 117 | * Fix: Block toolbar doesn't appear in HTML edit mode 118 | 119 | = 2.7.2 = 120 | * Change: Style to break lines in cells 121 | 122 | = 2.7.1 = 123 | * Change: Don't update cell tag when cell settings are cleared 124 | * Fix: Certain operations break the block 125 | 126 | = 2.7.0 = 127 | * Add: id, headers, scope attribute controls to cell settings 128 | * Fix: Browser warning error 129 | * Fix: Not transformed to core table block correctly 130 | 131 | = 2.6.2 = 132 | * Fix: Scrolli table doesn't show edges 133 | * Add: Help text about scroll table 134 | * Add: Style to break lines in cells 135 | 136 | = 2.6.1 = 137 | * Tested to WordPress 6.0 138 | * Fix: Clearing the table settings and then saving the post breaks the block 139 | * Fix: Adjust indicator style 140 | 141 | = 2.6.0 = 142 | * Add: Margin support 143 | * Fix: Cell CSS class is not cleared when cell settings are cleared 144 | * Fix: Output of incorrect inline CSS 145 | 146 | = 2.5.3 = 147 | * Fix: Cell CSS class is reset 148 | 149 | = 2.5.2 = 150 | * Update: Block preview 151 | * Fix: Error when installing from block directory 152 | 153 | = 2.5.0 = 154 | * Add: Option to move cells with the tab key 155 | * Fix: Cell content is not updated under certain conditions 156 | 157 | = 2.4.0 = 158 | * Tested to WordPress 5.9 159 | * Add: Block supports (link color, text-transform, font-style, font-weight, letter-spacing) 160 | * Fix: Zero values are not saved correctly in global settings 161 | * Fix: Changes to global settings are not reflected in iframe editor instances 162 | 163 | = 2.3.1 = 164 | * Fix: Table justify icon does not appear 165 | * Fix: Incorrect indigator direction 166 | 167 | = 2.3.0 = 168 | * Enhancement: Support for individual values in cell padding of global settings 169 | * Fix: Unable to deselect selected cells by clicking with the Ctrl key 170 | * Fix: Cell width is not set to 100% when 'Stack on mobile' is enabled 171 | * Fix: Accessibility support for controls 172 | 173 | = 2.2.0 = 174 | * Add: Option to merge content when merging cells 175 | 176 | = 2.1.2 = 177 | * Fix: Global settings options are not saved 178 | 179 | = 2.1.1 = 180 | * Fix: Adjust indicator style 181 | * Fix: Cell Line Height setting is not cleared 182 | 183 | = 2.1.0 = 184 | * Add: Cell line-height control 185 | 186 | = 2.0.9 = 187 | * Doc: Add translate context 188 | 189 | = 2.0.8 = 190 | * Fix: Accessibility support for controls, fix typo 191 | 192 | = 2.0.7 = 193 | * Fix: Missing text translation 194 | * Fix: Button text layout in popover is broken 195 | * Fix: Text in JavaScript is not translated 196 | 197 | = 2.0.6 = 198 | * Fix: typo 199 | 200 | = 2.0.5 = 201 | * Fix: deploy action 202 | 203 | = 2.0.4 = 204 | * Fix: deploy action 205 | 206 | = 2.0.3 = 207 | * Fix: deploy action 208 | 209 | = 2.0.2 = 210 | * Fix: deploy action 211 | 212 | = 2.0.1 = 213 | * Doc: add LICENSE 214 | * Clean: add deploy action 215 | * Clean: refactoring: edit.js to edit.tsx 216 | 217 | = 2.0.0 = 218 | * Initial release 219 | -------------------------------------------------------------------------------- /src/BlockAttributes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import type { 5 | CAPTION_SIDE_CONTROLS, 6 | CELL_TAG_CONTROLS, 7 | CELL_SCOPE_CONTROLS, 8 | CORNER_CONTROLS, 9 | DIRECTION_CONTROLS, 10 | SIDE_CONTROLS, 11 | CONTENT_JUSTIFY_CONTROLS, 12 | BORDER_COLLAPSE_CONTROLS, 13 | STICKY_CONTROLS, 14 | } from './constants'; 15 | 16 | type NestedObject = { 17 | [ key: string ]: NestedObject | null | undefined; 18 | }; 19 | 20 | // Controls Attributes value types 21 | export type CaptionSideValue = ( typeof CAPTION_SIDE_CONTROLS )[ number ][ 'value' ]; 22 | export type CellTagValue = ( typeof CELL_TAG_CONTROLS )[ number ][ 'value' ]; 23 | export type CellScopeValue = ( typeof CELL_SCOPE_CONTROLS )[ number ][ 'value' ]; 24 | export type CornerValue = ( typeof CORNER_CONTROLS )[ number ][ 'value' ]; 25 | export type DirectionValue = ( typeof DIRECTION_CONTROLS )[ number ][ 'value' ]; 26 | export type SideValue = ( typeof SIDE_CONTROLS )[ number ][ 'value' ]; 27 | export type ContentJustifyValue = ( typeof CONTENT_JUSTIFY_CONTROLS )[ number ][ 'value' ]; 28 | export type BorderCollapseValue = ( typeof BORDER_COLLAPSE_CONTROLS )[ number ][ 'value' ]; 29 | export type StickyValue = ( typeof STICKY_CONTROLS )[ number ][ 'value' ]; 30 | 31 | // Table section name types 32 | export type SectionName = 'head' | 'body' | 'foot'; 33 | 34 | // Table attributes 35 | export type TableAttributes = Record< SectionName, Row[] >; 36 | 37 | // Table row attributes 38 | export interface Row { 39 | cells: Cell[]; 40 | } 41 | 42 | // Table cell attributes 43 | export interface Cell { 44 | content: string; 45 | styles?: string; 46 | tag: CellTagValue; 47 | className?: string; 48 | id?: string; 49 | headers?: string; 50 | scope?: CellScopeValue; 51 | rowSpan?: string; 52 | colSpan?: string; 53 | } 54 | 55 | // Block attributes 56 | export interface BlockAttributes extends TableAttributes { 57 | contentJustification: ContentJustifyValue | undefined; 58 | hasFixedLayout: boolean; 59 | isScrollOnPc: boolean; 60 | isScrollOnMobile: boolean; 61 | isStackedOnMobile: boolean; 62 | sticky: StickyValue; 63 | tableStyles?: string; 64 | captionStyles?: string; 65 | captionSide: CaptionSideValue; 66 | caption?: string; 67 | style: NestedObject; 68 | } 69 | 70 | // Core Table Block attributes 71 | export interface CoreTableBlockAttributes { 72 | head: { 73 | cells: CoreTableCell[]; 74 | }[]; 75 | body: { 76 | cells: CoreTableCell[]; 77 | }[]; 78 | foot: { 79 | cells: CoreTableCell[]; 80 | }[]; 81 | hasFixedLayout: boolean; 82 | caption: string; 83 | style: NestedObject; 84 | } 85 | 86 | export interface CoreTableCell { 87 | content: string; 88 | tag: CellTagValue; 89 | rowspan?: string; 90 | colspan?: string; 91 | } 92 | -------------------------------------------------------------------------------- /src/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": 3, 3 | "name": "flexible-table-block/table", 4 | "title": "Flexible Table", 5 | "category": "text", 6 | "keywords": [ "table", "cell", "data" ], 7 | "description": "Create a flexible configuration table.", 8 | "textdomain": "flexible-table-block", 9 | "attributes": { 10 | "contentJustification": { 11 | "type": "string" 12 | }, 13 | "hasFixedLayout": { 14 | "type": "boolean", 15 | "default": true 16 | }, 17 | "isScrollOnPc": { 18 | "type": "boolean", 19 | "default": false 20 | }, 21 | "isScrollOnMobile": { 22 | "type": "boolean", 23 | "default": false 24 | }, 25 | "isStackedOnMobile": { 26 | "type": "boolean", 27 | "default": false 28 | }, 29 | "sticky": { 30 | "type": "string" 31 | }, 32 | "tableStyles": { 33 | "type": "string", 34 | "source": "attribute", 35 | "selector": "table", 36 | "attribute": "style" 37 | }, 38 | "captionSide": { 39 | "type": "string", 40 | "default": "bottom" 41 | }, 42 | "caption": { 43 | "type": "string", 44 | "source": "html", 45 | "selector": "figcaption", 46 | "__experimentalRole": "content" 47 | }, 48 | "captionStyles": { 49 | "type": "string", 50 | "source": "attribute", 51 | "selector": "figcaption", 52 | "attribute": "style" 53 | }, 54 | "head": { 55 | "type": "array", 56 | "default": [], 57 | "source": "query", 58 | "selector": "thead tr", 59 | "query": { 60 | "cells": { 61 | "type": "array", 62 | "default": [], 63 | "source": "query", 64 | "selector": "td,th", 65 | "query": { 66 | "content": { 67 | "type": "string", 68 | "source": "html", 69 | "__experimentalRole": "content" 70 | }, 71 | "styles": { 72 | "type": "string", 73 | "source": "attribute", 74 | "attribute": "style" 75 | }, 76 | "tag": { 77 | "type": "string", 78 | "default": "td", 79 | "source": "tag" 80 | }, 81 | "className": { 82 | "type": "string", 83 | "source": "attribute", 84 | "attribute": "class" 85 | }, 86 | "id": { 87 | "type": "string", 88 | "source": "attribute", 89 | "attribute": "id" 90 | }, 91 | "headers": { 92 | "type": "string", 93 | "source": "attribute", 94 | "attribute": "headers" 95 | }, 96 | "scope": { 97 | "enum": [ "row", "col", "rowgroup", "colgroup" ], 98 | "source": "attribute", 99 | "attribute": "scope" 100 | }, 101 | "rowSpan": { 102 | "type": "string", 103 | "source": "attribute", 104 | "attribute": "rowspan" 105 | }, 106 | "colSpan": { 107 | "type": "string", 108 | "source": "attribute", 109 | "attribute": "colspan" 110 | } 111 | } 112 | } 113 | } 114 | }, 115 | "body": { 116 | "type": "array", 117 | "default": [], 118 | "source": "query", 119 | "selector": "tbody tr", 120 | "query": { 121 | "cells": { 122 | "type": "array", 123 | "default": [], 124 | "source": "query", 125 | "selector": "td,th", 126 | "query": { 127 | "content": { 128 | "type": "string", 129 | "source": "html", 130 | "__experimentalRole": "content" 131 | }, 132 | "styles": { 133 | "type": "string", 134 | "source": "attribute", 135 | "attribute": "style" 136 | }, 137 | "tag": { 138 | "type": "string", 139 | "default": "td", 140 | "source": "tag" 141 | }, 142 | "className": { 143 | "type": "string", 144 | "source": "attribute", 145 | "attribute": "class" 146 | }, 147 | "id": { 148 | "type": "string", 149 | "source": "attribute", 150 | "attribute": "id" 151 | }, 152 | "headers": { 153 | "type": "string", 154 | "source": "attribute", 155 | "attribute": "headers" 156 | }, 157 | "scope": { 158 | "enum": [ "row", "col", "rowgroup", "colgroup" ], 159 | "source": "attribute", 160 | "attribute": "scope" 161 | }, 162 | "rowSpan": { 163 | "type": "string", 164 | "source": "attribute", 165 | "attribute": "rowspan" 166 | }, 167 | "colSpan": { 168 | "type": "string", 169 | "source": "attribute", 170 | "attribute": "colspan" 171 | } 172 | } 173 | } 174 | } 175 | }, 176 | "foot": { 177 | "type": "array", 178 | "default": [], 179 | "source": "query", 180 | "selector": "tfoot tr", 181 | "query": { 182 | "cells": { 183 | "type": "array", 184 | "default": [], 185 | "source": "query", 186 | "selector": "td,th", 187 | "query": { 188 | "content": { 189 | "type": "string", 190 | "source": "html", 191 | "__experimentalRole": "content" 192 | }, 193 | "styles": { 194 | "type": "string", 195 | "source": "attribute", 196 | "attribute": "style" 197 | }, 198 | "tag": { 199 | "type": "string", 200 | "default": "td", 201 | "source": "tag" 202 | }, 203 | "className": { 204 | "type": "string", 205 | "source": "attribute", 206 | "attribute": "class" 207 | }, 208 | "id": { 209 | "type": "string", 210 | "source": "attribute", 211 | "attribute": "id" 212 | }, 213 | "headers": { 214 | "type": "string", 215 | "source": "attribute", 216 | "attribute": "headers" 217 | }, 218 | "scope": { 219 | "enum": [ "row", "col", "rowgroup", "colgroup" ], 220 | "source": "attribute", 221 | "attribute": "scope" 222 | }, 223 | "rowSpan": { 224 | "type": "string", 225 | "source": "attribute", 226 | "attribute": "rowspan" 227 | }, 228 | "colSpan": { 229 | "type": "string", 230 | "source": "attribute", 231 | "attribute": "colspan" 232 | } 233 | } 234 | } 235 | } 236 | } 237 | }, 238 | "supports": { 239 | "anchor": true, 240 | "align": [ "left", "right", "wide", "full" ], 241 | "color": { 242 | "__experimentalSkipSerialization": [ "text", "background", "gradients" ], 243 | "gradients": true, 244 | "link": true 245 | }, 246 | "typography": { 247 | "fontSize": true, 248 | "lineHeight": true, 249 | "__experimentalFontFamily": true, 250 | "__experimentalTextTransform": true, 251 | "__experimentalFontStyle": true, 252 | "__experimentalFontWeight": true, 253 | "__experimentalLetterSpacing": true, 254 | "__experimentalDefaultControls": { 255 | "fontSize": false 256 | } 257 | }, 258 | "spacing": { 259 | "margin": true, 260 | "__experimentalDefaultControls": { 261 | "margin": false 262 | } 263 | }, 264 | "__experimentalSelector": ".wp-block-flexible-table-block-table > table" 265 | }, 266 | "editorScript": "flexible-table-block-editor", 267 | "editorStyle": "flexible-table-block-editor", 268 | "style": "flexible-table-block" 269 | } 270 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { __, _x } from '@wordpress/i18n'; 5 | import { 6 | justifyLeft, 7 | justifyCenter, 8 | justifyRight, 9 | alignLeft, 10 | alignCenter, 11 | alignRight, 12 | } from '@wordpress/icons'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import { 18 | alignTop, 19 | alignMiddle, 20 | alignBottom, 21 | borderSolid, 22 | borderDotted, 23 | borderDashed, 24 | borderDouble, 25 | borderCollapse as borderCollapseIcon, 26 | borderSeparate as borderSeparateIcon, 27 | } from './icons'; 28 | 29 | // Custom store name. 30 | export const STORE_NAME = 'flexible-table-block' as const; 31 | 32 | // Rest API routes. 33 | export const REST_API_ROUTE = '/flexible-table-block/v1/options' as const; 34 | 35 | // Table placeholder default settings. 36 | export const DEFAULT_PREVIEW_ROWS = 3 as const; 37 | export const DEFAULT_PREVIEW_COLUMNS = 3 as const; 38 | export const MIN_PREVIEW_TABLE_HEIGHT = 150 as const; 39 | export const MAX_PREVIEW_TABLE_COL = 50 as const; 40 | export const MAX_PREVIEW_TABLE_ROW = 50 as const; 41 | export const THRESHOLD_PREVIEW_TABLE_COL = 10 as const; 42 | export const THRESHOLD_PREVIEW_TABLE_ROW = 10 as const; 43 | 44 | // Upper and lower limits. 45 | export const MAX_BORDER_RADIUS = { 46 | px: 200, 47 | em: 20, 48 | rem: 20, 49 | } as const; 50 | 51 | export const MAX_BORDER_WIDTH = { 52 | px: 50, 53 | em: 5, 54 | rem: 5, 55 | } as const; 56 | 57 | export const MAX_BORDER_SPACING = { 58 | px: 50, 59 | em: 5, 60 | rem: 5, 61 | } as const; 62 | 63 | // Responsive breakpoint settings. 64 | export const DEFAULT_RESPONSIVE_BREAKPOINT = 768 as const; 65 | export const MIN_RESPONSIVE_BREAKPOINT = 200 as const; 66 | export const MAX_RESPONSIVE_BREAKPOINT = 1200 as const; 67 | 68 | // Available units on UnitControl component. 69 | export const FONT_SIZE_UNITS = [ 'px', 'em', 'rem', '%' ]; 70 | export const TABLE_WIDTH_UNITS = [ 'px', 'em', 'rem', '%' ]; 71 | export const CELL_WIDTH_UNITS = [ 'px', 'em', 'rem', '%' ]; 72 | export const BORDER_SPACING_UNITS = [ 'px', 'em', 'rem' ]; 73 | export const BORDER_RADIUS_UNITS = [ 'px', 'em', 'rem' ]; 74 | export const BORDER_WIDTH_UNITS = [ 'px', 'em', 'rem' ]; 75 | export const PADDING_UNITS = [ 'px', '%', 'em', 'rem', 'vw', 'vh' ]; 76 | 77 | // Cell label & text variations. 78 | export const CELL_ARIA_LABEL = { 79 | head: __( 'Header cell text', 'flexible-table-block' ), 80 | body: __( 'Body cell text', 'flexible-table-block' ), 81 | foot: __( 'Footer cell text', 'flexible-table-block' ), 82 | } as const; 83 | 84 | // Controls variations. 85 | export const CONTENT_JUSTIFY_CONTROLS = [ 86 | { 87 | icon: justifyLeft, 88 | label: __( 'Justify table left', 'flexible-table-block' ), 89 | value: 'left', 90 | }, 91 | { 92 | icon: justifyCenter, 93 | label: __( 'Justify table center', 'flexible-table-block' ), 94 | value: 'center', 95 | }, 96 | { 97 | icon: justifyRight, 98 | label: __( 'Justify table right', 'flexible-table-block' ), 99 | value: 'right', 100 | }, 101 | ]; 102 | 103 | export const BORDER_COLLAPSE_CONTROLS = [ 104 | { 105 | icon: borderCollapseIcon, 106 | label: __( 'Share', 'flexible-table-block' ), 107 | value: 'collapse', 108 | }, 109 | { 110 | icon: borderSeparateIcon, 111 | label: __( 'Separate', 'flexible-table-block' ), 112 | value: 'separate', 113 | }, 114 | ] as const; 115 | 116 | export const BORDER_STYLE_CONTROLS = [ 117 | { 118 | label: __( 'Solid', 'flexible-table-block' ), 119 | value: 'solid', 120 | icon: borderSolid, 121 | }, 122 | { 123 | label: __( 'Dotted', 'flexible-table-block' ), 124 | value: 'dotted', 125 | icon: borderDotted, 126 | }, 127 | { 128 | label: __( 'Dashed', 'flexible-table-block' ), 129 | value: 'dashed', 130 | icon: borderDashed, 131 | }, 132 | { 133 | label: __( 'Double', 'flexible-table-block' ), 134 | value: 'double', 135 | icon: borderDouble, 136 | }, 137 | ] as const; 138 | 139 | export const TEXT_ALIGNMENT_CONTROLS = [ 140 | { 141 | icon: alignLeft, 142 | label: __( 'Align left', 'flexible-table-block' ), 143 | value: 'left', 144 | }, 145 | { 146 | icon: alignCenter, 147 | label: __( 'Align center', 'flexible-table-block' ), 148 | value: 'center', 149 | }, 150 | { 151 | icon: alignRight, 152 | label: __( 'Align right', 'flexible-table-block' ), 153 | value: 'right', 154 | }, 155 | ] as const; 156 | 157 | export const VERTICAL_ALIGNMENT_CONTROLS = [ 158 | { 159 | icon: alignTop, 160 | label: __( 'Align top', 'flexible-table-block' ), 161 | value: 'top', 162 | }, 163 | { 164 | icon: alignMiddle, 165 | label: __( 'Align middle', 'flexible-table-block' ), 166 | value: 'middle', 167 | }, 168 | { 169 | icon: alignBottom, 170 | label: __( 'Align bottom', 'flexible-table-block' ), 171 | value: 'bottom', 172 | }, 173 | ] as const; 174 | 175 | export const STICKY_CONTROLS = [ 176 | { 177 | label: _x( 'none', 'Fixed control', 'flexible-table-block' ), 178 | value: 'none', 179 | }, 180 | { 181 | label: __( 'Fixed header', 'flexible-table-block' ), 182 | value: 'header', 183 | }, 184 | { 185 | label: __( 'Fixed first column', 'flexible-table-block' ), 186 | value: 'first-column', 187 | }, 188 | ] as const; 189 | 190 | export const CELL_TAG_CONTROLS = [ 191 | { 192 | label: __( 'TH', 'flexible-table-block' ), 193 | value: 'th', 194 | }, 195 | { 196 | label: __( 'TD', 'flexible-table-block' ), 197 | value: 'td', 198 | }, 199 | ] as const; 200 | 201 | export const CELL_SCOPE_CONTROLS = [ 202 | { 203 | label: _x( 'none', 'Cell scope control', 'flexible-table-block' ), 204 | value: 'none', 205 | }, 206 | { 207 | label: __( 'row', 'flexible-table-block' ), 208 | value: 'row', 209 | }, 210 | { 211 | label: __( 'col', 'flexible-table-block' ), 212 | value: 'col', 213 | }, 214 | { 215 | label: __( 'rowgroup', 'flexible-table-block' ), 216 | value: 'rowgroup', 217 | }, 218 | { 219 | label: __( 'colgroup', 'flexible-table-block' ), 220 | value: 'colgroup', 221 | }, 222 | ] as const; 223 | 224 | export const CAPTION_SIDE_CONTROLS = [ 225 | { 226 | label: __( 'Top', 'flexible-table-block' ), 227 | value: 'top', 228 | }, 229 | { 230 | label: __( 'Bottom', 'flexible-table-block' ), 231 | value: 'bottom', 232 | }, 233 | ] as const; 234 | 235 | export const CORNER_CONTROLS = [ 236 | { 237 | label: __( 'Top left', 'flexible-table-block' ), 238 | value: 'topLeft', 239 | }, 240 | { 241 | label: __( 'Top right', 'flexible-table-block' ), 242 | value: 'topRight', 243 | }, 244 | { 245 | label: __( 'Bottom right', 'flexible-table-block' ), 246 | value: 'bottomRight', 247 | }, 248 | { 249 | label: __( 'Bottom left', 'flexible-table-block' ), 250 | value: 'bottomLeft', 251 | }, 252 | ] as const; 253 | 254 | export const DIRECTION_CONTROLS = [ 255 | { 256 | label: __( 'Vertical', 'flexible-table-block' ), 257 | value: 'vertical', 258 | }, 259 | { 260 | label: __( 'Horizontal', 'flexible-table-block' ), 261 | value: 'horizontal', 262 | }, 263 | ] as const; 264 | 265 | export const SIDE_CONTROLS = [ 266 | { 267 | label: __( 'Top', 'flexible-table-block' ), 268 | value: 'top', 269 | }, 270 | { 271 | label: __( 'Right', 'flexible-table-block' ), 272 | value: 'right', 273 | }, 274 | { 275 | label: __( 'Bottom', 'flexible-table-block' ), 276 | value: 'bottom', 277 | }, 278 | { 279 | label: __( 'Left', 'flexible-table-block' ), 280 | value: 'left', 281 | }, 282 | ] as const; 283 | -------------------------------------------------------------------------------- /src/controls/border-color-control.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import type { Property } from 'csstype'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { __ } from '@wordpress/i18n'; 10 | import { link, linkOff } from '@wordpress/icons'; 11 | import { useSelect } from '@wordpress/data'; 12 | import { useState } from '@wordpress/element'; 13 | import { 14 | BaseControl, 15 | Button, 16 | Popover, 17 | ColorPalette, 18 | Flex, 19 | FlexBlock, 20 | FlexItem, 21 | __experimentalHStack as HStack, 22 | __experimentalVStack as VStack, 23 | __experimentalSpacer as Spacer, 24 | __experimentalText as Text, 25 | } from '@wordpress/components'; 26 | import { store as blockEditorStore } from '@wordpress/block-editor'; 27 | import { useInstanceId } from '@wordpress/compose'; 28 | 29 | /** 30 | * Internal dependencies 31 | */ 32 | import ColorIndicatorButton from './color-indicator-button'; 33 | import { SideIndicatorControl } from './indicator-control'; 34 | import { SIDE_CONTROLS } from '../constants'; 35 | import type { SideValue } from '../BlockAttributes'; 36 | 37 | type Props = { 38 | label: string; 39 | help?: string; 40 | onChange: ( event: any ) => void; 41 | values: { 42 | top?: Property.BorderTopColor; 43 | right?: Property.BorderRightColor; 44 | bottom?: Property.BorderBottomColor; 45 | left?: Property.BorderLeftColor; 46 | }; 47 | }; 48 | 49 | const DEFAULT_VALUES = { 50 | top: '', 51 | right: '', 52 | bottom: '', 53 | left: '', 54 | }; 55 | 56 | export default function BorderColorControl( { 57 | label = __( 'Border color', 'flexible-table-block' ), 58 | help, 59 | onChange, 60 | values: valuesProp, 61 | }: Props ) { 62 | const values = { 63 | ...DEFAULT_VALUES, 64 | ...valuesProp, 65 | }; 66 | const instanceId = useInstanceId( BorderColorControl, 'ftb-border-color-control' ); 67 | const headingId = `${ instanceId }-heading`; 68 | 69 | const isMixed = ! ( 70 | values.top === values.right && 71 | values.top === values.bottom && 72 | values.top === values.left 73 | ); 74 | 75 | const colors = useSelect( ( select ) => { 76 | const settings = select( 77 | blockEditorStore 78 | // @ts-ignore 79 | ).getSettings(); 80 | return settings?.colors ?? []; 81 | }, [] ); 82 | 83 | const [ isLinked, setIsLinked ] = useState< boolean >( true ); 84 | const [ isPickerOpen, setIsPickerOpen ] = useState< boolean >( false ); 85 | const [ pickerIndex, setPickerIndex ] = useState< number | undefined >( undefined ); 86 | 87 | const linkedLabel: string = isLinked 88 | ? __( 'Unlink sides', 'flexible-table-block' ) 89 | : __( 'Link sides', 'flexible-table-block' ); 90 | 91 | const allInputValue: string | 0 = isMixed ? '' : values.top; 92 | 93 | const toggleLinked = () => setIsLinked( ! isLinked ); 94 | 95 | const handleOnReset = () => { 96 | setIsLinked( true ); 97 | onChange( DEFAULT_VALUES ); 98 | }; 99 | 100 | const handleOnChangeAll = ( inputValue: string | undefined ) => { 101 | onChange( { 102 | top: inputValue, 103 | right: inputValue, 104 | bottom: inputValue, 105 | left: inputValue, 106 | } ); 107 | }; 108 | 109 | const handleOnChange = ( inputValue: string | undefined, targetSide: SideValue ) => { 110 | onChange( { 111 | ...values, 112 | [ targetSide ]: inputValue, 113 | } ); 114 | }; 115 | 116 | const handleOnPickerOpen = ( targetPickerIndex: number | undefined ) => { 117 | setIsPickerOpen( true ); 118 | setPickerIndex( targetPickerIndex ); 119 | }; 120 | 121 | const handleOnPickerClose = () => { 122 | setIsPickerOpen( false ); 123 | setPickerIndex( undefined ); 124 | }; 125 | 126 | return ( 127 | 128 | 129 | 130 | 131 | { label } 132 | 133 | 134 | 137 | 138 | 139 | 140 | { isLinked ? ( 141 | 142 | 143 | handleOnPickerOpen( undefined ) } 147 | isNone={ ! allInputValue && ! isMixed } 148 | isTransparent={ allInputValue === 'transparent' } 149 | isMixed={ isMixed } 150 | /> 151 | { isPickerOpen && ! pickerIndex && ( 152 | 153 | 154 | 159 | 160 | 161 | ) } 162 | 163 | ) : ( 164 | 165 | { SIDE_CONTROLS.map( ( item, index ) => ( 166 | 167 | 168 | handleOnPickerOpen( index ) } 172 | isNone={ ! values[ item.value ] } 173 | isTransparent={ values[ item.value ] === 'transparent' } 174 | /> 175 | { isPickerOpen && pickerIndex === index && ( 176 | 182 | 183 | handleOnChange( value, item.value ) } 187 | /> 188 | 189 | 190 | ) } 191 | 192 | ) ) } 193 | 194 | ) } 195 | 154 | 155 | 156 | 157 | 158 | 159 | { isLinked && ( 160 |
161 | 172 |
173 | ) } 174 |
175 | 128 | 129 | 130 | 131 | { isLinked ? ( 132 | 133 | 134 | 143 | 144 | ) : ( 145 | 146 | { DIRECTION_CONTROLS.map( ( item ) => ( 147 | 148 | 149 | handleOnChange( value, item.value ) } 155 | size="__unstable-large" 156 | __unstableInputWidth="100px" 157 | /> 158 | 159 | ) ) } 160 | 161 | ) } 162 | 131 | 132 | 133 | 134 | { isLinked ? ( 135 | 136 | { hasIndicator && } 137 | 146 | { BORDER_STYLE_CONTROLS.map( ( borderStyle ) => ( 147 | 153 | ) ) } 154 | 155 | 156 | ) : ( 157 | 158 | { SIDE_CONTROLS.map( ( item ) => ( 159 | 160 | { hasIndicator && } 161 | handleOnClick( value, item.value as ValuesKey ) } 169 | > 170 | { BORDER_STYLE_CONTROLS.map( ( borderStyle ) => ( 171 | 177 | ) ) } 178 | 179 | 180 | ) ) } 181 | 182 | ) } 183 | { allowSides && ( 184 | 154 | 155 | 156 | 157 | 158 | { hasIndicator && ( 159 | 160 | ) } 161 | { ( isLinked || ! allowSides ) && ( 162 |
163 | 172 |
173 | ) } 174 |
175 | { allowSides && ( 176 | 84 | 85 | 86 | 93 |
94 | { isPickerOpen && ( 95 | 96 | 97 | 102 | 103 | 104 | ) } 105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/controls/color-indicator-button.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import clsx from 'clsx'; 5 | import type { Property } from 'csstype'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { Button, ColorIndicator } from '@wordpress/components'; 11 | import { __ } from '@wordpress/i18n'; 12 | 13 | type Props = { 14 | label: string; 15 | value?: Property.Color; 16 | isNone: boolean; 17 | isTransparent: boolean; 18 | isMixed?: boolean; 19 | onClick: () => void; 20 | }; 21 | 22 | export default function ColorIndicatorButton( { 23 | label, 24 | value, 25 | isNone, 26 | isTransparent, 27 | isMixed = false, 28 | onClick, 29 | }: Props ) { 30 | return ( 31 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/controls/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BorderColorControl } from './border-color-control'; 2 | export { default as BorderRadiusControl } from './border-radius-control'; 3 | export { default as BorderStyleControl } from './border-style-control'; 4 | export { default as BorderWidthControl } from './border-width-control'; 5 | export { default as BorderSpacingControl } from './border-spacing-control'; 6 | export { default as ColorControl } from './color-control'; 7 | export { default as PaddingControl } from './padding-control'; 8 | -------------------------------------------------------------------------------- /src/controls/indicator-control.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { 5 | ViewBox, 6 | TopStroke, 7 | RightStroke, 8 | BottomStroke, 9 | LeftStroke, 10 | TopLeftStroke, 11 | TopRightStroke, 12 | BottomRightStroke, 13 | BottomLeftStroke, 14 | } from './styles'; 15 | import type { SideValue, CornerValue, DirectionValue } from '../BlockAttributes'; 16 | 17 | export function SideIndicatorControl( { sides }: { sides?: SideValue[] } ) { 18 | const top: boolean = ! sides || sides.includes( 'top' ); 19 | const right: boolean = ! sides || sides.includes( 'right' ); 20 | const bottom: boolean = ! sides || sides.includes( 'bottom' ); 21 | const left: boolean = ! sides || sides.includes( 'left' ); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export function CornerIndicatorControl( { corners }: { corners?: CornerValue[] } ) { 34 | const topLeft = ! corners || corners.includes( 'topLeft' ); 35 | const topRight = ! corners || corners.includes( 'topRight' ); 36 | const bottomRight = ! corners || corners.includes( 'bottomRight' ); 37 | const bottomLeft = ! corners || corners.includes( 'bottomLeft' ); 38 | 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | export function DirectionIndicatorControl( { directions }: { directions?: DirectionValue[] } ) { 50 | const horizontal = ! directions || directions.includes( 'horizontal' ); 51 | const vertical = ! directions || directions.includes( 'vertical' ); 52 | 53 | return ( 54 | 55 | 56 | 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/controls/padding-control.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import type { Property } from 'csstype'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import { __ } from '@wordpress/i18n'; 10 | import { link, linkOff } from '@wordpress/icons'; 11 | import { useState } from '@wordpress/element'; 12 | import { 13 | BaseControl, 14 | Button, 15 | Flex, 16 | FlexBlock, 17 | FlexItem, 18 | __experimentalGrid as Grid, 19 | __experimentalHStack as HStack, 20 | __experimentalVStack as VStack, 21 | __experimentalText as Text, 22 | __experimentalUnitControl as UnitControl, 23 | __experimentalUseCustomUnits as useCustomUnits, 24 | } from '@wordpress/components'; 25 | import { useInstanceId } from '@wordpress/compose'; 26 | 27 | /** 28 | * Internal dependencies 29 | */ 30 | import { PADDING_UNITS, SIDE_CONTROLS } from '../constants'; 31 | import { sanitizeUnitValue } from '../utils/helper'; 32 | import { SideIndicatorControl } from './indicator-control'; 33 | import type { SideValue } from '../BlockAttributes'; 34 | 35 | const DEFAULT_VALUES = { 36 | top: '', 37 | right: '', 38 | bottom: '', 39 | left: '', 40 | }; 41 | 42 | type Props = { 43 | label: string; 44 | help?: string; 45 | onChange: ( event: any ) => void; 46 | values: { 47 | top?: Property.PaddingTop; 48 | right?: Property.PaddingRight; 49 | bottom?: Property.PaddingBottom; 50 | left?: Property.PaddingLeft; 51 | }; 52 | }; 53 | 54 | type ValuesKey = keyof typeof DEFAULT_VALUES; 55 | 56 | export default function PaddingControl( { 57 | label = __( 'Padding', 'flexible-table-block' ), 58 | help, 59 | onChange, 60 | values: valuesProp, 61 | }: Props ) { 62 | const values = { ...DEFAULT_VALUES, ...valuesProp }; 63 | const instanceId = useInstanceId( PaddingControl, 'ftb-padding-control' ); 64 | const headingId = `${ instanceId }-heading`; 65 | 66 | const isMixed = ! ( 67 | values.top === values.right && 68 | values.top === values.bottom && 69 | values.top === values.left 70 | ); 71 | 72 | const paddingUnits = useCustomUnits( { availableUnits: PADDING_UNITS } ); 73 | 74 | const [ isLinked, setIsLinked ] = useState< boolean >( true ); 75 | const [ side, setSide ] = useState< SideValue | undefined >( undefined ); 76 | 77 | const linkedLabel: string = isLinked 78 | ? __( 'Unlink sides', 'flexible-table-block' ) 79 | : __( 'Link sides', 'flexible-table-block' ); 80 | 81 | const allInputPlaceholder: string = isMixed ? __( 'Mixed', 'flexible-table-block' ) : ''; 82 | const allInputValue: string | 0 = isMixed ? '' : values.top; 83 | 84 | const toggleLinked = () => { 85 | setIsLinked( ! isLinked ); 86 | setSide( undefined ); 87 | }; 88 | 89 | const handleOnReset = () => { 90 | setIsLinked( true ); 91 | setSide( undefined ); 92 | onChange( DEFAULT_VALUES ); 93 | }; 94 | 95 | const handleOnFocus = ( focusSide: SideValue ) => setSide( focusSide ); 96 | 97 | const handleOnChangeAll = ( inputValue: string | undefined ) => { 98 | const sanitizedValue = sanitizeUnitValue( inputValue ); 99 | onChange( { 100 | top: sanitizedValue, 101 | right: sanitizedValue, 102 | bottom: sanitizedValue, 103 | left: sanitizedValue, 104 | } ); 105 | }; 106 | 107 | const handleOnChange = ( inputValue: string | undefined, targetSide: SideValue ) => { 108 | onChange( { 109 | ...values, 110 | [ targetSide ]: sanitizeUnitValue( inputValue ), 111 | } ); 112 | }; 113 | 114 | return ( 115 | 116 | 117 | 118 | 119 | { label } 120 | 121 | 122 | 125 | 126 | 127 | 128 | 129 | 130 | { isLinked && ( 131 |
132 | 141 |
142 | ) } 143 |
144 | 69 | ) } 70 | 107 | 108 | 109 | 110 | 118 | 119 | 120 | 131 | 132 | 133 | 138 | 146 | { CAPTION_SIDE_CONTROLS.map( ( { label, value } ) => ( 147 | 148 | ) ) } 149 | 150 | 158 | { TEXT_ALIGNMENT_CONTROLS.map( ( { icon, label, value } ) => ( 159 | 165 | ) ) } 166 | 167 | 168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import type { AnyAction as Action } from 'redux'; 5 | import type { Property } from 'csstype'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import apiFetch from '@wordpress/api-fetch'; 11 | import { createReduxStore, register } from '@wordpress/data'; 12 | import type { NoticeProps } from '@wordpress/components/build-types/notice/types'; 13 | 14 | /** 15 | * Internal dependencies 16 | */ 17 | import { STORE_NAME, REST_API_ROUTE } from './constants'; 18 | 19 | export interface ApiResponse { 20 | status?: NoticeProps[ 'status' ]; 21 | message?: string; 22 | options?: StoreOptions; 23 | // eslint-disable-next-line camelcase 24 | block_css?: string; 25 | } 26 | 27 | export interface StoreOptions { 28 | /* eslint-disable camelcase */ 29 | show_label_on_section: boolean; 30 | show_control_button: boolean; 31 | focus_control_button: boolean; 32 | show_dot_on_th: boolean; 33 | tab_move: boolean; 34 | merge_content: boolean; 35 | show_global_setting: boolean; 36 | breakpoint: number; 37 | block_style: { 38 | table_width?: string; 39 | table_max_width?: string; 40 | row_odd_color?: string; 41 | row_even_color?: string; 42 | table_border_collapse?: string; 43 | cell_text_color_th?: string; 44 | cell_text_color_td?: string; 45 | cell_background_color_th?: string; 46 | cell_background_color_td?: string; 47 | cell_padding?: { 48 | top?: Property.PaddingTop; 49 | right?: Property.PaddingRight; 50 | bottom?: Property.PaddingBottom; 51 | left?: Property.PaddingLeft; 52 | }; 53 | cell_border_width?: string; 54 | cell_border_style?: string; 55 | cell_border_color?: string; 56 | cell_text_align?: string; 57 | cell_vertical_align?: string; 58 | }; 59 | /* eslint-enable camelcase */ 60 | } 61 | 62 | const DEFAULT_STATE = { 63 | options: {}, 64 | }; 65 | 66 | const actions = { 67 | getOptions( path: string ) { 68 | return { 69 | type: 'GET_OPTIONS', 70 | path, 71 | }; 72 | }, 73 | setOptions( options: StoreOptions ) { 74 | return { 75 | type: 'SET_OPTIONS', 76 | options, 77 | }; 78 | }, 79 | }; 80 | 81 | const reducer = ( state = DEFAULT_STATE, action: Action ) => { 82 | switch ( action.type ) { 83 | case 'SET_OPTIONS': { 84 | return { 85 | ...state, 86 | options: action.options, 87 | }; 88 | } 89 | default: { 90 | return state; 91 | } 92 | } 93 | }; 94 | 95 | const selectors = { 96 | getOptions( state: { options: StoreOptions } ) { 97 | const { options } = state; 98 | return options; 99 | }, 100 | }; 101 | 102 | const controls = { 103 | GET_OPTIONS( action: Action ) { 104 | return apiFetch< ApiResponse >( { path: action.path } ); 105 | }, 106 | }; 107 | 108 | const resolvers = { 109 | *getOptions() { 110 | const options: StoreOptions = yield actions.getOptions( REST_API_ROUTE ); 111 | return actions.setOptions( options ); 112 | }, 113 | }; 114 | 115 | const store = createReduxStore( STORE_NAME, { 116 | reducer, 117 | controls, 118 | selectors, 119 | resolvers, 120 | actions, 121 | } ); 122 | 123 | register( store ); 124 | 125 | export { STORE_NAME }; 126 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | .wp-block-flexible-table-block-table.wp-block-flexible-table-block-table { 2 | box-sizing: border-box; 3 | 4 | > table { 5 | 6 | &.has-fixed-layout { 7 | table-layout: fixed; 8 | } 9 | 10 | &.is-sticky-header:not(.is-stacked-on-mobile) thead { 11 | position: sticky; 12 | top: 0; 13 | z-index: 1; 14 | } 15 | 16 | &.is-sticky-first-column tr > *:first-child { 17 | position: sticky; 18 | left: 0; 19 | z-index: 1; 20 | } 21 | 22 | th, 23 | td { 24 | box-sizing: border-box; 25 | min-width: auto; 26 | word-break: normal; 27 | overflow-wrap: anywhere; 28 | 29 | img { 30 | max-width: 100%; 31 | } 32 | } 33 | } 34 | 35 | &.is-content-justification-left, 36 | &.is-content-justification-center, 37 | &.is-content-justification-right { 38 | display: flex; 39 | flex-flow: column; 40 | 41 | figcaption { 42 | align-self: stretch; 43 | } 44 | } 45 | 46 | &.is-content-justification-left { 47 | align-items: flex-start; 48 | } 49 | 50 | &.is-content-justification-center { 51 | align-items: center; 52 | } 53 | 54 | &.is-content-justification-right { 55 | align-items: flex-end; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/transforms.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | import { createBlock, store as blocksStore, type TransformBlock } from '@wordpress/blocks'; 5 | import { select } from '@wordpress/data'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { splitMergedCell, toVirtualRows, toVirtualTable, type VCell } from './utils/table-state'; 11 | import { normalizeRowColSpan } from './utils/helper'; 12 | import type { BlockAttributes, CoreTableCell, CoreTableBlockAttributes } from './BlockAttributes'; 13 | 14 | interface Transforms { 15 | readonly from: ReadonlyArray< TransformBlock< CoreTableBlockAttributes > >; 16 | readonly to: ReadonlyArray< TransformBlock< BlockAttributes > >; 17 | } 18 | 19 | const transforms: Transforms = { 20 | from: [ 21 | { 22 | type: 'block', 23 | blocks: [ 'core/table' ], 24 | transform: ( attributes ) => { 25 | const { hasFixedLayout, head, body, foot, caption, style } = attributes; 26 | 27 | // Mapping rowspan and colspan properties. 28 | const convertedSections = ( section: { cells: CoreTableCell[] }[] ) => { 29 | if ( ! section.length ) { 30 | return section; 31 | } 32 | return section.map( ( row ) => { 33 | if ( ! row.cells.length ) { 34 | return row; 35 | } 36 | return { 37 | cells: row.cells.map( ( cell ) => { 38 | const { content, tag, colspan, rowspan } = cell; 39 | return { 40 | content, 41 | tag, 42 | colSpan: normalizeRowColSpan( colspan ), 43 | rowSpan: normalizeRowColSpan( rowspan ), 44 | }; 45 | } ), 46 | }; 47 | } ); 48 | }; 49 | 50 | return createBlock( 'flexible-table-block/table', { 51 | head: convertedSections( head ), 52 | body: convertedSections( body ), 53 | foot: convertedSections( foot ), 54 | hasFixedLayout, 55 | caption, 56 | style, 57 | } ); 58 | }, 59 | }, 60 | ], 61 | to: [ 62 | { 63 | type: 'block', 64 | blocks: [ 'core/table' ], 65 | transform: ( attributes ) => { 66 | // Check if the core table block supports rowspan and colspan. 67 | const { 68 | // @ts-ignore 69 | getBlockType, 70 | } = select( blocksStore ); 71 | const blockType = getBlockType( 'core/table' ); 72 | const hasRowColSpanSupport = 73 | !! blockType.attributes.head.query.cells.query.rowspan && 74 | !! blockType.attributes.head.query.cells.query.colspan; 75 | 76 | // Create virtual object array with the cells placed in positions based on how they actually look. 77 | let vTable = toVirtualTable( attributes ); 78 | 79 | // Find rowspan & colspan cells. 80 | const vRows = toVirtualRows( vTable ); 81 | const rowColSpanCells = vRows 82 | .reduce( ( cells: VCell[], row ) => cells.concat( row.cells ), [] ) 83 | .filter( ( { rowSpan, colSpan } ) => rowSpan > 1 || colSpan > 1 ); 84 | 85 | // Split the found rowspan and colspan cells If the core table block doesn't support it. 86 | if ( rowColSpanCells.length && ! hasRowColSpanSupport ) { 87 | rowColSpanCells.forEach( ( cell ) => { 88 | vTable = splitMergedCell( vTable, cell ); 89 | } ); 90 | } 91 | 92 | // Convert to core table block attributes. 93 | const sectionAttributes = Object.entries( vTable ).reduce( 94 | ( coreTableAttributes: any, [ sectionName, section ] ) => { 95 | if ( ! section.length ) { 96 | return coreTableAttributes; 97 | } 98 | 99 | const newSection = section.map( ( { cells } ) => ( { 100 | cells: cells 101 | // Delete cells marked as deletion. 102 | .filter( ( cell ) => ! cell.isHidden ) 103 | // Keep only the properties needed. 104 | .map( ( cell ) => ( { 105 | content: cell.content, 106 | tag: 'head' === cell.sectionName ? 'th' : 'td', 107 | rowspan: hasRowColSpanSupport ? normalizeRowColSpan( cell.rowSpan ) : undefined, 108 | colspan: hasRowColSpanSupport ? normalizeRowColSpan( cell.colSpan ) : undefined, 109 | } ) ), 110 | } ) ); 111 | coreTableAttributes[ sectionName ] = newSection; 112 | return coreTableAttributes; 113 | }, 114 | {} 115 | ); 116 | 117 | return createBlock( 'core/table', { 118 | ...sectionAttributes, 119 | hasFixedLayout: attributes.hasFixedLayout, 120 | caption: attributes.caption, 121 | style: attributes.style, 122 | } ); 123 | }, 124 | }, 125 | ], 126 | }; 127 | 128 | export default transforms; 129 | -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import type { PropertyValue } from 'csstype'; 5 | 6 | const DEFAULT_PRECISION: number = 4; 7 | 8 | // Array with four values for CSS 9 | export type FourCssValues = [ string, string, string, string ]; 10 | 11 | // sanitizeUnitValue function option 12 | interface SanitizeOptions { 13 | minNum?: number; 14 | maxNum?: number; 15 | precision?: number; 16 | } 17 | 18 | /** 19 | * Removed falsy values from nested object. 20 | * 21 | * @param object Nested object. 22 | * @return Object cleaned from falsy values. 23 | */ 24 | export function cleanEmptyObject( object: {} ): {} | undefined { 25 | if ( object === null || typeof object !== 'object' || Array.isArray( object ) ) { 26 | return object; 27 | } 28 | 29 | const cleanedNestedObjects = Object.entries( object ) 30 | .map( ( [ key, value ] ) => [ key, cleanEmptyObject( value ) ] ) 31 | .filter( ( [ , value ] ) => value !== undefined ); 32 | return ! cleanedNestedObjects.length ? undefined : Object.fromEntries( cleanedNestedObjects ); 33 | } 34 | 35 | /** 36 | * Convert short-hand/long-hand CSS values into an array with four values. 37 | * 38 | * @param cssValue CSS value. 39 | * @return Array with four values. 40 | */ 41 | export function parseCssValue( cssValue: string ): FourCssValues { 42 | const cssValues: string[] = cssValue.split( ' ' ).map( ( value: string ) => value.toLowerCase() ); 43 | 44 | switch ( cssValues.length ) { 45 | case 1: 46 | return [ cssValues[ 0 ], cssValues[ 0 ], cssValues[ 0 ], cssValues[ 0 ] ]; 47 | case 2: 48 | return [ cssValues[ 0 ], cssValues[ 1 ], cssValues[ 0 ], cssValues[ 1 ] ]; 49 | case 3: 50 | return [ cssValues[ 0 ], cssValues[ 1 ], cssValues[ 2 ], cssValues[ 1 ] ]; 51 | case 4: 52 | return [ cssValues[ 0 ], cssValues[ 1 ], cssValues[ 2 ], cssValues[ 3 ] ]; 53 | default: 54 | return [ '', '', '', '' ]; 55 | } 56 | } 57 | 58 | /** 59 | * Sanitize the value of UnitControl. 60 | * 61 | * @param initialValue UnitControl value. 62 | * @param options Sanitize options. 63 | * @param options.minNum Minimum number. 64 | * @param options.maxNum Minimum number. 65 | * @param options.precision Precision. 66 | * @return Sanitized UnitControl value. 67 | */ 68 | export function sanitizeUnitValue( 69 | initialValue: PropertyValue< string | number > | undefined, 70 | options?: SanitizeOptions 71 | ): string { 72 | const value: string = String( initialValue ).trim(); 73 | let num: number = parseFloat( value ); 74 | 75 | if ( isNaN( num ) ) { 76 | return ''; 77 | } else if ( num < 0 ) { 78 | return ''; 79 | } else if ( num === 0 ) { 80 | return '0'; 81 | } 82 | 83 | // Sanitize value. 84 | if ( options?.minNum ) { 85 | num = Math.max( options.minNum, num ); 86 | } 87 | 88 | if ( options?.maxNum ) { 89 | num = Math.min( options.maxNum, num ); 90 | } 91 | 92 | const modifier = 10 ** ( options?.precision || DEFAULT_PRECISION ); 93 | num = Math.round( num * modifier ) / modifier; 94 | 95 | const unit: string = value.match( /[\d.\-+]*\s*(.*)/ )?.[ 1 ] ?? ''; 96 | 97 | return `${ num }${ unit.toLowerCase() }`; 98 | } 99 | 100 | /** 101 | * Parses a number and unit from a value. 102 | * 103 | * @param initialValue Value to parse 104 | * @return The extracted number and unit. 105 | */ 106 | export function parseUnit( initialValue: string ): [ number, string ] { 107 | const value: string = String( initialValue ).trim(); 108 | const num: number = parseFloat( value ); 109 | 110 | if ( isNaN( num ) ) { 111 | return [ 0, '' ]; 112 | } 113 | 114 | const unit: string = value.match( /[\d.\-+]*\s*(.*)/ )?.[ 1 ] ?? ''; 115 | 116 | return [ num, unit.toLowerCase() ]; 117 | } 118 | 119 | // Convert string to number. 120 | // JSDoc is not used because parsing in eslint fails 121 | export function toInteger( value: number | string | undefined, defaultValue = 0 ): number { 122 | if ( ! value ) { 123 | return defaultValue; 124 | } 125 | 126 | const converted = parseInt( String( value ), 10 ); 127 | 128 | if ( isNaN( converted ) ) { 129 | return defaultValue; 130 | } 131 | 132 | return converted || defaultValue; 133 | } 134 | 135 | /** 136 | * Normalize the rowspan/colspan value. 137 | * Returns undefined if the parameter is not a positive number 138 | * or the default value (1) for rowspan/colspan. 139 | * 140 | * @param rowColSpan rowspan/colspan value. 141 | * @return normalized rowspan/colspan value. 142 | */ 143 | export function normalizeRowColSpan( rowColSpan: any ) { 144 | const parsedValue = parseInt( rowColSpan, 10 ); 145 | if ( ! Number.isInteger( parsedValue ) ) { 146 | return undefined; 147 | } 148 | return parsedValue < 0 || parsedValue === 1 ? undefined : parsedValue.toString(); 149 | } 150 | -------------------------------------------------------------------------------- /src/utils/style-converter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import type { Properties } from 'csstype'; 5 | 6 | /** 7 | * Convert inline CSS styles to object. 8 | * 9 | * @param inlineStyles Inline CSS styles. 10 | * @return CSS styles object. 11 | */ 12 | export function convertToObject( inlineStyles: string | undefined ): Properties { 13 | if ( ! inlineStyles ) { 14 | return {}; 15 | } 16 | 17 | return inlineStyles 18 | .split( ';' ) 19 | .filter( ( style ) => style.split( ':' )[ 0 ] && style.split( ':' )[ 1 ] ) 20 | .map( ( style ) => [ 21 | style 22 | .split( ':' )[ 0 ] 23 | .trim() 24 | .replace( /-./g, ( c ) => c.substr( 1 ).toUpperCase() ), 25 | style.split( ':' )[ 1 ].trim(), 26 | ] ) 27 | .reduce( 28 | ( styleObj, style ) => ( { 29 | ...styleObj, 30 | [ style[ 0 ] ]: style[ 1 ], 31 | } ), 32 | {} 33 | ); 34 | } 35 | 36 | /** 37 | * Convert CSS styles object to Inline CSS styles. 38 | * 39 | * @param stylesObj CSS styles object. 40 | * @return Inline CSS styles 41 | */ 42 | export function convertToInline( stylesObj: Properties ): string { 43 | const lines: string[] = Object.keys( stylesObj ).reduce< string[] >( 44 | ( result: string[], key: string ) => { 45 | const property = key.replace( /([a-z])([A-Z])/g, '$1-$2' ).toLowerCase(); 46 | const value = stylesObj[ key as keyof Properties ]; 47 | 48 | if ( value !== undefined && ( typeof value === 'string' || value === 0 ) ) { 49 | result.push( `${ property }:${ value };` ); 50 | } 51 | return result; 52 | }, 53 | [] as string[] 54 | ); 55 | 56 | return lines.join( '' ); 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/style-picker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import type { Properties } from 'csstype'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import { parseCssValue, type FourCssValues } from './helper'; 10 | 11 | export interface DirectionProps { 12 | top: string; 13 | right: string; 14 | bottom: string; 15 | left: string; 16 | } 17 | 18 | export interface CornerProps { 19 | topLeft: string; 20 | topRight: string; 21 | bottomRight: string; 22 | bottomLeft: string; 23 | } 24 | 25 | export interface CrossProps { 26 | horizontal: string; 27 | vertical: string; 28 | } 29 | 30 | /** 31 | * Pick padding style as object from style object. 32 | * 33 | * @param stylesObj styles object. 34 | * @return padding styles object. 35 | */ 36 | export function pickPadding( stylesObj: Properties ): DirectionProps { 37 | if ( stylesObj.padding ) { 38 | const paddingValues: FourCssValues = parseCssValue( stylesObj.padding ); 39 | 40 | return { 41 | top: paddingValues[ 0 ], 42 | right: paddingValues[ 1 ], 43 | bottom: paddingValues[ 2 ], 44 | left: paddingValues[ 3 ], 45 | }; 46 | } 47 | 48 | return { 49 | top: stylesObj?.paddingTop || '', 50 | right: stylesObj?.paddingRight || '', 51 | bottom: stylesObj?.paddingBottom || '', 52 | left: stylesObj?.paddingLeft || '', 53 | }; 54 | } 55 | 56 | /** 57 | * Pick border-width style as object from style object. 58 | * 59 | * @param stylesObj styles object. 60 | * @return border-width styles object. 61 | */ 62 | export function pickBorderWidth( stylesObj: Properties ): DirectionProps { 63 | if ( stylesObj.borderWidth ) { 64 | const borderWidthValues: FourCssValues = parseCssValue( stylesObj.borderWidth ); 65 | 66 | return { 67 | top: borderWidthValues[ 0 ], 68 | right: borderWidthValues[ 1 ], 69 | bottom: borderWidthValues[ 2 ], 70 | left: borderWidthValues[ 3 ], 71 | }; 72 | } 73 | 74 | return { 75 | top: stylesObj?.borderTopWidth || '', 76 | right: stylesObj?.borderRightWidth || '', 77 | bottom: stylesObj?.borderBottomWidth || '', 78 | left: stylesObj?.borderLeftWidth || '', 79 | }; 80 | } 81 | 82 | /** 83 | * Pick border-color style as object from style object. 84 | * 85 | * @param stylesObj styles object. 86 | * @return border-color styles object. 87 | */ 88 | export function pickBorderColor( stylesObj: Properties ): DirectionProps { 89 | if ( stylesObj.borderColor ) { 90 | const borderColorValues: FourCssValues = parseCssValue( stylesObj.borderColor ); 91 | 92 | return { 93 | top: borderColorValues[ 0 ], 94 | right: borderColorValues[ 1 ], 95 | bottom: borderColorValues[ 2 ], 96 | left: borderColorValues[ 3 ], 97 | }; 98 | } 99 | 100 | return { 101 | top: stylesObj?.borderTopColor || '', 102 | right: stylesObj?.borderRightColor || '', 103 | bottom: stylesObj?.borderBottomColor || '', 104 | left: stylesObj?.borderLeftColor || '', 105 | }; 106 | } 107 | 108 | /** 109 | * Pick border-style style as object from style object. 110 | * 111 | * @param stylesObj styles object. 112 | * @return border-style styles object. 113 | */ 114 | export function pickBorderStyle( stylesObj: Properties ): DirectionProps { 115 | if ( stylesObj.borderStyle ) { 116 | const borderStyleValues: FourCssValues = parseCssValue( stylesObj.borderStyle ); 117 | 118 | return { 119 | top: borderStyleValues[ 0 ], 120 | right: borderStyleValues[ 1 ], 121 | bottom: borderStyleValues[ 2 ], 122 | left: borderStyleValues[ 3 ], 123 | }; 124 | } 125 | 126 | return { 127 | top: stylesObj?.borderTopStyle || '', 128 | right: stylesObj?.borderRightStyle || '', 129 | bottom: stylesObj?.borderBottomStyle || '', 130 | left: stylesObj?.borderLeftStyle || '', 131 | }; 132 | } 133 | 134 | /** 135 | * Pick border-radius style as object from style object. 136 | * 137 | * @param stylesObj styles object. 138 | * @param stylesObj.borderRadius 139 | * @param stylesObj.borderTopLeftRadius 140 | * @param stylesObj.borderTopRightRadius 141 | * @param stylesObj.borderBottomRightRadius 142 | * @param stylesObj.borderBottomLeftRadius 143 | * @return border-radius styles object. 144 | */ 145 | export function pickBorderRadius( stylesObj: Properties ): CornerProps { 146 | if ( stylesObj.borderRadius ) { 147 | const borderRadiusValues: FourCssValues = parseCssValue( stylesObj.borderRadius ); 148 | return { 149 | topLeft: borderRadiusValues[ 0 ], 150 | topRight: borderRadiusValues[ 1 ], 151 | bottomRight: borderRadiusValues[ 2 ], 152 | bottomLeft: borderRadiusValues[ 3 ], 153 | }; 154 | } 155 | 156 | return { 157 | topLeft: stylesObj?.borderTopLeftRadius || '', 158 | topRight: stylesObj?.borderTopRightRadius || '', 159 | bottomRight: stylesObj?.borderBottomRightRadius || '', 160 | bottomLeft: stylesObj?.borderBottomLeftRadius || '', 161 | }; 162 | } 163 | 164 | /** 165 | * Pick border-spacing style as object from style object. 166 | * 167 | * @param stylesObj styles object. 168 | * @return border-spacing styles object. 169 | */ 170 | export function pickBorderSpacing( stylesObj: Properties ): CrossProps { 171 | const borderSpacingValues: FourCssValues = parseCssValue( stylesObj.borderSpacing || '' ); 172 | 173 | return { 174 | horizontal: borderSpacingValues[ 0 ], 175 | vertical: borderSpacingValues[ 1 ], 176 | }; 177 | } 178 | -------------------------------------------------------------------------------- /src/utils/test/helper.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { parseCssValue, parseUnit, sanitizeUnitValue, toInteger } from '../helper'; 5 | 6 | describe( 'helper', () => { 7 | describe( 'parseCssValue', () => { 8 | it( 'should return array with four values', () => { 9 | expect( parseCssValue( '' ) ).toStrictEqual( [ '', '', '', '' ] ); 10 | expect( parseCssValue( '10px' ) ).toStrictEqual( [ '10px', '10px', '10px', '10px' ] ); 11 | expect( parseCssValue( '10px 20em' ) ).toStrictEqual( [ '10px', '20em', '10px', '20em' ] ); 12 | expect( parseCssValue( '10px 20em 30.11%' ) ).toStrictEqual( [ 13 | '10px', 14 | '20em', 15 | '30.11%', 16 | '20em', 17 | ] ); 18 | expect( parseCssValue( '10px 20em 30.11% 40CM' ) ).toStrictEqual( [ 19 | '10px', 20 | '20em', 21 | '30.11%', 22 | '40cm', 23 | ] ); 24 | } ); 25 | } ); 26 | 27 | describe( 'sanitizeUnitValue', () => { 28 | it( 'should not change the correct value', () => { 29 | expect( sanitizeUnitValue( '10px' ) ).toBe( '10px' ); 30 | expect( sanitizeUnitValue( '10%' ) ).toBe( '10%' ); 31 | } ); 32 | 33 | it( 'should remove the unit for zero', () => { 34 | expect( sanitizeUnitValue( '0px' ) ).toBe( '0' ); 35 | expect( sanitizeUnitValue( '0%' ) ).toBe( '0' ); 36 | } ); 37 | 38 | it( 'should return lowercase unit', () => { 39 | expect( sanitizeUnitValue( '10PX' ) ).toBe( '10px' ); 40 | expect( sanitizeUnitValue( '10CM' ) ).toBe( '10cm' ); 41 | } ); 42 | 43 | it( 'should return empty string if it is not a number or minus value', () => { 44 | expect( sanitizeUnitValue( '' ) ).toBe( '' ); 45 | expect( sanitizeUnitValue( '-10rem' ) ).toBe( '' ); 46 | expect( sanitizeUnitValue( 'red' ) ).toBe( '' ); 47 | } ); 48 | 49 | it( 'should return minNum option value if minNum option is set', () => { 50 | expect( sanitizeUnitValue( '10px', { minNum: 40 } ) ).toBe( '40px' ); 51 | expect( sanitizeUnitValue( '20.11%', { minNum: 40 } ) ).toBe( '40%' ); 52 | } ); 53 | 54 | it( 'should return maxNum option value if maxNum option is set', () => { 55 | expect( sanitizeUnitValue( '40em', { maxNum: 10 } ) ).toBe( '10em' ); 56 | expect( sanitizeUnitValue( '20.11px', { maxNum: 10 } ) ).toBe( '10px' ); 57 | } ); 58 | 59 | it( 'should return truncated value', () => { 60 | expect( sanitizeUnitValue( '10.11111111px' ) ).toBe( '10.1111px' ); 61 | } ); 62 | 63 | it( 'should return truncated value if precision option is set', () => { 64 | expect( sanitizeUnitValue( '10.11111111em', { precision: 6 } ) ).toBe( '10.111111em' ); 65 | } ); 66 | 67 | it( 'should return sanitized value if multiple option is set', () => { 68 | expect( sanitizeUnitValue( '10.11111111CM', { maxNum: 10 } ) ).toBe( '10cm' ); 69 | } ); 70 | } ); 71 | 72 | describe( 'parseUnit', () => { 73 | it( 'should return taple', () => { 74 | expect( parseUnit( '10px' ) ).toStrictEqual( [ 10, 'px' ] ); 75 | expect( parseUnit( '10%' ) ).toStrictEqual( [ 10, '%' ] ); 76 | } ); 77 | 78 | it( 'should return lowercase unit', () => { 79 | expect( parseUnit( '10PX' ) ).toStrictEqual( [ 10, 'px' ] ); 80 | expect( parseUnit( '10CM' ) ).toStrictEqual( [ 10, 'cm' ] ); 81 | } ); 82 | 83 | it( 'should return zero if it is not a number.', () => { 84 | expect( parseUnit( 'red' ) ).toStrictEqual( [ 0, '' ] ); 85 | } ); 86 | } ); 87 | 88 | describe( 'toInteger', () => { 89 | it( 'should return numbers as numbers', () => { 90 | expect( toInteger( 10 ) ).toStrictEqual( 10 ); 91 | expect( toInteger( 2.71828 ) ).toStrictEqual( 2 ); 92 | } ); 93 | 94 | it( 'should convert string to number', () => { 95 | expect( toInteger( '20' ) ).toStrictEqual( 20 ); 96 | expect( toInteger( '3.1415' ) ).toStrictEqual( 3 ); 97 | } ); 98 | 99 | it( 'should return the default value, if falsy is passed.', () => { 100 | expect( toInteger( '', 1 ) ).toStrictEqual( 1 ); 101 | expect( toInteger( undefined, 5 ) ).toStrictEqual( 5 ); 102 | expect( toInteger( 0, 5 ) ).toStrictEqual( 5 ); 103 | } ); 104 | } ); 105 | } ); 106 | -------------------------------------------------------------------------------- /src/utils/test/style-converter.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { convertToInline, convertToObject } from '../style-converter'; 5 | 6 | describe( 'style-converter', () => { 7 | describe( 'convertToObject', () => { 8 | it( 'should convert inline style to object', () => { 9 | expect( 10 | convertToObject( ` 11 | background: red; 12 | border-radius: 10px; 13 | border-right: 1px solid blue; 14 | ` ) 15 | ).toStrictEqual( { 16 | background: 'red', 17 | borderRadius: '10px', 18 | borderRight: '1px solid blue', 19 | } ); 20 | } ); 21 | } ); 22 | 23 | describe( 'convertToInline', () => { 24 | it( 'should convert object to inline', () => { 25 | expect( 26 | convertToInline( { 27 | background: 'red', 28 | borderRadius: '10px', 29 | borderRight: '1px solid blue', 30 | } ) 31 | ).toBe( 'background:red;border-radius:10px;border-right:1px solid blue;' ); 32 | } ); 33 | } ); 34 | } ); 35 | -------------------------------------------------------------------------------- /src/utils/test/style-picker.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { 5 | pickBorderColor, 6 | pickBorderRadius, 7 | pickBorderSpacing, 8 | pickBorderWidth, 9 | pickPadding, 10 | } from '../style-picker'; 11 | 12 | describe( 'style-picker', () => { 13 | describe( 'pickPadding', () => { 14 | it( 'should separate shorthand prop', () => { 15 | expect( pickPadding( { padding: '10px' } ) ).toStrictEqual( { 16 | top: '10px', 17 | right: '10px', 18 | bottom: '10px', 19 | left: '10px', 20 | } ); 21 | } ); 22 | 23 | it( 'should separate vertical and horizontal', () => { 24 | expect( pickPadding( { padding: '30px 10px' } ) ).toStrictEqual( { 25 | top: '30px', 26 | right: '10px', 27 | bottom: '30px', 28 | left: '10px', 29 | } ); 30 | } ); 31 | 32 | it( 'should split vertical and horizontal and side', () => { 33 | expect( pickPadding( { padding: '30px 10px 20px' } ) ).toStrictEqual( { 34 | top: '30px', 35 | right: '10px', 36 | bottom: '20px', 37 | left: '10px', 38 | } ); 39 | } ); 40 | 41 | it( 'should split four direction', () => { 42 | expect( pickPadding( { padding: '30px 10px 20px 40px' } ) ).toStrictEqual( { 43 | top: '30px', 44 | right: '10px', 45 | bottom: '20px', 46 | left: '40px', 47 | } ); 48 | } ); 49 | } ); 50 | 51 | describe( 'pickBorderWidth', () => { 52 | it( 'should separate shorthand prop', () => { 53 | expect( pickBorderWidth( { borderWidth: '10px' } ) ).toStrictEqual( { 54 | bottom: '10px', 55 | left: '10px', 56 | right: '10px', 57 | top: '10px', 58 | } ); 59 | } ); 60 | 61 | it( 'should separate vertical and horizontal', () => { 62 | expect( pickBorderWidth( { borderWidth: '30px 10px' } ) ).toStrictEqual( { 63 | top: '30px', 64 | right: '10px', 65 | bottom: '30px', 66 | left: '10px', 67 | } ); 68 | } ); 69 | 70 | it( 'should split vertical and horizontal and side', () => { 71 | expect( pickBorderWidth( { borderWidth: '30px 10px 20px' } ) ).toStrictEqual( { 72 | top: '30px', 73 | right: '10px', 74 | bottom: '20px', 75 | left: '10px', 76 | } ); 77 | } ); 78 | 79 | it( 'should split four direction', () => { 80 | expect( pickBorderWidth( { borderWidth: '30px 10px 20px 40px' } ) ).toStrictEqual( { 81 | top: '30px', 82 | right: '10px', 83 | bottom: '20px', 84 | left: '40px', 85 | } ); 86 | } ); 87 | } ); 88 | 89 | describe( 'pickBorderColor', () => { 90 | it( 'should separate shorthand prop', () => { 91 | expect( pickBorderColor( { borderColor: 'red' } ) ).toStrictEqual( { 92 | top: 'red', 93 | right: 'red', 94 | bottom: 'red', 95 | left: 'red', 96 | } ); 97 | } ); 98 | 99 | it( 'should separate vertical and horizontal', () => { 100 | expect( pickBorderColor( { borderColor: 'red #f015ca' } ) ).toStrictEqual( { 101 | top: 'red', 102 | right: '#f015ca', 103 | bottom: 'red', 104 | left: '#f015ca', 105 | } ); 106 | } ); 107 | 108 | it( 'should split vertical and horizontal and side', () => { 109 | expect( 110 | pickBorderColor( { 111 | borderColor: 'red rgb(240,30,50,.7) green', 112 | } ) 113 | ).toStrictEqual( { 114 | top: 'red', 115 | right: 'rgb(240,30,50,.7)', 116 | bottom: 'green', 117 | left: 'rgb(240,30,50,.7)', 118 | } ); 119 | } ); 120 | 121 | it( 'should split four direction', () => { 122 | expect( pickBorderColor( { borderColor: 'red yellow green blue' } ) ).toStrictEqual( { 123 | top: 'red', 124 | right: 'yellow', 125 | bottom: 'green', 126 | left: 'blue', 127 | } ); 128 | } ); 129 | } ); 130 | 131 | describe( 'pickBorderRadius', () => { 132 | it( 'should separate shorthand prop', () => { 133 | expect( pickBorderRadius( { borderRadius: '10px' } ) ).toStrictEqual( { 134 | topLeft: '10px', 135 | topRight: '10px', 136 | bottomRight: '10px', 137 | bottomLeft: '10px', 138 | } ); 139 | } ); 140 | 141 | it( 'should separate two value', () => { 142 | expect( pickBorderRadius( { borderRadius: '10px 5%' } ) ).toStrictEqual( { 143 | topLeft: '10px', 144 | topRight: '5%', 145 | bottomRight: '10px', 146 | bottomLeft: '5%', 147 | } ); 148 | } ); 149 | 150 | it( 'should split three value', () => { 151 | expect( pickBorderRadius( { borderRadius: '2px 4px 2px' } ) ).toStrictEqual( { 152 | topLeft: '2px', 153 | topRight: '4px', 154 | bottomRight: '2px', 155 | bottomLeft: '4px', 156 | } ); 157 | } ); 158 | 159 | it( 'should split four value', () => { 160 | expect( pickBorderRadius( { borderRadius: '1px 0 3px 4px' } ) ).toStrictEqual( { 161 | topLeft: '1px', 162 | topRight: '0', 163 | bottomRight: '3px', 164 | bottomLeft: '4px', 165 | } ); 166 | } ); 167 | } ); 168 | 169 | describe( 'pickBorderSpacing', () => { 170 | it( 'should split value', () => { 171 | expect( pickBorderSpacing( { borderSpacing: '2px' } ) ).toStrictEqual( { 172 | vertical: '2px', 173 | horizontal: '2px', 174 | } ); 175 | } ); 176 | 177 | it( 'should parsed correctly', () => { 178 | expect( pickBorderSpacing( { borderSpacing: '1rem 2em' } ) ).toStrictEqual( { 179 | vertical: '2em', 180 | horizontal: '1rem', 181 | } ); 182 | } ); 183 | } ); 184 | } ); 185 | -------------------------------------------------------------------------------- /src/utils/test/table-state.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { 5 | createTable, 6 | deleteColumn, 7 | deleteRow, 8 | insertRow, 9 | type VTable, 10 | type VRow, 11 | type VCell, 12 | } from '../table-state'; 13 | import type { SectionName } from '../../BlockAttributes'; 14 | 15 | const getRow = ( 16 | cells: number, 17 | sectionName: SectionName, 18 | rowIndex: number, 19 | tag: 'th' | 'td', 20 | content = '', 21 | options = {} 22 | ): VRow => { 23 | return { 24 | cells: Array.from( { length: cells } ).map( ( _, vColIndex ): VCell => { 25 | return { 26 | content, 27 | tag, 28 | rowIndex, 29 | vColIndex, 30 | sectionName, 31 | rowSpan: 1, 32 | colSpan: 1, 33 | isHidden: false, 34 | isFirstSelected: false, 35 | ...options, 36 | }; 37 | } ), 38 | }; 39 | }; 40 | 41 | const table: VTable = { 42 | head: [ getRow( 3, 'head', 0, 'th', 'head' ) ], 43 | body: [ 44 | getRow( 3, 'body', 0, 'td', 'body-0' ), 45 | getRow( 3, 'body', 1, 'td', 'body-1' ), 46 | getRow( 3, 'body', 2, 'td', 'body-2' ), 47 | ], 48 | foot: [ getRow( 3, 'foot', 0, 'td', 'foot' ) ], 49 | }; 50 | 51 | describe( 'table-state', () => { 52 | describe( 'createTable', () => { 53 | it( 'should create the right virtual table', () => { 54 | expect( 55 | createTable( { rowCount: 3, colCount: 3, headerSection: false, footerSection: false } ) 56 | ).toStrictEqual( { 57 | head: [], 58 | body: [ 59 | getRow( 3, 'body', 0, 'td' ), 60 | getRow( 3, 'body', 1, 'td' ), 61 | getRow( 3, 'body', 2, 'td' ), 62 | ], 63 | foot: [], 64 | } ); 65 | } ); 66 | 67 | it( 'should create virtual table with head and foot', () => { 68 | expect( 69 | createTable( { rowCount: 3, colCount: 3, headerSection: true, footerSection: true } ) 70 | ).toStrictEqual( { 71 | head: [ getRow( 3, 'head', 0, 'th' ) ], 72 | body: [ 73 | getRow( 3, 'body', 0, 'td' ), 74 | getRow( 3, 'body', 1, 'td' ), 75 | getRow( 3, 'body', 2, 'td' ), 76 | ], 77 | foot: [ getRow( 3, 'foot', 0, 'td' ) ], 78 | } ); 79 | } ); 80 | } ); 81 | 82 | describe( 'insertRow', () => { 83 | it( 'should return the table with the correct number of rows', () => { 84 | expect( insertRow( { ...table }, { sectionName: 'body', rowIndex: 1 } ) ).toStrictEqual( { 85 | head: [ getRow( 3, 'head', 0, 'th', 'head' ) ], 86 | body: [ 87 | getRow( 3, 'body', 0, 'td', 'body-0' ), 88 | getRow( 3, 'body', 1, 'td', '' ), 89 | getRow( 3, 'body', 2, 'td', 'body-1' ), 90 | getRow( 3, 'body', 3, 'td', 'body-2' ), 91 | ], 92 | foot: [ getRow( 3, 'foot', 0, 'td', 'foot' ) ], 93 | } ); 94 | expect( insertRow( { ...table }, { sectionName: 'body', rowIndex: 3 } ) ).toStrictEqual( { 95 | head: [ getRow( 3, 'head', 0, 'th', 'head' ) ], 96 | body: [ 97 | getRow( 3, 'body', 0, 'td', 'body-0' ), 98 | getRow( 3, 'body', 1, 'td', 'body-1' ), 99 | getRow( 3, 'body', 2, 'td', 'body-2' ), 100 | getRow( 3, 'body', 3, 'td', '' ), 101 | ], 102 | foot: [ getRow( 3, 'foot', 0, 'td', 'foot' ) ], 103 | } ); 104 | } ); 105 | } ); 106 | 107 | describe( 'deleteRow', () => { 108 | it( 'should return the table with the correct number of rows', () => { 109 | expect( deleteRow( { ...table }, { sectionName: 'body', rowIndex: 0 } ) ).toStrictEqual( { 110 | head: [ getRow( 3, 'head', 0, 'th', 'head' ) ], 111 | body: [ getRow( 3, 'body', 0, 'td', 'body-1' ), getRow( 3, 'body', 1, 'td', 'body-2' ) ], 112 | foot: [ getRow( 3, 'foot', 0, 'td', 'foot' ) ], 113 | } ); 114 | expect( deleteRow( { ...table }, { sectionName: 'body', rowIndex: 1 } ) ).toStrictEqual( { 115 | head: [ getRow( 3, 'head', 0, 'th', 'head' ) ], 116 | body: [ getRow( 3, 'body', 0, 'td', 'body-0' ), getRow( 3, 'body', 1, 'td', 'body-2' ) ], 117 | foot: [ getRow( 3, 'foot', 0, 'td', 'foot' ) ], 118 | } ); 119 | } ); 120 | } ); 121 | 122 | describe( 'deleteColumn', () => { 123 | it( 'should return the table with the correct number of columns', () => { 124 | expect( deleteColumn( { ...table }, { vColIndex: 0 } ) ).toStrictEqual( { 125 | head: [ getRow( 2, 'head', 0, 'th', 'head' ) ], 126 | body: [ 127 | getRow( 2, 'body', 0, 'td', 'body-0' ), 128 | getRow( 2, 'body', 1, 'td', 'body-1' ), 129 | getRow( 2, 'body', 2, 'td', 'body-2' ), 130 | ], 131 | foot: [ getRow( 2, 'foot', 0, 'td', 'foot' ) ], 132 | } ); 133 | expect( deleteColumn( { ...table }, { vColIndex: 2 } ) ).toStrictEqual( { 134 | head: [ getRow( 2, 'head', 0, 'th', 'head' ) ], 135 | body: [ 136 | getRow( 2, 'body', 0, 'td', 'body-0' ), 137 | getRow( 2, 'body', 1, 'td', 'body-1' ), 138 | getRow( 2, 'body', 2, 'td', 'body-2' ), 139 | ], 140 | foot: [ getRow( 2, 'foot', 0, 'td', 'foot' ) ], 141 | } ); 142 | } ); 143 | } ); 144 | } ); 145 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Block-Support-dimensions-settings-should-be-applied-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Block-Support-typography-settings-should-be-applied-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-all-cells-in-a-vertical-line-to-be-merge-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-all-cells-side-by-side-to-be-merge-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-cells-in-a-vertical-line-to-be-merge-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-cells-side-by-side-to-be-merge-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-cells-to-be-merge-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-merged-cells-to-be-split-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-to-delete-column-even-if-they-contain-merged-cells-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-allows-to-delete-rows-even-if-they-contain-merged-cells-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-cell-allows-cell-movement-with-tab-key-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Cell 1Cell 2
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-cell-allows-keyboard-operation-within-the-link-popover-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Link
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-cell-allows-keyboard-operation-within-the-link-popover-2-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Link
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-should-be-inserted-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Flexible Table Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-should-create-block-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Flexible-table-should-create-block-with-option-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Styles-caption-styles-should-be-applied-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Flexible Table Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Styles-cell-styles-should-be-applied-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Styles-cell-styles-should-be-applied-to-multiple-cells-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Styles-table-styles-should-be-applied-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-core-table-block-should-be-tran-aff73--block-keeping-Fixed-width-table-cells-option-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-core-table-block-should-be-tran-de743-table-block-keeping-header-and-footer-section-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-core-table-block-should-be-tran-e2ee4--block-with-no-Fixed-width-table-cells-option-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Core Table Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--0572b-le-block-with-no-unnecessary-attributes-cells-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--0f411-core-table-block-with-rowspan-colspan-cells-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Cell 1
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--122cc-o-core-table-block-with-appropriate-tag-cells-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--2c3de--block-with-no-Fixed-width-table-cells-option-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Flexible Table Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--43933--core-table-block-with-no-option-caption-text-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Flexible Table Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--82299-ore-table-block-with-no-style-and-class-cells-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Flexible Table Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--86681--block-keeping-Fixed-width-table-cells-option-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Flexible Table Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be--8ea6a-ore-table-block-with-no-style-and-class-table-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
3 | -------------------------------------------------------------------------------- /test/e2e/specs/__snapshots__/Transform-from-flexible-table-block-should-be-transformed-to-core-table-block-keeping-caption-text-1-chromium.txt: -------------------------------------------------------------------------------- 1 | 2 |
Flexible
Table
Block
3 | -------------------------------------------------------------------------------- /test/e2e/specs/block-support.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import FlexibleTableBlockUtils from '../util'; 10 | 11 | test.use( { 12 | fsbUtils: async ( { page, editor }, use ) => { 13 | await use( new FlexibleTableBlockUtils( { page, editor } ) ); 14 | }, 15 | } ); 16 | 17 | test.describe( 'Block Support', () => { 18 | test.beforeAll( async ( { requestUtils } ) => { 19 | await requestUtils.activateTheme( 'twentytwentyfour' ); 20 | } ); 21 | 22 | test.beforeEach( async ( { admin } ) => { 23 | await admin.createNewPost(); 24 | } ); 25 | 26 | test( 'typography settings should be applied', async ( { 27 | editor, 28 | page, 29 | pageUtils, 30 | fsbUtils, 31 | } ) => { 32 | const wpVersion = await fsbUtils.getWpVersion(); 33 | await fsbUtils.createFlexibleTableBlock(); 34 | // Open the sidebar. 35 | await editor.openDocumentSettingsSidebar(); 36 | await page 37 | .getByRole( 'region', { name: 'Editor settings' } ) 38 | .getByRole( 'tab', { name: 'Styles' } ) 39 | .click(); 40 | // Show all typography controls. 41 | await page.getByRole( 'button', { name: 'Typography options' } ).click(); 42 | for ( let i = 0; i < 6; i++ ) { 43 | await page 44 | .getByRole( 'menu', { name: 'Typography options' } ) 45 | .getByRole( 'menuitemcheckbox' ) 46 | .nth( i ) 47 | .click(); 48 | } 49 | await page.getByRole( 'button', { name: 'Typography options' } ).click(); 50 | // Change font family. 51 | if ( [ '6-6', '6-7' ].includes( wpVersion ) ) { 52 | await page.getByRole( 'combobox', { name: 'Font' } ).selectOption( 'System Sans-serif' ); 53 | } else { 54 | await page.getByRole( 'combobox', { name: 'Font' } ).click(); 55 | await page.getByRole( 'option', { name: 'System Sans-serif' } ).click(); 56 | } 57 | // Change font size. 58 | await page 59 | .getByRole( 'radiogroup', { name: 'Font size' } ) 60 | .getByRole( 'radio', { name: 'Large', exact: true } ) 61 | .click(); 62 | 63 | // Change font appearance. 64 | await page 65 | .getByRole( wpVersion === '6-6' ? 'button' : 'combobox', { 66 | name: 'Appearance', 67 | } ) 68 | .click(); 69 | await page.getByRole( 'listbox', { name: 'Appearance' } ); 70 | await pageUtils.pressKeys( 'ArrowDown', { times: 5 } ); 71 | await pageUtils.pressKeys( 'Enter' ); 72 | // Change line height. 73 | await page.getByRole( 'spinbutton', { name: 'Line height' } ).fill( '3' ); 74 | // Change letter case. 75 | await page.getByRole( 'button', { name: 'Lowercase' } ).click(); 76 | // Change letter spacing. 77 | await page.getByRole( 'spinbutton', { name: 'Letter spacing' } ).fill( '10' ); 78 | 79 | expect( await editor.getEditedPostContent() ).toMatchSnapshot(); 80 | } ); 81 | 82 | test( 'dimensions settings should be applied', async ( { editor, page, fsbUtils } ) => { 83 | const wpVersion = await fsbUtils.getWpVersion(); 84 | await fsbUtils.createFlexibleTableBlock(); 85 | // Open the sidebar. 86 | await editor.openDocumentSettingsSidebar(); 87 | await page 88 | .getByRole( 'region', { name: 'Editor settings' } ) 89 | .getByRole( 'tab', { name: 'Styles' } ) 90 | .click(); 91 | // Show margin control. 92 | await page.getByRole( 'button', { name: 'Dimensions options' } ).click(); 93 | await page 94 | .getByRole( 'menu', { name: 'Dimensions options' } ) 95 | .getByRole( 'menuitemcheckbox', { name: 'Show Margin' } ) 96 | .click(); 97 | await page.getByRole( 'button', { name: 'Dimensions options' } ).click(); 98 | // Show custom controls. 99 | if ( wpVersion === '6-6' ) { 100 | // WP 6.6 101 | await page.getByRole( 'button', { name: 'Margin options' } ).click(); 102 | await page 103 | .getByRole( 'menu', { name: 'Margin options' } ) 104 | .getByRole( 'menuitemradio', { name: 'Custom' } ) 105 | .click(); 106 | } else if ( wpVersion === '6-7' ) { 107 | // WP 6.7 108 | await page.getByRole( 'button', { name: 'Unlink sides' } ).click(); 109 | } else { 110 | // WP 6.8 111 | await page.getByRole( 'button', { name: 'Unlink' } ).click(); 112 | } 113 | // Change margin values. 114 | for ( let i = 0; i < 4; i++ ) { 115 | await page.getByRole( 'button', { name: 'Set custom size' } ).nth( 0 ).click(); 116 | } 117 | await page.getByRole( 'spinbutton', { name: 'Top margin' } ).fill( '10' ); 118 | await page.getByRole( 'spinbutton', { name: 'Right margin' } ).fill( '20' ); 119 | await page.getByRole( 'spinbutton', { name: 'Bottom margin' } ).fill( '30' ); 120 | await page.getByRole( 'spinbutton', { name: 'Left margin' } ).fill( '40' ); 121 | 122 | expect( await editor.getEditedPostContent() ).toMatchSnapshot(); 123 | } ); 124 | } ); 125 | -------------------------------------------------------------------------------- /test/e2e/specs/global-setting.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import FlexibleTableBlockUtils from '../util'; 10 | 11 | test.use( { 12 | fsbUtils: async ( { page, editor }, use ) => { 13 | await use( new FlexibleTableBlockUtils( { page, editor } ) ); 14 | }, 15 | } ); 16 | 17 | test.describe( 'Global Setting', () => { 18 | test.beforeEach( async ( { admin } ) => { 19 | await admin.createNewPost(); 20 | } ); 21 | 22 | test( 'should output inline style reflecting the settings', async ( { 23 | editor, 24 | page, 25 | pageUtils, 26 | fsbUtils, 27 | } ) => { 28 | await fsbUtils.createFlexibleTableBlock(); 29 | // Open the global setting. 30 | await editor.openDocumentSettingsSidebar(); 31 | await page.getByRole( 'button', { name: 'Global setting' } ).click(); 32 | // Restore settings. 33 | await page.getByRole( 'button', { name: 'Restore default settings' } ).click(); 34 | await page.getByRole( 'button', { name: 'Restore', exact: true } ).click(); 35 | await expect( page.locator( '.ftb-global-setting-modal__notice' ) ).toContainText( 36 | 'Global setting restored.' 37 | ); 38 | await page.locator( '.ftb-global-setting-modal__notice' ).getByRole( 'button' ).click(); 39 | // Change table width. 40 | await page.getByRole( 'spinbutton', { name: 'Table width' } ).fill( '90' ); 41 | // Change table max width. 42 | await page.getByRole( 'spinbutton', { name: 'Table max width' } ).fill( '110' ); 43 | await page.getByRole( 'button', { name: 'Separate' } ).click(); 44 | // Change striped colors. 45 | const tableColors = [ 46 | { color: '111111', label: 'Striped style background color ( odd rows )' }, 47 | { color: '222222', label: 'Striped style background color ( even rows )' }, 48 | ]; 49 | for ( let i = 0; i < tableColors.length; i++ ) { 50 | const { color, label } = tableColors[ i ]; 51 | await page 52 | .getByRole( 'group', { name: label } ) 53 | .getByRole( 'button', { name: 'Color' } ) 54 | .click(); 55 | await pageUtils.pressKeys( 'Enter' ); 56 | await page.getByRole( 'textbox', { name: 'Hex color' } ).fill( color ); 57 | await pageUtils.pressKeys( 'Escape', { times: 2 } ); 58 | } 59 | // Apply cell styles. 60 | await page.getByRole( 'tab', { name: 'Cell styles' } ).click(); 61 | // Change cell colors. 62 | const cellColors = [ 63 | { color: '333333', selector: 'text-color-th' }, 64 | { color: '444444', selector: 'text-color-td' }, 65 | { color: '555555', selector: 'background-color-th' }, 66 | { color: '666666', selector: 'background-color-td' }, 67 | { color: '777777', selector: 'border-color' }, 68 | ]; 69 | for ( let i = 0; i < cellColors.length; i++ ) { 70 | await page 71 | .getByRole( 'dialog', { name: 'Flexible Table Block Global setting' } ) 72 | .getByRole( 'button', { name: 'Color' } ) 73 | .nth( i ) 74 | .click(); 75 | await pageUtils.pressKeys( 'Enter' ); 76 | await page.getByRole( 'textbox', { name: 'Hex color' } ).fill( cellColors[ i ].color ); 77 | await pageUtils.pressKeys( 'Escape', { times: 2 } ); 78 | } 79 | // Change cell padding. 80 | await page.getByRole( 'button', { name: 'Unlink sides' } ).click(); 81 | await page.getByRole( 'spinbutton', { name: 'Top' } ).fill( '1' ); 82 | await page.getByRole( 'spinbutton', { name: 'Right' } ).fill( '2' ); 83 | await page.getByRole( 'spinbutton', { name: 'Bottom' } ).fill( '3' ); 84 | await page.getByRole( 'spinbutton', { name: 'Left' } ).fill( '4' ); 85 | // Change cell border width. 86 | await page.getByRole( 'spinbutton', { name: 'All' } ).fill( '2' ); 87 | // Change cell border style. 88 | await page.getByRole( 'button', { name: 'Dotted' } ).click(); 89 | // Change cell alignments. 90 | await page.getByRole( 'button', { name: 'Align center' } ).click(); 91 | await page.getByRole( 'button', { name: 'Align bottom' } ).click(); 92 | // Save settings. 93 | await page.getByRole( 'button', { name: 'Save settings' } ).click(); 94 | await page.locator( '.ftb-global-setting-modal__notice' ); 95 | await expect( page.locator( '.ftb-global-setting-modal__notice' ) ).toContainText( 96 | 'Global setting saved.' 97 | ); 98 | const styleTagContent = await page.evaluate( () => { 99 | const styleTag = document.querySelector( 'style#flexible-table-block-editor-inline-css' ); 100 | return styleTag ? styleTag.textContent : ''; 101 | } ); 102 | 103 | expect( styleTagContent ).toBe( 104 | `.editor-styles-wrapper .wp-block-flexible-table-block-table>table{width:90%;max-width:110%;border-collapse:separate;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(odd) th{background-color:#111111;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(odd) td{background-color:#111111;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(even) th{background-color:#222222;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(even) td{background-color:#222222;}.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr th,.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr td{padding:1em 2em 3em 4em;border-width:2px;border-style:dotted;border-color:#777777;text-align:center;vertical-align:bottom;}.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr th{color:#333333;background-color:#555555;}.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr td{color:#444444;background-color:#666666;}` 105 | ); 106 | 107 | // Restore settings. 108 | await page.getByRole( 'button', { name: 'Restore default settings' } ).click(); 109 | await page.getByRole( 'button', { name: 'Restore', exact: true } ).click(); 110 | await expect( page.locator( '.ftb-global-setting-modal__notice' ) ).toContainText( 111 | 'Global setting restored.' 112 | ); 113 | await page.locator( '.ftb-global-setting-modal__notice ' ).getByRole( 'button' ).click(); 114 | 115 | const defaultStyleTagContent = await page.evaluate( () => { 116 | const styleTag = document.querySelector( 'style#flexible-table-block-editor-inline-css' ); 117 | return styleTag ? styleTag.textContent : ''; 118 | } ); 119 | 120 | expect( defaultStyleTagContent ).toBe( 121 | `.editor-styles-wrapper .wp-block-flexible-table-block-table>table{width:100%;max-width:100%;border-collapse:collapse;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(odd) th{background-color:#f0f0f1;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(odd) td{background-color:#f0f0f1;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(even) th{background-color:#ffffff;}.editor-styles-wrapper .wp-block-flexible-table-block-table.is-style-stripes tbody tr:nth-child(even) td{background-color:#ffffff;}.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr th,.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr td{padding:0.5em;border-width:1px;border-style:solid;border-color:#000000;text-align:left;vertical-align:middle;}.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr th{background-color:#f0f0f1;}.editor-styles-wrapper .wp-block-flexible-table-block-table>table tr td{background-color:#ffffff;}` 122 | ); 123 | } ); 124 | } ); 125 | -------------------------------------------------------------------------------- /test/e2e/specs/table-cell.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import FlexibleTableBlockUtils from '../util'; 10 | 11 | test.use( { 12 | fsbUtils: async ( { page, editor }, use ) => { 13 | await use( new FlexibleTableBlockUtils( { page, editor } ) ); 14 | }, 15 | } ); 16 | 17 | test.describe( 'Flexible table cell', () => { 18 | test.beforeEach( async ( { admin } ) => { 19 | await admin.createNewPost(); 20 | } ); 21 | 22 | test( 'allows cell movement with tab key.', async ( { editor, page, pageUtils, fsbUtils } ) => { 23 | await fsbUtils.createFlexibleTableBlock(); 24 | await editor.openDocumentSettingsSidebar(); 25 | await page.getByRole( 'button', { name: 'Global setting' } ).click(); 26 | 27 | // Restore settings. 28 | await page.getByRole( 'button', { name: 'Restore default settings' } ).click(); 29 | await page.getByRole( 'button', { name: 'Restore', exact: true } ).click(); 30 | await expect( page.locator( '.ftb-global-setting-modal__notice' ) ).toContainText( 31 | 'Global setting restored.' 32 | ); 33 | await page.locator( '.ftb-global-setting-modal__notice ' ).getByRole( 'button' ).click(); 34 | 35 | // Update Editor Option. 36 | await page.getByRole( 'tab', { name: 'Editor options' } ).click(); 37 | await page.getByRole( 'checkbox', { name: 'Use the tab key to move cells' } ).check(); 38 | await page.getByRole( 'button', { name: 'Save settings' } ).click(); 39 | await page.locator( '.ftb-global-setting-modal__notice' ).getByRole( 'button' ).click(); 40 | await page 41 | .getByRole( 'dialog', { name: 'Flexible Table Block Global setting' } ) 42 | .getByRole( 'button', { name: 'Close' } ) 43 | .click(); 44 | // Try to move within cells. 45 | await editor.canvas 46 | .getByRole( 'textbox', { name: 'Body cell text' } ) 47 | .nth( 0 ) 48 | .fill( 'Cell 1' ); 49 | await pageUtils.pressKeys( 'Tab', { times: 2 } ); 50 | await pageUtils.pressKeys( 'shift+Tab' ); 51 | await page.keyboard.type( 'Cell 2' ); 52 | expect( await editor.getEditedPostContent() ).toMatchSnapshot(); 53 | } ); 54 | 55 | test( 'allows keyboard operation within the link popover', async ( { 56 | editor, 57 | page, 58 | pageUtils, 59 | fsbUtils, 60 | } ) => { 61 | await fsbUtils.createFlexibleTableBlock(); 62 | await editor.canvas.getByRole( 'textbox', { name: 'Body cell text' } ).nth( 0 ).fill( 'Link' ); 63 | await pageUtils.pressKeys( 'primary+a' ); 64 | await editor.clickBlockToolbarButton( 'Link' ); 65 | 66 | // Create a link. 67 | await page.keyboard.type( '#anchor' ); 68 | await pageUtils.pressKeys( 'Enter' ); 69 | 70 | expect( await editor.getEditedPostContent() ).toMatchSnapshot(); 71 | 72 | // Edit the link. 73 | await pageUtils.pressKeys( 'Tab' ); 74 | await pageUtils.pressKeys( 'Enter' ); 75 | await page.getByRole( 'combobox', { name: 'Link' } ).fill( '#anchor-updated' ); 76 | await pageUtils.pressKeys( 'Enter' ); 77 | 78 | // Toggle "Open in new tab". 79 | await pageUtils.pressKeys( 'Enter' ); 80 | await pageUtils.pressKeys( 'Tab' ); 81 | await pageUtils.pressKeys( 'Enter' ); 82 | await page 83 | .locator( '.block-editor-link-control__tools ' ) 84 | .getByRole( 'button', { name: 'Advanced' } ) 85 | .click(); 86 | await page.getByRole( 'checkbox', { name: 'Open in new tab' } ).click(); 87 | await page.getByRole( 'button', { name: 'Save', exact: true } ).click(); 88 | 89 | expect( await editor.getEditedPostContent() ).toMatchSnapshot(); 90 | } ); 91 | } ); 92 | -------------------------------------------------------------------------------- /test/e2e/specs/various.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import FlexibleTableBlockUtils from '../util'; 10 | 11 | test.use( { 12 | fsbUtils: async ( { page, editor }, use ) => { 13 | await use( new FlexibleTableBlockUtils( { page, editor } ) ); 14 | }, 15 | } ); 16 | 17 | test.describe( 'Various', () => { 18 | test.beforeEach( async ( { admin } ) => { 19 | await admin.createNewPost(); 20 | } ); 21 | 22 | test( 'contentOnly mode should be enabled', async ( { editor, page } ) => { 23 | await editor.insertBlock( { 24 | name: 'core/group', 25 | attributes: { 26 | templateLock: 'contentOnly', 27 | }, 28 | innerBlocks: [ { name: 'flexible-table-block/table' } ], 29 | } ); 30 | await editor.canvas.getByRole( 'button', { name: 'Create Table' } ).click(); 31 | await expect( 32 | page 33 | .getByRole( 'region', { name: 'Editor settings' } ) 34 | .getByRole( 'button', { name: 'Flexible Table' } ) 35 | ).toBeVisible(); 36 | } ); 37 | } ); 38 | -------------------------------------------------------------------------------- /test/e2e/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import type { Page } from '@playwright/test'; 5 | 6 | /** 7 | * WordPress dependencies 8 | */ 9 | import type { Editor } from '@wordpress/e2e-test-utils-playwright'; 10 | 11 | export default class FlexibleTableBlockUtils { 12 | editor: Editor; 13 | page: Page; 14 | 15 | constructor( { page, editor } ) { 16 | this.editor = editor; 17 | this.page = page; 18 | } 19 | 20 | async createFlexibleTableBlock( { 21 | col, 22 | row, 23 | header = false, 24 | footer = false, 25 | }: { 26 | col?: number; 27 | row?: number; 28 | header?: boolean; 29 | footer?: boolean; 30 | } = {} ) { 31 | await this.editor.insertBlock( { name: 'flexible-table-block/table' } ); 32 | 33 | if ( header ) { 34 | await this.editor.canvas.getByRole( 'checkbox', { name: 'Header section' } ).check(); 35 | } 36 | if ( footer ) { 37 | await this.editor.canvas.getByRole( 'checkbox', { name: 'Footer section' } ).check(); 38 | } 39 | if ( col ) { 40 | await this.editor.canvas 41 | .getByRole( 'spinbutton', { name: 'Column count' } ) 42 | .fill( String( col ) ); 43 | } 44 | if ( row ) { 45 | await this.editor.canvas 46 | .getByRole( 'spinbutton', { name: 'Row count' } ) 47 | .fill( String( row ) ); 48 | } 49 | 50 | await this.editor.canvas.getByRole( 'button', { name: 'Create Table' } ).click(); 51 | } 52 | 53 | async createCoreTableBlock( { 54 | col, 55 | row, 56 | }: { 57 | col?: number; 58 | row?: number; 59 | } = {} ) { 60 | await this.editor.insertBlock( { name: 'core/table' } ); 61 | 62 | if ( col ) { 63 | await this.editor.canvas 64 | .getByRole( 'spinbutton', { name: 'Column count' } ) 65 | .fill( String( col ) ); 66 | } 67 | if ( row ) { 68 | await this.editor.canvas 69 | .getByRole( 'spinbutton', { name: 'Row count' } ) 70 | .fill( String( row ) ); 71 | } 72 | 73 | await this.editor.canvas.getByRole( 'button', { name: 'Create Table' } ).click(); 74 | } 75 | 76 | async getWpVersion() { 77 | const body = await this.page.$( 'body' ); 78 | if ( ! body ) { 79 | throw new Error( 'Could not find body element' ); 80 | } 81 | const bodyClassNames = await ( await body.getProperty( 'className' ) ).jsonValue(); 82 | const matches = bodyClassNames.match( /branch-([0-9]*-*[0-9])/ ); 83 | return matches?.[ 1 ]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/unit/jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WordPress dependencies 3 | */ 4 | const config = require( '@wordpress/scripts/config/jest-unit.config.js' ); 5 | 6 | module.exports = { 7 | ...config, 8 | rootDir: '../../', 9 | testPathIgnorePatterns: [ '/test/e2e' ], 10 | }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": false, 5 | "allowSyntheticDefaultImports": true, 6 | "jsx": "preserve", 7 | "target": "esnext", 8 | "module": "esnext", 9 | "lib": [ "dom", "esnext" ], 10 | "composite": true, 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": [ "./src/*" ] 14 | }, 15 | "noEmit": true, 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "verbatimModuleSyntax": true, 22 | "moduleResolution": "node", 23 | "esModuleInterop": true, 24 | "resolveJsonModule": true, 25 | "typeRoots": [ "./node_modules/@types" ], 26 | "skipLibCheck": true 27 | }, 28 | "include": [ "src/**/*", "src/**/*.json" ], 29 | "exclude": [ "**/build/**" ] 30 | } 31 | --------------------------------------------------------------------------------