diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index 03696bd4..00000000 --- a/.browserslistrc +++ /dev/null @@ -1 +0,0 @@ -extends browserslist-config-nk diff --git a/src/vendor/sabberworm/php-css-parser/tests/files/-empty.css b/.cache/.gitkeep similarity index 100% rename from src/vendor/sabberworm/php-css-parser/tests/files/-empty.css rename to .cache/.gitkeep diff --git a/.distignore b/.distignore new file mode 100644 index 00000000..d3f0dcd2 --- /dev/null +++ b/.distignore @@ -0,0 +1,28 @@ +# Directories to ignore +/.git +/.cache +/.github +/.husky +/.vscode +/.wordpress-org +/artifacts +/dist-zip +/node_modules +/tests +/vendor + +# Files to ignore +/.* +/composer.json +/composer.lock +/gulpfile.js +/LICENSE.txt +/lint-staged.config.js +/package-lock.json +/package.json +/phpcs.xml +/phpcs.xml.dist +/phpunit.xml +/phpunit.xml.dist +/README.md +/webpack.config.js diff --git a/.editorconfig b/.editorconfig index 8de798cc..8b5972e0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,13 +5,13 @@ root = true [*] charset = utf-8 end_of_line = lf -indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true -indent_style = space +indent_style = tab -[*.php] -indent_size = 4 +[*.yml] +indent_style = space +indent_size = 2 [*.md] trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..7bb3f348 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +assets/vendor/** diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index f07ca3c9..00000000 --- a/.eslintrc +++ /dev/null @@ -1,18 +0,0 @@ - -{ - "extends": [ - "eslint-config-nk" - ], - "globals": { - "wp": true - }, - "settings": { - "react": { - "pragma": "wp" - } - }, - "rules": { - "react/prefer-stateless-function": "off", - "react/prop-types": "off" - } -} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..c3ad4757 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,40 @@ +const groups = { + // The default grouping, but with no blank lines. + groups: [ + // Side effect imports. + ['^\\u0000'], + // Node.js builtins prefixed with `node:`. + ['^node:'], + // Packages. + // Things that start with a letter (or digit or underscore), or `@` followed by a letter. + ['^@?\\w'], + // WordPress imports + ['^@wordpress'], + // Absolute imports and other imports such as Vue-style `@/foo`. + // Anything not matched in another group. + ['^'], + // Relative imports. + // Anything that starts with a dot. + ['^\\.'], + ], +}; + +module.exports = { + extends: ['plugin:@wordpress/eslint-plugin/recommended'], + rules: { + '@wordpress/no-unsafe-wp-apis': 0, + '@wordpress/i18n-translator-comments': 0, + 'jsdoc/no-undefined-types': 0, + 'jsdoc/require-param-type': 0, + 'jsdoc/require-returns-description': 0, + 'jsdoc/check-tag-names': 0, + 'react-hooks/rules-of-hooks': 0, + 'jsdoc/check-param-names': 0, + 'simple-import-sort/imports': ['error', groups], + 'simple-import-sort/exports': 'error', + }, + settings: { + 'import/core-modules': ['jquery', 'lodash'], + }, + plugins: ['simple-import-sort'], +}; diff --git a/.github/setup-node/action.yml b/.github/setup-node/action.yml new file mode 100644 index 00000000..928196a5 --- /dev/null +++ b/.github/setup-node/action.yml @@ -0,0 +1,44 @@ +name: 'Setup Node.js and install npm dependencies' +description: 'Configure Node.js and install npm dependencies while managing all aspects of caching.' +inputs: + node-version: + description: 'Optional. The Node.js version to use. When not specified, the version specified in .nvmrc will be used.' + required: false + type: string + +runs: + using: 'composite' + steps: + - name: Use desired version of Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + node-version: ${{ inputs.node-version }} + cache: npm + + - name: Get Node.js and npm version + id: node-version + run: | + echo "NODE_VERSION=$(node -v)" >> $GITHUB_OUTPUT + shell: bash + + - name: Cache node_modules + id: cache-node_modules + uses: actions/cache@v3 + with: + path: '**/node_modules' + key: node_modules-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('package-lock.json') }} + + - name: Install npm dependencies + if: ${{ steps.cache-node_modules.outputs.cache-hit != 'true' }} + run: npm ci + shell: bash + + # On cache hit, we run the post-install script to match the native `npm ci` behavior. + # An example of this is to patch `node_modules` using patch-package. + - name: Post-install + if: ${{ steps.cache-node_modules.outputs.cache-hit == 'true' }} + run: | + # Run the post-install script for the root project. + npm run postinstall + shell: bash diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 1c9ac56b..00000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Deploy to WordPress.org - -on: - workflow_dispatch: - push: - tags: - - 'v*' - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - - name: Disable xDebug - fixes PHP Fatal Error for `i18n make-pot` - uses: shivammathur/setup-php@v2 - with: - php-version: '7.4' - coverage: none - - - name: Install NPM and Composer Dependencies - run: npm install --force - - - name: Run Production Task - run: npm run production - - - name: WordPress Plugin Deploy - uses: nk-o/action-wordpress-plugin-deploy@master - env: - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} - SOURCE_DIR: dist/ghostkit/ - SLUG: ghostkit diff --git a/.github/workflows/push-asset-readme-update.yml b/.github/workflows/push-asset-readme-update.yml new file mode 100644 index 00000000..705f25f5 --- /dev/null +++ b/.github/workflows/push-asset-readme-update.yml @@ -0,0 +1,22 @@ +name: Plugin Asset/Readme Update + +on: + workflow_dispatch: + push: + branches: + - master + +jobs: + trunk: + name: Push to trunk + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: WordPress.org plugin asset/readme update + uses: 10up/action-wordpress-plugin-asset-update@stable + env: + SLUG: ghostkit + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} diff --git a/.github/workflows/push-deploy.yml b/.github/workflows/push-deploy.yml new file mode 100644 index 00000000..002b7955 --- /dev/null +++ b/.github/workflows/push-deploy.yml @@ -0,0 +1,22 @@ +name: Deploy to WordPress.org + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +jobs: + tag: + name: New release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: WordPress Plugin Deploy + uses: 10up/action-wordpress-plugin-deploy@stable + env: + SLUG: ghostkit + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} diff --git a/.github/workflows/tests-e2e.yml b/.github/workflows/tests-e2e.yml new file mode 100644 index 00000000..0495ce47 --- /dev/null +++ b/.github/workflows/tests-e2e.yml @@ -0,0 +1,48 @@ +name: End-to-End Tests + +on: + pull_request: + push: + branches: + - master + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +jobs: + playwright: + name: Playwright - ${{ matrix.part }} + runs-on: ubuntu-latest + if: ${{ github.repository == 'nk-crew/ghostkit' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + matrix: + part: [1, 2, 3, 4] + totalParts: [4] + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node + + - name: Npm build + run: npm run build + + - name: Install Playwright dependencies + run: | + npx playwright install chromium firefox webkit --with-deps + + - name: Install WordPress and start the server + run: | + npm run wp-env start + + - name: Run the tests + run: | + xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e -- --shard=${{ matrix.part }}/${{ matrix.totalParts }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..6bc93621 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,224 @@ +name: Unit Tests + +on: + pull_request: + push: + branches: + - master + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +jobs: + compute-previous-wordpress-version: + name: Compute previous WordPress version + runs-on: ubuntu-latest + outputs: + previous-wordpress-version: ${{ steps.get-previous-wordpress-version.outputs.previous-wordpress-version }} + + steps: + - name: Get previous WordPress version + id: get-previous-wordpress-version + run: | + curl \ + -H "Accept: application/json" \ + -o versions.json \ + "http://api.wordpress.org/core/stable-check/1.0/" + LATEST_WP_VERSION=$(jq --raw-output 'with_entries(select(.value=="latest"))|keys[]' versions.json) + IFS='.' read LATEST_WP_MAJOR LATEST_WP_MINOR LATEST_WP_PATCH <<< "${LATEST_WP_VERSION}" + if [[ ${LATEST_WP_MINOR} == "0" ]]; then + PREVIOUS_WP_SERIES="$((LATEST_WP_MAJOR - 1)).9" + else + PREVIOUS_WP_SERIES="${LATEST_WP_MAJOR}.$((LATEST_WP_MINOR - 1))" + fi + PREVIOUS_WP_VERSION=$(jq --raw-output --arg series "${PREVIOUS_WP_SERIES}" 'with_entries(select(.key|startswith($series)))|keys[-1]' versions.json) + echo "previous-wordpress-version=${PREVIOUS_WP_VERSION}" >> $GITHUB_OUTPUT + rm versions.json + + test-php: + name: PHP ${{ matrix.php }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on ubuntu-latest + needs: compute-previous-wordpress-version + runs-on: ubuntu-latest + timeout-minutes: 20 + if: ${{ github.repository == 'nk-crew/ghostkit' || github.event_name == 'pull_request' }} + strategy: + fail-fast: true + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + wordpress: [''] # Latest WordPress version. + include: + # Test with the previous WP version. + - php: '7.4' + wordpress: ${{ needs.compute-previous-wordpress-version.outputs.previous-wordpress-version }} + - php: '8.2' + wordpress: ${{ needs.compute-previous-wordpress-version.outputs.previous-wordpress-version }} + + env: + WP_ENV_PHP_VERSION: ${{ matrix.php }} + WP_ENV_CORE: ${{ matrix.wordpress == '' && 'WordPress/WordPress' || format( 'https://wordpress.org/wordpress-{0}.zip', matrix.wordpress ) }} + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node + + ## + # This allows Composer dependencies to be installed using a single step. + # + # Since the tests are currently run within the Docker containers where the PHP version varies, + # the same PHP version needs to be configured for the action runner machine so that the correct + # dependency versions are installed and cached. + ## + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '${{ matrix.php }}' + ini-file: development + coverage: none + + # Ensure that Composer installs the correct versions of packages. + - name: Override PHP version in composer.json + run: composer config platform.php ${{ matrix.php }} + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + with: + custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F") + + - name: Npm build + run: npm run build + + - name: Docker debug information + run: | + docker -v + docker-compose -v + + - name: General debug information + run: | + npm --version + node --version + curl --version + git --version + svn --version + locale -a + + - name: Start Docker environment + run: npm run wp-env start + + - name: Log running Docker containers + run: docker ps -a + + - name: Docker container debug information + run: | + npm run wp-env run tests-mysql mysql -- --version + npm run wp-env run tests-wordpress php -- --version + npm run wp-env run tests-wordpress php -m + npm run wp-env run tests-wordpress php -i + npm run wp-env run tests-wordpress /var/www/html/wp-content/plugins/ghostkit/vendor/bin/phpunit -- --version + npm run wp-env run tests-wordpress locale -a + + - name: Running unit tests + run: | + set -o pipefail + npm run test:unit:php | tee phpunit.log + + # Verifies that PHPUnit actually runs in the first place. We want visibility + # into issues which can cause it to fail silently, so we check the output + # to verify that at least 5 tests have passed. This is an arbitrary + # number, but makes sure a drastic change doesn't happen without us noticing. + - name: Check number of passed tests + run: | + # Note: relies on PHPUnit execution to fail on test failure. + # Extract the number of executed tests from the log file. + if ! num_tests=$(grep -Eo 'OK \([0-9]+ tests' phpunit.log) ; then + if ! num_tests=$(grep -Eo 'Tests: [0-9]+, Assertions:' phpunit.log) ; then + echo "PHPUnit failed or did not run. Check the PHPUnit output in the previous step to debug." && exit 1 + fi + fi + # Extract just the number of tests from the string. + num_tests=$(echo "$num_tests" | grep -Eo '[0-9]+') + if [ $num_tests -lt 2 ] ; then + echo "Only $num_tests tests passed, which is much fewer than expected." && exit 1 + fi + echo "$num_tests tests passed." + + lint: + name: Lint JS + CSS + runs-on: ubuntu-latest + timeout-minutes: 20 + if: ${{ github.repository == 'nk-crew/ghostkit' || github.event_name == 'pull_request' }} + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node + + - name: Npm build + run: npm run build + + - name: Running the lint + run: npm run lint + + phpcs: + name: PHP coding standards + runs-on: ubuntu-latest + timeout-minutes: 20 + if: ${{ github.repository == 'nk-crew/ghostkit' || github.event_name == 'pull_request' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + tools: cs2pr + + # This date is used to ensure that the PHPCS cache is cleared at least once every week. + # http://man7.org/linux/man-pages/man1/date.1.html + - name: "Get last Monday's date" + id: get-date + run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> $GITHUB_OUTPUT + + - name: Cache PHPCS scan cache + uses: actions/cache@v3 + with: + path: .cache/phpcs.json + key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-phpcs-cache-${{ hashFiles('**/composer.json', 'phpcs.xml.dist') }} + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + with: + custom-cache-suffix: ${{ steps.get-date.outputs.date }} + + - name: Make Composer packages available globally + run: echo "${PWD}/vendor/bin" >> $GITHUB_PATH + + - name: Run PHPCS on all Visual Portfolio files + id: phpcs-ghostkit + run: phpcs --report-full --report-checkstyle=./.cache/phpcs-report.xml + + - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs-ghostkit.outcome == 'failure' }} + run: cs2pr ./.cache/phpcs-report.xml + + - name: Ensure version-controlled files are not modified during the tests + run: git diff --exit-code diff --git a/.gitignore b/.gitignore index 5e0bf37f..8015832e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,20 @@ +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.phpunit.result.cache +/artifacts .DS_Store .sass-cache .publish .git .idea -/src/languages/* /node_modules /vendor /dist +/dist-zip +/tests/e2e/artifacts npm-debug.log .phpcs.xml phpcs.xml diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..3c032078 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 00000000..2de2a6da --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":[],"times":[]} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 30ae76ef..75527dd1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,7 +2,12 @@ **/*.scss **/*.html **/*.php +**/*.yml node_modules +assets/vendor dist +dist-zip vendor vendors +build +composer-libraries diff --git a/.prettierrc.js b/.prettierrc.js index 1727b1c6..e596d24c 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1 +1 @@ -module.exports = require('prettier-config-nk'); +module.exports = require('@wordpress/prettier-config'); diff --git a/.stylelintignore b/.stylelintignore index 97da4b89..03a0ca67 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,9 +1,14 @@ **/*.js **/*.jsx **/*.min.css +**/*style.css +**/*style-rtl.css **/*.build.css **/gutenberg/wordpress/**/*.scss node_modules +assets/vendor dist +dist-zip +build vendor vendors diff --git a/.stylelintrc.js b/.stylelintrc.js index b82b088a..6e2e23ac 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,3 +1,25 @@ module.exports = { - extends: ['stylelint-config-nk'], + extends: '@wordpress/stylelint-config/scss', + rules: { + 'at-rule-empty-line-before': null, + 'at-rule-no-unknown': null, + 'comment-empty-line-before': null, + 'font-weight-notation': null, + 'max-line-length': null, + 'no-descending-specificity': null, + 'rule-empty-line-before': null, + 'selector-class-pattern': null, + 'value-keyword-case': null, + 'scss/operator-no-unspaced': null, + 'scss/selector-no-redundant-nesting-selector': null, + 'scss/at-import-partial-extension': null, + 'scss/no-global-function-names': null, + 'scss/comment-no-empty': null, + 'scss/at-extend-no-missing-placeholder': null, + 'scss/operator-no-newline-after': null, + 'scss/at-if-closing-brace-newline-after': null, + 'scss/at-else-empty-line-before': null, + 'scss/at-if-closing-brace-space-after': null, + 'no-invalid-position-at-import-rule': null, + }, }; diff --git a/.vscode/settings.json b/.vscode/settings.json index a6550636..4ae4caec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,30 +1,37 @@ { - "editor.defaultFormatter": null, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[php]": { - "editor.defaultFormatter": null, - "editor.formatOnSave": false - }, - "editor.formatOnSave": true, - - "scss.validate": false, - "css.validate": false, - "less.validate": false, - - "editor.codeActionsOnSave": { - "source.fixAll.stylelint": true - }, - "stylelint.validate": [ - "css", - "scss" - ], - "stylelint.snippet": [ - "css", - "scss" - ], - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } + "editor.defaultFormatter": null, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[php]": { + "editor.defaultFormatter": null, + "editor.formatOnSave": false + }, + "editor.formatOnSave": true, + "scss.validate": false, + "css.validate": false, + "less.validate": false, + "editor.codeActionsOnSave": { + "source.fixAll.stylelint": "explicit" + }, + "stylelint.validate": [ + "css", + "scss" + ], + "stylelint.snippet": [ + "css", + "scss" + ], + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "search.exclude": { + "**/.git": true, + "**/node_modules": true, + "**/build": true, + "**/dist": true, + "**/dist-zip": true, + "**/vendor": true, + "**/composer-libraries": true + }, } diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 00000000..c4e779aa --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,15 @@ +{ + "core": null, + "themes": ["./tests/themes/empty-theme"], + "plugins": [ + ".", + "./tests/plugins/gutenberg-test-plugin-disables-the-css-animations" + ], + "env": { + "tests": { + "mappings": { + "wp-content/themes/empty-theme-php": "./tests/themes/empty-theme-php" + } + } + } +} diff --git a/artifacts/storage-states/admin.json b/artifacts/storage-states/admin.json new file mode 100644 index 00000000..ab8e750e --- /dev/null +++ b/artifacts/storage-states/admin.json @@ -0,0 +1 @@ +{"cookies":[{"name":"wordpress_test_cookie","value":"WP%20Cookie%20check","domain":"localhost","path":"/","expires":-1,"httpOnly":false,"secure":false,"sameSite":"Lax"},{"name":"wordpress_23778236db82f19306f247e20a353a99","value":"admin%7C1702651902%7CViDbSLgcMdANwtqHSeuPTQEyDqfvbPXOU5cCASWrclU%7C5cbac360064658215922d7e8fdc5789881c464eb0b51d056768aded712f3eac5","domain":"localhost","path":"/wp-content/plugins","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wordpress_23778236db82f19306f247e20a353a99","value":"admin%7C1702651902%7CViDbSLgcMdANwtqHSeuPTQEyDqfvbPXOU5cCASWrclU%7C5cbac360064658215922d7e8fdc5789881c464eb0b51d056768aded712f3eac5","domain":"localhost","path":"/wp-admin","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wordpress_logged_in_23778236db82f19306f247e20a353a99","value":"admin%7C1702651902%7CViDbSLgcMdANwtqHSeuPTQEyDqfvbPXOU5cCASWrclU%7Ced13a2a1f9c4cf8fefbd789c056ab3cf9c5509e009c76124a5dee9b97c7842b6","domain":"localhost","path":"/","expires":-1,"httpOnly":true,"secure":false,"sameSite":"Lax"},{"name":"wp-settings-time-1","value":"1702479102","domain":"localhost","path":"/","expires":1734015102.263,"httpOnly":false,"secure":false,"sameSite":"Lax"}],"nonce":"06ec6c599b","rootURL":"http://localhost:8889/index.php?rest_route=/"} \ No newline at end of file diff --git a/assets/admin/css/admin.scss b/assets/admin/css/admin.scss new file mode 100644 index 00000000..3c288d57 --- /dev/null +++ b/assets/admin/css/admin.scss @@ -0,0 +1,31 @@ +/** + * Admin Styles. + */ +@import "../../../gutenberg/variables"; + +/* + * Go Pro menu link + */ +#adminmenu a[href*="admin.php?page=ghostkit_go_pro"], +.wp-list-table.plugins a[href*="admin.php?page=ghostkit_go_pro"] { + font-weight: 700; + color: $color-success; + + .dashicons { + transition: none; + } + + &:hover, + &:focus { + color: $color-success; + filter: contrast(1.1) brightness(1.2); + } +} + +/** + * Admin menu icon. + */ +#adminmenu .toplevel_page_ghostkit > .wp-menu-image.svg { + filter: brightness(1.3); + background-size: 16px auto; +} diff --git a/assets/admin/css/style.scss b/assets/admin/css/style.scss new file mode 100644 index 00000000..cec33015 --- /dev/null +++ b/assets/admin/css/style.scss @@ -0,0 +1,114 @@ +/** + * Editor Additional Styles. + */ +@import "../../../gutenberg/variables"; + +.ghostkit-help-text { + font-size: 90%; + font-style: italic; + color: #a0a0a0; +} + +.ghostkit-code { + background: #eef0f3; + border-radius: 3px; +} + +/** + * Grid for controls. + */ +.ghostkit-grid-controls { + display: flex; + flex-wrap: wrap; + margin: 0 -10px; + + > * { + flex: 1; + min-width: 80px; + margin: 0 10px !important; + } +} + +/** + * Extensions icon in panel body + */ +.components-panel__body-toggle > .ghostkit-ext-icon { + position: absolute; + top: 50%; + display: block; + transform: translateY(-50%); + + svg { + display: block; + } + + + span { + margin-left: 35px; + } +} + +/** + * Tabs Control. + */ +.ghostkit-control-tabs { + > .components-tab-panel__tabs { + display: flex; + flex-wrap: wrap; + margin-bottom: 15px; + border-bottom: 1px solid $light-gray-400; + + // single tab + > .ghostkit-control-tabs-tab { + padding: 9px 11px; + padding-bottom: 11px; + margin: 0; + margin-bottom: -1px; + color: #555d66; + cursor: pointer; + background: none; + border: none; + border-bottom: 2px solid transparent; + border-radius: 0; + outline-offset: -1px; + + svg { + display: block; + width: auto; + height: 1.4em; + } + + &.is-active { + position: relative; + z-index: 1; + border-bottom: 2px solid $blue-medium-focus; + } + } + } + + // wide + &.ghostkit-control-tabs-wide > .components-tab-panel__tabs > .ghostkit-control-tabs-tab { + flex: 1; + justify-content: center; + text-align: center; + } + + .ghostkit-control-tabs-separator { + padding: 15px; + margin-right: -15px; + margin-left: -15px; + border-top: 1px solid $light-gray-400; + + &:first-child { + margin-top: -16px; + } + } +} + +/** + * + */ +.ghostkit-component-input-drag { + input { + cursor: n-resize; + } +} diff --git a/assets/admin/js/reusable-widget.js b/assets/admin/js/reusable-widget.js new file mode 100644 index 00000000..796ac015 --- /dev/null +++ b/assets/admin/js/reusable-widget.js @@ -0,0 +1,43 @@ +const { jQuery: $ } = window; + +function updateReusableBlockLinks($widgets) { + if (!$widgets) { + $widgets = $('[id*="ghostkit_reusable_widget"].widget'); + } + + $widgets.each(function () { + const $widget = $(this); + const $buttonWrap = $widget.find('.gkt-reusable-block-edit-button'); + const $select = $widget.find('.gkt-reusable-block-select'); + + if (!$buttonWrap.length || !$select.length) { + return; + } + + const adminURL = $buttonWrap.attr('data-admin-url'); + const selectVal = $select.val(); + + if (selectVal) { + $buttonWrap + .find('a') + .attr( + 'href', + `${adminURL}post.php?post=${selectVal}&action=edit` + ); + $buttonWrap.show(); + } else { + $buttonWrap.hide(); + } + }); +} +updateReusableBlockLinks(); + +$(document).on('widget-added widget-updated', (e, widget) => { + updateReusableBlockLinks(widget); +}); + +$(document).on('change', '.gkt-reusable-block-select', function () { + updateReusableBlockLinks( + $(this).closest('[id*="ghostkit_reusable_widget"].widget') + ); +}); diff --git a/src/assets/css/fallback-classic-theme.css b/assets/css/fallback-classic-theme.css similarity index 83% rename from src/assets/css/fallback-classic-theme.css rename to assets/css/fallback-classic-theme.css index 61d8aaea..fffb4a95 100644 --- a/src/assets/css/fallback-classic-theme.css +++ b/assets/css/fallback-classic-theme.css @@ -5,7 +5,7 @@ * Use :where selector to add possibility to easily overwrite these styles. https://css-tricks.com/almanac/selectors/w/where/ */ :where(:root) { - --gkt-blocks-margin-bottom: 28px; + --gkt-blocks-margin-bottom: 28px; } :where( @@ -29,7 +29,9 @@ .ghostkit-instagram, .ghostkit-twitter, .ghostkit-toc, -.ghostkit-form +.ghostkit-form, +.ghostkit-form-field, +.ghostkit-form-submit-button ) { - margin-bottom: var(--gkt-blocks-margin-bottom); + margin-bottom: var(--gkt-blocks-margin-bottom); } diff --git a/src/assets/images/admin-icon.svg b/assets/images/admin-icon.svg similarity index 100% rename from src/assets/images/admin-icon.svg rename to assets/images/admin-icon.svg diff --git a/assets/js/event-fallbacks.js b/assets/js/event-fallbacks.js new file mode 100644 index 00000000..de08b4dd --- /dev/null +++ b/assets/js/event-fallbacks.js @@ -0,0 +1,200 @@ +/** + * Fallbacks for JS events for Ghost Kit < v3.0 + */ +import getjQuery from './utils/get-jquery'; + +const { GHOSTKIT } = window; +const { events } = GHOSTKIT; + +class GhostKitFallbackClass { + constructor() { + const self = this; + + self.deprecatedWarning = self.deprecatedWarning.bind(self); + self.initBlocks = self.deprecatedWarning.bind(self); + self.initBlocksThrottled = self.deprecatedWarning.bind(self); + self.prepareSR = self.deprecatedWarning.bind(self); + self.prepareCounters = self.deprecatedWarning.bind(self); + self.prepareNumberedLists = self.deprecatedWarning.bind(self); + self.prepareFallbackCustomStyles = self.deprecatedWarning.bind(self); + } + + // eslint-disable-next-line class-methods-use-this + deprecatedWarning() { + // eslint-disable-next-line no-console + console.warn( + 'Using `classObject` methods are deprecated since version 3.0.0. The main class object is removed and no more used.' + ); + } +} + +const classObject = new GhostKitFallbackClass(); +GHOSTKIT.classObject = classObject; + +function trigger(name, ...args) { + const $ = getjQuery(); + + if ($) { + $(document).trigger(`${name}.ghostkit`, [...args]); + } +} +function getElement(element) { + const $ = getjQuery(); + + if ($) { + return $(element); + } + + return element; +} + +let warnOnce = true; +GHOSTKIT.triggerEvent = (...args) => { + if (warnOnce) { + warnOnce = false; + + // eslint-disable-next-line no-console + console.warn( + 'Using `GHOSTKIT.triggerEvent` function is deprecated since version 3.0.0. Please use `GHOSTKIT.events.trigger` function instead.' + ); + } + + trigger(...args); +}; + +// Init. +events.on(document, 'init.gkt', () => { + trigger('beforeInit', classObject); + + window.requestAnimationFrame(() => { + trigger('afterInit', classObject); + }); +}); + +// Init blocks. +events.on(document, 'init.blocks.gkt', () => { + trigger('beforeInitBlocks', classObject); + trigger('initBlocks', classObject); + + trigger('beforePrepareNumberedLists', classObject); + trigger('beforePrepareCounters', classObject); + trigger('beforePrepareSR', classObject); + trigger('beforePrepareAccordions', classObject); + trigger('beforePrepareCarousels', classObject); + trigger('beforePrepareChangelog', classObject); + trigger('beforePrepareCountdown', classObject); + trigger('beforePrepareGist', classObject); + trigger('beforePrepareGoogleMaps', classObject); + trigger('beforePrepareTabs', classObject); + trigger('beforePrepareVideo', classObject); + + window.requestAnimationFrame(() => { + trigger('afterPrepareNumberedLists', classObject); + trigger('afterPrepareCounters', classObject); + trigger('afterPrepareSR', classObject); + trigger('afterPrepareAccordions', classObject); + trigger('afterPrepareCarousels', classObject); + trigger('afterPrepareChangelog', classObject); + trigger('afterPrepareCountdown', classObject); + trigger('afterPrepareGist', classObject); + trigger('afterPrepareGoogleMaps', classObject); + trigger('afterPrepareTabs', classObject); + trigger('afterPrepareVideo', classObject); + + trigger('afterInitBlocks', classObject); + }); +}); + +// Counters. +events.on(document, 'prepare.counter.gkt', ({ config }) => { + trigger('prepareCounters', classObject, config); +}); +events.on(document, 'counted.counter.gkt', ({ config }) => { + trigger('animatedCounters', classObject, config); +}); + +// Scroll Reveal. +events.on(document, 'prepare.scrollReveal.gkt', ({ config, target }) => { + trigger('beforePrepareSRStart', classObject, getElement(target)); + trigger('beforeInitSR', classObject, getElement(target), config); +}); +events.on(document, 'prepared.scrollReveal.gkt', ({ target }) => { + trigger('beforePrepareSREnd', classObject, getElement(target)); +}); + +// Accordion. +events.on( + document, + 'show.accordion.gkt hide.accordion.gkt', + ({ relatedTarget }) => { + trigger('toggleAccordionItem', classObject, getElement(relatedTarget)); + trigger( + 'activateAccordionItem', + classObject, + getElement(relatedTarget) + ); + } +); +events.on( + document, + 'shown.accordion.gkt hidden.accordion.gkt', + ({ relatedTarget }) => { + trigger( + 'afterActivateAccordionItem', + classObject, + getElement(relatedTarget) + ); + } +); + +// Alert. +events.on(document, 'closed.alert.gkt', ({ target }) => { + trigger('dismissedAlert', classObject, getElement(target)); +}); + +// Carousel. +events.on(document, 'touchStart.carousel.gkt', ({ target, originalEvent }) => { + trigger('swiperTouchStart', classObject, target.swiper, originalEvent); +}); +events.on(document, 'touchMove.carousel.gkt', ({ target, originalEvent }) => { + trigger('swiperTouchMove', classObject, target.swiper, originalEvent); +}); +events.on(document, 'touchEnd.carousel.gkt', ({ target, originalEvent }) => { + trigger('swiperTouchEnd', classObject, target.swiper, originalEvent); +}); + +// Google Maps. +events.on(document, 'prepare.googleMaps.gkt', ({ target }) => { + trigger('beforePrepareGoogleMapsStart', classObject, getElement(target)); +}); +events.on(document, 'prepared.googleMaps.gkt', ({ target, instance }) => { + trigger( + 'beforePrepareGoogleMapsEnd', + classObject, + getElement(target), + instance + ); +}); + +// Image Compare. +events.on(document, 'move.imageCompare.gkt', ({ target, originalEvent }) => { + trigger( + 'movedImageCompare', + classObject, + getElement(target), + originalEvent + ); +}); + +// Tabs. +events.on(document, 'show.tab.gkt', ({ target }) => { + window.requestAnimationFrame(() => { + const tabNameEncoded = target.getAttribute('href'); + trigger('activateTab', classObject, getElement(target), tabNameEncoded); + }); +}); + +// Video. +events.on(document, 'prepare.videoObserver.gkt', ({ config }) => { + trigger('prepareVideoObserver', classObject, config); +}); diff --git a/assets/js/helper.js b/assets/js/helper.js new file mode 100644 index 00000000..4da34539 --- /dev/null +++ b/assets/js/helper.js @@ -0,0 +1,129 @@ +import { getBlockType } from '@wordpress/blocks'; + +import EventHandler from './utils/event-handler'; +import Instance from './utils/instance'; + +const { + version, + pro, + + themeName, + settings, + media_sizes: mediaSizes, + disabledBlocks, + allowPluginColorPalette, + allowPluginCustomizer, + allowTemplates, + sidebars, + timezone, + + googleMapsAPIKey, + googleMapsAPIUrl, + + googleReCaptchaAPISiteKey, + googleReCaptchaAPISecretKey, + + icons, + shapes, + fonts, + customTypographyList, + + admin_url: adminUrl, + admin_templates_url: adminTemplatesUrl, +} = window.ghostkitVariables; + +// prepare media vars. +const vars = {}; +const screenSizes = []; +Object.keys(mediaSizes).forEach((k) => { + vars[`media_${k}`] = mediaSizes[k]; + screenSizes.push(mediaSizes[k]); +}); + +function escapeRegExp(s) { + return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +const GHOSTKIT = { + version, + pro, + + themeName, + settings, + + disabledBlocks, + + allowPluginColorPalette, + allowPluginCustomizer, + allowTemplates, + + vars, + replaceVars(str) { + Object.keys(this.vars).forEach((key) => { + str = str.replace( + new RegExp(`#{ghostkitvar:${escapeRegExp(key)}}`, 'g'), + `(max-width: ${this.vars[key]}px)` + ); + }); + + return str; + }, + + screenSizes, + sidebars, + timezone, + + googleMapsAPIKey, + googleMapsAPIUrl, + + googleReCaptchaAPISiteKey, + googleReCaptchaAPISecretKey, + + icons, + shapes, + fonts, + customTypographyList, + + adminUrl, + adminTemplatesUrl, + + /** + * Instance helper functions. + */ + instance: Instance, + + /** + * Events helper functions. + */ + events: EventHandler, + + /** + * Check for block support GhostKit features. + * + * @param {Mixed} block - block props / block name + * @param {string} featureName - feature name + * @param {Mixed} defaultVal - default return value + * + * @return {Mixed} - supports flag + */ + hasBlockSupport(block, featureName, defaultVal = false) { + if (typeof block === 'string' && wp && wp.blocks) { + if (getBlockType) { + block = getBlockType(block); + } + } + + if ( + block && + block.ghostkit && + block.ghostkit.supports && + typeof block.ghostkit.supports[featureName] !== 'undefined' + ) { + return block.ghostkit.supports[featureName]; + } + + return defaultVal; + }, +}; + +window.GHOSTKIT = GHOSTKIT; diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 00000000..b8175658 --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,23 @@ +import rafSchd from 'raf-schd'; +import { throttle } from 'throttle-debounce'; + +const { GHOSTKIT } = window; +const { events } = GHOSTKIT; + +events.trigger(document, 'init.gkt'); + +const initBlocksThrottled = throttle( + 200, + rafSchd(() => { + events.trigger(document, 'init.blocks.gkt'); + }) +); + +// Init blocks. +new window.MutationObserver(initBlocksThrottled).observe( + document.documentElement, + { + childList: true, + subtree: true, + } +); diff --git a/assets/js/utils/event-handler.js b/assets/js/utils/event-handler.js new file mode 100644 index 00000000..8fffa990 --- /dev/null +++ b/assets/js/utils/event-handler.js @@ -0,0 +1,423 @@ +/** + * Event handler helper function. + * Thanks to Bootstrap. + */ +import getjQuery from './get-jquery'; + +// TODO: once we move to the wp-scripts and it's eslint config, +// most probably these problems will be resolved. +/* eslint-disable prefer-const */ +/* eslint-disable no-continue */ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-plusplus */ + +/** + * Constants + */ +const namespaceRegex = /[^.]*(?=\..*)\.|.*/; +const stripNameRegex = /\..*/; +const stripUidRegex = /::\d+$/; +const eventRegistry = {}; // Events storage +let uidEvent = 1; +const customEvents = { + mouseenter: 'mouseover', + mouseleave: 'mouseout', +}; + +const nativeEvents = new Set([ + 'click', + 'dblclick', + 'mouseup', + 'mousedown', + 'contextmenu', + 'mousewheel', + 'DOMMouseScroll', + 'mouseover', + 'mouseout', + 'mousemove', + 'selectstart', + 'selectend', + 'keydown', + 'keypress', + 'keyup', + 'orientationchange', + 'touchstart', + 'touchmove', + 'touchend', + 'touchcancel', + 'pointerdown', + 'pointermove', + 'pointerup', + 'pointerleave', + 'pointercancel', + 'gesturestart', + 'gesturechange', + 'gestureend', + 'focus', + 'blur', + 'change', + 'reset', + 'select', + 'submit', + 'focusin', + 'focusout', + 'load', + 'unload', + 'beforeunload', + 'resize', + 'move', + 'DOMContentLoaded', + 'readystatechange', + 'error', + 'abort', + 'scroll', +]); + +/** + * Public methods + */ + +const EventHandler = {}; + +/** + * Private methods + */ + +function hydrateObj(obj, meta = {}) { + for (const [key, value] of Object.entries(meta)) { + try { + obj[key] = value; + } catch { + Object.defineProperty(obj, key, { + configurable: true, + get() { + return value; + }, + }); + } + } + + return obj; +} + +function makeEventUid(element, uid) { + return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++; +} + +function getElementEvents(element) { + const uid = makeEventUid(element); + + element.uidEvent = uid; + eventRegistry[uid] = eventRegistry[uid] || {}; + + return eventRegistry[uid]; +} + +function bootstrapHandler(element, fn) { + return function handler(event) { + hydrateObj(event, { delegateTarget: element }); + + if (handler.oneOff) { + EventHandler.off(element, event.type, fn); + } + + return fn.apply(element, [event]); + }; +} + +function bootstrapDelegationHandler(element, selector, fn) { + return function handler(event) { + const domElements = element.querySelectorAll(selector); + + for ( + let { target } = event; + target && target !== this; + target = target.parentNode + ) { + // eslint-disable-next-line no-restricted-syntax + for (const domElement of domElements) { + if (domElement !== target) { + continue; + } + + hydrateObj(event, { delegateTarget: target }); + + if (handler.oneOff) { + EventHandler.off(element, event.type, selector, fn); + } + + return fn.apply(target, [event]); + } + } + + return false; + }; +} + +function findHandler(events, callable, delegationSelector = null) { + return Object.values(events).find( + (event) => + event.callable === callable && + event.delegationSelector === delegationSelector + ); +} + +function getTypeEvent(event) { + // allow to get the native events from namespaced events ('click.ghostkit.button' --> 'click') + event = event.replace(stripNameRegex, ''); + return customEvents[event] || event; +} + +function normalizeParameters(originalTypeEvent, handler, delegationFunction) { + const isDelegated = typeof handler === 'string'; + // TODO: tooltip passes `false` instead of selector, so we need to check + const callable = isDelegated + ? delegationFunction + : handler || delegationFunction; + let typeEvent = getTypeEvent(originalTypeEvent); + + if (!nativeEvents.has(typeEvent)) { + typeEvent = originalTypeEvent; + } + + return [isDelegated, callable, typeEvent]; +} + +function addHandler( + element, + originalTypeEvent, + handler, + delegationFunction, + oneOff +) { + let [isDelegated, callable, typeEvent] = normalizeParameters( + originalTypeEvent, + handler, + delegationFunction + ); + + // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position + // this prevents the handler from being dispatched the same way as mouseover or mouseout does + if (originalTypeEvent in customEvents) { + const wrapFunction = (fn) => { + return function (event) { + if ( + !event.relatedTarget || + (event.relatedTarget !== event.delegateTarget && + !event.delegateTarget.contains(event.relatedTarget)) + ) { + return fn.call(this, event); + } + + return false; + }; + }; + + callable = wrapFunction(callable); + } + + const events = getElementEvents(element); + const handlers = events[typeEvent] || (events[typeEvent] = {}); + const previousFunction = findHandler( + handlers, + callable, + isDelegated ? handler : null + ); + + if (previousFunction) { + previousFunction.oneOff = previousFunction.oneOff && oneOff; + + return; + } + + const uid = makeEventUid( + callable, + originalTypeEvent.replace(namespaceRegex, '') + ); + const fn = isDelegated + ? bootstrapDelegationHandler(element, handler, callable) + : bootstrapHandler(element, callable); + + fn.delegationSelector = isDelegated ? handler : null; + fn.callable = callable; + fn.oneOff = oneOff; + fn.uidEvent = uid; + handlers[uid] = fn; + + element.addEventListener(typeEvent, fn, isDelegated); +} + +function removeHandler( + element, + events, + typeEvent, + handler, + delegationSelector +) { + const fn = findHandler(events[typeEvent], handler, delegationSelector); + + if (!fn) { + return; + } + + element.removeEventListener(typeEvent, fn, Boolean(delegationSelector)); + delete events[typeEvent][fn.uidEvent]; +} + +function removeNamespacedHandlers(element, events, typeEvent, namespace) { + const storeElementEvent = events[typeEvent] || {}; + + for (const [handlerKey, event] of Object.entries(storeElementEvent)) { + if (handlerKey.includes(namespace)) { + removeHandler( + element, + events, + typeEvent, + event.callable, + event.delegationSelector + ); + } + } +} + +/** + * Public methods + */ + +EventHandler.on = function (element, event, handler, delegationFunction) { + if (typeof event !== 'string' || !element) { + return; + } + + event.split(' ').forEach((originalTypeEvent) => { + addHandler( + element, + originalTypeEvent, + handler, + delegationFunction, + false + ); + }); +}; + +EventHandler.one = function (element, event, handler, delegationFunction) { + if (typeof event !== 'string' || !element) { + return; + } + + event.split(' ').forEach((originalTypeEvent) => { + addHandler( + element, + originalTypeEvent, + handler, + delegationFunction, + true + ); + }); +}; + +EventHandler.off = function (element, event, handler, delegationFunction) { + if (typeof originalTypeEvent !== 'string' || !element) { + return; + } + + event.split(' ').forEach((originalTypeEvent) => { + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const [isDelegated, callable, typeEvent] = normalizeParameters( + originalTypeEvent, + handler, + delegationFunction + ); + const inNamespace = typeEvent !== originalTypeEvent; + const events = getElementEvents(element); + const storeElementEvent = events[typeEvent] || {}; + + if (typeof callable !== 'undefined') { + // Simplest case: handler is passed, remove that listener ONLY. + if (!Object.keys(storeElementEvent).length) { + return; + } + + removeHandler( + element, + events, + typeEvent, + callable, + isDelegated ? handler : null + ); + return; + } + + const isNamespace = originalTypeEvent.startsWith('.'); + + if (isNamespace) { + for (const elementEvent of Object.keys(events)) { + removeNamespacedHandlers( + element, + events, + elementEvent, + originalTypeEvent.slice(1) + ); + } + } + + for (const [keyHandlers, evt] of Object.entries(storeElementEvent)) { + const handlerKey = keyHandlers.replace(stripUidRegex, ''); + + if (!inNamespace || originalTypeEvent.includes(handlerKey)) { + removeHandler( + element, + events, + typeEvent, + evt.callable, + evt.delegationSelector + ); + } + } + }); +}; + +EventHandler.trigger = function (element, event, args) { + if (typeof event !== 'string' || !element) { + return null; + } + + const $ = getjQuery(); + const typeEvent = getTypeEvent(event); + const inNamespace = event !== typeEvent; + + let jQueryEvent = null; + let bubbles = true; + let nativeDispatch = true; + let defaultPrevented = false; + + if (inNamespace && $) { + jQueryEvent = $.Event(event, args); + + $(element).trigger(jQueryEvent); + bubbles = !jQueryEvent.isPropagationStopped(); + nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); + defaultPrevented = jQueryEvent.isDefaultPrevented(); + } + + const evt = hydrateObj( + new Event(event, { bubbles, cancelable: true }), + args + ); + + if (defaultPrevented) { + evt.preventDefault(); + } + + if (nativeDispatch) { + element.dispatchEvent(evt); + } + + if (evt.defaultPrevented && jQueryEvent) { + jQueryEvent.preventDefault(); + } + + return evt; +}; + +export default EventHandler; diff --git a/assets/js/utils/get-jquery.js b/assets/js/utils/get-jquery.js new file mode 100644 index 00000000..b7eed2d5 --- /dev/null +++ b/assets/js/utils/get-jquery.js @@ -0,0 +1,7 @@ +export default function getjQuery() { + if (window.jQuery) { + return window.jQuery; + } + + return null; +} diff --git a/assets/js/utils/instance.js b/assets/js/utils/instance.js new file mode 100644 index 00000000..0003b950 --- /dev/null +++ b/assets/js/utils/instance.js @@ -0,0 +1,53 @@ +/** + * Helper class to get and set instances from elements. + * These instances contains many helpful data such as block APIs and extensions data. + * + * Example: + * GHOSTKIT.instance.get(element, 'effects'); + */ +const elementsMap = new Map(); + +export default { + getAll() { + return elementsMap; + }, + + get(element, key) { + if (elementsMap.has(element)) { + if (key) { + return elementsMap.get(element).get(key) || null; + } + + return elementsMap.get(element) || null; + } + + return null; + }, + + set(element, key, val) { + if (!elementsMap.has(element)) { + elementsMap.set(element, new Map()); + } + + const instanceMap = elementsMap.get(element); + + instanceMap.set(key, val); + }, + + remove(element, key) { + if (!elementsMap.has(element)) { + return; + } + + const instanceMap = elementsMap.get(element); + + if (key) { + instanceMap.delete(key); + } + + // Free up element references if there are no instances left for an element or key is missing. + if (!key || instanceMap.size === 0) { + elementsMap.delete(element); + } + }, +}; diff --git a/assets/js/utils/transition-callback.js b/assets/js/utils/transition-callback.js new file mode 100644 index 00000000..562c96c6 --- /dev/null +++ b/assets/js/utils/transition-callback.js @@ -0,0 +1,69 @@ +const MILLISECONDS_MULTIPLIER = 1000; + +function getTransitionDurationFromElement(element) { + if (!element) { + return 0; + } + + // Get transition-duration of the element + let { transitionDuration, transitionDelay } = + window.getComputedStyle(element); + + const floatTransitionDuration = Number.parseFloat(transitionDuration); + const floatTransitionDelay = Number.parseFloat(transitionDelay); + + // Return 0 if element or transition duration is not found + if (!floatTransitionDuration && !floatTransitionDelay) { + return 0; + } + + // If multiple durations are defined, take the first + [transitionDuration] = transitionDuration.split(','); + [transitionDelay] = transitionDelay.split(','); + + return ( + (Number.parseFloat(transitionDuration) + + Number.parseFloat(transitionDelay)) * + MILLISECONDS_MULTIPLIER + ); +} + +function execute(possibleCallback, args = [], defaultValue = possibleCallback) { + return typeof possibleCallback === 'function' + ? possibleCallback(...args) + : defaultValue; +} + +export default function transitionCallback( + callback, + transitionElement, + waitForTransition = true +) { + if (!waitForTransition) { + execute(callback); + return; + } + + const durationPadding = 5; + const emulatedDuration = + getTransitionDurationFromElement(transitionElement) + durationPadding; + + let called = false; + + const handler = ({ target }) => { + if (target !== transitionElement) { + return; + } + + called = true; + transitionElement.removeEventListener('transitionend', handler); + execute(callback); + }; + + transitionElement.addEventListener('transitionend', handler); + setTimeout(() => { + if (!called) { + transitionElement.dispatchEvent(new Event('transitionend')); + } + }, emulatedDuration); +} diff --git a/assets/vendor/gist-simple/dist/gist-simple.css b/assets/vendor/gist-simple/dist/gist-simple.css new file mode 100644 index 00000000..f8805994 --- /dev/null +++ b/assets/vendor/gist-simple/dist/gist-simple.css @@ -0,0 +1,46 @@ +.gist-simple .gist-simple-wrap { + font-size: 16px; + margin: 0; + margin-bottom: 1em; + padding: 8px; + border: 1px solid #ddd; + border-radius: 3px; + color: #333; + text-align: left; + background: #fff; +} + +.gist-simple-loading-icon { + display: flex; + align-items: center; + justify-content: center; + user-select: none; + margin: 0 auto; + padding: 1.2em; +} +.gist-simple-loading-icon > i { + display: inline-block; + width: 0.25em; + height: 0.25em; + margin: 0 calc(0.25em / 2 * 1); + border-radius: 50%; + opacity: 0.2; + background-color: currentColor; + animation: gist-simple-loading-blink 1.4s infinite both; +} +.gist-simple-loading-icon > i:nth-child(2) { + animation-delay: 0.2s; +} +.gist-simple-loading-icon > i:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes gist-simple-loading-blink { + 0%, + 100% { + opacity: 0.2; + } + 30% { + opacity: 1; + } +} diff --git a/assets/vendor/gist-simple/dist/gist-simple.min.js b/assets/vendor/gist-simple/dist/gist-simple.min.js new file mode 100644 index 00000000..0de90227 --- /dev/null +++ b/assets/vendor/gist-simple/dist/gist-simple.min.js @@ -0,0 +1,6 @@ +/*! + * Gist Simple v2.0.1 (https://github.com/nk-o/gist-simple) + * Copyright 2023 nK + * Licensed under MIT (https://github.com/nk-o/gist-simple/blob/master/LICENSE) + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).gistSimple=t()}(this,(function(){"use strict";let e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};var t=e;function n(e,...t){return e=e||{},Object.keys(t).forEach((n=>{t[n]&&Object.keys(t[n]).forEach((o=>{e[o]=t[n][o]}))})),e}function o(){this.doneCallbacks=[],this.failCallbacks=[]}o.prototype={execute(e,t){let n=e.length;for(t=Array.prototype.slice.call(t);n;)n-=1,e[n].apply(null,t)},resolve(...e){this.execute(this.doneCallbacks,e)},reject(...e){this.execute(this.failCallbacks,e)},done(e){this.doneCallbacks.push(e)},fail(e){this.failCallbacks.push(e)}};const i="__gist_simple_jsonp__";const s="__gist_simple_css_loaded__";var l={id:"",file:"",caption:"",lines:"",linesExpanded:!1,highlightLines:"",showFooter:!0,showLineNumbers:!0,enableCache:!0,onInit:null,onInitEnd:null,onDestroy:null,onDestroyEnd:null,onAjaxBeforeSend:null,onAjaxSuccess:null,onAjaxLoaded:null};const r={};let c=0;class a{constructor(e,t){const o=this;o.instanceID=c,c+=1,o.$container=e,o.defaults={...l};const i=o.$container.dataset||{},s={};Object.keys(i).forEach((e=>{e&&void 0!==o.defaults[e]&&(s[e]=i[e])})),o.options=n({},o.defaults,s,t),o.pureOptions=n({},o.options),Object.keys(o.options).forEach((e=>{"true"===o.options[e]?o.options[e]=!0:"false"===o.options[e]&&(o.options[e]=!1)})),o.init()}init(){const e=this,{options:t}=e,n=`https://gist.github.com/${t.id}.json`,{lines:l}=t,c={};if(e.options.onInit&&e.options.onInit.call(e),t.file&&(c.file=t.file),e.$container.classList.add("gist-simple"),!t.id)return void e.insertContent("Gist ID is required",!0);const a=n+t.file,d=t.enableCache||r[a];function p(n){const o=document.createElement("div");o.innerHTML=n.div,o.firstChild&&o.firstChild.removeAttribute("id"),e.insertContent(o.innerHTML),e.highlightLines(t.highlightLines),e.showSpecificLines(l,t.linesExpanded),e.showCaption(t.caption),t.showFooter||e.removeFooter(),t.showLineNumbers||e.removeLineNumbers(),e.options.onAjaxLoaded&&e.options.onAjaxLoaded.call(e,n)}function u(t){if(t&&t.div){let{stylesheet:n}=t;n?(0===n.indexOf("{o[s]=!0,t(o)}),!1)}}(n,(()=>{p(t)}),e.$container.ownerDocument)):p(t)}else e.insertContent(`Failed loading gist ${n}`,!0)}function f(t){e.insertContent(`Failed loading gist ${n}: ${t}`,!0)}e.insertContent('',!0),function(e,t){const{data:n={},beforeSend:o,success:s}=t;if(window[i]=(window[i]||0)+1,n.callback=`${i}_cb_${window[i]}`,Object.keys(n).forEach((t=>{e.match(/\?/)?e+=`&${t}=${n[t]}`:e+=`?${t}=${n[t]}`})),o&&!o())return;let l=document.createElement("script");l.type="text/javascript",l.src=e,window[n.callback]=function(e){s.call(window,e),document.head.removeChild(l),l=null,delete window[n.callback]},document.head.appendChild(l)}(n,{data:c,beforeSend(){if(e.options.onAjaxBeforeSend&&e.options.onAjaxBeforeSend.call(e),d){if(r[a])return r[a].div?(u(r[a]),!1):(r[a].done((e=>{u(e)})),r[a].fail((e=>{f(e)})),!1);r[a]=new o}return!0},success(t){e.options.onAjaxSuccess&&e.options.onAjaxSuccess.call(e,t),d&&r[a]&&r[a].resolve&&(r[a].resolve(t),r[a]=t),u(t)},error(e){f(e)}}),e.options.onInitEnd&&e.options.onInitEnd.call(e)}destroy(){const e=this;e.options.onDestroy&&e.options.onDestroy.call(e),e.$container.innerHTML="",delete e.$container.GistSimple,e.options.onDestroyEnd&&e.options.onDestroyEnd.call(e)}chunkBy(e,t){return 0===e.length?[]:e.slice(1).reduce(((e,n)=>(t(n)?e.push([n]):e.push(e.pop().concat([n])),e)),[e.slice(0,1)])}getLineNumbers(e){const t=[];let n,o;if("number"==typeof e)t.push(e);else{o=e.split(",");for(let e=0;e${e}`),this.$container.innerHTML=e}highlightLines(e){if(!e)return;const t=this.getLineNumbers(e);this.$container.querySelectorAll("td.line-data").forEach((e=>{e.style.width="100%"})),this.$container.querySelectorAll(".js-file-line").forEach(((e,n)=>{-1!==t.indexOf(n+1)&&(e.style.backgroundColor="rgb(255, 255, 204)")}))}showSpecificLines(e,t){if(!e)return;const n=this.getLineNumbers(e),o=[];if(this.$container.querySelectorAll(".js-file-line").forEach(((e,i)=>{-1===n.indexOf(i+1)&&(t?(o.push(i+1),e.parentNode.style.display="none"):e.parentNode.remove())})),t){this.chunkBy(o,(e=>!o.includes(e-1))).forEach((e=>{const t=e[0],n=t-1,o=e[e.length-1],i=document.createElement("a");i.setAttribute("lines",e.join()),i.style.display="block",i.style.cursor="pointer",i.innerHTML='\n \n',i.addEventListener("click",(e=>{e.preventDefault(),i.closest("table.highlight").querySelectorAll('tr[style*="display: none"] td[data-line-number]').forEach((function(e){const t=i.getAttribute("lines").split(","),n=e.getAttribute("data-line-number");-1!==t.indexOf(n)&&(e.parentNode.style.display="")})),i.closest("tr").remove()}));const s=`\n