diff --git a/.distignore b/.distignore index 0d5b7613..9e3a99ca 100644 --- a/.distignore +++ b/.distignore @@ -28,3 +28,6 @@ phpstan-baseline.neon .gitkeep .wordpress-org AGENTS.md +phpcs.baseline.xml +.eslintrc.json +.prettierignore \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..4ed2e999 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "extends": ["plugin:@wordpress/eslint-plugin/recommended"], + "globals": { + "jQuery": "readonly", + "$": "readonly" + }, + "rules": { + "camelcase": "off" + }, + "ignorePatterns": ["**/*.min.js", "**/*.bundle.js", "js/inputmask/**", "js/sweetalert/**", "tests/e2e/**", "backend/assets/**"] +} diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..27d00fba --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,38 @@ +name: "Copilot Setup Steps" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + + # You can define any steps you want, and they will run before the agent starts. + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: "20" + cache: "npm" + - name: Install Agent Browser + run: | + npm install -g agent-browser + agent-browser install + - name: Install NPM deps + run: | + npm ci + npm install -g playwright-cli + npx playwright install --with-deps chromium + - name: Install composer deps + run: composer install --no-dev + - name: Install environment + run: | + npm run wp-env start diff --git a/.github/workflows/create-build-zip.yml b/.github/workflows/create-build-zip.yml index c94d80b9..2dfe404b 100644 --- a/.github/workflows/create-build-zip.yml +++ b/.github/workflows/create-build-zip.yml @@ -18,17 +18,17 @@ jobs: git-sha-8: ${{ steps.retrieve-git-sha-8.outputs.sha8 }} steps: - name: Check out source files - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + uses: actions/checkout@master + - uses: actions/setup-node@v6 with: - node-version: "18" - cache: "yarn" + node-version: "20" + cache: "npm" - name: Install composer deps run: composer install --no-dev --prefer-dist --no-progress --no-suggest - - name: Install yarn deps - run: yarn install --frozen-lockfile + - name: Install npm deps + run: npm ci - name: Build files - run: yarn run build + run: npm run build - name: Bump the plugin version run: | CURRENT_VERSION=$(node -p -e "require('./package.json').version") @@ -36,7 +36,7 @@ jobs: DEV_VERSION="${CURRENT_VERSION}-dev.${COMMIT_HASH}" npm run grunt version::${DEV_VERSION} - name: Create zip - run: yarn run dist + run: npm run dist - name: Retrieve branch name id: retrieve-branch-name run: echo "::set-output name=branch_name::$(REF=${GITHUB_HEAD_REF:-$GITHUB_REF} && echo ${REF#refs/heads/} | sed 's/\//-/g')" diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 22a2fdb4..d8af80fd 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -10,23 +10,23 @@ jobs: if: "! contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - uses: actions/checkout@master with: persist-credentials: false - name: Build files using ${{ matrix.node-version }} - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - cache: "yarn" + cache: "npm" - name: Release new version id: release run: | - yarn install --frozen-lockfile + npm ci composer install --prefer-dist --no-progress --no-suggest - yarn run build - yarn run release + npm run build + npm run release env: CI: true GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/.github/workflows/deploy-s3-store.yml b/.github/workflows/deploy-s3-store.yml index 19f6e879..648f8e75 100644 --- a/.github/workflows/deploy-s3-store.yml +++ b/.github/workflows/deploy-s3-store.yml @@ -10,17 +10,17 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master - - name: Setup node 18 - uses: actions/setup-node@v4 + - name: Setup node 20 + uses: actions/setup-node@v6 with: - node-version: 18.x - cache: "yarn" + node-version: 20.x + cache: "npm" - name: Build & create dist/artifact run: | - yarn install --frozen-lockfile + npm ci composer install --no-dev --prefer-dist --no-progress --no-suggest - yarn run build - yarn run dist + npm run build + npm run dist - name: Upload Latest Version to S3 uses: jakejarvis/s3-sync-action@master with: diff --git a/.github/workflows/deploy-svn.yml b/.github/workflows/deploy-svn.yml index b71e509b..b6f2ae32 100644 --- a/.github/workflows/deploy-svn.yml +++ b/.github/workflows/deploy-svn.yml @@ -11,11 +11,11 @@ jobs: - uses: actions/checkout@master - name: Build run: | - yarn install --frozen-lockfile - yarn run build + npm ci + npm run build composer install --no-dev --prefer-dist --no-progress --no-suggest - name: WordPress Plugin Deploy uses: 10up/action-wordpress-plugin-deploy@master env: SVN_PASSWORD: ${{ secrets.SVN_THEMEISLE_PASSWORD }} - SVN_USERNAME: ${{ secrets.SVN_THEMEISLE_USERNAME }} \ No newline at end of file + SVN_USERNAME: ${{ secrets.SVN_THEMEISLE_USERNAME }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 53e97a17..02d4c8d7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -14,14 +14,14 @@ jobs: fail-fast: false runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@master + - uses: actions/setup-node@v6 with: - node-version: "18" - cache: "yarn" + node-version: "20" + cache: "npm" - name: Install NPM deps run: | - yarn install --frozen-lockfile + npm ci npm install -g playwright-cli npx playwright install --with-deps chromium - name: Install composer deps @@ -29,8 +29,6 @@ jobs: - name: Install environment run: | npm run wp-env start - - name: Prepare Database - run: bash ./bin/e2e-after-setup.sh - name: Run the tests run: | npm run test:e2e diff --git a/.github/workflows/sync-branches.yml b/.github/workflows/sync-branches.yml index 50592488..e25bd239 100644 --- a/.github/workflows/sync-branches.yml +++ b/.github/workflows/sync-branches.yml @@ -2,7 +2,7 @@ name: Sync branches on: push: branches: - - 'master' + - "master" jobs: sync-branch: runs-on: ubuntu-latest @@ -19,4 +19,4 @@ jobs: type: now from_branch: master target_branch: development - github_token: ${{ secrets.BOT_TOKEN }} \ No newline at end of file + github_token: ${{ secrets.BOT_TOKEN }} diff --git a/.github/workflows/sync-wporg-assets.yml b/.github/workflows/sync-wporg-assets.yml index 9eb055c1..ba2b33ed 100644 --- a/.github/workflows/sync-wporg-assets.yml +++ b/.github/workflows/sync-wporg-assets.yml @@ -5,8 +5,8 @@ on: branches: - master paths: - - 'readme.txt' - - '.wordpress-org/**' + - "readme.txt" + - ".wordpress-org/**" jobs: run: runs-on: ubuntu-22.04 diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index ebe5b83b..fddab970 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -10,21 +10,19 @@ jobs: name: Phpunit runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@master + - uses: actions/setup-node@v6 with: - node-version: "18" - cache: "yarn" + node-version: "20" + cache: "npm" - name: Install NPM deps run: | - yarn install --frozen-lockfile + npm ci - name: Install composer deps run: composer install - name: Install environment run: | npm run wp-env start - - name: Prepare Database - run: bash ./bin/e2e-after-setup.sh - name: Run the tests run: | npm run test:unit:php @@ -40,9 +38,25 @@ jobs: php-version: "7.4" extensions: simplexml - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@master - name: Install composer run: | composer install --no-progress - name: Run phpstan run: composer run phpstan + phpcs: + name: PHPCS + runs-on: ubuntu-latest + steps: + - name: Setup PHP version + uses: shivammathur/setup-php@v2 + with: + php-version: "7.4" + extensions: simplexml + - name: Checkout source code + uses: actions/checkout@master + - name: Install composer + run: | + composer install --no-progress + - name: Run linter for PHP + run: composer run lint diff --git a/.gitignore b/.gitignore index b4eb628d..94b3e277 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ vendor languages/woocommerce-product-addon.pot *.log artifacts -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +tests/e2e/fixtures/generated/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..f328cd2f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +**/*.min.js +**/*.bundle.js +js/inputmask/** +js/select2.js +js/sweetalert/** +backend/assets/** diff --git a/.wp-env.json b/.wp-env.json index ffc3cc43..105a6400 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,7 +1,12 @@ { - "plugins": [ - "https://downloads.wordpress.org/plugin/woocommerce.9.3.3.zip", - "." - ], - "phpVersion": "8.1" -} \ No newline at end of file + "$schema": "https://schemas.wp.org/trunk/wp-env.json", + "plugins": [ + "https://downloads.wordpress.org/plugin/woocommerce.zip", + "." + ], + "mappings": { + "wp-content/mu-plugins": "./bin/wp-env/mu-plugins" + }, + "phpVersion": "8.1", + "autoPort": true +} diff --git a/AGENTS.md b/AGENTS.md index 99ff6f53..706cbe71 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,14 +22,13 @@ npm run dist ```bash # PHPUnit — requires Docker (wp-env) -npm run test:unit:php:setup # Start WordPress Docker environment -bash ./bin/e2e-after-setup.sh # Prepare database/test data +npm run env:setup # Start WordPress Docker environment npm run test:unit:php # Run PHPUnit tests # Run a single PHPUnit test file wp-env run --env-cwd='wp-content/plugins/woocommerce-product-addon' tests-wordpress vendor/bin/phpunit -c phpunit.xml --filter TestClassName -# E2E tests (Playwright, Chromium only) +# E2E tests (Playwright, Chromium only) that runs on docker. npm run test:e2e npm run test:e2e:debug # Opens Playwright UI @@ -38,28 +37,38 @@ composer run phpstan # PHPStan level 6 composer run phpstan:generate:baseline ``` +You can also use `agent-browser` CLI if available with WP Docker environments for a more interactive testing experience, with credentials: + +``` +Username: admin +Password: password +``` + ## Code Quality - **PHP standard**: Themeisle ruleset (WordPress-based) via `phpcs.xml`. Text domain: `woocommerce-product-addon`. -- **PHPStan**: Level 6 with a large baseline file (`phpstan-baseline.neon`). Scans `inc/`, `classes/`, `backend/`, `templates/`, and the main plugin file. -- **Min PHP**: 7.2 (composer platform config). CI runs PHPStan on PHP 7.4. +- **PHPStan**: Level 8 with a large baseline file (`phpstan-baseline.neon`). Scans `inc/`, `classes/`, `backend/`, `templates/`, and the main plugin file. +- **Min PHP**: 7.4 +- **PHPDoc `@see`**: Use for cross-file related entry points (especially `inc/` versus `classes/`). Optional file-level docblocks may list **two to four** canonical functions or methods as a small index; avoid long or misleading lists—use `./ARCHITECTURE.md` and the overview above for layout. ## Architecture +You can read more about it on `./ARCHITECTURE.md`, but here’s a high-level overview of the main components and their relationships. + ### Entry Point & Bootstrap `woocommerce-product-addon.php` — defines constants, loads Composer autoload, manually `require_once`s all class/include files (no PSR-4 autoloading for plugin code), then hooks `PPOM()` on `woocommerce_init`. -### Core Classes (all singleton pattern via `get_instance()`) +### Core Classes -| Class | File | Role | -|---|---|---| -| `NM_PersonalizedProduct` | `classes/plugin.class.php` | Main plugin — registers all WooCommerce hooks, loads input types | -| `NM_PersonalizedProduct_Admin` | `classes/admin.class.php` | Admin-only — loaded only in `is_admin()` | -| `PPOM_Meta` | `classes/ppom.class.php` | Field group CRUD against custom DB table | -| `PPOM_Form` | `classes/form.class.php` | Frontend form rendering | -| `PPOM_Fields_Meta` | `classes/fields.class.php` | Field type registry and metadata | -| `PPOM_Inputs` | `classes/input.class.php` | Input type manager | +| Class | File | Role | +| ------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `NM_PersonalizedProduct` | `classes/plugin.class.php` | Main plugin — registers all WooCommerce hooks, loads input types | +| `NM_PersonalizedProduct_Admin` | `classes/admin.class.php` | Admin-only coordinator for menus, settings, attach flows, and admin AJAX | +| `PPOM_Meta` | `classes/ppom.class.php` | Product-side field-group resolver that reads attached groups and loads settings/fields from the custom DB table | +| `PPOM_Form` | `classes/form.class.php` | Frontend form rendering | +| `PPOM_Fields_Meta` | `classes/fields.class.php` | Admin field-builder UI, modals, and builder asset loading | +| `PPOM_Inputs` | `classes/input.class.php` | Input type manager | ### Input Type System @@ -68,6 +77,7 @@ Each input type has a class in `classes/inputs/` (e.g. `input.text.php`, `input. ### Include Files (`inc/`) Procedural utility code organized by concern: + - `functions.php` — general helpers - `hooks.php` — filter/action callbacks - `validation.php` — server-side field validation @@ -116,14 +126,49 @@ Declares WooCommerce Custom Order Tables compatibility via `FeaturesUtil::declar ## WooCommerce Security + Workflow -- Treat all input as untrusted (POST/AJAX/cart session/order meta). -- For state-changing actions, require both capability checks and nonce verification. -- Never trust frontend option pricing; recompute server-side in cart/checkout. -- Validate product/variation context with Woo objects before processing. -- Sanitize on input (type-aware) and escape on output (context-aware). -- Use `$wpdb->prepare()` (plus `$wpdb->esc_like()` for LIKE queries); never concatenate user input. -- For uploads, enforce extension/mime/size rules and block executable files. -- Prefer WooCommerce CRUD/order APIs (HPOS-safe) over direct post/meta SQL. -- Keep pricing hooks idempotent, especially in `woocommerce_before_calculate_totals`. +### Trust Boundaries + +- Treat product page fields, AJAX/REST payloads, cart item data, restored sessions, order item meta, and admin imports/settings as untrusted input. +- Never trust browser-sent prices, fee amounts, labels, field IDs, variation IDs, conditional flags, upload metadata, or Pro gating flags. Recompute from saved PPOM/WooCommerce configuration on the server. +- Validate submitted field names/options against the field schema attached to the current product/meta group before storing or pricing anything. +- Resolve product and variation IDs to real WooCommerce objects and confirm the variation belongs to the parent product before processing. + +### Authorization + Request Integrity + +- For every state-changing admin, AJAX, or REST action, require both a capability check and nonce verification. For REST routes, always implement a strict `permission_callback`. +- Never use `is_admin()` as an authorization check. +- Scope privileged actions to the narrowest capability that fits the action: field-group CRUD, settings changes, file deletion, import/export, license actions, and diagnostic tools should not share a blanket permission model. + +### Data Handling Rules + +- Sanitize on input with type-appropriate functions, validate against business rules, and escape on output with the correct context-aware `esc_*()` function. +- Use `$wpdb->prepare()` for every query containing dynamic input, and pair `LIKE` clauses with `$wpdb->esc_like()`. Never concatenate request data into SQL. +- Prefer WooCommerce CRUD APIs and order/item meta APIs over direct post/meta SQL so behavior stays HPOS-safe. +- Do not persist raw `$_POST` or `$_REQUEST` payloads into cart item data, session data, or order meta. Store only the normalized values the plugin actually needs. +- Do not expose addon values, upload URLs, or order item metadata in logs, notices, REST responses, emails, or templates unless the current user/context is explicitly allowed to see them. + +### Pricing + Cart Integrity + +- Keep all pricing logic server-authoritative and idempotent. Hooks like `woocommerce_before_calculate_totals` may run multiple times per request. +- Recalculate addon totals from canonical field definitions during validation, cart restore, and checkout instead of trusting values carried forward from the browser or session. +- Guard against double-charging when cart items are restored from session, when quantities change, or when multiple pricing hooks run in sequence. +- When pricing depends on product type, variation, quantity, tax mode, currency, or coupon state, use current WooCommerce objects/state at calculation time instead of stale cart snapshots. + +### Upload Safety + +- Enforce an allowlist for extensions, MIME types, and size limits; reject executable/scriptable files, double extensions, and unexpected archive types. +- Generate filenames and paths server-side, keep uploads inside the dedicated PPOM upload directory, and never trust client-provided path, MIME, or filename values. +- Re-check authorization and attachment ownership before serving, deleting, or attaching uploaded files to cart/order data. + +### WooCommerce Lifecycle + - Preferred option lifecycle hooks: `woocommerce_add_to_cart_validation` -> `woocommerce_add_cart_item_data` -> `woocommerce_get_cart_item_from_session` -> `woocommerce_get_item_data` -> `woocommerce_checkout_create_order_line_item`. -- Minimum regression checks per addon: simple/variable products, guest/logged-in checkout, tax modes, coupons/sales, session restore, and (if enabled) multi-currency/multi-language. +- Validate as early as possible, normalize before storing in cart data, and only persist to order items after the cart payload has been revalidated. +- Treat session restore, reorder, and edit-cart flows as fresh untrusted input, not trusted historical state. + +### Minimum Regression Checks + +- Per addon/security-sensitive change, cover simple and variable products, guest and logged-in checkout, tax modes, coupons/sales, quantity changes, and session restore. +- If uploads are involved, test invalid MIME/extension cases, oversized files, duplicate filenames, and cleanup paths. +- If REST, AJAX, or admin writes are involved, explicitly test success, nonce failure, and capability failure paths. +- If enabled in the target store, also verify multi-currency, multi-language, and HPOS behavior. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 066a079a..016d9655 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,7 +83,7 @@ To set-up the environment you can follow the example from `test-php.yml` or the To run with Docker: ```bash -npm run test:unit:php:setup # start the wordpress instance +npm run env:setup # start the wordpress instance npm run test:unit:php # run the wp tests inside the tests-wordpress container ``` @@ -97,7 +97,6 @@ npm install -g playwright-cli npx playwright install --with-deps chromium npm run wp-env start # start the wordpress instance -bash ./bin/e2e-after-setup.sh # create some woocommerce products (run it only once if you do not delete the previous instance) on the test instance npm run test:e2e # run the tests in the CLI ``` diff --git a/Gruntfile.js b/Gruntfile.js index dd1245ed..ef852f63 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,64 +1,64 @@ -const path = require('path') +const path = require( 'path' ); const paths = { - global: { - config: path.join(__dirname, 'grunt/'), - grunt: path.join(__dirname, 'grunt/'), - }, - config: 'grunt/', - grunt: 'grunt/', - languages: 'languages/', - logs: 'logs/', - images: 'images/', - vendor: 'packages/', - css: 'assets/css/' + global: { + config: path.join( __dirname, 'grunt/' ), + grunt: path.join( __dirname, 'grunt/' ), + }, + config: 'grunt/', + grunt: 'grunt/', + languages: 'languages/', + logs: 'logs/', + images: 'images/', + vendor: 'packages/', + css: 'assets/css/', }; const taskMap = { - addtextdomain: 'grunt-wp-i18n', - wp_readme_to_markdown: 'grunt-wp-readme-to-markdown', -} + addtextdomain: 'grunt-wp-i18n', + wp_readme_to_markdown: 'grunt-wp-readme-to-markdown', +}; const files = { - php: [ - '*.php', - '**/*.php', - '!.git/**', - '!vendor/**', - '!node_modules/**', - '!logs/**' - ], - css: [ - '*.css', - '**/*.css', - '!*.min.css', - '!**/*.min.css', - '!vendor/**', - '!node_modules/**', - '!logs/**' - ], - js: [ - '*.js', - '**/*.js', - '!*.min.js', - '!**/*.min.js', - '!.git/**', - '!vendor/**', - '!node_modules/**', - '!logs/**' - ] -} + php: [ + '*.php', + '**/*.php', + '!.git/**', + '!vendor/**', + '!node_modules/**', + '!logs/**', + ], + css: [ + '*.css', + '**/*.css', + '!*.min.css', + '!**/*.min.css', + '!vendor/**', + '!node_modules/**', + '!logs/**', + ], + js: [ + '*.js', + '**/*.js', + '!*.min.js', + '!**/*.min.js', + '!.git/**', + '!vendor/**', + '!node_modules/**', + '!logs/**', + ], +}; -const gruntConfig = (grunt) => { - 'use strict' +const gruntConfig = ( grunt ) => { + 'use strict'; - const config = {} - const loader = require('load-project-config') + const config = {}; + const loader = require( 'load-project-config' ); - config.paths = paths - config.taskMap = taskMap - config.files = files - loader(grunt, config).init() -} + config.paths = paths; + config.taskMap = taskMap; + config.files = files; + loader( grunt, config ).init(); +}; -module.exports = gruntConfig; \ No newline at end of file +module.exports = gruntConfig; diff --git a/architecture.md b/architecture.md new file mode 100644 index 00000000..7b6c4eac --- /dev/null +++ b/architecture.md @@ -0,0 +1,560 @@ +# PPOM Architecture + +## Overview + +PPOM extends WooCommerce products with configurable field groups. A field group adds extra product inputs such as text fields, selects, dates, uploads, quantities, price matrices, and other option types that affect: + +- what the shopper can submit on the product page +- whether add to cart is allowed +- how cart and checkout prices are recalculated +- what metadata is stored on cart items and order items +- how uploaded files are stored and finalized after checkout + +Conceptually, PPOM is a product-configuration layer on top of WooCommerce: + +1. Admins create one or more PPOM field groups. +2. A product receives those groups directly or through category matching. +3. PPOM renders those fields on the product page. +4. Submitted values are validated and stored in the cart item. +5. PPOM recalculates price and fees from server-side field metadata. +6. PPOM persists readable metadata and raw payloads into the order item. +7. Uploaded files are moved from temporary storage into an order-scoped confirmed directory. + +```mermaid +flowchart TD + A["WordPress loads plugin"] --> B["woocommerce-product-addon.php"] + B --> C["Load constants, includes, classes, settings, REST"] + C --> D["woocommerce_init"] + D --> E["NM_PersonalizedProduct singleton"] + + E --> F["PPOM_Meta resolves field groups"] + E --> G["Frontend rendering and assets"] + E --> H["Validation and add-to-cart hooks"] + E --> I["Cart/session pricing hooks"] + E --> J["Order item persistence and file finalization"] + E --> K["Admin CRUD and product edit integration"] + + F --> L["Custom table: {prefix}_nm_personalized"] + F --> M["Product meta: _product_meta_id"] + H --> N["Cart item ppom payload"] + I --> O["Adjusted line-item prices and fees"] + J --> P["Order meta, _ppom_fields, confirmed uploads"] +``` + +## Runtime Structure + +### Bootstrap + +The bootstrap lives in [`woocommerce-product-addon.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/woocommerce-product-addon.php). It: + +- defines plugin constants such as `PPOM_PATH`, `PPOM_URL`, `PPOM_VERSION`, `PPOM_PRODUCT_META_KEY`, `PPOM_TABLE_META`, and `PPOM_UPLOAD_DIR_NAME` +- loads Composer autoload plus the procedural runtime files under [`inc/`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/inc) +- loads the class files under [`classes/`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes) and the settings framework under [`backend/`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/backend) +- registers translation loading on `init` +- declares HPOS compatibility on `before_woocommerce_init` +- instantiates admin-only services when `is_admin()` +- registers activation and deactivation hooks +- starts the main runtime on `woocommerce_init` by calling `PPOM()` + +The plugin is not PSR-4 for its runtime code. The main file manually includes the plugin PHP files and then hands control to the `NM_PersonalizedProduct` singleton. + +### Main Components + +| Component | File | Responsibility | +| --- | --- | --- | +| Bootstrap | [`woocommerce-product-addon.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/woocommerce-product-addon.php) | Defines constants, loads the plugin, registers top-level hooks | +| Main runtime | [`classes/plugin.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/plugin.class.php) | Registers WooCommerce hooks for rendering, validation, pricing, cart, orders, admin AJAX, cron, and loop behavior | +| Product field resolver | [`classes/ppom.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/ppom.class.php) | Resolves applicable PPOM field groups for a product, merges their fields, and derives the runtime settings row from the custom DB table | +| Frontend form renderer | [`classes/form.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/form.class.php) | Renders modern template-based product fields and hidden runtime state | +| Input registry | [`classes/input.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/input.class.php) | Loads input-type classes and add-on input classes | +| Admin field UI | [`classes/fields.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/fields.class.php) | Powers the field-group builder UI and admin-side assets | +| Admin coordinator | [`classes/admin.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/admin.class.php) | Registers PPOM admin menus, settings integration, attach flows, and admin initialization hooks | +| Frontend asset loader | [`classes/frontend-scripts.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/frontend-scripts.class.php) | Registers and localizes frontend JS and CSS for pricing, uploads, validation, conditions, and field widgets | +| Script registry | [`classes/scripts.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/scripts.class.php) | Shared wrapper for registering, enqueuing, localizing, and inlining PPOM frontend assets | +| WooCommerce flow functions | [`inc/woocommerce.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/inc/woocommerce.php) | Product-page rendering, validation, cart item payloads, order item metadata, file finalization | +| Pricing engine | [`inc/prices.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/inc/prices.php) | Server-side option pricing, matrix pricing, cart fee calculation, line-item price updates | +| Upload subsystem | [`inc/files.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/inc/files.php) | AJAX upload and delete handlers, thumbnails, cropped files, confirmed-file storage, cleanup cron | +| Admin CRUD | [`inc/admin.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/inc/admin.php) | Field-group create/update/delete handlers and product-attachment UI | +| REST API | [`inc/rest.class.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/inc/rest.class.php) | Optional product and order PPOM API surface under `/wp-json/ppom/v1/` | + +### Data and Resolution Model + +PPOM stores field-group definitions in the custom table: + +- `{prefix}_nm_personalized` + +Each row contains both group-level settings and the full field definition payload. Important columns include: + +- `productmeta_name` +- `dynamic_price_display` +- `productmeta_style` +- `productmeta_js` +- `productmeta_categories` +- `productmeta_tags` when extensions or add-ons use tag-aware assignment +- `the_meta` as JSON + +Products are linked to PPOM groups through the normal post meta key: + +- `_product_meta_id` + +That value may contain one or more field-group IDs. + +`PPOM_Meta` is the read-side resolution layer. For a given product, it: + +1. reads `_product_meta_id` +2. checks category-linked groups +3. merges or overrides group IDs through filters +4. loads the matching row or rows from the custom table +5. merges field definitions from all matched groups +6. derives one active settings row for runtime values such as inline CSS, inline JS, price-display mode, and group title + +```mermaid +flowchart TD + A["WooCommerce product"] --> B["Read _product_meta_id"] + A --> C["Read product categories"] + C --> D["Match category-linked PPOM groups"] + B --> E["PPOM_Meta"] + D --> E + E --> F["Apply merge / override filters"] + F --> G["Load rows from {prefix}_nm_personalized"] + G --> H["Decode the_meta JSON"] + H --> I["Merged active PPOM fields"] + H --> J["Derived runtime settings row"] + I --> K["Frontend rendering"] + I --> L["Validation and pricing lookup"] + J --> M["Inline CSS / JS / display mode"] +``` + +The stable key throughout the whole runtime is the field `data_name`. PPOM uses that key in: + +- posted form values: `$_POST['ppom']['fields'][data_name]` +- cart item payloads +- order item metadata +- field lookup helpers such as `ppom_get_field_meta_by_dataname()` + +### Input Type System + +The input system is split into two sides: + +- PHP input classes in [`classes/inputs/`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/classes/inputs) +- frontend templates in [`templates/frontend/inputs/`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/templates/frontend/inputs) + +`PPOM_Inputs` loads input classes dynamically from files such as `input.text.php`, `input.select.php`, `input.file.php`, and add-on input classes through filters like `nm_input_class-{type}`. + +This gives PPOM a modular input model: + +- admin-side field builder metadata comes from the input classes +- frontend rendering comes from the matching templates +- add-ons can register more input types without changing the main bootstrap + +## WooCommerce Lifecycle + +The best way to understand the plugin is to follow the data from product page to order item. + +```mermaid +sequenceDiagram + participant Shopper + participant ProductPage as "WooCommerce Product Page" + participant PPOM as "PPOM Runtime" + participant DB as "WP Post Meta + PPOM Table" + participant Uploads as "PPOM Upload Storage" + participant Cart as "WooCommerce Cart" + participant Session as "WooCommerce Session Restore" + participant Order as "WooCommerce Order" + + Shopper->>ProductPage: Open product + ProductPage->>PPOM: Resolve field groups for product + PPOM->>DB: Read _product_meta_id and field-group rows + DB-->>PPOM: Return settings and fields + alt Legacy rendering mode + PPOM-->>ProductPage: Render via legacy template path + else Modern rendering mode + PPOM-->>ProductPage: Render via PPOM_Form and input templates + end + Note over ProductPage,PPOM: Hidden inputs carry PPOM IDs, product ID, conditional-hide state, option-price state, and cart-edit state + Shopper->>ProductPage: Fill fields and choose options + opt Product includes uploads + Shopper->>Uploads: Upload files to temporary PPOM storage + Uploads-->>ProductPage: Return filenames and preview state + end + Shopper->>ProductPage: Submit ppom[fields] + alt Client validation enabled + ProductPage->>PPOM: AJAX validation request + PPOM->>PPOM: Run ppom_check_validation() + PPOM-->>ProductPage: Return validation result + else Client validation disabled + ProductPage->>PPOM: WooCommerce add-to-cart validation + PPOM->>PPOM: Run ppom_check_validation() + end + PPOM->>PPOM: Normalize payload and add cart item data + PPOM->>Cart: Save ppom payload in cart item + Cart->>Session: Restore cart item from session + Session->>PPOM: Rehydrate ppom payload + alt New price mode + PPOM->>PPOM: Resolve matrix state and recompute line-item price + PPOM->>Cart: Add cart fees where needed + else Legacy price mode + PPOM->>PPOM: Recompute legacy option-price and fee data + PPOM->>Cart: Add fixed fees + end + Note over PPOM,Cart: Price is recalculated from server-side field metadata, not trusted from browser totals + Shopper->>Order: Checkout + Order->>PPOM: Create order line item + PPOM->>Order: Save order-item meta and _ppom_fields + Note over PPOM,Order: _ppom_fields stores the raw PPOM payload for replay and later formatting + PPOM->>Uploads: On order_processed, move temp files to confirmed/order_id + opt Order again + Order->>PPOM: Read _ppom_fields from prior order item + PPOM->>Cart: Clone ppom payload into new cart item + end +``` + +### 1. Product Page Rendering + +The main runtime hooks into WooCommerce product pages through `woocommerce_before_add_to_cart_button`. + +PPOM supports two rendering modes: + +- legacy mode via `ppom_woocommerce_show_fields()` and [`templates/render-fields.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/templates/render-fields.php) +- modern mode via `ppom_woocommerce_inputs_template_base()`, `PPOM_Form`, and [`templates/frontend/ppom-fields.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/templates/frontend/ppom-fields.php) + +The modern path renders: + +- the active field groups +- a price-table container +- hidden inputs and wrapper state through `PPOM_Form::form_contents()` + +That hidden form state is what keeps the frontend JS and backend PHP in sync. + +### 2. Frontend Assets and Client Behavior + +`PPOM_FRONTEND_SCRIPTS` registers the asset catalog and enqueues only the assets required for the current product or shortcode render. + +It is responsible for: + +- base PPOM styles and scripts +- field-type-specific assets such as datepicker, cropper, zoom, slider, tooltip, file upload, and input mask libraries +- localization of product price, field metadata, nonce values, upload paths, labels, conditional rules, and other runtime data +- injecting inline CSS and inline JS defined on the field group itself + +The result is a configuration-driven frontend: PHP serializes field and product state into JS, then the browser uses that data for price tables, conditions, upload UI, and optional client-side validation. + +### 3. Add to Cart and Validation + +The add-to-cart phase is where PPOM turns form input into a cart payload. + +Important hooks: + +- `woocommerce_add_to_cart_validation` +- `woocommerce_add_cart_item_data` +- `woocommerce_add_to_cart_quantity` +- `woocommerce_add_to_cart_redirect` + +Key behaviors: + +- validation logic runs through `ppom_check_validation()` +- conditionally hidden fields are skipped during validation +- checkbox and min/max-like rules are enforced through field-aware helpers and hooks +- the submitted PPOM payload is stored under `$cart_item['ppom']` +- shortcode renders can redirect directly to the cart after add to cart + +Validation can reach `ppom_check_validation()` in two ways: + +- through `woocommerce_add_to_cart_validation` when client validation is disabled +- through the PPOM AJAX validation endpoint when client validation is enabled + +### 4. Cart, Session Restore, and Pricing + +After add to cart, PPOM extends WooCommerce cart behavior through hooks such as: + +- `woocommerce_get_cart_item_from_session` +- `woocommerce_cart_loaded_from_session` +- `woocommerce_cart_calculate_fees` +- `woocommerce_cart_item_quantity` +- `woocommerce_cart_item_subtotal` +- `woocommerce_widget_shopping_cart_before_buttons` + +Pricing is recalculated on the server from saved field metadata, not trusted from the browser. + +The pricing layer can handle: + +- option surcharges +- one-time charges +- quantity-driven pricing +- bulk quantity ranges +- price matrix rows +- measure-based pricing +- fixed-price addons +- discounts that apply to base price or base-plus-options + +PPOM has two price modes: + +- `new`, where product line-item prices are recalculated directly and some charges become cart fees +- `legacy`, where more behavior stays in the older fee-based flow and relies more on the hidden option-price payload + +This stage also controls quantity display, mini-cart behavior, and cart-weight adjustments when PPOM fields change product characteristics. + +### 5. Order Persistence and File Finalization + +When WooCommerce creates order line items, PPOM persists both a readable view and a raw replayable view of the submitted data. + +Important hooks: + +- `woocommerce_checkout_create_order_line_item` +- `woocommerce_order_item_display_meta_key` +- `woocommerce_order_item_display_meta_value` +- `woocommerce_order_item_get_formatted_meta_data` +- `woocommerce_checkout_order_processed` +- `woocommerce_order_again_cart_item_data` + +Order persistence does three things: + +- saves readable field values into order item meta +- saves the raw PPOM payload into `_ppom_fields` +- reformats labels and values for files, cropper outputs, and option displays + +File finalization is a separate later step. On `woocommerce_checkout_order_processed`, `ppom_woocommerce_rename_files()` moves files from temporary storage into `confirmed/{order_id}/` and prefixes filenames with product ID. `order again` support works by cloning `_ppom_fields` back into a new cart item. + +### 6. Catalog and Product-Edit Behavior + +PPOM also affects normal WooCommerce product behavior outside the checkout pipeline. + +On the storefront it: + +- changes loop add-to-cart URLs so configurable products lead to the single-product page +- changes the loop button text to a select-options style CTA +- disables `ajax_add_to_cart` support when PPOM input is required + +In wp-admin it: + +- adds a product-side PPOM selection UI through either a legacy meta box or a product data tab/panel +- duplicates PPOM attachments when products are duplicated + +## Operational Subsystems + +### Upload Subsystem + +Uploads live under: + +- `wp-content/uploads/ppom_files/` + +Important subdirectories include: + +- `thumbs/` +- `cropped/` +- `edits/` +- `confirmed/{order_id}/` + +The upload subsystem handles: + +- AJAX upload and delete endpoints for guests and logged-in users +- nonce verification +- mime-type and extension checks +- thumbnail generation +- cropper support +- cleanup of stale temporary uploads + +On activation, PPOM schedules the cleanup hook `do_action_remove_images`, which removes temporary uploads older than 7 days. + +### Admin and Settings + +The admin side has two main responsibilities: + +- field-group CRUD +- settings and permissions + +Field-group management is split across `NM_PersonalizedProduct_Admin`, `PPOM_Fields_Meta`, and the AJAX CRUD handlers in [`inc/admin.php`](/Users/robert/Desktop/sites/plugins-dev/web/app/plugins/woocommerce-product-addon/inc/admin.php): + +- create, edit, clone, and delete field groups +- bulk-attach field groups to products +- choose which group is attached on the product edit screen + +`PPOM_Meta` is not the CRUD layer. It resolves product-side field groups and reads their settings/fields at runtime. + +Settings use two storage models: + +- legacy WooCommerce settings-tab storage +- the newer `PPOM_SettingsFramework` + +`ppom_get_option()` abstracts over both so the runtime can read settings without caring which backend currently stores them. + +#### How settings are stored in the database + +PPOM settings are stored in `wp_options`, but the shape depends on whether settings migration has been completed. + +Legacy storage: + +- the old WooCommerce settings tab calls `woocommerce_update_options( ppom_array_settings() )` +- each setting is stored as its own option row +- examples include keys such as `ppom_legacy_price`, `ppom_enable_legacy_inputs_rendering`, `ppom_new_conditions`, `ppom_permission_mfields`, and `ppom_label_product_price` + +Migrated storage: + +- `PPOM_SettingsFramework` sets its save key to `ppom-settings_panel` +- the settings form posts values as `ppom-settings_panel[setting_key]` +- the AJAX save handler sanitizes the array and stores the whole settings payload in a single option row: `ppom-settings_panel` +- generated CSS derived from settings is stored separately in `ppom_css_output` + +Migration state: + +- `ppom_settings_migration_done = 1` tells `ppom_get_option()` to read from `ppom-settings_panel` +- if that flag is absent, `ppom_get_option()` falls back to reading individual legacy option keys +- the migration process copies existing legacy per-key options into `ppom-settings_panel`; it does not need to delete the old option rows in order for runtime reads to switch over + +Related operational options: + +- `personalizedproduct_db_version` tracks the plugin schema version for the custom PPOM table +- `ppom_legacy_user` is a separate flag used to detect whether an installation should be treated as a legacy user for some admin behavior + +```mermaid +flowchart TD + A["Admin changes PPOM settings"] --> B{"Settings migrated?"} + + B -- "No" --> C["WooCommerce settings tab save"] + C --> D["woocommerce_update_options(...)"] + D --> E["Store each setting as its own wp_options row"] + + B -- "Yes" --> F["PPOM_SettingsFramework AJAX save"] + F --> G["Sanitize ppom-settings_panel[setting_key] array"] + G --> H["update_option('ppom-settings_panel', settings_array)"] + G --> I["update_option('ppom_css_output', generated_css)"] + + J["ppom_settings_migration_done"] --> K["ppom_get_option() chooses read path"] + E --> K + H --> K +``` + +### REST API + +If API access is enabled, `PPOM_Rest` registers routes under: + +- `/wp-json/ppom/v1/` + +The API supports: + +- reading product PPOM field-group metadata +- creating or updating product PPOM field definitions +- deleting some or all product PPOM fields +- reading order PPOM metadata +- updating order PPOM metadata +- deleting order PPOM metadata + +The important implementation detail is that the routes use open permission callbacks, while write operations validate a PPOM secret key inside the handler. That makes the API optional and configuration-gated, but still part of the plugin's public integration surface. + +## Extension and Compatibility Model + +PPOM is designed to be extended through filters, actions, template overrides, and companion plugins. + +Common extension seams include: + +- template path override via `ppom_input_templates_path` +- input rendering hooks such as `ppom_rendering_inputs` and `ppom_rendering_inputs_{type}` +- price hooks such as `ppom_cart_line_total`, `ppom_option_price`, and `ppom_price_mode` +- field resolution hooks such as `ppom_product_meta_id` +- metadata formatting hooks such as `ppom_order_display_value` +- input-class loading through `nm_input_class-{type}` + +The plugin also has explicit compatibility seams for: + +- PPOM Pro feature gating through `defined( 'PPOM_PRO_PATH' )` +- Themeisle SDK and freemium UI +- WPML and Polylang translation support +- Elementor shortcode rendering +- wholesale-pricing and currency-switcher integrations +- invoice and order-export plugins + +Important runtime variants are controlled by settings: + +- legacy vs modern field rendering +- legacy vs new price calculation +- legacy vs new conditional-logic script +- legacy vs migrated settings storage + +## Database Inventory + +PPOM stores data in several WordPress and WooCommerce persistence layers. + +### Custom database table + +- `{prefix}_nm_personalized` + Stores PPOM field-group definitions and group-level settings such as `productmeta_name`, `dynamic_price_display`, `productmeta_style`, `productmeta_js`, category assignment, optional tag data for extensions/add-ons, and `the_meta` JSON. + +### WordPress post meta + +- `_product_meta_id` + Attached to WooCommerce products and used to link one or more PPOM field groups to the product. + +### WooCommerce cart and order item data + +- `$cart_item['ppom']` + Runtime cart payload containing submitted fields, conditional-hide state, option-price data, and other PPOM state. +- order item meta entries keyed by field `data_name` + Human-readable PPOM values persisted into the order line item. +- `_ppom_fields` + Raw PPOM payload stored on the order item so PPOM can format values later and rebuild cart data for `order again`. + +### WordPress options + +- `ppom-settings_panel` + Single-array settings storage used by the migrated settings framework. +- `ppom_settings_migration_done` + Flag that switches runtime setting reads from legacy per-key options to `ppom-settings_panel`. +- `ppom_css_output` + Generated CSS derived from settings-framework values. +- legacy per-key options such as `ppom_legacy_price`, `ppom_enable_legacy_inputs_rendering`, `ppom_new_conditions`, `ppom_permission_mfields`, and pricing-label options + Used before migration, and still available as the fallback read path when migration is not enabled. +- `personalizedproduct_db_version` + Tracks the schema version for the custom PPOM table. +- `ppom_legacy_user` + Marks whether an installation should be treated as a legacy user for some admin-side behavior. +- `ppom_demo_meta_installed` + Marks whether the demo PPOM field group has already been inserted on activation. + +### Upload storage + +- `wp-content/uploads/ppom_files/` + Temporary upload area. +- `wp-content/uploads/ppom_files/thumbs/` + Generated thumbnails. +- `wp-content/uploads/ppom_files/cropped/` + Cropper output files. +- `wp-content/uploads/ppom_files/edits/` + Edited files. +- `wp-content/uploads/ppom_files/confirmed/{order_id}/` + Finalized order-scoped file storage after checkout. + +## Security Model + +The main trust boundaries in the plugin are: + +- posted PPOM field payloads +- frontend-computed option-price state +- uploaded files +- admin CRUD requests +- REST write requests + +The runtime tries to protect those boundaries by: + +- using nonce checks for admin AJAX and frontend AJAX actions +- checking roles and capabilities for admin operations +- validating add-to-cart data on the server +- recalculating prices from server-side field metadata +- restricting dangerous file types and owning the upload directories +- relying on WooCommerce cart and order APIs for persistence + +The highest-risk areas are pricing, file handling, and any place where a field `data_name` must map back to the correct field definition. + +## Contributor Mental Model + +When changing PPOM, follow this sequence: + +1. How does the product receive the field group? +2. How does `PPOM_Meta` resolve the active fields? +3. How is that field metadata rendered into HTML and JS? +4. Which `data_name` keys get posted back? +5. How does that payload get validated and stored in the cart item? +6. How does the server recalculate price and fees from the saved payload? +7. How does the order item store both readable metadata and `_ppom_fields`? +8. Does the change affect uploads, quantity rules, conditions, session restore, or product-loop behavior? + +If a change fits that path cleanly, it is usually aligned with the plugin's actual architecture. diff --git a/backend/changelog_handler.php b/backend/changelog_handler.php index da822b70..556e777c 100644 --- a/backend/changelog_handler.php +++ b/backend/changelog_handler.php @@ -3,7 +3,6 @@ * Changleog Handler * * Handles parsing for Changelog files. - * */ /** @@ -20,11 +19,11 @@ class PPOM_Changelog_Handler { public function get_changelog( $changelog_path ) { if ( ! is_file( $changelog_path ) ) { - return []; + return array(); } if ( ! WP_Filesystem() ) { - return []; + return array(); } return $this->parse_changelog( $changelog_path ); @@ -45,7 +44,7 @@ private function parse_changelog( $changelog_path ) { $changelog = ''; } $changelog = explode( PHP_EOL, $changelog ); - $releases = []; + $releases = array(); $release_count = 0; foreach ( $changelog as $changelog_line ) { @@ -53,7 +52,7 @@ private function parse_changelog( $changelog_path ) { continue; } if ( substr( ltrim( $changelog_line ), 0, 4 ) === '####' ) { - $release_count ++; + ++$release_count; preg_match( '/[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/', $changelog_line, $found_v ); preg_match( '/[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}/', $changelog_line, $found_d ); diff --git a/backend/options.php b/backend/options.php index c5507cec..d69bdea2 100644 --- a/backend/options.php +++ b/backend/options.php @@ -1,6 +1,11 @@ array( 'type' => 'checkbox', 'title' => __( 'Enable Legacy Price Calculations', 'woocommerce-product-addon' ), - 'desc' => __( 'Enable this option to use the legacy method for price calculations.', 'woocommerce-product-addon' ), + 'desc' => __( 'Enable this option to use the legacy method for price calculations.', 'woocommerce-product-addon' ), ), 'ppom_permission_mfields' => array( 'type' => 'select', @@ -131,6 +146,21 @@ function ppom_load_free_options() { } add_action( 'init', 'ppom_load_free_options' ); +// Pro and integration settings registration. + +/** + * Registers the pro, styling, addon, and integration settings definitions. + * + * Extends the base settings panel with advanced pricing, style output, REST + * API, and addon-specific settings that are consumed by the PPOM runtime. + * + * @return void + * + * @see PPOMSETTINGS() + * @see PPOM_SettingsFramework::register_panel() + * @see PPOM_SettingsFramework::register_setting() + * @see PPOM_Rest::init_api() + */ function ppom_load_pro_options() { $pro_settings = array( 'ppom_pro_basics' => array( @@ -648,30 +678,30 @@ function ppom_load_pro_options() { 'ppom_repeater_clone_mode' => array( 'type' => 'select', 'is_available' => false, - 'options' => [ + 'options' => array( 'first_box' => 'Clone from first box only', 'each_box' => 'Clone from each box', - ], + ), 'title' => __( 'Clone Mode', 'woocommerce-product-addon' ), 'desc' => __( 'How to clone the fields', 'woocommerce-product-addon' ), ), 'ppom_repeater_clone_position' => array( 'type' => 'select', 'is_available' => false, - 'options' => [ + 'options' => array( 'top' => 'Top', 'bottom' => 'Bottom', - ], + ), 'title' => __( 'Clone Icons Position', 'woocommerce-product-addon' ), 'desc' => __( 'Set the placement of the clone icons within the repeater fields.', 'woocommerce-product-addon' ), ), 'ppom_repeater_icon_lib' => array( 'type' => 'select', 'is_available' => false, - 'options' => [ + 'options' => array( 'dashicons' => 'Dashicons', 'fontawesome' => 'FontAwesome', - ], + ), 'title' => __( 'Icons Library', 'woocommerce-product-addon' ), 'desc' => __( 'Select the icon library to be used for displaying icons.', 'woocommerce-product-addon' ), ), @@ -693,10 +723,10 @@ function ppom_load_pro_options() { 'default' => 'Bulk Quantity Standard', 'options' => array( 'bq_standard' => __( 'Bulk Quantity Standard', 'woocommerce-product-addon' ), - 'bq_packaged' => __( 'Bulk Quantity Packaged', 'woocommerce-product-addon' ) + 'bq_packaged' => __( 'Bulk Quantity Packaged', 'woocommerce-product-addon' ), ), 'desc' => __( 'Choose how bulk quantities are displayed. Select \'Bulk Quantity Standard\' for standard bulk orders or \'Bulk Quantity Packaged\' for pre-packaged bulk quantities.', 'woocommerce-product-addon' ), - ) + ), ); PPOMSETTINGS()->register_setting( 'ppom_admin_fields_settings', $settings ); @@ -746,9 +776,8 @@ function ppom_load_pro_options() { 'is_available' => false, 'title' => __( 'Integrations', 'woocommerce-product-addon' ), 'is_sabpanel' => true, - ) + ), ); PPOMSETTINGS()->register_panel( 'ppom_general_tab', $panels )->register_setting( 'ppom_integrations', $integration_settings ); - } -add_action('init', 'ppom_load_pro_options', 99); \ No newline at end of file +add_action( 'init', 'ppom_load_pro_options', 99 ); diff --git a/backend/settings-panel.class.php b/backend/settings-panel.class.php index e3a26727..ad39d5af 100644 --- a/backend/settings-panel.class.php +++ b/backend/settings-panel.class.php @@ -1,11 +1,14 @@ config = array( 'id' => 'ppom', 'plugin_url' => PPOM_URL, @@ -105,6 +119,8 @@ function __construct() { // delete_option('ppom_settings_migration_done'); } + // Settings structure registration. + /** * Get class instace @@ -170,9 +186,11 @@ public function add_settings_tab( $settings_tabs ) { /** - * Render settings panel template + * Renders the configured PPOM settings tabs and panels. + * + * @return void */ - function render_settings_panel() { + public function render_settings_panel() { wp_dequeue_script( 'woocommerce_settings' ); @@ -211,7 +229,7 @@ public function register_panel( $tab_id, $panels ) { // only used for store all Panels on single array $this->panels = array_merge( $this->panels, $panels ); - $existing_panels = ! isset( $this->tabs[ $tab_id ]['panels'] ) ? [] : $this->tabs[ $tab_id ]['panels']; + $existing_panels = ! isset( $this->tabs[ $tab_id ]['panels'] ) ? array() : $this->tabs[ $tab_id ]['panels']; $this->tabs[ $tab_id ]['panels'] = array_merge( $existing_panels, $panels ); return $this; @@ -219,7 +237,15 @@ public function register_panel( $tab_id, $panels ) { /** - * Register Panel Settings + * Registers settings definitions under a panel. + * + * @param string $panel_id Panel ID that receives the settings. + * @param array $settings Settings definitions keyed by setting ID. + * + * @return self + * + * @see ppom_load_free_options() + * @see ppom_load_pro_options() */ public function register_setting( $panel_id, $settings ) { @@ -230,8 +256,8 @@ public function register_setting( $panel_id, $settings ) { $this->tabs = array_map( function ( $tab ) use ( $panel_id, $settings ) { if ( isset( $tab['panels'] ) && isset( $tab['panels'][ $panel_id ] ) ) { - $existing_settings = ! isset( $tab['panels'][ $panel_id ]['settings'] ) ? [] : $tab['panels'][ $panel_id ]['settings']; - $tab['panels'][ $panel_id ]['settings'] = array_merge( $existing_settings, $settings ); + $existing_settings = ! isset( $tab['panels'][ $panel_id ]['settings'] ) ? array() : $tab['panels'][ $panel_id ]['settings']; + $tab['panels'][ $panel_id ]['settings'] = array_merge( $existing_settings, $settings ); } return $tab; @@ -242,6 +268,8 @@ function ( $tab ) use ( $panel_id, $settings ) { return $this; } + // Settings persistence and CSS generation. + /** * Load inputs types @@ -289,7 +317,7 @@ public static function reposition_array_element( $arr, $find, $move ) { return $arr; } - $elem = [ $move => $arr[ $move ] ]; + $elem = array( $move => $arr[ $move ] ); $start = array_splice( $arr, 0, array_search( $find, array_keys( $arr ) ) ); unset( $start[ $move ] ); @@ -298,13 +326,19 @@ public static function reposition_array_element( $arr, $find, $move ) { /** - * Save all settings action callback + * Persists the submitted PPOM settings-panel payload. + * + * Sanitizes the submitted settings array, regenerates the aggregated + * frontend CSS output, and writes the normalized settings into the panel + * option key. + * + * @return void */ - function save_settings() { + public function save_settings() { if ( ! isset( $_POST['ppom_settings_nonce'] ) - || ! wp_verify_nonce( $_POST['ppom_settings_nonce'], 'ppom_settings_nonce_action' ) - || ! ppom_security_role() + || ! wp_verify_nonce( $_POST['ppom_settings_nonce'], 'ppom_settings_nonce_action' ) + || ! ppom_security_role() ) { $response = array( 'status' => 'error', @@ -317,9 +351,13 @@ function save_settings() { $response = array(); if ( isset( $_REQUEST[ self::$save_key ] ) ) { - $_REQUEST[ self::$save_key ] = array_filter( $_REQUEST[ self::$save_key ] , function ( $key ) { - return strpos( $key, '_locked' ) === false; - }, ARRAY_FILTER_USE_KEY ); + $_REQUEST[ self::$save_key ] = array_filter( + $_REQUEST[ self::$save_key ], + function ( $key ) { + return strpos( $key, '_locked' ) === false; + }, + ARRAY_FILTER_USE_KEY + ); // $settings_meta = $_REQUEST[self::$save_key]; $settings_meta = array_map( function ( $setting ) { @@ -353,9 +391,15 @@ function ( $setting ) { /** - * Generate output css + * Regenerates the CSS output derived from settings-panel fields. + * + * @param array $settings_meta Sanitized settings-panel values. + * + * @return void + * + * @see PPOM_FRONTEND_SCRIPTS::add_inline_css() */ - function generate_css( $settings_meta ) { + public function generate_css( $settings_meta ) { $setting_fields = array_intersect_key( $this->settings_array, $settings_meta ); @@ -447,13 +491,16 @@ public static function get_saved_settings( $key, $default = null ) { /** - * Migration process - * It only used for PPOM plugin + * Migrates legacy PPOM options into the settings-panel option format. + * + * @return void + * + * @see ppom_settings_migrated() */ - function ppom_migrate_settings_panel() { + public function ppom_migrate_settings_panel() { if ( ! isset( $_GET['ppom_migrate_nonce'] ) - || ! wp_verify_nonce( $_GET['ppom_migrate_nonce'], 'ppom_migrate_nonce_action' ) + || ! wp_verify_nonce( $_GET['ppom_migrate_nonce'], 'ppom_migrate_nonce_action' ) ) { wp_die( 'Sorry, you are not allowed to clone', 'ppom' ); } @@ -478,7 +525,7 @@ function ppom_migrate_settings_panel() { exit; } else { - $legacy_values = []; + $legacy_values = array(); $panel_fields = $this->settings_array; // Getting the keys @@ -495,6 +542,8 @@ function ppom_migrate_settings_panel() { } } + // Settings-page assets and helpers. + /** * Get border styles @@ -593,10 +642,8 @@ public function generate_css_editor_output_css( $settings, $css_val ) { if ( isset( $css_val[ $key ], $css_val['style'], $css_val['color'] ) && $css_val[ $key ] != '' && $css_val['color'] != '' && $css_val['style'] != 'none' ) { $css_prop .= $mode . '-' . $key . '-style:' . $css_val['style'] . '!important;'; } - } else { - if ( isset( $css_val[ $key ] ) && $css_val[ $key ] != '' ) { + } elseif ( isset( $css_val[ $key ] ) && $css_val[ $key ] != '' ) { $css_prop .= $mode . '-' . $key . ':' . $css_val[ $key ] . '!important;'; - } } } @@ -693,7 +740,7 @@ public function add_admin_menu() { /** * Register settings panel scripts */ - function get_scripts() { + public function get_scripts() { $register_scripts = array( 'nmsf-notifications-lib' => array( @@ -735,7 +782,7 @@ function get_scripts() { /** * Register settings panel styles */ - function get_styles() { + public function get_styles() { $register_styles = array( 'nmsf-notifications-lib' => array( @@ -770,7 +817,9 @@ function get_styles() { /** - * Load styles/scripts on settings page + * Loads the PPOM settings-page scripts, styles, and localized data. + * + * @return string|void */ public function load_scripts() { @@ -800,14 +849,14 @@ public function load_scripts() { PPOM_SCRIPTS::enqueue_style( 'nmsf-tooltip-lib' ); PPOM_SCRIPTS::enqueue_script( 'nmsf-tooltip-lib' ); - // Videopopup library + // Videopopup library PPOM_SCRIPTS::enqueue_style( 'nmsf-videopopup-lib' ); PPOM_SCRIPTS::enqueue_script( 'nmsf-videopopup-lib' ); - // nmsf grid library + // nmsf grid library PPOM_SCRIPTS::enqueue_style( 'nmsf-grid-lib' ); - // Condition base settings library + // Condition base settings library PPOM_SCRIPTS::enqueue_script( 'nmsf-deps-lib' ); // Settings panel scripts @@ -821,17 +870,24 @@ public function load_scripts() { /** - * Localize scripts data + * Localizes runtime data for the settings-panel scripts. + * + * @param string $handle Script handle receiving the data. + * @param string $var_name JS variable name. + * @param array $js_vars Handle-specific JS vars. + * @param array $global_js_vars Shared JS vars. + * + * @return void */ public function set_localize_data( $handle, $var_name, $js_vars = array(), $global_js_vars = array() ) { switch ( $handle ) { case 'nmsf-settings-panel': - $localize_data = [ + $localize_data = array( 'migrate_back_msg' => __( 'Are you sure?', 'woocommerce-product-addon' ), 'administrator_role_cannot_be_removed' => esc_html__( 'The administrator role cannot be removed.', 'woocommerce-product-addon' ), - ]; + ); break; } @@ -847,7 +903,7 @@ public function set_localize_data( $handle, $var_name, $js_vars = array(), $glob /** * Remove all admin notices */ - function remove_admin_notices() { + public function remove_admin_notices() { $is_settings_page = $this->is_settings_page(); @@ -859,9 +915,11 @@ function remove_admin_notices() { /** - * Is admin settings page + * Determines whether the current admin request targets the PPOM settings page. + * + * @return bool */ - function is_settings_page() { + public function is_settings_page() { $current_screen = get_current_screen(); @@ -886,14 +944,28 @@ function is_settings_page() { } - function insert_at( $array = [], $item = [], $position = 0 ) { - $previous_items = array_slice( $array, 0, $position, true ); - $next_items = array_slice( $array, $position, null, true ); + /** + * Inserts an item into an array at a fixed position. + * + * @param array $items Source array. + * @param array $item Item to insert. + * @param int $position Numeric insertion offset. + * + * @return array + */ + public function insert_at( $items = array(), $item = array(), $position = 0 ) { + $previous_items = array_slice( $items, 0, $position, true ); + $next_items = array_slice( $items, $position, null, true ); return $previous_items + $item + $next_items; } } +/** + * Returns the shared PPOM settings framework instance. + * + * @return PPOM_SettingsFramework + */ PPOMSETTINGS(); function PPOMSETTINGS() { return PPOM_SettingsFramework::get_instance(); diff --git a/backend/templates/admin-settings.php b/backend/templates/admin-settings.php index 849a8ca0..15b91b9f 100644 --- a/backend/templates/admin-settings.php +++ b/backend/templates/admin-settings.php @@ -42,19 +42,19 @@ ?> + value="get_config( 'id' ) ); ?>_settings_panel_action"/> + value="get_config( 'version' ) ); ?>"/>
-

- -

+

+ +

$tab_meta ) { $panel_meta = isset( $tab_meta['panels'] ) ? $tab_meta['panels'] : array(); @@ -62,20 +62,20 @@ ?>
+ data-panel-id="">
$subtab_meta ) { - $id = isset( $subtab_meta['id'] ) ? $subtab_meta['id'] : ''; - $title = isset( $subtab_meta['title'] ) ? $subtab_meta['title'] : ''; - $desc = isset( $subtab_meta['desc'] ) ? $subtab_meta['desc'] : ''; + $id = isset( $subtab_meta['id'] ) ? $subtab_meta['id'] : ''; + $title = isset( $subtab_meta['title'] ) ? $subtab_meta['title'] : ''; + $desc = isset( $subtab_meta['desc'] ) ? $subtab_meta['desc'] : ''; $is_available = isset( $subtab_meta['is_available'] ) ? $subtab_meta['is_available'] : true; - $active = isset( $subtab_meta['active'] ) ? $subtab_meta['active'] : ''; - $is_sabpanel = isset( $subtab_meta['is_sabpanel'] ) ? $subtab_meta['is_sabpanel'] : ''; - $params = isset( $subtab_meta['settings'] ) ? $subtab_meta['settings'] : array(); + $active = isset( $subtab_meta['active'] ) ? $subtab_meta['active'] : ''; + $is_sabpanel = isset( $subtab_meta['is_sabpanel'] ) ? $subtab_meta['is_sabpanel'] : ''; + $params = isset( $subtab_meta['settings'] ) ? $subtab_meta['settings'] : array(); // Change the settings position $params = $class_ins::settings_position_controller( $params ); @@ -88,18 +88,19 @@ if ( $is_sabpanel ) { ?> > + class="nmsf-panel-handler" + id="nmsf-" > -
- -

- +

+ +

+ -

-
- + ); + ?> +

+
+ " > - - + + - +
+ + id = $id; + $this->id = $id; $this->renderer = $renderer; } diff --git a/classes/attach-popup/select-component.class.php b/classes/attach-popup/select-component.class.php index 69ff6167..f31b4f18 100644 --- a/classes/attach-popup/select-component.class.php +++ b/classes/attach-popup/select-component.class.php @@ -31,7 +31,7 @@ class SelectComponent extends ContainerView { */ protected $status = 'valid'; - public function __construct( ) {} + public function __construct() {} /** * Render the select component. @@ -43,73 +43,73 @@ public function render() { $select_name = isset( $select['name'] ) && is_string( $select['name'] ) ? $select['name'] : ''; $select_options = ! empty( $select['options'] ) && is_array( $select['options'] ) ? $select['options'] : array(); $is_multiple = isset( $select['multiple'] ) && $select['multiple']; - $input_label = isset( $select['label'] ) && is_string( $select['label'] ) ? $select['label'] : ''; - $is_used = isset( $select['is_used'] ) && $select['is_used']; + $input_label = isset( $select['label'] ) && is_string( $select['label'] ) ? $select['label'] : ''; + $is_used = isset( $select['is_used'] ) && $select['is_used']; $initial_values = array(); foreach ( $select_options as $option ) { if ( ! $option['selected'] ) { continue; } - $initial_values[]= $option['value']; + $initial_values[] = $option['value']; } ob_start(); - $status = 'valid' !== $this->get_status() ? 'disabled' : '' + $status = 'valid' !== $this->get_status() ? 'disabled' : '' ?> -
-
- -
-
- get_title() ) ?> get_status() ) { ?> -
-
-
-
+ id="get_id() ); ?>" + > +
+ +
+
+ get_title() ); ?> get_status() ) { ?> +
+
+
+
- + - - > + > ' . esc_html( $label ) . ''; + echo ''; } } else { echo ''; } ?> - -
- - get_description() ) ?> - - get_status() ) { ?> + +
+ + get_description() ); ?> + + get_status() ) { ?> - ' . esc_html__( 'Available in PRO', 'woocommerce-product-addon' ) . ''; ?> + ' . esc_html__( 'Available in PRO', 'woocommerce-product-addon' ) . ''; ?> - + -
-
-
+
+
+ [ - 'validation'=>[ - 'end_bigger_than_start' => esc_html__('The end value of the range must be greater than the start value. (range: {range})', 'woocommerce-product-addon'), - 'start_cannot_be_equal_with_end' => esc_html__('The start value cannot be equal to the end value. (range: {range})', 'woocommerce-product-addon'), - 'range_intersection' => esc_html__( 'Values in two ranges intersect. Every range of numbers should be covered by only one range. Intersects ranges: {range1} AND {range2}', 'woocommerce-product-addon' ), - 'invalid_pattern' => esc_html__( 'Range format is invalid. (range: {range})', 'woocommerce-product-addon' ) - ] - ] - ] ); + wp_localize_script( + 'ppom-bulkquantity', + 'ppom_bq', + array( + 'i18n' => array( + 'validation' => array( + 'end_bigger_than_start' => esc_html__( 'The end value of the range must be greater than the start value. (range: {range})', 'woocommerce-product-addon' ), + 'start_cannot_be_equal_with_end' => esc_html__( 'The start value cannot be equal to the end value. (range: {range})', 'woocommerce-product-addon' ), + 'range_intersection' => esc_html__( 'Values in two ranges intersect. Every range of numbers should be covered by only one range. Intersects ranges: {range1} AND {range2}', 'woocommerce-product-addon' ), + 'invalid_pattern' => esc_html__( 'Range format is invalid. (range: {range})', 'woocommerce-product-addon' ), + ), + ), + ) + ); wp_enqueue_script( 'ppom-inputmask', PPOM_URL . '/js/inputmask/jquery.inputmask.min.js', array( 'jquery' ), '5.0.6', true ); // Popup - wp_enqueue_script( 'ppom-popup', PPOM_URL . '/js/popup.js', [], PPOM_VERSION, true ); + wp_enqueue_script( 'ppom-popup', PPOM_URL . '/js/popup.js', array(), PPOM_VERSION, true ); // PPOM Meta Table File - wp_enqueue_script( 'ppom-meta-table', PPOM_URL . '/js/admin/ppom-meta-table.js', array( 'jquery', 'ppom-popup' ), PPOM_VERSION, true ); + wp_enqueue_script( 'ppom-meta-table', PPOM_URL . '/js/admin/ppom-meta-table.js', array( 'jquery', 'ppom-popup', 'ppom-select2' ), PPOM_VERSION, true ); // Font-awesome File if ( ppom_load_fontawesome() ) { @@ -132,7 +167,7 @@ function load_script( $hook ) { // Description Tooltips JS File wp_enqueue_script( 'ppom-tooltip', PPOM_URL . '/js/ppom-tooltip.js', array( 'jquery' ), PPOM_VERSION, true ); - wp_register_script( 'serializejson', PPOM_URL . '/js/admin/serializejson.js', array( 'jquery' ), '2.8.1'); + wp_register_script( 'serializejson', PPOM_URL . '/js/admin/serializejson.js', array( 'jquery' ), '2.8.1' ); // Add the color picker css file wp_enqueue_style( 'wp-color-picker' ); @@ -163,39 +198,39 @@ function load_script( $hook ) { $ppom_admin_meta = array( 'plugin_admin_page' => admin_url( 'admin.php?page=ppom' ), 'loader' => PPOM_URL . '/images/loading.gif', - 'ppomProActivated'=>ppom_pro_is_installed() && PPOM()->is_license_of_type( 'pro' ) ? 'yes' : 'no', - 'i18n' => [ - 'addGroupUrl' => esc_url( add_query_arg( array( 'action' => 'new' ) ) ), - 'addGroupLabel'=>esc_html__( 'Add New Group', 'woocommerce-product-addon' ), - 'bulkActionsLabel'=>esc_html__( 'Bulk Actions', 'woocommerce-product-addon' ), - 'deleteLabel'=>esc_html__( 'Delete', 'woocommerce-product-addon' ), - 'exportLabel'=>esc_html__( 'Export', 'woocommerce-product-addon' ), - 'exportLockedLabel'=> esc_html__( 'Export', 'woocommerce-product-addon' ) . ' (' . esc_html__( 'PRO', 'woocommerce-product-addon' ) . ')', - 'importLabel'=>esc_html__( 'Import Field Groups ', 'woocommerce-product-addon' ), - 'freemiumCFRContent' => \PPOM_Freemium::get_instance()->get_freemium_cfr_content(), - 'freemiumCFRTab' => \PPOM_Freemium::TAB_KEY_FREEMIUM_CFR, - 'popup' => [ - 'confirmTitle' => __( 'Are you sure?', 'woocommerce-product-addon' ), + 'ppomProActivated' => ppom_pro_is_installed() && PPOM()->is_license_of_type( 'pro' ) ? 'yes' : 'no', + 'i18n' => array( + 'addGroupUrl' => esc_url( add_query_arg( array( 'action' => 'new' ) ) ), + 'addGroupLabel' => esc_html__( 'Add New Group', 'woocommerce-product-addon' ), + 'bulkActionsLabel' => esc_html__( 'Bulk Actions', 'woocommerce-product-addon' ), + 'deleteLabel' => esc_html__( 'Delete', 'woocommerce-product-addon' ), + 'exportLabel' => esc_html__( 'Export', 'woocommerce-product-addon' ), + 'exportLockedLabel' => esc_html__( 'Export', 'woocommerce-product-addon' ) . ' (' . esc_html__( 'PRO', 'woocommerce-product-addon' ) . ')', + 'importLabel' => esc_html__( 'Import Field Groups ', 'woocommerce-product-addon' ), + 'freemiumCFRContent' => \PPOM_Freemium::get_instance()->get_freemium_cfr_content(), + 'freemiumCFRTab' => \PPOM_Freemium::TAB_KEY_FREEMIUM_CFR, + 'popup' => array( + 'confirmTitle' => __( 'Are you sure?', 'woocommerce-product-addon' ), 'confirmationBtn' => __( 'Confirm', 'woocommerce-product-addon' ), - 'cancelBtn' => __( 'Cancel', 'woocommerce-product-addon' ), - 'finishTitle' => __( 'Done', 'woocommerce-product-addon' ), - 'errorTitle' => __( 'Error', 'woocommerce-product-addon' ), + 'cancelBtn' => __( 'Cancel', 'woocommerce-product-addon' ), + 'finishTitle' => __( 'Done', 'woocommerce-product-addon' ), + 'errorTitle' => __( 'Error', 'woocommerce-product-addon' ), 'checkFieldTitle' => __( 'Please at least check one field!', 'woocommerce-product-addon' ), - ], - 'errorOccurred' => __( 'An error occurred. Please try again.', 'woocommerce-product-addon' ), - 'yes' => __( 'Yes', 'woocommerce-product-addon' ), - 'no' => __( 'No', 'woocommerce-product-addon' ), - 'updatedField' => __( 'Update Field', 'woocommerce-product-addon' ), - 'pricePlaceholder' => __( 'Price (fix or %)', 'woocommerce-product-addon' ), - 'choseFile' => __( 'Choose File', 'woocommerce-product-addon' ), - 'upload' => __( 'Upload', 'woocommerce-product-addon' ), - 'stock' => __( 'Stock', 'woocommerce-product-addon' ), - 'metaIds' => __( 'Meta IDs', 'woocommerce-product-addon' ), + ), + 'errorOccurred' => __( 'An error occurred. Please try again.', 'woocommerce-product-addon' ), + 'yes' => __( 'Yes', 'woocommerce-product-addon' ), + 'no' => __( 'No', 'woocommerce-product-addon' ), + 'updatedField' => __( 'Update Field', 'woocommerce-product-addon' ), + 'pricePlaceholder' => __( 'Price (fix or %)', 'woocommerce-product-addon' ), + 'choseFile' => __( 'Choose File', 'woocommerce-product-addon' ), + 'upload' => __( 'Upload', 'woocommerce-product-addon' ), + 'stock' => __( 'Stock', 'woocommerce-product-addon' ), + 'metaIds' => __( 'Meta IDs', 'woocommerce-product-addon' ), 'cannotRemoveMoreOption' => __( 'Cannot Remove More Option', 'woocommerce-product-addon' ), - 'dataNameRequired' => __( 'Data Name must be required', 'woocommerce-product-addon' ), - 'dataNameExists' => __( 'Data Name already exists', 'woocommerce-product-addon' ) - ] + 'dataNameRequired' => __( 'Data Name must be required', 'woocommerce-product-addon' ), + 'dataNameExists' => __( 'Data Name already exists', 'woocommerce-product-addon' ), + ), ); // localize ppom_vars @@ -207,7 +242,7 @@ function load_script( $hook ) { if ( 'new' === $action ) { $page_slug = 'new-field'; - } elseif( 'edit' === $action ) { + } elseif ( 'edit' === $action ) { $page_slug = 'edit-field'; } @@ -215,12 +250,14 @@ function load_script( $hook ) { } - /* - **============ Render all fields =========== - */ - function render_field_settings() { - // ppom_pa(PPOM() -> inputs); + // Field-builder rendering. + /** + * Renders the modal shells for every registered PPOM input type. + * + * @return void + */ + public function render_field_settings() { $html = ''; $html .= '
'; foreach ( PPOM()->inputs as $fields_type => $meta ) { @@ -253,11 +290,17 @@ function render_field_settings() { echo $html; } - /* - **============ Render all fields meta =========== - */ - function render_field_meta( $field_meta, $fields_type, $field_index = '', $save_meta = '' ) { - // ppom_pa($save_meta); + /** + * Renders the editable settings panels for a single PPOM input type. + * + * @param array $field_meta Input settings schema. + * @param string $fields_type PPOM input type slug. + * @param string|int $field_index Field index in the builder payload. + * @param array|string $save_meta Stored field values being edited. + * + * @return string + */ + public function render_field_meta( $field_meta, $fields_type, $field_index = '', $save_meta = '' ) { $html = ''; $html .= '
'; $html .= ''; @@ -288,12 +331,12 @@ function render_field_meta( $field_meta, $fields_type, $field_index = '', $save_ foreach ( $field_meta as $fields_meta_key => $meta ) { - $title = isset( $meta['title'] ) ? $meta['title'] : ''; - $desc = isset( $meta['desc'] ) ? $meta['desc'] : ''; - $type = isset( $meta['type'] ) ? $meta['type'] : ''; - $link = isset( $meta['link'] ) ? $meta['link'] : ''; + $title = isset( $meta['title'] ) ? $meta['title'] : ''; + $desc = isset( $meta['desc'] ) ? $meta['desc'] : ''; + $type = isset( $meta['type'] ) ? $meta['type'] : ''; + $link = isset( $meta['link'] ) ? $meta['link'] : ''; $learn_more = isset( $meta['learn_more'] ) ? $meta['learn_more'] : array(); - $values = isset( $save_meta[ $fields_meta_key ] ) ? $save_meta[ $fields_meta_key ] : ''; + $values = isset( $save_meta[ $fields_meta_key ] ) ? $save_meta[ $fields_meta_key ] : ''; $default_value = isset( $meta ['default'] ) ? $meta ['default'] : ''; // ppom_pa($meta); @@ -324,8 +367,8 @@ function render_field_meta( $field_meta, $fields_type, $field_index = '', $save_ $html .= '
'; @@ -941,7 +996,7 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu $html_input .= '
'; $condition_index = $last_array_id; - $condition_index ++; + ++$condition_index; } $html_input .= ''; } else { @@ -969,7 +1024,7 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu $html_input .= '
'; $html_input .= '
'; - $html_input .= '
'; + $html_input .= '
'; // conditional elements $html_input .= '
'; @@ -1020,7 +1075,7 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu $html_input .= '
'; // Upsell - $html_input .= ' ' . __('Upgrade to Unlock', 'woocommerce-product-addon') . ''; + $html_input .= ' ' . __( 'Upgrade to Unlock', 'woocommerce-product-addon' ) . ''; $html_input .= '
'; @@ -1070,14 +1125,14 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu $html_input .= ''; $html_input .= ''; $html_input .= ''; - $html_input .= ''; + $html_input .= ''; $html_input .= ''; $html_input .= '
'; $html_input .= ''; $opt_index0 = $last_array_id; - $opt_index0 ++; + ++$opt_index0; } } $html_input .= ''; @@ -1118,14 +1173,14 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu $html_input .= ''; $html_input .= ''; $html_input .= ''; - $html_input .= ''; + $html_input .= ''; $html_input .= ''; $html_input .= ''; $html_input .= ''; $html_input .= ''; $opt_index0 = $last_array_id; - $opt_index0 ++; + ++$opt_index0; } } $html_input .= ''; @@ -1167,7 +1222,7 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu $html_input .= ''; $opt_index0 = $last_array_id; - $opt_index0 ++; + ++$opt_index0; } } @@ -1249,11 +1304,11 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu $html_input .= '
- -

- +

+ " esc_url( tsdk_translate_link( tsdk_utmify( PPOM_UPGRADE_URL, $id ) ) ), __( 'Upgrade to the Pro', 'woocommerce-product-addon' ) ) - ); ?> -

-
+ ); + ?> +

+

@@ -182,7 +186,7 @@ class="" + title=""> @@ -191,7 +195,7 @@ class="" + target="_blank"> @@ -199,7 +203,7 @@ class="" + video-url=""> @@ -214,7 +218,7 @@ class="" + target="_blank"> @@ -222,7 +226,7 @@ class="" + video-url=""> @@ -290,10 +294,10 @@ class="" -

- -

+

+ +

diff --git a/bin/e2e-after-setup.sh b/bin/e2e-after-setup.sh deleted file mode 100644 index 0ef3efca..00000000 --- a/bin/e2e-after-setup.sh +++ /dev/null @@ -1,5 +0,0 @@ -# Run after `npm run wp-env start` - -# Add some woocommerce products. -npm run wp-env run tests-cli bash ./wp-content/plugins/woocommerce-product-addon/bin/env/create-products.sh - diff --git a/bin/wp-env/mu-plugins/ppom-e2e-bootstrap.php b/bin/wp-env/mu-plugins/ppom-e2e-bootstrap.php new file mode 100644 index 00000000..ea950010 --- /dev/null +++ b/bin/wp-env/mu-plugins/ppom-e2e-bootstrap.php @@ -0,0 +1,1253 @@ + 'valid', + 'plan' => 1, + ); +} + +/** + * Resolved license fixture for filters and AJAX responses. + * + * @return array{status:string,plan:int} + */ +function ppom_e2e_get_license_fixture() { + $defaults = ppom_e2e_default_license_fixture(); + $stored = get_option( PPOM_E2E_LICENSE_FIXTURE_OPTION, null ); + + if ( ! is_array( $stored ) ) { + return $defaults; + } + + $status = isset( $stored['status'] ) && 'invalid' === $stored['status'] ? 'invalid' : 'valid'; + $plan = isset( $stored['plan'] ) ? max( 1, min( 3, absint( $stored['plan'] ) ) ) : $defaults['plan']; + + return array( + 'status' => $status, + 'plan' => $plan, + ); +} + +/** + * The attach modal only enables tag selection when the license filter returns valid. + * wp-env runs the free build without a store key; unlock valid for automated admin UI tests. + * Use ppom_e2e_set_license_fixture to simulate inactive licenses in E2E. + */ +add_filter( + 'product_ppom_license_status', + static function ( $value ) { + $config = ppom_e2e_get_license_fixture(); + + if ( 'valid' === $config['status'] ) { + return 'valid'; + } + + return ''; + }, + PPOM_E2E_LICENSE_FILTER_PRIORITY, + 1 +); + +add_filter( + 'product_ppom_license_plan', + static function ( $value ) { + $config = ppom_e2e_get_license_fixture(); + + if ( 'valid' !== $config['status'] ) { + return 0; + } + + return (int) $config['plan']; + }, + PPOM_E2E_LICENSE_FILTER_PRIORITY, + 1 +); + +/** + * Ensure the current request is authorized to manage E2E bootstrap data. + * + * @return void + */ +function ppom_e2e_require_capability() { + if ( current_user_can( 'manage_woocommerce' ) || current_user_can( 'manage_options' ) ) { + return; + } + + wp_send_json_error( + array( + 'message' => 'You are not allowed to manage PPOM E2E fixtures.', + ), + 403 + ); +} + +/** + * Ensure a valid bootstrap nonce is present on the current AJAX request. + * + * @return void + */ +function ppom_e2e_require_nonce() { + $nonce = isset( $_REQUEST['_ajax_nonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_ajax_nonce'] ) ) : ''; + + if ( ! $nonce || ! wp_verify_nonce( $nonce, PPOM_E2E_BOOTSTRAP_NONCE_ACTION ) ) { + wp_send_json_error( + array( + 'message' => 'Invalid or missing PPOM E2E bootstrap nonce.', + ), + 403 + ); + } +} + +/** + * Return a JSON-decoded request value. + * + * @param string $key Request key. + * @param mixed $default Default value when the key is missing. + * + * @return mixed|WP_Error + */ +function ppom_e2e_decode_json_request( $key, $default = array() ) { + if ( ! isset( $_POST[ $key ] ) ) { + return $default; + } + + $raw_value = wp_unslash( $_POST[ $key ] ); + + if ( ! is_string( $raw_value ) ) { + return new WP_Error( + 'invalid_json', + sprintf( + /* translators: %s: request parameter name. */ + __( 'The "%s" payload is not valid JSON.', 'woocommerce-product-addon' ), + $key + ) + ); + } + + if ( '' === $raw_value ) { + return $default; + } + + $decoded = json_decode( $raw_value, true ); + + if ( JSON_ERROR_NONE !== json_last_error() ) { + return new WP_Error( + 'invalid_json', + sprintf( + /* translators: %s: request parameter name. */ + __( 'The "%s" payload is not valid JSON.', 'woocommerce-product-addon' ), + $key + ) + ); + } + + return $decoded; +} + +/** + * Send a JSON error response from a WP_Error instance. + * + * @param WP_Error $error Error instance. + * @param int $status_code HTTP status code. + * + * @return void + */ +function ppom_e2e_send_wp_error( $error, $status_code = 400 ) { + wp_send_json_error( + array( + 'code' => $error->get_error_code(), + 'message' => $error->get_error_message(), + ), + $status_code + ); +} + +/** + * Track a PPOM group ID created by the E2E bootstrap layer. + * + * @param int $meta_id PPOM group ID. + * + * @return void + */ +function ppom_e2e_track_meta_id( $meta_id ) { + $meta_id = absint( $meta_id ); + + if ( $meta_id <= 0 ) { + return; + } + + $tracked_ids = get_option( PPOM_E2E_META_IDS_OPTION, array() ); + $tracked_ids = is_array( $tracked_ids ) ? array_map( 'absint', $tracked_ids ) : array(); + $tracked_ids[] = $meta_id; + + update_option( PPOM_E2E_META_IDS_OPTION, array_values( array_unique( array_filter( $tracked_ids ) ) ), false ); +} + +/** + * Return the tracked PPOM group IDs. + * + * @return int[] + */ +function ppom_e2e_get_tracked_meta_ids() { + $tracked_ids = get_option( PPOM_E2E_META_IDS_OPTION, array() ); + + return is_array( $tracked_ids ) + ? array_values( array_unique( array_filter( array_map( 'absint', $tracked_ids ) ) ) ) + : array(); +} + +/** + * Mark a post as fixture-owned data. + * + * @param int $post_id Fixture post ID. + * + * @return void + */ +function ppom_e2e_mark_fixture_post( $post_id ) { + $post_id = absint( $post_id ); + + if ( $post_id > 0 ) { + update_post_meta( $post_id, PPOM_E2E_FIXTURE_MARKER_META_KEY, '1' ); + } +} + +/** + * Mark a term as fixture-owned data. + * + * @param int $term_id Fixture term ID. + * + * @return void + */ +function ppom_e2e_mark_fixture_term( $term_id ) { + $term_id = absint( $term_id ); + + if ( $term_id > 0 ) { + update_term_meta( $term_id, PPOM_E2E_FIXTURE_MARKER_META_KEY, '1' ); + } +} + +/** + * Recursively delete directory contents while preserving the root directory. + * + * @param string $directory Absolute directory path. + * + * @return int Number of removed filesystem entries. + */ +function ppom_e2e_cleanup_directory_contents( $directory ) { + if ( ! $directory || ! is_dir( $directory ) ) { + return 0; + } + + $removed_entries = 0; + $items = scandir( $directory ); + + if ( ! is_array( $items ) ) { + return 0; + } + + foreach ( $items as $item ) { + if ( '.' === $item || '..' === $item ) { + continue; + } + + $path = trailingslashit( $directory ) . $item; + + if ( is_dir( $path ) ) { + $removed_entries += ppom_e2e_cleanup_directory_contents( $path ); + + if ( is_dir( $path ) && rmdir( $path ) ) { + ++$removed_entries; + } + + continue; + } + + if ( file_exists( $path ) && unlink( $path ) ) { + ++$removed_entries; + } + } + + return $removed_entries; +} + +/** + * Normalize a list of category IDs from request data. + * + * @param mixed $category_ids Request payload. + * + * @return int[] + */ +function ppom_e2e_normalize_category_ids( $category_ids ) { + return is_array( $category_ids ) + ? array_values( array_unique( array_filter( array_map( 'absint', $category_ids ) ) ) ) + : array(); +} + +/** + * Convert fixture attribute definitions into WooCommerce product attributes. + * + * @param array $attribute_definitions Attribute payload. + * + * @return array + */ +function ppom_e2e_build_product_attributes( $attribute_definitions ) { + if ( ! is_array( $attribute_definitions ) ) { + return array(); + } + + $product_attributes = array(); + + foreach ( $attribute_definitions as $index => $attribute_definition ) { + if ( ! is_array( $attribute_definition ) || empty( $attribute_definition['name'] ) ) { + continue; + } + + $options = isset( $attribute_definition['options'] ) && is_array( $attribute_definition['options'] ) + ? array_values( + array_filter( + array_map( 'sanitize_text_field', $attribute_definition['options'] ), + static function ( $option ) { + return '' !== $option; + } + ) + ) + : array(); + + if ( empty( $options ) ) { + continue; + } + + $product_attribute = new WC_Product_Attribute(); + $product_attribute->set_id( 0 ); + $product_attribute->set_name( sanitize_text_field( $attribute_definition['name'] ) ); + $product_attribute->set_options( $options ); + $product_attribute->set_position( (int) $index ); + $product_attribute->set_visible( ! isset( $attribute_definition['visible'] ) || (bool) $attribute_definition['visible'] ); + $product_attribute->set_variation( ! isset( $attribute_definition['variation'] ) || (bool) $attribute_definition['variation'] ); + + $product_attributes[] = $product_attribute; + } + + return $product_attributes; +} + +/** + * Normalize variation attribute input into WooCommerce format. + * + * @param mixed $attributes Variation attribute payload. + * + * @return array + */ +function ppom_e2e_normalize_variation_attributes( $attributes ) { + if ( ! is_array( $attributes ) ) { + return array(); + } + + $normalized_attributes = array(); + + foreach ( $attributes as $attribute_name => $attribute_value ) { + if ( ! is_scalar( $attribute_name ) || ! is_scalar( $attribute_value ) ) { + continue; + } + + $normalized_name = sanitize_title( (string) $attribute_name ); + $normalized_value = sanitize_text_field( (string) $attribute_value ); + + if ( '' === $normalized_name || '' === $normalized_value ) { + continue; + } + + $normalized_attributes[ $normalized_name ] = $normalized_value; + } + + return $normalized_attributes; +} + +/** + * Prepare a PPOM field payload for persistence. + * + * @param mixed $ppom_fields Submitted PPOM fields. + * @param int|string $productmeta_id Existing PPOM group ID for filters. + * + * @return array|WP_Error + */ +function ppom_e2e_prepare_form_meta_fields( $ppom_fields, $productmeta_id = '' ) { + if ( ! is_array( $ppom_fields ) || empty( $ppom_fields ) ) { + return new WP_Error( + 'no_fields', + __( 'No fields found.', 'woocommerce-product-addon' ) + ); + } + + $product_meta = apply_filters( 'ppom_meta_data_saving', $ppom_fields, $productmeta_id ); + $product_meta = ppom_sanitize_array_data( $product_meta ); + + $serialized_meta = array_values( + array_filter( + $product_meta, + static function ( $field ) { + return ! empty( $field['type'] ) || ! empty( $field['data_name'] ); + } + ) + ); + + if ( empty( $serialized_meta ) ) { + return new WP_Error( + 'no_fields', + __( 'No fields found.', 'woocommerce-product-addon' ) + ); + } + + $final_meta = array_values( + array_filter( + $product_meta, + static function ( $field ) { + return ! empty( $field['type'] ) && ! empty( $field['data_name'] ); + } + ) + ); + + return array( + 'serialized' => wp_json_encode( $serialized_meta ), + 'final' => $final_meta, + ); +} + +/** + * Insert a PPOM field group via the E2E bootstrap layer. + * + * @param array $args Group settings and field payload. + * + * @return array|WP_Error + */ +function ppom_e2e_insert_ppom_group( $args ) { + $db_version = (float) get_option( 'personalizedproduct_db_version' ); + + if ( $db_version < 22.1 ) { + return new WP_Error( + 'db_version_outdated', + __( 'Since version 22.0, Database has some changes. Please Deactivate & then activate the PPOM plugin.', 'woocommerce-product-addon' ) + ); + } + + $ppom_fields = isset( $args['fields'] ) && is_array( $args['fields'] ) + ? $args['fields'] + : array(); + + $prepared_fields = ppom_e2e_prepare_form_meta_fields( $ppom_fields ); + + if ( is_wp_error( $prepared_fields ) ) { + return $prepared_fields; + } + + $productmeta_name = isset( $args['group_name'] ) + ? sanitize_text_field( wp_unslash( $args['group_name'] ) ) + : ''; + $product_id = isset( $args['product_id'] ) ? absint( $args['product_id'] ) : 0; + $settings = isset( $args['settings'] ) && is_array( $args['settings'] ) + ? $args['settings'] + : array(); + + if ( strlen( $productmeta_name ) > 50 ) { + return new WP_Error( + 'group_name_too_long', + __( 'PPOM title is too long to save, please make it less than 50 characters.', 'woocommerce-product-addon' ) + ); + } + + $ppom_settings_meta_data = array( + 'productmeta_name' => $productmeta_name, + 'dynamic_price_display' => isset( $settings['dynamic_price_hide'] ) ? sanitize_text_field( wp_unslash( $settings['dynamic_price_hide'] ) ) : 'no', + 'send_file_attachment' => isset( $settings['send_file_attachment'] ) ? sanitize_text_field( wp_unslash( $settings['send_file_attachment'] ) ) : '', + 'show_cart_thumb' => isset( $settings['show_cart_thumb'] ) ? sanitize_text_field( wp_unslash( $settings['show_cart_thumb'] ) ) : 'no', + 'aviary_api_key' => isset( $settings['aviary_api_key'] ) ? sanitize_text_field( wp_unslash( $settings['aviary_api_key'] ) ) : '', + 'the_meta' => $prepared_fields['serialized'], + 'productmeta_created' => current_time( 'mysql' ), + ); + + if ( ! ppom_is_legacy_user() ) { + $ppom_settings_meta_data['productmeta_style'] = isset( $settings['productmeta_style'] ) ? sanitize_text_field( wp_unslash( $settings['productmeta_style'] ) ) : ''; + $ppom_settings_meta_data['productmeta_js'] = isset( $settings['productmeta_js'] ) ? sanitize_text_field( wp_unslash( $settings['productmeta_js'] ) ) : ''; + } + + $dt = apply_filters( 'ppom_settings_meta_data_new', $ppom_settings_meta_data ); + + global $wpdb; + $ppom_table = $wpdb->prefix . PPOM_TABLE_META; + $inserted = $wpdb->insert( + $ppom_table, + $dt, + array_fill( 0, count( $dt ), '%s' ) + ); + + if ( false === $inserted ) { + return new WP_Error( + 'meta_insert_failed', + __( 'PPOM group could not be saved.', 'woocommerce-product-addon' ) + ); + } + + $ppom_id = (int) $wpdb->insert_id; + $final_fields = ppom_e2e_prepare_form_meta_fields( $ppom_fields, $ppom_id ); + + if ( is_wp_error( $final_fields ) ) { + return $final_fields; + } + + ppom_admin_update_ppom_meta_only( $ppom_id, $final_fields['final'] ); + + if ( $product_id > 0 ) { + ppom_attach_fields_to_product( $ppom_id, $product_id ); + } + + return array( + 'productmeta_id' => $ppom_id, + ); +} + +/** + * Attach a PPOM group to products and taxonomy targets via the bootstrap layer. + * + * @param array $args Attachment payload. + * + * @return array|WP_Error + */ +function ppom_e2e_attach_group( $args ) { + $ppom_id = isset( $args['ppom_id'] ) ? absint( $args['ppom_id'] ) : 0; + + if ( $ppom_id <= 0 ) { + return new WP_Error( + 'invalid_ppom_id', + __( 'A valid PPOM group is required.', 'woocommerce-product-addon' ) + ); + } + + $is_pro_user = 'valid' === apply_filters( 'product_ppom_license_status', '' ); + $products_to_attach = isset( $args['product_ids'] ) && is_array( $args['product_ids'] ) ? array_values( array_unique( array_filter( array_map( 'absint', $args['product_ids'] ) ) ) ) : array(); + $products_to_attach_initial = isset( $args['product_ids_initial'] ) && is_array( $args['product_ids_initial'] ) ? array_values( array_unique( array_filter( array_map( 'absint', $args['product_ids_initial'] ) ) ) ) : array(); + + $products_to_add = array_diff( $products_to_attach, $products_to_attach_initial ); + foreach ( $products_to_add as $product_to_add ) { + if ( $is_pro_user ) { + $current_attached_fields = get_post_meta( $product_to_add, PPOM_PRODUCT_META_KEY, true ); + + if ( is_array( $current_attached_fields ) ) { + $current_attached_fields[] = $ppom_id; + $current_attached_fields = array_unique( $current_attached_fields ); + } elseif ( is_numeric( $current_attached_fields ) ) { + $current_attached_fields = array( $current_attached_fields, $ppom_id ); + } else { + $current_attached_fields = array( $ppom_id ); + } + + $current_attached_fields = array_filter( $current_attached_fields, 'is_numeric' ); + update_post_meta( $product_to_add, PPOM_PRODUCT_META_KEY, $current_attached_fields ); + } else { + update_post_meta( $product_to_add, PPOM_PRODUCT_META_KEY, array( $ppom_id ) ); + } + } + + $products_to_remove = array_diff( $products_to_attach_initial, $products_to_attach ); + foreach ( $products_to_remove as $product_to_remove ) { + $should_delete = true; + $current_attached_fields = get_post_meta( $product_to_remove, PPOM_PRODUCT_META_KEY, true ); + + if ( is_array( $current_attached_fields ) ) { + $key = array_search( $ppom_id, $current_attached_fields ); + + if ( false !== $key ) { + unset( $current_attached_fields[ $key ] ); + + if ( ! empty( $current_attached_fields ) ) { + $should_delete = false; + update_post_meta( $product_to_remove, PPOM_PRODUCT_META_KEY, $current_attached_fields ); + } + } + } + + if ( $should_delete ) { + delete_post_meta( $product_to_remove, PPOM_PRODUCT_META_KEY ); + } + } + + $category_slugs = isset( $args['category_slugs'] ) && is_array( $args['category_slugs'] ) + ? array_values( array_unique( array_filter( array_map( 'sanitize_title', $args['category_slugs'] ) ) ) ) + : array(); + $tag_slugs = isset( $args['tag_slugs'] ) && is_array( $args['tag_slugs'] ) + ? array_values( array_unique( array_filter( array_map( 'sanitize_title', $args['tag_slugs'] ) ) ) ) + : false; + + NM_PersonalizedProduct_Admin::save_categories_and_tags( $ppom_id, $category_slugs, $tag_slugs ); + + return array( + 'updated_products' => count( $products_to_add ) + count( $products_to_remove ), + 'updated_categories' => count( $category_slugs ), + 'updated_tags' => is_array( $tag_slugs ) ? count( $tag_slugs ) : 0, + ); +} + +/** + * Return a nonce for authenticated E2E bootstrap requests. + * + * @return void + */ +function ppom_e2e_get_nonce() { + ppom_e2e_require_capability(); + + wp_send_json_success( + array( + 'nonce' => wp_create_nonce( PPOM_E2E_BOOTSTRAP_NONCE_ACTION ), + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_get_nonce', 'ppom_e2e_get_nonce' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_get_nonce', 'ppom_e2e_get_nonce' ); + +/** + * Create a WooCommerce product category for fixtures. + * + * @return void + */ +function ppom_e2e_create_product_category() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; + $slug = isset( $_POST['slug'] ) ? sanitize_title( wp_unslash( $_POST['slug'] ) ) : ''; + + if ( '' === $name ) { + wp_send_json_error( + array( + 'message' => 'Category name is required.', + ), + 400 + ); + } + + $term = wp_insert_term( + $name, + 'product_cat', + array( + 'slug' => $slug, + ) + ); + + if ( is_wp_error( $term ) ) { + ppom_e2e_send_wp_error( $term ); + } + + ppom_e2e_mark_fixture_term( $term['term_id'] ); + + $created_term = get_term( $term['term_id'], 'product_cat' ); + + wp_send_json_success( + array( + 'id' => (int) $created_term->term_id, + 'name' => $created_term->name, + 'slug' => $created_term->slug, + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_create_product_category', 'ppom_e2e_create_product_category' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_create_product_category', 'ppom_e2e_create_product_category' ); + +/** + * Create a WooCommerce product tag for fixtures. + * + * @return void + */ +function ppom_e2e_create_product_tag() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; + $slug = isset( $_POST['slug'] ) ? sanitize_title( wp_unslash( $_POST['slug'] ) ) : ''; + + if ( '' === $name ) { + wp_send_json_error( + array( + 'message' => 'Tag name is required.', + ), + 400 + ); + } + + $term = wp_insert_term( + $name, + 'product_tag', + array( + 'slug' => $slug, + ) + ); + + if ( is_wp_error( $term ) ) { + ppom_e2e_send_wp_error( $term ); + } + + ppom_e2e_mark_fixture_term( $term['term_id'] ); + + $created_term = get_term( $term['term_id'], 'product_tag' ); + + wp_send_json_success( + array( + 'id' => (int) $created_term->term_id, + 'name' => $created_term->name, + 'slug' => $created_term->slug, + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_create_product_tag', 'ppom_e2e_create_product_tag' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_create_product_tag', 'ppom_e2e_create_product_tag' ); + +/** + * Read category/tag attachment columns for a PPOM group (E2E assertions). + * + * @return void + */ +function ppom_e2e_get_ppom_attach_row() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + $ppom_id = isset( $_POST['ppom_id'] ) ? absint( wp_unslash( $_POST['ppom_id'] ) ) : 0; + + if ( $ppom_id <= 0 ) { + wp_send_json_error( + array( + 'message' => 'A valid ppom_id is required.', + ), + 400 + ); + } + + if ( ! defined( 'PPOM_TABLE_META' ) ) { + wp_send_json_error( + array( + 'message' => 'PPOM meta table constant is unavailable.', + ), + 500 + ); + } + + global $wpdb; + + $table = $wpdb->prefix . PPOM_TABLE_META; + $row = $wpdb->get_row( + $wpdb->prepare( + "SELECT productmeta_categories, productmeta_tags FROM {$table} WHERE productmeta_id = %d", + $ppom_id + ), + ARRAY_A + ); + + if ( empty( $row ) || ! is_array( $row ) ) { + wp_send_json_error( + array( + 'message' => 'PPOM row not found.', + ), + 404 + ); + } + + wp_send_json_success( $row ); +} +add_action( 'wp_ajax_ppom_e2e_get_ppom_attach_row', 'ppom_e2e_get_ppom_attach_row' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_get_ppom_attach_row', 'ppom_e2e_get_ppom_attach_row' ); + +/** + * Create a WooCommerce simple product for fixtures. + * + * @return void + */ +function ppom_e2e_create_simple_product() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + if ( ! class_exists( 'WC_Product_Simple' ) ) { + wp_send_json_error( + array( + 'message' => 'WooCommerce simple product support is unavailable.', + ), + 500 + ); + } + + $category_ids = ppom_e2e_decode_json_request( 'category_ids', array() ); + + if ( is_wp_error( $category_ids ) ) { + ppom_e2e_send_wp_error( $category_ids ); + } + + $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; + $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'publish'; + $regular_price = isset( $_POST['regular_price'] ) ? wc_format_decimal( wp_unslash( $_POST['regular_price'] ) ) : '9.99'; + + if ( '' === $name ) { + wp_send_json_error( + array( + 'message' => 'Product name is required.', + ), + 400 + ); + } + + if ( ! in_array( $status, array( 'draft', 'pending', 'private', 'publish' ), true ) ) { + $status = 'publish'; + } + + $product = new WC_Product_Simple(); + $product->set_name( $name ); + $product->set_status( $status ); + $product->set_regular_price( $regular_price ); + $product->set_price( $regular_price ); + $product->set_category_ids( ppom_e2e_normalize_category_ids( $category_ids ) ); + + $product_id = $product->save(); + + if ( ! $product_id ) { + wp_send_json_error( + array( + 'message' => 'WooCommerce product could not be saved.', + ), + 500 + ); + } + + ppom_e2e_mark_fixture_post( $product_id ); + + wp_send_json_success( + array( + 'id' => (int) $product_id, + 'name' => $product->get_name(), + 'status' => $product->get_status(), + 'type' => $product->get_type(), + 'regular_price' => $product->get_regular_price(), + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_create_simple_product', 'ppom_e2e_create_simple_product' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_create_simple_product', 'ppom_e2e_create_simple_product' ); + +/** + * Create a WooCommerce variable product for fixtures. + * + * @return void + */ +function ppom_e2e_create_variable_product() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + if ( ! class_exists( 'WC_Product_Variable' ) ) { + wp_send_json_error( + array( + 'message' => 'WooCommerce variable product support is unavailable.', + ), + 500 + ); + } + + $category_ids = ppom_e2e_decode_json_request( 'category_ids', array() ); + $attributes = ppom_e2e_decode_json_request( 'attributes', array() ); + $default_attributes = ppom_e2e_decode_json_request( 'default_attributes', array() ); + + if ( is_wp_error( $category_ids ) ) { + ppom_e2e_send_wp_error( $category_ids ); + } + + if ( is_wp_error( $attributes ) ) { + ppom_e2e_send_wp_error( $attributes ); + } + + if ( is_wp_error( $default_attributes ) ) { + ppom_e2e_send_wp_error( $default_attributes ); + } + + $name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : ''; + $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'publish'; + + if ( '' === $name ) { + wp_send_json_error( + array( + 'message' => 'Variable product name is required.', + ), + 400 + ); + } + + if ( ! in_array( $status, array( 'draft', 'pending', 'private', 'publish' ), true ) ) { + $status = 'publish'; + } + + $product = new WC_Product_Variable(); + $product->set_name( $name ); + $product->set_status( $status ); + $product->set_category_ids( ppom_e2e_normalize_category_ids( $category_ids ) ); + $product->set_attributes( ppom_e2e_build_product_attributes( $attributes ) ); + $product->set_default_attributes( ppom_e2e_normalize_variation_attributes( $default_attributes ) ); + + $product_id = $product->save(); + + if ( ! $product_id ) { + wp_send_json_error( + array( + 'message' => 'WooCommerce variable product could not be saved.', + ), + 500 + ); + } + + ppom_e2e_mark_fixture_post( $product_id ); + + wp_send_json_success( + array( + 'id' => (int) $product_id, + 'name' => $product->get_name(), + 'status' => $product->get_status(), + 'type' => $product->get_type(), + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_create_variable_product', 'ppom_e2e_create_variable_product' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_create_variable_product', 'ppom_e2e_create_variable_product' ); + +/** + * Create a WooCommerce variation for a variable product fixture. + * + * @return void + */ +function ppom_e2e_create_product_variation() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + if ( ! class_exists( 'WC_Product_Variation' ) ) { + wp_send_json_error( + array( + 'message' => 'WooCommerce product variation support is unavailable.', + ), + 500 + ); + } + + $attributes = ppom_e2e_decode_json_request( 'attributes', array() ); + + if ( is_wp_error( $attributes ) ) { + ppom_e2e_send_wp_error( $attributes ); + } + + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; + $parent_product = $product_id ? wc_get_product( $product_id ) : false; + + if ( ! $parent_product || ! $parent_product->is_type( 'variable' ) ) { + wp_send_json_error( + array( + 'message' => 'A valid variable parent product is required.', + ), + 400 + ); + } + + $status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'publish'; + $regular_price = isset( $_POST['regular_price'] ) ? wc_format_decimal( wp_unslash( $_POST['regular_price'] ) ) : '12.99'; + + if ( ! in_array( $status, array( 'draft', 'pending', 'private', 'publish' ), true ) ) { + $status = 'publish'; + } + + $variation = new WC_Product_Variation(); + $variation->set_parent_id( $product_id ); + $variation->set_status( $status ); + $variation->set_regular_price( $regular_price ); + $variation->set_price( $regular_price ); + $variation->set_attributes( ppom_e2e_normalize_variation_attributes( $attributes ) ); + + $variation_id = $variation->save(); + + if ( ! $variation_id ) { + wp_send_json_error( + array( + 'message' => 'WooCommerce variation could not be saved.', + ), + 500 + ); + } + + ppom_e2e_mark_fixture_post( $variation_id ); + + wp_send_json_success( + array( + 'id' => (int) $variation_id, + 'parent_id' => $product_id, + 'status' => $variation->get_status(), + 'regular_price' => $variation->get_regular_price(), + 'attributes' => $variation->get_attributes(), + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_create_product_variation', 'ppom_e2e_create_product_variation' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_create_product_variation', 'ppom_e2e_create_product_variation' ); + +/** + * Create a PPOM field group for fixtures. + * + * @return void + */ +function ppom_e2e_create_ppom_group() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + $fields = ppom_e2e_decode_json_request( 'fields', array() ); + $settings = ppom_e2e_decode_json_request( 'settings', array() ); + + if ( is_wp_error( $fields ) ) { + ppom_e2e_send_wp_error( $fields ); + } + + if ( is_wp_error( $settings ) ) { + ppom_e2e_send_wp_error( $settings ); + } + + $group_name = isset( $_POST['group_name'] ) ? sanitize_text_field( wp_unslash( $_POST['group_name'] ) ) : ''; + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0; + + $group_result = ppom_e2e_insert_ppom_group( + array( + 'group_name' => $group_name, + 'product_id' => $product_id, + 'fields' => is_array( $fields ) ? $fields : array(), + 'settings' => is_array( $settings ) ? $settings : array(), + ) + ); + + if ( is_wp_error( $group_result ) ) { + ppom_e2e_send_wp_error( $group_result ); + } + + ppom_e2e_track_meta_id( $group_result['productmeta_id'] ); + + wp_send_json_success( + array( + 'ppom_id' => (int) $group_result['productmeta_id'], + 'productmeta_id' => (int) $group_result['productmeta_id'], + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_create_ppom_group', 'ppom_e2e_create_ppom_group' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_create_ppom_group', 'ppom_e2e_create_ppom_group' ); + +/** + * Attach a PPOM field group to products or categories. + * + * @return void + */ +function ppom_e2e_attach_ppom_group() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + $product_ids = ppom_e2e_decode_json_request( 'product_ids', array() ); + $product_ids_initial = ppom_e2e_decode_json_request( 'product_ids_initial', array() ); + $category_slugs = ppom_e2e_decode_json_request( 'category_slugs', array() ); + $tag_slugs = ppom_e2e_decode_json_request( 'tag_slugs', array() ); + + foreach ( array( $product_ids, $product_ids_initial, $category_slugs, $tag_slugs ) as $decoded_payload ) { + if ( is_wp_error( $decoded_payload ) ) { + ppom_e2e_send_wp_error( $decoded_payload ); + } + } + + $attachment_result = ppom_e2e_attach_group( + array( + 'ppom_id' => isset( $_POST['ppom_id'] ) ? $_POST['ppom_id'] : 0, + 'product_ids' => is_array( $product_ids ) ? $product_ids : array(), + 'product_ids_initial' => is_array( $product_ids_initial ) ? $product_ids_initial : array(), + 'category_slugs' => is_array( $category_slugs ) ? $category_slugs : array(), + 'tag_slugs' => is_array( $tag_slugs ) ? $tag_slugs : array(), + ) + ); + + if ( is_wp_error( $attachment_result ) ) { + ppom_e2e_send_wp_error( $attachment_result ); + } + + wp_send_json_success( $attachment_result ); +} +add_action( 'wp_ajax_ppom_e2e_attach_ppom_group', 'ppom_e2e_attach_ppom_group' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_attach_ppom_group', 'ppom_e2e_attach_ppom_group' ); + +/** + * Set PPOM license fixture for E2E (drives product_ppom_license_* filters). + * + * @return void + */ +function ppom_e2e_set_license_fixture() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + $status_raw = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : ''; + $plan_raw = isset( $_POST['plan'] ) ? absint( wp_unslash( $_POST['plan'] ) ) : 0; + + $status = ( 'invalid' === $status_raw ) ? 'invalid' : 'valid'; + $plan = max( 1, min( 3, $plan_raw > 0 ? $plan_raw : 1 ) ); + + $stored = array( + 'status' => $status, + 'plan' => $plan, + ); + + update_option( PPOM_E2E_LICENSE_FIXTURE_OPTION, $stored, false ); + + wp_send_json_success( ppom_e2e_get_license_fixture() ); +} +add_action( 'wp_ajax_ppom_e2e_set_license_fixture', 'ppom_e2e_set_license_fixture' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_set_license_fixture', 'ppom_e2e_set_license_fixture' ); + +/** + * Read the current PPOM license fixture (for E2E assertions). + * + * @return void + */ +function ppom_e2e_read_license_fixture() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + wp_send_json_success( ppom_e2e_get_license_fixture() ); +} +add_action( 'wp_ajax_ppom_e2e_read_license_fixture', 'ppom_e2e_read_license_fixture' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_read_license_fixture', 'ppom_e2e_read_license_fixture' ); + +/** + * Reset PPOM E2E fixture state. + * + * @return void + */ +function ppom_e2e_reset_state() { + ppom_e2e_require_capability(); + ppom_e2e_require_nonce(); + + $deleted_meta_rows = 0; + $tracked_meta_ids = ppom_e2e_get_tracked_meta_ids(); + + if ( ! empty( $tracked_meta_ids ) && defined( 'PPOM_TABLE_META' ) ) { + global $wpdb; + + $ppom_table = $wpdb->prefix . PPOM_TABLE_META; + $placeholders = implode( ',', array_fill( 0, count( $tracked_meta_ids ), '%d' ) ); + $deleted_result = $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$ppom_table} WHERE productmeta_id IN ({$placeholders})", + $tracked_meta_ids + ) + ); + + if ( false !== $deleted_result ) { + $deleted_meta_rows = (int) $deleted_result; + } + } + + delete_option( PPOM_E2E_META_IDS_OPTION ); + delete_option( PPOM_E2E_LICENSE_FIXTURE_OPTION ); + + if ( defined( 'PPOM_PRODUCT_META_KEY' ) ) { + delete_post_meta_by_key( PPOM_PRODUCT_META_KEY ); + } + + $fixture_post_ids = get_posts( + array( + 'post_type' => array( 'product', 'product_variation' ), + 'post_status' => 'any', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'meta_key' => PPOM_E2E_FIXTURE_MARKER_META_KEY, + 'meta_value' => '1', + ) + ); + + $deleted_posts = 0; + foreach ( $fixture_post_ids as $fixture_post_id ) { + if ( wp_delete_post( $fixture_post_id, true ) ) { + ++$deleted_posts; + } + } + + $deleted_terms = 0; + + foreach ( array( 'product_cat', 'product_tag' ) as $fixture_taxonomy ) { + $fixture_term_ids = get_terms( + array( + 'taxonomy' => $fixture_taxonomy, + 'hide_empty' => false, + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => PPOM_E2E_FIXTURE_MARKER_META_KEY, + 'value' => '1', + ), + ), + ) + ); + + if ( is_wp_error( $fixture_term_ids ) ) { + continue; + } + + foreach ( $fixture_term_ids as $fixture_term_id ) { + $deleted_term = wp_delete_term( $fixture_term_id, $fixture_taxonomy ); + + if ( ! is_wp_error( $deleted_term ) && $deleted_term ) { + ++$deleted_terms; + } + } + } + + $deleted_upload_entries = 0; + if ( function_exists( 'ppom_get_dir_path' ) ) { + $deleted_upload_entries = ppom_e2e_cleanup_directory_contents( ppom_get_dir_path() ); + } + + wp_send_json_success( + array( + 'deleted_meta_rows' => $deleted_meta_rows, + 'deleted_posts' => $deleted_posts, + 'deleted_terms' => $deleted_terms, + 'deleted_upload_entries' => $deleted_upload_entries, + ) + ); +} +add_action( 'wp_ajax_ppom_e2e_reset_state', 'ppom_e2e_reset_state' ); +add_action( 'wp_ajax_nopriv_ppom_e2e_reset_state', 'ppom_e2e_reset_state' ); diff --git a/classes/admin.class.php b/classes/admin.class.php index d715fa9f..36c95775 100644 --- a/classes/admin.class.php +++ b/classes/admin.class.php @@ -1,6 +1,9 @@ plugin_meta = ppom_get_plugin_meta(); @@ -80,6 +94,8 @@ function __construct() { add_action( 'admin_init', array( $this, 'ppom_create_db_tables' ) ); } + // Admin page registration. + /** * Menu page options. */ @@ -112,23 +128,24 @@ public function upgrade_to_pro_plugin_action( $actions, $plugin_file ) { return array_merge( array( 'upgrade_link' => '' . __( 'Get Pro', 'woocommerce-product-addon' ) . '', + array( + 'utm_source' => 'wpadmin', + 'utm_medium' => 'plugins', + 'utm_campaign' => 'rowaction', + ), + tsdk_translate_link( PPOM_UPGRADE_URL ) + ) . '" title="' . __( 'More Features', 'woocommerce-product-addon' ) . '" target="_blank" rel="noopener noreferrer" style="color: #009E29; font-weight: 700;" onmouseover="this.style.color=\'#008a20\';" onmouseout="this.style.color=\'#009528\';" >' . __( 'Get Pro', 'woocommerce-product-addon' ) . '', ), $actions ); - } - /* - * creating menu page for this plugin - */ - function add_menu_pages() { + /** + * Registers the PPOM admin menu entries and page-load callbacks. + * + * @return string|void + */ + public function add_menu_pages() { if ( ! $this->menu_pages ) { return ''; @@ -186,33 +203,43 @@ function add_menu_pages() { } } + // Field-group pages and attach UI. - /* - * CALLBACKS - */ - function product_meta() { - //hide this on PPOM page since is conflicting with floating widget. + /** + * Renders the main PPOM field-group management screen. + * + * Routes between the field-group list, field editor, clone flow, addons + * listing, and changelog views based on the current admin request. + * + * @return void + * + * @see PPOM_Fields_Meta::render_field_settings() + * @see ppom_admin_save_form_meta() + * @see ppom_admin_update_form_meta() + */ + public function product_meta() { + // hide this on PPOM page since is conflicting with floating widget. add_filter( 'update_footer', '__return_empty_string' ); echo '
'; echo '

- - -
+ + +
-
'; $html_input .= '
'; $html_input .= '
'; - $html_input .= ' '; - $html_input .= ''; + $html_input .= ' '; + $html_input .= ''; if ( ! empty( $bulk_data ) ) { - $html_input .= ""; + $html_input .= ""; } else { $html_input .= ""; } @@ -1271,7 +1326,14 @@ function render_all_input_types( $name, $data, $fields_type, $field_index, $valu } - function ppom_fields_tabs( $fields_type ) { + /** + * Returns the field-builder tab configuration for an input type. + * + * @param string $fields_type PPOM input type slug. + * + * @return array + */ + public function ppom_fields_tabs( $fields_type ) { $tabs = array(); @@ -1336,7 +1398,6 @@ function ppom_fields_tabs( $fields_type ) { ); return apply_filters( 'ppom_fields_tabs_show', $tabs, $fields_type ); - } /** @@ -1345,7 +1406,7 @@ function ppom_fields_tabs( $fields_type ) { * @param array $settings * @return array Returns setting fields as updated their HTML classes. */ - function update_html_classes( $settings ) { + public function update_html_classes( $settings ) { foreach ( $settings as $fields_meta_key => $meta ) { @@ -1393,9 +1454,13 @@ function update_html_classes( $settings ) { // ppom_pa return apply_filters( 'ppom_tabs_panel_classes', $settings ); } - } +/** + * Returns the shared PPOM field-builder registry instance. + * + * @return PPOM_Fields_Meta + */ PPOM_FIELDS_META(); function PPOM_FIELDS_META() { return PPOM_Fields_Meta::get_instance(); diff --git a/classes/form.class.php b/classes/form.class.php index 73035972..f7386dcc 100644 --- a/classes/form.class.php +++ b/classes/form.class.php @@ -1,48 +1,45 @@ product ); @@ -80,9 +69,13 @@ function wrapper_inner_classes() { } /** - * PPOM fields rendering callback. + * Renders the fields for a PPOM group. * - * @param int $meta_id Meta ID. + * @param int $meta_id PPOM group ID. + * + * @return void + * + * @see PPOM_Meta::get_fields() */ function ppom_fields_render( $meta_id = 0 ) { @@ -95,16 +88,16 @@ function ppom_fields_render( $meta_id = 0 ) { // posted value being // used ppom-pro - $posted_values = apply_filters( 'ppom_default_values', $posted_values, $_POST, $this->product_id, self::$args ); - $fields = array_filter( + $posted_values = apply_filters( 'ppom_default_values', $posted_values, $_POST, $this->product_id, self::$args ); + $fields = array_filter( self::$ppom->fields, - function( $field ) use ( $meta_id ) { + function ( $field ) use ( $meta_id ) { return (int) $meta_id === (int) $field['ppom_id']; } ); $collapse_fields = array_filter( $fields, - function( $collapse_field ) { + function ( $collapse_field ) { return isset( $collapse_field['type'] ) && 'collapse' === $collapse_field['type']; } ); @@ -114,7 +107,7 @@ function( $collapse_field ) { $type = isset( $meta['type'] ) ? $meta['type'] : ''; $title = isset( $meta['title'] ) ? ppom_wpml_translate( $meta['title'], 'PPOM' ) : ''; $data_name = isset( $meta['data_name'] ) ? $meta['data_name'] : $title; - $ppom_field_counter ++; + ++$ppom_field_counter; // Set ID on meta against dataname $meta['id'] = $data_name; @@ -181,7 +174,7 @@ function( $collapse_field ) { } $section_started = true; - $ppom_collapse_counter ++; + ++$ppom_collapse_counter; if ( count( $fields ) === $ppom_field_counter ) { echo '
'; @@ -233,7 +226,17 @@ function( $collapse_field ) { } } - function render_input_template( $meta, $default_value ) { + /** + * Loads the frontend template for a field. + * + * @param array $meta Field definition. + * @param mixed $default_value Default value for the field. + * + * @return void + * + * @see ppom_load_input_templates() + */ + public function render_input_template( $meta, $default_value ) { $type = isset( $meta['type'] ) ? $meta['type'] : ''; @@ -378,10 +381,10 @@ function get_field_default_value( $posted_values, $data_name, $meta ) { } } elseif ( isset( $_GET[ $data_name ] ) ) { // When Cart Edit addon used. - $edit_data = isset( $_GET[ $data_name ] ) ? $_GET[ $data_name ] : ''; + $edit_data = isset( $_GET[ $data_name ] ) ? $_GET[ $data_name ] : ''; $default_value = is_array( $edit_data ) ? map_deep( $edit_data, 'sanitize_text_field' ) : sanitize_text_field( $_GET[ $data_name ] ); } elseif ( isset( $_POST['ppom']['fields'][ $data_name ] ) && apply_filters( 'ppom_retain_after_add_to_cart', true ) ) { - $edit_data = isset( $_POST['ppom']['fields'][ $data_name ] ) ? $_POST['ppom']['fields'][ $data_name ] : ''; + $edit_data = isset( $_POST['ppom']['fields'][ $data_name ] ) ? $_POST['ppom']['fields'][ $data_name ] : ''; $default_value = is_array( $edit_data ) ? map_deep( $edit_data, 'sanitize_text_field' ) : sanitize_text_field( $_POST['ppom']['fields'][ $data_name ] ); } else { // Default values in settings diff --git a/classes/freemium.class.php b/classes/freemium.class.php index 1078dcf1..d6df5c09 100644 --- a/classes/freemium.class.php +++ b/classes/freemium.class.php @@ -19,8 +19,7 @@ class PPOM_Freemium { * * @return void */ - private function __construct() - { + private function __construct() { add_filter( 'ppom_fields_tabs_show', array( $this, 'add_locked_cfr_tab' ), 10, 1 ); add_filter( 'ppom_all_inputs', array( $this, 'locked_cfr_register_form_elements' ), PHP_INT_MAX ); } @@ -31,7 +30,7 @@ private function __construct() * @return void */ public static function get_instance() { - if( is_null( self::$instance ) ) { + if ( is_null( self::$instance ) ) { self::$instance = new self(); } @@ -48,10 +47,10 @@ public function add_locked_cfr_tab( $tabs ) { if ( ppom_pro_is_installed() && PPOM()->is_license_of_type( 'plus' ) ) { return $tabs; } - $tabs[self::TAB_KEY_FREEMIUM_CFR] = array( - 'label' => __( 'Conditional Repeater', 'woocommerce-product-addon' ). ' (' . __( 'PRO', 'woocommerce-product-addon' ) . ')', - 'class' => array( 'ppom-tabs-label' ), - 'field_depend' => array( 'all' ) + $tabs[ self::TAB_KEY_FREEMIUM_CFR ] = array( + 'label' => __( 'Conditional Repeater', 'woocommerce-product-addon' ) . ' (' . __( 'PRO', 'woocommerce-product-addon' ) . ')', + 'class' => array( 'ppom-tabs-label' ), + 'field_depend' => array( 'all' ), ); return $tabs; } @@ -82,151 +81,154 @@ public function get_freemium_cfr_content() { } /** - * Adds admin setting fields to all input types. - * - * @param array $inputs current input classes - * @return array - */ - public function locked_cfr_register_form_elements( $inputs ) { - return array_map( function($input_class) { - if( ! is_object( $input_class ) || ! property_exists( $input_class, 'settings' ) || !is_array($input_class->settings) ) { - return $input_class; - } + * Adds admin setting fields to all input types. + * + * @param array $inputs current input classes + * @return array + */ + public function locked_cfr_register_form_elements( $inputs ) { + return array_map( + function ( $input_class ) { + if ( ! is_object( $input_class ) || ! property_exists( $input_class, 'settings' ) || ! is_array( $input_class->settings ) ) { + return $input_class; + } - $input_class->settings['locked_cfr'] = array( - 'type' => 'checkbox', - 'title' => __( 'Enable Conditional Repeat', 'woocommerce-product-addon' ), - 'disabled' => true, - 'desc' => '', - 'tabs_class' => array( 'ppom_handle_' . self::TAB_KEY_FREEMIUM_CFR ) - ); + $input_class->settings['locked_cfr'] = array( + 'type' => 'checkbox', + 'title' => __( 'Enable Conditional Repeat', 'woocommerce-product-addon' ), + 'disabled' => true, + 'desc' => '', + 'tabs_class' => array( 'ppom_handle_' . self::TAB_KEY_FREEMIUM_CFR ), + ); - return $input_class; - }, $inputs ); - } + return $input_class; + }, + $inputs + ); + } public function get_pro_fields() { - return [ - [ + return array( + array( 'title' => __( 'Collapse', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Emojis', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Phone Input', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Chained Input', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Conditional Images', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Domain', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Fonts Picker', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Personalization Preview', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Text Counter', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Fixed Price', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Select Option Quantity', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Image DropDown', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Super List', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Quantity Option', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Quantities Pack', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Radio Switcher', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Variation Matrix', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'DateRange Input', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Color picker', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'File Input', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Image Cropper', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Timezone Input', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Variation Quantity', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Images', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Price Matrix', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'HTML', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Color Palettes', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Audio / Video', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Measure Input', 'woocommerce-product-addon' ), 'icon' => '', - ], - [ + ), + array( 'title' => __( 'Divider', 'woocommerce-product-addon' ), 'icon' => '', - ], - ]; + ), + ); } } diff --git a/classes/frontend-scripts.class.php b/classes/frontend-scripts.class.php index 2519fdb3..2e307603 100644 --- a/classes/frontend-scripts.class.php +++ b/classes/frontend-scripts.class.php @@ -1,16 +1,26 @@ admin_url( 'admin-ajax.php', ( is_ssl() ? 'https' : 'http' ) ), - 'plugin_url' => PPOM_URL, - 'product_id' => $product_id, - 'sp_force_display_block' => apply_filters( 'ppom_sp_ac_force_css_display_block', true ) ? 'on' : 'off' // force display:block instead of display:flex for add to cart form of the single product page + 'ajaxurl' => admin_url( 'admin-ajax.php', ( is_ssl() ? 'https' : 'http' ) ), + 'plugin_url' => PPOM_URL, + 'product_id' => $product_id, + 'sp_force_display_block' => apply_filters( 'ppom_sp_ac_force_css_display_block', true ) ? 'on' : 'off', // force display:block instead of display:flex for add to cart form of the single product page ); $decimal_palces = wc_get_price_decimals(); @@ -464,7 +495,7 @@ public static function load_scripts_by_product_id( $product_id, $ppom_id = null, } $field_conditions['rules'][ $rule_index ]['element_values'] = ppom_wpml_translate( $rule['element_values'], 'PPOM' ); - $rule_index ++; + ++$rule_index; } $ppom_conditional_fields[ $data_name ] = $field_conditions; @@ -533,6 +564,17 @@ public static function load_scripts_by_product_id( $product_id, $ppom_id = null, } + /** + * Localizes runtime data onto an enqueued PPOM frontend script handle. + * + * @param string $handle Registered script handle. + * @param string $var_name JS variable name. + * @param WC_Product $product Product in the current render context. + * @param array $js_vars Handle-specific JS vars. + * @param array $global_js_vars Shared JS vars. + * + * @return void + */ private static function set_localize_data( $handle, $var_name, $product, $js_vars = array(), $global_js_vars = array() ) { if ( ! wp_script_is( $handle ) ) { @@ -611,6 +653,14 @@ private static function set_localize_data( $handle, $var_name, $product, $js_var } + /** + * Appends inline CSS generated from PPOM field presentation settings. + * + * @param string $type Inline CSS variant to include. + * @param array $field_meta Optional field definition for field-specific CSS. + * + * @return void + */ public static function add_inline_css( $type, $field_meta = array() ) { ob_start(); diff --git a/classes/input-meta.class.php b/classes/input-meta.class.php index 18b5b2f6..8419b147 100644 --- a/classes/input-meta.class.php +++ b/classes/input-meta.class.php @@ -70,7 +70,7 @@ function desc() { $desc = apply_filters( 'ppom_description_content', $desc, self::$input_meta ); $desc = apply_filters( 'ppom_input_meta_desc', $desc, self::$input_meta ); - return do_shortcode($desc); + return do_shortcode( $desc ); } @@ -256,7 +256,7 @@ function audio_video() { */ function field_inner_wrapper_classes() { - $classes = [ 'form-group' ]; + $classes = array( 'form-group' ); $wrapper_classes = implode( ' ', $classes ); // return apply_filters_deprecated( 'ppom_input_wrapper_class', array( $wrapper_classes, self::$input_meta ), '21.3', 'ppom_input_wrapper_classes' ); @@ -271,7 +271,7 @@ function field_inner_wrapper_classes() { */ function label_classes() { - $classes = [ 'form-control-label' ]; + $classes = array( 'form-control-label' ); $label_classes = apply_filters( 'ppom_input_label_classes', $classes, self::$input_meta ); @@ -313,7 +313,7 @@ function input_classes_array() { } if ( ( $this->input_type == 'radio' && ( $key = array_search( 'form-control', $classes ) ) !== false ) || - $this->input_type == 'checkbox' && ( $key = array_search( 'form-control', $classes ) ) !== false ) { + $this->input_type == 'checkbox' && ( $key = array_search( 'form-control', $classes ) ) !== false ) { unset( $classes[ $key ] ); $classes[] = 'ppom-check-input'; } @@ -355,7 +355,7 @@ function input_classes() { */ function radio_label_classes() { - $classes = [ 'form-check-label' ]; + $classes = array( 'form-check-label' ); $label_class = apply_filters( 'ppom_radio_input_label_classes', $classes, self::$input_meta ); @@ -372,7 +372,7 @@ function radio_label_classes() { */ function checkbox_label_classes() { - $classes = [ 'form-check-label' ]; + $classes = array( 'form-check-label' ); $label_class = apply_filters( 'ppom_checkbox_input_label_classes', $classes, self::$input_meta ); diff --git a/classes/input.class.php b/classes/input.class.php index 5b1acb44..568b691b 100644 --- a/classes/input.class.php +++ b/classes/input.class.php @@ -1,10 +1,14 @@ desc = __( 'Audio File Selection', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.checkbox.php b/classes/inputs/input.checkbox.php index a8b05152..bcecc51a 100644 --- a/classes/inputs/input.checkbox.php +++ b/classes/inputs/input.checkbox.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular checkbox input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.color.php b/classes/inputs/input.color.php index 6ae477d0..8d59a1ef 100644 --- a/classes/inputs/input.color.php +++ b/classes/inputs/input.color.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'Color pallete input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.cropper.php b/classes/inputs/input.cropper.php index bf668181..5e1180c3 100644 --- a/classes/inputs/input.cropper.php +++ b/classes/inputs/input.cropper.php @@ -24,7 +24,6 @@ function __construct() { $this->desc = __( 'Crop images', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } diff --git a/classes/inputs/input.date.php b/classes/inputs/input.date.php index 00f5a494..558ebd0a 100644 --- a/classes/inputs/input.date.php +++ b/classes/inputs/input.date.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular date input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { @@ -103,7 +102,7 @@ private function get_settings() { 'title' => __( 'Year Range', 'woocommerce-product-addon' ), 'desc' => sprintf( // translators: %1%s: the current year date, %2%s the next yar date. - esc_html__( '[ This feature requires jQuery datePicker ] Years to allow date selections. Example: c-10:c+10. TIP: The letter "c" indicates the current year so "c+1" will indicate next year. Thus c:c+1 will be %1$s:%2$s', 'woocommerce-product-addon'), + esc_html__( '[ This feature requires jQuery datePicker ] Years to allow date selections. Example: c-10:c+10. TIP: The letter "c" indicates the current year so "c+1" will indicate next year. Thus c:c+1 will be %1$s:%2$s', 'woocommerce-product-addon' ), date( 'Y' ), date( 'Y', strtotime( '+1 year' ) ) ), diff --git a/classes/inputs/input.daterange.php b/classes/inputs/input.daterange.php index 3074b397..f9bb7ca7 100644 --- a/classes/inputs/input.daterange.php +++ b/classes/inputs/input.daterange.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = '' . __( 'More detail', 'woocommerce-product-addon' ) . ''; $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.divider.php b/classes/inputs/input.divider.php index 97baade7..1f9e4303 100644 --- a/classes/inputs/input.divider.php +++ b/classes/inputs/input.divider.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular divider input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } function ppom_divider_style() { diff --git a/classes/inputs/input.email.php b/classes/inputs/input.email.php index 203e0afc..309d85cf 100644 --- a/classes/inputs/input.email.php +++ b/classes/inputs/input.email.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular email input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.file.php b/classes/inputs/input.file.php index 8e624e4e..6c691ff8 100644 --- a/classes/inputs/input.file.php +++ b/classes/inputs/input.file.php @@ -26,7 +26,6 @@ function __construct() { $this->desc = __( 'regular file input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.hidden.php b/classes/inputs/input.hidden.php index a717bd49..082b060b 100644 --- a/classes/inputs/input.hidden.php +++ b/classes/inputs/input.hidden.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular hidden input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } diff --git a/classes/inputs/input.image.php b/classes/inputs/input.image.php index 93572ea9..30087e9b 100644 --- a/classes/inputs/input.image.php +++ b/classes/inputs/input.image.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'Images selection', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.number.php b/classes/inputs/input.number.php index f35399cb..ab8880e1 100644 --- a/classes/inputs/input.number.php +++ b/classes/inputs/input.number.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular number input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.palettes.php b/classes/inputs/input.palettes.php index 5fc01c45..201440c0 100644 --- a/classes/inputs/input.palettes.php +++ b/classes/inputs/input.palettes.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'color boxes', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.pricematrix.php b/classes/inputs/input.pricematrix.php index 8d7dd97d..4cae4c3f 100644 --- a/classes/inputs/input.pricematrix.php +++ b/classes/inputs/input.pricematrix.php @@ -26,7 +26,6 @@ function __construct() { $this->desc = __( 'Price/Quantity', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.quantities.php b/classes/inputs/input.quantities.php index f3d945a1..733b1db5 100644 --- a/classes/inputs/input.quantities.php +++ b/classes/inputs/input.quantities.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular select-box input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } function variation_layout() { diff --git a/classes/inputs/input.radio.php b/classes/inputs/input.radio.php index 9cdb0c0f..572717c4 100644 --- a/classes/inputs/input.radio.php +++ b/classes/inputs/input.radio.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular radio input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.section.php b/classes/inputs/input.section.php index 9e955c19..51a590c1 100644 --- a/classes/inputs/input.section.php +++ b/classes/inputs/input.section.php @@ -30,7 +30,6 @@ function __construct() { $this->desc = __( 'HTML content', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } diff --git a/classes/inputs/input.select.php b/classes/inputs/input.select.php index 83d1d19c..7fd6e599 100644 --- a/classes/inputs/input.select.php +++ b/classes/inputs/input.select.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular select input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.text.php b/classes/inputs/input.text.php index eb385e1d..8a18fe6c 100644 --- a/classes/inputs/input.text.php +++ b/classes/inputs/input.text.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular text input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/inputs/input.textarea.php b/classes/inputs/input.textarea.php index 4a6d0373..0ac483f6 100644 --- a/classes/inputs/input.textarea.php +++ b/classes/inputs/input.textarea.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'regular textarea input', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } diff --git a/classes/inputs/input.timezone.php b/classes/inputs/input.timezone.php index 6b2962b0..409f6bd8 100644 --- a/classes/inputs/input.timezone.php +++ b/classes/inputs/input.timezone.php @@ -25,7 +25,6 @@ function __construct() { $this->desc = __( 'Show Timezone', 'woocommerce-product-addon' ); $this->icon = ''; $this->settings = self::get_settings(); - } private function get_settings() { diff --git a/classes/integrations/elementor/elementor.class.php b/classes/integrations/elementor/elementor.class.php index d5e2051f..97aa1141 100644 --- a/classes/integrations/elementor/elementor.class.php +++ b/classes/integrations/elementor/elementor.class.php @@ -21,7 +21,7 @@ class PPOM_ELEMENTOR { public function __construct() { - add_action( 'plugins_loaded', [ $this, 'load' ] ); + add_action( 'plugins_loaded', array( $this, 'load' ) ); } @@ -44,7 +44,7 @@ public static function instance() { public function load() { if ( $this->is_compatible() ) { - add_action( 'elementor/init', [ $this, 'init' ] ); + add_action( 'elementor/init', array( $this, 'init' ) ); } } @@ -71,13 +71,13 @@ public function init() { // $frontend = \Elementor\Plugin::$instance->frontend->has_elementor_in_page(); // Register New Weidget - add_action( 'elementor/widgets/widgets_registered', [ $this, 'init_widgets' ] ); + add_action( 'elementor/widgets/widgets_registered', array( $this, 'init_widgets' ) ); // Register New Controls // add_action( 'elementor/controls/controls_registered', [ $this, 'init_controls' ] ); // Register Widget Styles - add_action( 'elementor/frontend/after_enqueue_styles', [ $this, 'widget_styles' ] ); + add_action( 'elementor/frontend/after_enqueue_styles', array( $this, 'widget_styles' ) ); } diff --git a/classes/integrations/elementor/shortcode-widget.php b/classes/integrations/elementor/shortcode-widget.php index 097bad5a..400646fd 100644 --- a/classes/integrations/elementor/shortcode-widget.php +++ b/classes/integrations/elementor/shortcode-widget.php @@ -37,7 +37,7 @@ public function get_icon() { * Set this widget to category */ public function get_categories() { - return [ 'general' ]; + return array( 'general' ); } @@ -48,19 +48,19 @@ protected function _register_controls() { $this->start_controls_section( 'content_section', - [ + array( 'label' => __( 'Product Details', 'woocommerce-product-addon' ), 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, - ] + ) ); $this->add_control( 'ppom_product_id', - [ + array( 'label' => __( 'Product ID', 'woocommerce-product-addon' ), 'type' => \Elementor\Controls_Manager::TEXT, 'placeholder' => __( 'Provide Product ID', 'woocommerce-product-addon' ), - ] + ) ); $this->end_controls_section(); diff --git a/classes/legacy-meta.class.php b/classes/legacy-meta.class.php index 65945125..08cd6023 100644 --- a/classes/legacy-meta.class.php +++ b/classes/legacy-meta.class.php @@ -71,7 +71,7 @@ function desc() { // old Filter $desc = apply_filters( 'ppom_description_content', $desc, self::$input_meta ); - return do_shortcode($desc); + return do_shortcode( $desc ); } @@ -158,10 +158,9 @@ function input_classes_array() { } if ( ( $this->input_type == 'radio' && ( $key = array_search( 'form-control', $classes ) ) !== false ) || - ( $this->input_type == 'checkbox' && ( $key = array_search( 'form-control', $classes ) ) !== false ) || - ( $this->input_type == 'fixedprice' && self::$input_meta['view_type'] === 'radio' && ( $key = array_search( 'form-control', $classes ) ) !== false ) - ) - { + ( $this->input_type == 'checkbox' && ( $key = array_search( 'form-control', $classes ) ) !== false ) || + ( $this->input_type == 'fixedprice' && self::$input_meta['view_type'] === 'radio' && ( $key = array_search( 'form-control', $classes ) ) !== false ) + ) { unset( $classes[ $key ] ); $classes[] = 'ppom-check-input'; } diff --git a/classes/plugin.class.php b/classes/plugin.class.php index 5f17c37d..0cb364cf 100644 --- a/classes/plugin.class.php +++ b/classes/plugin.class.php @@ -1,16 +1,25 @@ is_exists ) { return $url; @@ -477,7 +498,6 @@ function nm_add_bulk_meta() { } $wp_list_table = _get_list_table( 'WP_Posts_List_Table' ); - } function nm_meta_bulk_action() { @@ -528,7 +548,7 @@ function nm_meta_bulk_action() { $meta_id = array( intval( substr( $action, 10 ) ) ); update_post_meta( $post_id, PPOM_PRODUCT_META_KEY, $meta_id ); - $nm_updated ++; + ++$nm_updated; } $sendback = add_query_arg( array( @@ -545,7 +565,7 @@ function nm_meta_bulk_action() { delete_post_meta( $post_id, PPOM_PRODUCT_META_KEY ); - $nm_removed ++; + ++$nm_removed; } $sendback = add_query_arg( array( @@ -572,28 +592,27 @@ function nm_meta_bulk_action() { function nm_add_meta_notices() { global $post_type, $pagenow; - if ($pagenow == 'edit.php' && $post_type == 'product' && isset($_REQUEST['nm_updated']) && (int) $_REQUEST['nm_updated']) { + if ( $pagenow == 'edit.php' && $post_type == 'product' && isset( $_REQUEST['nm_updated'] ) && (int) $_REQUEST['nm_updated'] ) { $count = (int) $_REQUEST['nm_updated']; - if ($count === 1) { - $message = __('Product meta updated.', 'woocommerce-product-addon'); + if ( $count === 1 ) { + $message = __( 'Product meta updated.', 'woocommerce-product-addon' ); } else { $message = sprintf( /* translators: %s: number of products */ - __('%s Products meta updated.', 'woocommerce-product-addon'), - number_format_i18n($count) + __( '%s Products meta updated.', 'woocommerce-product-addon' ), + number_format_i18n( $count ) ); } echo "

{$message}

"; - } - elseif ($pagenow == 'edit.php' && $post_type == 'product' && isset($_REQUEST['nm_removed']) && (int) $_REQUEST['nm_removed']) { + } elseif ( $pagenow == 'edit.php' && $post_type == 'product' && isset( $_REQUEST['nm_removed'] ) && (int) $_REQUEST['nm_removed'] ) { $count = (int) $_REQUEST['nm_removed']; - if ($count === 1) { - $message = __('Product meta removed.', 'woocommerce-product-addon'); + if ( $count === 1 ) { + $message = __( 'Product meta removed.', 'woocommerce-product-addon' ); } else { $message = sprintf( /* translators: %s: number of products */ - __('%s Products meta removed.', 'woocommerce-product-addon'), - number_format_i18n($count) + __( '%s Products meta removed.', 'woocommerce-product-addon' ), + number_format_i18n( $count ) ); } echo "

{$message}

"; @@ -639,12 +658,12 @@ public static function get_product_meta_count( $limit = null ) { global $wpdb; $qry = 'SELECT COUNT(*) FROM ' . $wpdb->prefix . PPOM_TABLE_META; - if ($limit !== null) { - $qry .= ' LIMIT ' . intval($limit); + if ( $limit !== null ) { + $qry .= ' LIMIT ' . intval( $limit ); } - $count = $wpdb->get_var($qry); + $count = $wpdb->get_var( $qry ); - return intval($count); + return intval( $count ); } function get_product_meta( $meta_id ) { @@ -720,7 +739,6 @@ public static function activate_plugin() { // migration done update_option( 'ppom_settings_migration_done', 1 ); - } public static function deactivate_plugin() { @@ -749,10 +767,8 @@ public static function set_ppom_menu_permission() { $wp_role->add_cap( 'ppom_options_page' ); } } - } else { - if ( $wp_role->has_cap( 'ppom_options_page' ) ) { + } elseif ( $wp_role->has_cap( 'ppom_options_page' ) ) { $wp_role->remove_cap( 'ppom_options_page' ); - } } } } @@ -783,7 +799,7 @@ public static function remove_ppom_menu_permission() { function clone_product_meta( $meta_id ) { if ( ! isset( $_GET['ppom_clone_nonce'] ) - || ! wp_verify_nonce( $_GET['ppom_clone_nonce'], 'ppom_clone_nonce_action' ) + || ! wp_verify_nonce( $_GET['ppom_clone_nonce'], 'ppom_clone_nonce_action' ) ) { _e( 'Sorry, you are not allowed to clone', 'woocommerce-product-addon' ); @@ -803,13 +819,12 @@ function clone_product_meta( $meta_id ) { $result = $wpdb->query( $wpdb->prepare( $sql, array( $meta_id ) ) ); wp_safe_redirect( admin_url( 'admin.php?page=ppom&productmeta_id=' . intval( $wpdb->insert_id ) . '&do_meta=edit' ) ); - die(); + die(); /* - var_dump($result); + var_dump($result); $wpdb->show_errors(); $wpdb->print_error(); */ - } /* @@ -836,15 +851,15 @@ function ( $free_input ) use ( $nm_inputs ) { */ public function ppom_free_inputs() { return array( - 'text' => 'text', + 'text' => 'text', 'textarea' => 'textarea', - 'select' => 'select', - 'radio' => 'radio', + 'select' => 'select', + 'radio' => 'radio', 'checkbox' => 'checkbox', - 'email' => 'email', - 'date' => 'date', - 'number' => 'number', - 'hidden' => 'hidden', + 'email' => 'email', + 'date' => 'date', + 'number' => 'number', + 'hidden' => 'hidden', ); } @@ -896,7 +911,7 @@ public static function ppom_install_demo_meta() { $table = $wpdb->prefix . PPOM_TABLE_META; $qry = "INSERT INTO {$table} SET "; - $meta_count ++; + ++$meta_count; foreach ( $meta as $key => $val ) { @@ -996,7 +1011,6 @@ function add_ppom_meta_panel() { echo '
'; ppom_meta_list( $post ); echo '
'; - } /** @@ -1015,7 +1029,7 @@ public function show_tooltip( $description, $meta ) { // Check if the tooltip is enabled. if ( isset( $meta['desc_tooltip'] ) && 'on' === $meta['desc_tooltip'] ) { $icon_color = ppom_get_option( 'ppom_input_tooltip_iconclr', '#000000' ); - $description = ( ! empty( $meta['description'] ) ) ? ' ' : ''; + $description = ( ! empty( $meta['description'] ) ) ? ' ' : ''; } return $description; } @@ -1064,7 +1078,7 @@ public function is_license_of_type( $type ) { case 'plus': return self::get_license_category( $plan ) >= self::LICENSE_PLAN_2; case 'pro': - return self::get_license_category( $plan ) >= self::LICENSE_PLAN_1; + return self::get_license_category( $plan ) >= self::LICENSE_PLAN_1; } return false; diff --git a/classes/ppom.class.php b/classes/ppom.class.php index 4154db50..de74b92c 100644 --- a/classes/ppom.class.php +++ b/classes/ppom.class.php @@ -1,11 +1,16 @@ category_meta = []; + $this->category_meta = array(); $this->ppom_categories_and_tags_row = $this->all_ppom_with_categories(); $this->meta_id = $this->get_meta_id( $product_id ); self::$product_id = $product_id; @@ -150,8 +165,19 @@ public static function get_instance( $product_id ) { return self::$ins; } - // QM-5 - function get_meta_id( $product_id ) { + /** + * Resolves PPOM meta IDs for a product. + * + * Reads direct product assignments and category assignments before applying + * merge and override filters. + * + * @param int|null $product_id Product ID being resolved. + * + * @return array|int|null + * + * @see PPOM_PRODUCT_META_KEY + */ + public function get_meta_id( $product_id ) { $ppom_product_id = get_post_meta( $product_id, PPOM_PRODUCT_META_KEY, true ); @@ -226,8 +252,8 @@ function single_meta_id() { $single_meta = ( $this->meta_id == 0 || $this->meta_id == 'None' || empty( $this->meta_id ) ) ? null : $this->meta_id; - if ( is_array( $single_meta) && 0 < count( $single_meta ) ) { - $single_meta = $single_meta[0]; + if ( is_array( $single_meta ) && 0 < count( $single_meta ) ) { + $single_meta = $single_meta[0]; } return $single_meta; @@ -244,9 +270,12 @@ function has_multiple_meta() { return $multiple_meta; } - // getting settings - // QM-5 - function settings() { + /** + * Loads the primary settings row for the resolved PPOM group. + * + * @return object|null + */ + public function settings() { $meta_id = $this->single_meta_id(); @@ -264,7 +293,7 @@ function settings() { $meta_settings = $wpdb->get_results( $qry ); $filter_meta = array_filter( $meta_settings, - function( $meta ) { + function ( $meta ) { return 'on' === $meta->productmeta_validation ? $meta : false; } ); @@ -275,8 +304,14 @@ function( $meta ) { return apply_filters( 'ppom_meta_settings', $meta_settings, $this ); } - // getting fields - function get_fields() { + /** + * Loads active field definitions for the resolved PPOM group or groups. + * + * @return array|null + * + * @see ppom_get_field_meta_by_dataname() + */ + public function get_fields() { if ( ! $this->is_exists() ) { return null; @@ -362,7 +397,7 @@ function ( $field ) { return apply_filters( 'ppom_meta_fields_by_id', $meta_fields, $ppom_ids, $this ); } - function ppom_has_category_meta($product_id ) { + function ppom_has_category_meta( $product_id ) { $product_categories = get_the_terms( $product_id, 'product_cat' ); @@ -375,7 +410,7 @@ function ppom_has_category_meta($product_id ) { } else { // making array of meta cats - $meta_cat_array = preg_split('/\r\n|\n/', $row->productmeta_categories); + $meta_cat_array = preg_split( '/\r\n|\n/', $row->productmeta_categories ); // Now iterating the product_categories to check it's slug in meta cats foreach ( $product_categories as $cat ) { if ( in_array( $cat->slug, $meta_cat_array ) ) { @@ -443,13 +478,13 @@ function inline_css() { $template = stripslashes( strip_tags( $this->ppom_settings->productmeta_style ) ); if ( is_array( $this->meta_id ) ) { - $field_selector = []; - foreach( $this->meta_id as $field_id ) { - $field_selector[] = ".ppom-id-" . $field_id; + $field_selector = array(); + foreach ( $this->meta_id as $field_id ) { + $field_selector[] = '.ppom-id-' . $field_id; } $selector = ':where(' . implode( ', ', $field_selector ) . ')'; - } else if ( is_numeric( $this->meta_id ) ) { - $selector = ".ppom-id-" . $this->meta_id; + } elseif ( is_numeric( $this->meta_id ) ) { + $selector = '.ppom-id-' . $this->meta_id; } $inline_css = str_replace( 'selector', $selector, $template ); } @@ -573,5 +608,4 @@ function get_settings_by_id( $meta_id ) { return apply_filters( 'ppom_get_settings_by_id', $meta_settings, $meta_id, $this ); } - } diff --git a/classes/survey.class.php b/classes/survey.class.php index c448a471..88ec8fe6 100644 --- a/classes/survey.class.php +++ b/classes/survey.class.php @@ -69,13 +69,13 @@ public function get_survey_metadata( $data, $page_slug ) { $install_days_number = intval( ( time() - get_option( 'woocommerce_product_addon_install', time() ) ) / DAY_IN_SECONDS ); $data = array( - 'environmentId' => 'clza3s4zm000h10km1699nlli', - 'attributes' => array( + 'environmentId' => 'clza3s4zm000h10km1699nlli', + 'attributes' => array( 'install_days_number' => $install_days_number, 'free_version' => PPOM_VERSION, 'license_status' => $license_status, - 'field_groups_count' => intval( $group_fields_count ) - ) + 'field_groups_count' => intval( $group_fields_count ), + ), ); if ( 1 <= $license_plan ) { diff --git a/composer.json b/composer.json index 7559055c..8c484b6e 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,11 @@ } ], "scripts": { - "phpstan": "phpstan", - "phpstan:generate:baseline": "phpstan --generate-baseline" + "lint": "phpcs --standard=phpcs.xml", + "lint:generate:baseline": "php vendor/bin/phpcs '--report=\\DR\\CodeSnifferBaseline\\Reports\\Baseline' --report-file=phpcs.baseline.xml --basepath=.", + "format": "phpcbf --standard=phpcs.xml", + "phpstan": "phpstan --memory-limit=-1", + "phpstan:generate:baseline": "phpstan --generate-baseline --memory-limit=-1" }, "minimum-stability": "dev", "prefer-stable": true, @@ -20,10 +23,11 @@ "optimize-autoloader": true, "platform-check": false, "platform": { - "php": "7.2" + "php": "7.4" }, "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "digitalrevolution/php-codesniffer-baseline": true } }, "extra": { @@ -40,10 +44,13 @@ "require-dev": { "phpunit/phpunit": "^8.5", "yoast/phpunit-polyfills": "^2.0", - "codeinwp/phpcs-ruleset": "dev-main", - "phpstan/phpstan": "^1.12", - "szepeviktor/phpstan-wordpress": "^1.3", - "php-stubs/woocommerce-stubs": "^10.1", - "damian-elenbaas/elementor-stubs": "^3.31" + "phpstan/phpstan": "^2", + "szepeviktor/phpstan-wordpress": "^2.0", + "php-stubs/woocommerce-stubs": "^10.6", + "damian-elenbaas/elementor-stubs": "^3.31", + "wp-coding-standards/wpcs": "^3.0", + "phpcompatibility/phpcompatibility-wp": "^3.0@dev", + "automattic/vipwpcs": "^3.0", + "digitalrevolution/php-codesniffer-baseline": "^1.1" } } diff --git a/composer.lock b/composer.lock index 89f55008..c4347e31 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7c849397d791db90b84e36f5da2a9387", + "content-hash": "6ef24b433812bec6d7a228950e36b9d0", "packages": [ { "name": "codeinwp/themeisle-sdk", @@ -51,31 +51,32 @@ "packages-dev": [ { "name": "automattic/vipwpcs", - "version": "2.3.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/Automattic/VIP-Coding-Standards.git", - "reference": "b8610e3837f49c5f2fcc4b663b6c0a7c9b3509b6" + "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/b8610e3837f49c5f2fcc4b663b6c0a7c9b3509b6", - "reference": "b8610e3837f49c5f2fcc4b663b6c0a7c9b3509b6", + "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/2b1d206d81b74ed999023cffd924f862ff2753c8", + "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "sirbrillig/phpcs-variable-analysis": "^2.11.17", - "squizlabs/php_codesniffer": "^3.7.1", - "wp-coding-standards/wpcs": "^2.3" + "phpcsstandards/phpcsextra": "^1.2.1", + "phpcsstandards/phpcsutils": "^1.0.11", + "sirbrillig/phpcs-variable-analysis": "^2.11.18", + "squizlabs/php_codesniffer": "^3.9.2", + "wp-coding-standards/wpcs": "^3.1.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9", "phpcsstandards/phpcsdevtools": "^1.0", - "phpunit/phpunit": "^4 || ^5 || ^6 || ^7" + "phpunit/phpunit": "^4 || ^5 || ^6 || ^7 || ^8 || ^9" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -100,42 +101,7 @@ "source": "https://github.com/Automattic/VIP-Coding-Standards", "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki" }, - "time": "2023-08-24T15:11:13+00:00" - }, - { - "name": "codeinwp/phpcs-ruleset", - "version": "dev-main", - "source": { - "type": "git", - "url": "https://github.com/Codeinwp/phpcs-ruleset.git", - "reference": "982f9881312252e6213cde07704b74da47b39475" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/phpcs-ruleset/zipball/982f9881312252e6213cde07704b74da47b39475", - "reference": "982f9881312252e6213cde07704b74da47b39475", - "shasum": "" - }, - "require": { - "automattic/vipwpcs": "^2.0", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "sirbrillig/phpcs-variable-analysis": "^2.10", - "wptrt/wpthemereview": "*" - }, - "default-branch": true, - "bin": [ - "bin/phpcbf-fix-exit-0" - ], - "type": "phpcodesniffer-standard", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "description": "PHPCS coding standards for Themeisle products.", - "support": { - "source": "https://github.com/Codeinwp/phpcs-ruleset/tree/main" - }, - "time": "2021-05-05T16:55:27+00:00" + "time": "2024-05-10T20:31:09+00:00" }, { "name": "damian-elenbaas/elementor-stubs", @@ -176,35 +142,38 @@ }, { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.2", + "version": "v1.2.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", - "reference": "1c968e542d8843d7cd71de3c5c9c3ff3ad71a1db", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", + "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" }, "require-dev": { - "composer/composer": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -214,17 +183,16 @@ "authors": [ { "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" }, { "name": "Contributors", - "homepage": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer/graphs/contributors" + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -244,10 +212,80 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2022-02-04T12:51:07+00:00" + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-11T04:32:07+00:00" + }, + { + "name": "digitalrevolution/php-codesniffer-baseline", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/123inkt/php-codesniffer-baseline.git", + "reference": "00d7cd414cc0fc12e88ee3321d92fe3d2313a9e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/123inkt/php-codesniffer-baseline/zipball/00d7cd414cc0fc12e88ee3321d92fe3d2313a9e7", + "reference": "00d7cd414cc0fc12e88ee3321d92fe3d2313a9e7", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=7.4", + "squizlabs/php_codesniffer": "^3.6" + }, + "require-dev": { + "composer/composer": "^2.0", + "micheh/phpcs-gitlab": "^1.1", + "mikey179/vfsstream": "1.6.10", + "phpmd/phpmd": "@stable", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-latest" + }, + "type": "composer-plugin", + "extra": { + "class": "DR\\CodeSnifferBaseline\\Plugin\\Plugin" + }, + "autoload": { + "psr-4": { + "DR\\CodeSnifferBaseline\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Digital Revolution PHP_Codesniffer baseline extension", + "support": { + "issues": "https://github.com/123inkt/php-codesniffer-baseline/issues", + "source": "https://github.com/123inkt/php-codesniffer-baseline/tree/v1.1.2" + }, + "time": "2022-05-31T08:26:56+00:00" }, { "name": "doctrine/instantiator", @@ -499,16 +537,16 @@ }, { "name": "php-stubs/woocommerce-stubs", - "version": "v10.1.1", + "version": "v10.6.1", "source": { "type": "git", "url": "https://github.com/php-stubs/woocommerce-stubs.git", - "reference": "b8e010db32f6a5876f0c49fda467e15ea5d6f97f" + "reference": "ef96143054f60a2f0b2cfe8351f5d78448729e98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/woocommerce-stubs/zipball/b8e010db32f6a5876f0c49fda467e15ea5d6f97f", - "reference": "b8e010db32f6a5876f0c49fda467e15ea5d6f97f", + "url": "https://api.github.com/repos/php-stubs/woocommerce-stubs/zipball/ef96143054f60a2f0b2cfe8351f5d78448729e98", + "reference": "ef96143054f60a2f0b2cfe8351f5d78448729e98", "shasum": "" }, "require": { @@ -537,22 +575,22 @@ ], "support": { "issues": "https://github.com/php-stubs/woocommerce-stubs/issues", - "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v10.1.1" + "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v10.6.1" }, - "time": "2025-08-20T15:58:51+00:00" + "time": "2026-03-12T19:28:12+00:00" }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.8.2", + "version": "v6.9.1", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8" + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", - "reference": "9c8e22e437463197c1ec0d5eaa9ddd4a0eb6d7f8", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", "shasum": "" }, "conflict": { @@ -563,9 +601,10 @@ "nikic/php-parser": "^5.5", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.4.1", + "phpdocumentor/reflection-docblock": "^6.0", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, @@ -588,39 +627,47 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.2" + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" }, - "time": "2025-07-16T06:41:00+00:00" + "time": "2026-02-03T19:29:21+00:00" }, { "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", + "version": "10.0.0-alpha2", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", + "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "replace": { + "wimg/php-compatibility": "*" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.3", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.5.32 || ^11.3.3", + "yoast/phpunit-polyfills": "^1.1.5 || ^2.0.5 || ^3.1.0" }, "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev", + "dev-develop": "10.x-dev" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "LGPL-3.0-or-later" @@ -642,44 +689,59 @@ } ], "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "homepage": "https://techblog.wimgodden.be/tag/codesniffer/", "keywords": [ "compatibility", "phpcs", - "standards" + "standards", + "static analysis" ], "support": { "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibility/security/policy", "source": "https://github.com/PHPCompatibility/PHPCompatibility" }, - "time": "2019-12-27T09:44:58+00:00" + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-11-28T11:36:33+00:00" }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.3", + "version": "2.0.0-alpha2", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" + "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/7a979711c87d8202b52f56c56bd719d09d8ed7f5", + "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^10.0@dev" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "paragonie/random_compat": "dev-master", "paragonie/sodium_compat": "dev-master" }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." - }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ @@ -722,34 +784,31 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:30:46+00:00" + "time": "2025-11-29T13:09:49+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.5", + "version": "3.0.0-alpha2", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082" + "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/01c1ff2704a58e46f0cb1ca9d06aee07b3589082", - "reference": "01c1ff2704a58e46f0cb1ca9d06aee07b3589082", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", + "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", "shasum": "" }, "require": { - "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "phpcompatibility/php-compatibility": "^10.0@dev", + "phpcompatibility/phpcompatibility-paragonie": "^2.0@dev" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -792,26 +851,200 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:37:59+00:00" + "time": "2025-12-16T13:35:20+00:00" }, { - "name": "phpstan/phpstan", - "version": "1.12.28", + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.43", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d01bebe3edfd4d49b9666ee5b8271ddca561042f", + "reference": "d01bebe3edfd4d49b9666ee5b8271ddca561042f", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -852,7 +1085,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2026-03-24T20:40:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1979,29 +2212,27 @@ }, { "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.11.19", + "version": "v2.13.0", "source": { "type": "git", "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1" + "reference": "a15e970b8a0bf64cfa5e86d941f5e6b08855f369" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1", - "reference": "bc8d7e30e2005bce5c59018b7cdb08e9fb45c0d1", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/a15e970b8a0bf64cfa5e86d941f5e6b08855f369", + "reference": "a15e970b8a0bf64cfa5e86d941f5e6b08855f369", "shasum": "" }, "require": { "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.5.6" + "squizlabs/php_codesniffer": "^3.5.7 || ^4.0.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", - "phpcsstandards/phpcsdevcs": "^1.1", - "phpstan/phpstan": "^1.7", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", - "sirbrillig/phpcs-import-detection": "^1.1", - "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" + "phpstan/phpstan": "^1.7 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0 || ^10.5.32 || ^11.3.3", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0 || ^6.0 || ^7.0" }, "type": "phpcodesniffer-standard", "autoload": { @@ -2033,20 +2264,20 @@ "source": "https://github.com/sirbrillig/phpcs-variable-analysis", "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" }, - "time": "2024-06-26T20:08:34+00:00" + "time": "2025-09-30T22:22:48+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.2", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/86e5f5dd9a840c46810ebe5ff1885581c42a3017", - "reference": "86e5f5dd9a840c46810ebe5ff1885581c42a3017", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -2063,11 +2294,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -2111,116 +2337,40 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" - } - ], - "time": "2024-07-21T23:26:44+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.33.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.33.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://github.com/nicolas-grekas", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.5", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7" + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/7f8cfe992faa96b6a33bbd75c7bace98864161e7", - "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/aa722f037b2d034828cd6c55ebe9e5c74961927e", + "reference": "aa722f037b2d034828cd6c55ebe9e5c74961927e", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "php-stubs/wordpress-stubs": "^4.7 || ^5.0 || ^6.0", - "phpstan/phpstan": "^1.10.31", - "symfony/polyfill-php73": "^1.12.0" + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": "^6.6.2", + "phpstan/phpstan": "^2.0" }, "require-dev": { "composer/composer": "^2.1.14", + "composer/semver": "^3.4", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "php-parallel-lint/php-parallel-lint": "^1.1", - "phpstan/phpstan-strict-rules": "^1.2", - "phpunit/phpunit": "^8.0 || ^9.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, @@ -2254,9 +2404,9 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.5" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.3" }, - "time": "2024-06-28T22:27:19+00:00" + "time": "2025-09-14T02:58:22+00:00" }, { "name": "theseer/tokenizer", @@ -2310,30 +2460,38 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "2.3.0", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "7da1894633f168fe244afc6de00d141f27517b62" + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", - "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", "shasum": "" }, "require": { - "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.3.1" + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", - "phpcompatibility/php-compatibility": "^9.0", - "phpcsstandards/phpcsdevtools": "^1.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -2350,6 +2508,7 @@ "keywords": [ "phpcs", "standards", + "static analysis", "wordpress" ], "support": { @@ -2357,81 +2516,13 @@ "source": "https://github.com/WordPress/WordPress-Coding-Standards", "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" }, - "time": "2020-05-13T23:57:56+00:00" - }, - { - "name": "wptrt/wpthemereview", - "version": "0.2.1", - "source": { - "type": "git", - "url": "https://github.com/WPTT/WPThemeReview.git", - "reference": "462e59020dad9399ed2fe8e61f2a21b5e206e420" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/WPTT/WPThemeReview/zipball/462e59020dad9399ed2fe8e61f2a21b5e206e420", - "reference": "462e59020dad9399ed2fe8e61f2a21b5e206e420", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "phpcompatibility/phpcompatibility-wp": "^2.0", - "squizlabs/php_codesniffer": "^3.3.1", - "wp-coding-standards/wpcs": "^2.2.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", - "phpcompatibility/php-compatibility": "^9.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0", - "roave/security-advisories": "dev-master" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." - }, - "type": "phpcodesniffer-standard", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Theme Review Team", - "homepage": "https://make.wordpress.org/themes/handbook/", - "role": "Strategy and rule setting" - }, - { - "name": "Ulrich Pogson", - "homepage": "https://github.com/grappler", - "role": "Lead developer" - }, - { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "Lead developer" - }, - { - "name": "Denis Žoljom", - "homepage": "https://github.com/dingo-d", - "role": "Plugin integration lead" - }, + "funding": [ { - "name": "Contributors", - "homepage": "https://github.com/WPTRT/WPThemeReview/graphs/contributors" + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" } ], - "description": "PHP_CodeSniffer rules (sniffs) to verify theme compliance with the rules for theme hosting on wordpress.org", - "homepage": "https://make.wordpress.org/themes/handbook/review/", - "keywords": [ - "phpcs", - "standards", - "themes", - "wordpress" - ], - "support": { - "issues": "https://github.com/WPTRT/WPThemeReview/issues", - "source": "https://github.com/WPTRT/WPThemeReview" - }, - "time": "2019-11-17T20:05:55+00:00" + "time": "2025-11-25T12:08:04+00:00" }, { "name": "yoast/phpunit-polyfills", @@ -2500,14 +2591,14 @@ "aliases": [], "minimum-stability": "dev", "stability-flags": { - "codeinwp/phpcs-ruleset": 20 + "phpcompatibility/phpcompatibility-wp": 20 }, "prefer-stable": true, "prefer-lowest": false, "platform": {}, "platform-dev": {}, "platform-overrides": { - "php": "7.2" + "php": "7.4" }, "plugin-api-version": "2.9.0" } diff --git a/grunt/addtextdomain.js b/grunt/addtextdomain.js index 70e2d036..4ecc5402 100644 --- a/grunt/addtextdomain.js +++ b/grunt/addtextdomain.js @@ -1,26 +1,22 @@ /* jshint node:true */ // https://github.com/blazersix/grunt-wp-i18n module.exports = { - plugin: { - options: { - updateDomains: true, - textdomain: '<%= package.plugin.textdomain %>' - }, - files: { - src: [ - '<%= files.php %>' - ] - } - }, - composer: { - options: { - textdomain: '<%= package.plugin.textdomain %>', - updateDomains: ['textdomain'] - }, - files: { - src: [ - 'vendor/codeinwp/**/*.php' - ] - } - } -} + plugin: { + options: { + updateDomains: true, + textdomain: '<%= package.plugin.textdomain %>', + }, + files: { + src: [ '<%= files.php %>' ], + }, + }, + composer: { + options: { + textdomain: '<%= package.plugin.textdomain %>', + updateDomains: [ 'textdomain' ], + }, + files: { + src: [ 'vendor/codeinwp/**/*.php' ], + }, + }, +}; diff --git a/grunt/version.js b/grunt/version.js index 1f770046..9e716c5e 100644 --- a/grunt/version.js +++ b/grunt/version.js @@ -1,25 +1,25 @@ /* jshint node:true */ // https://github.com/kswedberg/grunt-version module.exports = { - project: { - src: ['package.json'], - }, - load_php: { - options: { - prefix: "PPOM_VERSION', '", - }, - src: ['woocommerce-product-addon.php'], - }, - readmetxt: { - options: { - prefix: 'Stable tag: ', - }, - src: ['readme.txt'], - }, - entryHeader: { - options: { - prefix: 'Version\\:.*\\s', - }, - src: ['woocommerce-product-addon.php'], - }, -}; \ No newline at end of file + project: { + src: [ 'package.json' ], + }, + load_php: { + options: { + prefix: "PPOM_VERSION', '", + }, + src: [ 'woocommerce-product-addon.php' ], + }, + readmetxt: { + options: { + prefix: 'Stable tag: ', + }, + src: [ 'readme.txt' ], + }, + entryHeader: { + options: { + prefix: 'Version\\:.*\\s', + }, + src: [ 'woocommerce-product-addon.php' ], + }, +}; diff --git a/grunt/wp_readme_to_markdown.js b/grunt/wp_readme_to_markdown.js index baec3d30..53b36faf 100644 --- a/grunt/wp_readme_to_markdown.js +++ b/grunt/wp_readme_to_markdown.js @@ -1,9 +1,9 @@ /* jshint node:true */ // https://github.com/stephenharris/wp-readme-to-markdown module.exports = { - main: { - files: { - 'README.md': 'readme.txt', - }, - }, -}; \ No newline at end of file + main: { + files: { + 'README.md': 'readme.txt', + }, + }, +}; diff --git a/inc/admin.php b/inc/admin.php index d8cca3a6..c9d12761 100644 --- a/inc/admin.php +++ b/inc/admin.php @@ -1,14 +1,17 @@ has_multiple_meta() ) { - $total_items = count( $ppom->meta_id ); // Get the total number of items. + $total_items = count( $ppom->meta_id ); // Get the total number of items. $current_item = 0; // Counter to track the current iteration. - $has_fields = false; + $has_fields = false; foreach ( $ppom->meta_id as $meta_id ) { - $current_item++; // Increment the counter. + ++$current_item; // Increment the counter. $ppom_setting = $ppom->get_settings_by_id( $meta_id ); if ( $ppom_setting ) { @@ -51,18 +54,17 @@ function ppom_admin_product_meta_column( $column, $post_id ) { ), $ppom_settings_url ); - echo sprintf( '%2$s', esc_url( $url_edit ), $meta_title ); + printf( '%2$s', esc_url( $url_edit ), $meta_title ); // Add a comma only if it's not the last item if ( $current_item < $total_items ) { echo ', '; } - $has_fields = true; - } - + $has_fields = true; + } + } + if ( ! $has_fields ) { + printf( '%2$s', esc_url( $ppom_settings_url ), __( 'Add Fields', 'woocommerce-product-addon' ) ); } - if( ! $has_fields ) { - echo sprintf( '%2$s', esc_url( $ppom_settings_url ), __( 'Add Fields', 'woocommerce-product-addon' ) ); - } } elseif ( $ppom->ppom_settings ) { $url_edit = add_query_arg( array( @@ -71,9 +73,9 @@ function ppom_admin_product_meta_column( $column, $post_id ) { ), $ppom_settings_url ); - echo sprintf( '%2$s', esc_url( $url_edit ), $ppom->meta_title ); + printf( '%2$s', esc_url( $url_edit ), $ppom->meta_title ); } else { - echo sprintf( '%2$s', esc_url( $ppom_settings_url ), __( 'Add Fields', 'woocommerce-product-addon' ) ); + printf( '%2$s', esc_url( $ppom_settings_url ), __( 'Add Fields', 'woocommerce-product-addon' ) ); } break; @@ -86,13 +88,25 @@ function ppom_admin_product_meta_metabox() { add_meta_box( 'ppom-select-meta', __( 'Select PPOM Meta', 'woocommerce-product-addon' ), 'ppom_meta_list', 'product', 'side', 'default' ); } +/** + * Renders the product edit metabox for selecting a PPOM field group. + * + * Reads the current product assignment from {@see PPOM_Meta} and lists every + * available group returned by {@see PPOM()} for attachment. + * + * @param WP_Post $post Product post being edited. + * + * @return void + * + * @see PPOM_Meta::__construct() + */ function ppom_meta_list( $post ) { $ppom = new PPOM_Meta( $post->ID ); $all_meta = PPOM()->get_product_meta_all(); $ppom_setting = admin_url( 'admin.php?page=ppom' ); - $html = '
'; + $html = '
'; if ( count( $all_meta ) > 1 ) { // UP-SELL @@ -103,7 +117,7 @@ function ppom_meta_list( $post ) { } // PPOM Fields select table. $html .= ''; - //Hide search if we don't have many metas + // Hide search if we don't have many metas $html .= '
'; if ( count( $all_meta ) > 3 ) { @@ -171,7 +185,7 @@ function ppom_meta_list( $post ) { $html .= '
'; $html .= '
'; $html .= '
'; + echo '
'; + do_action( 'ppom_meta_box_after_list', $post ); + echo '
'; } -/* - * saving meta data against product +// Product-field assignment persistence. + +/** + * Persists the selected PPOM field group IDs on a WooCommerce product. + * + * Normalizes the submitted `ppom_product_meta` payload to an integer array and + * stores it under {@see PPOM_PRODUCT_META_KEY}. + * + * @param int $post_id Product ID receiving the PPOM assignment. + * + * @return void */ function ppom_admin_process_product_meta( $post_id ) { @@ -230,7 +253,7 @@ function ppom_admin_process_product_meta( $post_id ) { if ( is_numeric( $ppom_meta_selected ) ) { $ppom_meta_selected = array( $ppom_meta_selected ); - } else if ( ! is_array( $ppom_meta_selected ) ) { + } elseif ( ! is_array( $ppom_meta_selected ) ) { $ppom_meta_selected = array(); } @@ -259,12 +282,25 @@ function ppom_admin_show_notices() { } } -/* - * saving form meta in admin call +// Field group create and update. + +/** + * Creates a PPOM field group from the admin builder request. + * + * Verifies the admin nonce and capability, sanitizes the submitted field + * schema, inserts the field-group row into the PPOM custom table, then updates + * each field entry with the generated PPOM ID. When a product ID is submitted, + * the new field group is attached to that product. + * + * @return void + * + * @see ppom_sanitize_array_data() + * @see ppom_attach_fields_to_product() */ function ppom_admin_save_form_meta() { - $db_version = floatval( get_option( 'personalizedproduct_db_version' ) ); + $db_version = floatval( get_option( 'personalizedproduct_db_version' ) ); + $ppom_form_nonce = isset( $_POST['ppom_form_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ppom_form_nonce'] ) ) : ''; if ( $db_version < 22.1 ) { $resp = array( @@ -277,9 +313,9 @@ function ppom_admin_save_form_meta() { // print_r($_REQUEST); exit; - if ( ! isset( $_POST['ppom_form_nonce'] ) - || ! wp_verify_nonce( $_POST['ppom_form_nonce'], 'ppom_form_nonce_action' ) - || ! ppom_security_role() + if ( empty( $ppom_form_nonce ) + || ! wp_verify_nonce( $ppom_form_nonce, 'ppom_form_nonce_action' ) + || ! ppom_security_role() ) { $resp = array( 'message' => __( 'Sorry, you are not allowed to perform this action.', 'woocommerce-product-addon' ), @@ -300,16 +336,16 @@ function ppom_admin_save_form_meta() { if ( is_string( $_REQUEST['ppom'] ) ) { $ppom_encoded = $_REQUEST['ppom']; - parse_str( $ppom_encoded, $ppom_decoded); + parse_str( $ppom_encoded, $ppom_decoded ); $ppom = $ppom_decoded['ppom']; } - $ppom_meta = isset($_REQUEST['ppom_meta']) ? $_REQUEST['ppom_meta'] : $ppom; + $ppom_meta = isset( $_REQUEST['ppom_meta'] ) ? $_REQUEST['ppom_meta'] : $ppom; if ( empty( $ppom_meta ) ) { $resp = array( - 'message' => __( 'No fields found.', 'woocommerce-product-addon' ), - 'status' => 'error', + 'message' => __( 'No fields found.', 'woocommerce-product-addon' ), + 'status' => 'error', ); wp_send_json( $resp ); } @@ -318,21 +354,21 @@ function ppom_admin_save_form_meta() { $product_meta = ppom_sanitize_array_data( $product_meta ); $product_meta = array_filter( $product_meta, - function( $pm ) { + function ( $pm ) { return ! empty( $pm['type'] ) || ! empty( $pm['data_name'] ); } ); $product_meta = json_encode( $product_meta ); // sanitize - $productmeta_name = isset( $_REQUEST['productmeta_name'] ) ? sanitize_text_field( $_REQUEST['productmeta_name'] ) : ''; - $dynamic_price_hide = isset( $_REQUEST['dynamic_price_hide'] ) ? sanitize_text_field( $_REQUEST['dynamic_price_hide'] ) : ''; - $send_file_attachment = isset( $_REQUEST['send_file_attachment'] ) ? sanitize_text_field( $_REQUEST['send_file_attachment'] ) : ''; - $show_cart_thumb = isset( $_REQUEST['show_cart_thumb'] ) ? sanitize_text_field( $_REQUEST['show_cart_thumb'] ) : ''; - $aviary_api_key = isset( $_REQUEST['aviary_api_key'] ) ? sanitize_text_field( $_REQUEST['aviary_api_key'] ) : ''; - $productmeta_style = isset( $_REQUEST['productmeta_style'] ) ? sanitize_text_field( $_REQUEST['productmeta_style'] ) : ''; - $productmeta_js = isset( $_REQUEST['productmeta_js'] ) ? sanitize_text_field( $_REQUEST['productmeta_js'] ) : ''; - $product_id = isset( $_REQUEST['product_id'] ) ? intval( $_REQUEST['product_id'] ) : 0; + $productmeta_name = isset( $_REQUEST['productmeta_name'] ) ? sanitize_text_field( $_REQUEST['productmeta_name'] ) : ''; + $dynamic_price_hide = isset( $_REQUEST['dynamic_price_hide'] ) ? sanitize_text_field( $_REQUEST['dynamic_price_hide'] ) : ''; + $send_file_attachment = isset( $_REQUEST['send_file_attachment'] ) ? sanitize_text_field( $_REQUEST['send_file_attachment'] ) : ''; + $show_cart_thumb = isset( $_REQUEST['show_cart_thumb'] ) ? sanitize_text_field( $_REQUEST['show_cart_thumb'] ) : ''; + $aviary_api_key = isset( $_REQUEST['aviary_api_key'] ) ? sanitize_text_field( $_REQUEST['aviary_api_key'] ) : ''; + $productmeta_style = isset( $_REQUEST['productmeta_style'] ) ? sanitize_text_field( $_REQUEST['productmeta_style'] ) : ''; + $productmeta_js = isset( $_REQUEST['productmeta_js'] ) ? sanitize_text_field( $_REQUEST['productmeta_js'] ) : ''; + $product_id = isset( $_REQUEST['product_id'] ) ? intval( $_REQUEST['product_id'] ) : 0; if ( strlen( $productmeta_name ) > 50 ) { $resp = array( @@ -344,13 +380,13 @@ function( $pm ) { } $ppom_settings_meta_data = array( - 'productmeta_name' => $productmeta_name, - 'dynamic_price_display' => $dynamic_price_hide, - 'send_file_attachment' => $send_file_attachment, - 'show_cart_thumb' => $show_cart_thumb, - 'aviary_api_key' => trim( $aviary_api_key ), - 'the_meta' => $product_meta, - 'productmeta_created' => current_time( 'mysql' ), + 'productmeta_name' => $productmeta_name, + 'dynamic_price_display' => $dynamic_price_hide, + 'send_file_attachment' => $send_file_attachment, + 'show_cart_thumb' => $show_cart_thumb, + 'aviary_api_key' => trim( $aviary_api_key ), + 'the_meta' => $product_meta, + 'productmeta_created' => current_time( 'mysql' ), ); if ( ! ppom_is_legacy_user() ) { @@ -380,7 +416,7 @@ function( $pm ) { $ppom_id = $wpdb->insert_id; if ( is_string( $ppom ) ) { $ppom_encoded = $ppom; - parse_str( $ppom_encoded, $ppom_decoded); + parse_str( $ppom_encoded, $ppom_decoded ); $ppom = $ppom_decoded['ppom']; } @@ -388,7 +424,7 @@ function( $pm ) { $product_meta = ppom_sanitize_array_data( $product_meta ); $product_meta = array_filter( $product_meta, - function( $pm ) { + function ( $pm ) { return ! empty( $pm['type'] ) && ! empty( $pm['data_name'] ); } ); @@ -434,14 +470,22 @@ function( $pm ) { wp_send_json( $resp ); } -/* - * updating form meta in admin call +/** + * Updates an existing PPOM field group from the admin builder request. + * + * Rebuilds the stored `the_meta` JSON payload from the submitted field schema + * and updates the field-group settings row in the PPOM custom table. + * + * @return void + * + * @see ppom_sanitize_array_data() */ function ppom_admin_update_form_meta() { - $return_page = isset( $_REQUEST['ppom_meta'] ) ? 'ppom-energy' : 'ppom'; - $productmeta_id = isset( $_REQUEST['productmeta_id'] ) ? sanitize_text_field( $_REQUEST['productmeta_id'] ) : ''; + $return_page = isset( $_REQUEST['ppom_meta'] ) ? 'ppom-energy' : 'ppom'; + $productmeta_id = isset( $_REQUEST['productmeta_id'] ) ? sanitize_text_field( $_REQUEST['productmeta_id'] ) : ''; + $ppom_form_nonce = isset( $_POST['ppom_form_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ppom_form_nonce'] ) ) : ''; $ppom_args = array( 'page' => $return_page, @@ -464,9 +508,9 @@ function ppom_admin_update_form_meta() { } - if ( ! isset( $_POST['ppom_form_nonce'] ) - || ! wp_verify_nonce( $_POST['ppom_form_nonce'], 'ppom_form_nonce_action' ) - || ! ppom_security_role() + if ( empty( $ppom_form_nonce ) + || ! wp_verify_nonce( $ppom_form_nonce, 'ppom_form_nonce_action' ) + || ! ppom_security_role() ) { $resp = array( 'message' => __( 'Sorry, you are not allowed to perform this action.', 'woocommerce-product-addon' ), @@ -479,7 +523,7 @@ function ppom_admin_update_form_meta() { if ( is_string( $_REQUEST['ppom'] ) ) { $ppom_encoded = $_REQUEST['ppom']; - parse_str( $ppom_encoded, $ppom_decoded); + parse_str( $ppom_encoded, $ppom_decoded ); $_REQUEST['ppom'] = $ppom_decoded['ppom']; } @@ -489,19 +533,19 @@ function ppom_admin_update_form_meta() { // Remove the meta row if the type or data_name is empty. $product_meta = array_filter( $product_meta, - function( $pm ) { + function ( $pm ) { return ! empty( $pm['type'] ) || ! empty( $pm['data_name'] ); } ); $product_meta = json_encode( $product_meta ); - $productmeta_name = isset( $_REQUEST['productmeta_name'] ) ? sanitize_text_field( $_REQUEST['productmeta_name'] ) : ''; - $dynamic_price_hide = isset( $_REQUEST['dynamic_price_hide'] ) ? sanitize_text_field( $_REQUEST['dynamic_price_hide'] ) : ''; - $send_file_attachment = isset( $_REQUEST['send_file_attachment'] ) ? sanitize_text_field( $_REQUEST['send_file_attachment'] ) : ''; - $show_cart_thumb = isset( $_REQUEST['show_cart_thumb'] ) ? sanitize_text_field( $_REQUEST['show_cart_thumb'] ) : ''; - $aviary_api_key = isset( $_REQUEST['aviary_api_key'] ) ? sanitize_text_field( $_REQUEST['aviary_api_key'] ) : ''; - $productmeta_style = isset( $_REQUEST['productmeta_style'] ) ? sanitize_text_field( $_REQUEST['productmeta_style'] ) : ''; - $productmeta_js = isset( $_REQUEST['productmeta_js'] ) ? sanitize_text_field( $_REQUEST['productmeta_js'] ) : ''; + $productmeta_name = isset( $_REQUEST['productmeta_name'] ) ? sanitize_text_field( $_REQUEST['productmeta_name'] ) : ''; + $dynamic_price_hide = isset( $_REQUEST['dynamic_price_hide'] ) ? sanitize_text_field( $_REQUEST['dynamic_price_hide'] ) : ''; + $send_file_attachment = isset( $_REQUEST['send_file_attachment'] ) ? sanitize_text_field( $_REQUEST['send_file_attachment'] ) : ''; + $show_cart_thumb = isset( $_REQUEST['show_cart_thumb'] ) ? sanitize_text_field( $_REQUEST['show_cart_thumb'] ) : ''; + $aviary_api_key = isset( $_REQUEST['aviary_api_key'] ) ? sanitize_text_field( $_REQUEST['aviary_api_key'] ) : ''; + $productmeta_style = isset( $_REQUEST['productmeta_style'] ) ? sanitize_text_field( $_REQUEST['productmeta_style'] ) : ''; + $productmeta_js = isset( $_REQUEST['productmeta_js'] ) ? sanitize_text_field( $_REQUEST['productmeta_js'] ) : ''; if ( strlen( $productmeta_name ) > 50 ) { $resp = array( @@ -513,12 +557,12 @@ function( $pm ) { } $ppom_settings_meta_data = array( - 'productmeta_name' => $productmeta_name, - 'dynamic_price_display' => $dynamic_price_hide, - 'send_file_attachment' => $send_file_attachment, - 'show_cart_thumb' => $show_cart_thumb, - 'aviary_api_key' => trim( $aviary_api_key ), - 'the_meta' => $product_meta, + 'productmeta_name' => $productmeta_name, + 'dynamic_price_display' => $dynamic_price_hide, + 'send_file_attachment' => $send_file_attachment, + 'show_cart_thumb' => $show_cart_thumb, + 'aviary_api_key' => trim( $aviary_api_key ), + 'the_meta' => $product_meta, ); if ( ! ppom_is_legacy_user() ) { $ppom_settings_meta_data['productmeta_style'] = $productmeta_style; @@ -585,7 +629,14 @@ function( $pm ) { wp_send_json( $resp ); } -// Update PPOM Fields Only +/** + * Rewrites only the stored PPOM field schema for a field group. + * + * @param int $ppom_id PPOM field-group ID. + * @param array $ppom_meta Normalized field definitions for `the_meta`. + * + * @return bool + */ function ppom_admin_update_ppom_meta_only( $ppom_id, $ppom_meta ) { // print_r($_REQUEST); exit; @@ -620,17 +671,25 @@ function ppom_admin_update_ppom_meta_only( $ppom_id, $ppom_meta ) { } else { return false; } - } -/* - * delete meta +// Field group deletion. + +/** + * Deletes a single PPOM field group from the admin UI. + * + * Verifies the admin nonce and capability before removing the row from the + * PPOM custom table. + * + * @return void */ function ppom_admin_delete_meta() { - if ( ! isset( $_POST['ppom_meta_nonce'] ) - || ! wp_verify_nonce( $_POST['ppom_meta_nonce'], 'ppom_meta_nonce_action' ) - || ! ppom_security_role() + $ppom_meta_nonce = isset( $_POST['ppom_meta_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ppom_meta_nonce'] ) ) : ''; + + if ( empty( $ppom_meta_nonce ) + || ! wp_verify_nonce( $ppom_meta_nonce, 'ppom_meta_nonce_action' ) + || ! ppom_security_role() ) { $response = array( 'status' => 'error', @@ -649,7 +708,7 @@ function ppom_admin_delete_meta() { $res = $wpdb->query( $wpdb->prepare( "DELETE FROM {$tbl_name} WHERE productmeta_id = %d", $productmeta_id ) ); - $response = []; + $response = array(); if ( $res ) { $response = array( 'status' => 'success', @@ -665,16 +724,23 @@ function ppom_admin_delete_meta() { wp_send_json( $response ); } -/* - * delete meta +/** + * Deletes multiple PPOM field groups from the admin list table. + * + * Sanitizes the submitted field-group IDs and removes the matching rows from + * the PPOM custom table in a single prepared query. + * + * @return void */ function ppom_admin_delete_selected_meta() { - if ( ! isset( $_POST['ppom_meta_nonce'] ) - || ! wp_verify_nonce( $_POST['ppom_meta_nonce'], 'ppom_meta_nonce_action' ) - || ! ppom_security_role() - || ! array_key_exists( 'productmeta_ids', $_POST ) - || ! is_array( $_POST['productmeta_ids'] ) + $ppom_meta_nonce = isset( $_POST['ppom_meta_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ppom_meta_nonce'] ) ) : ''; + + if ( empty( $ppom_meta_nonce ) + || ! wp_verify_nonce( $ppom_meta_nonce, 'ppom_meta_nonce_action' ) + || ! ppom_security_role() + || ! array_key_exists( 'productmeta_ids', $_POST ) + || ! is_array( $_POST['productmeta_ids'] ) ) { _e( 'Sorry, you are not allowed to perform this action', 'woocommerce-product-addon' ); die( 0 ); @@ -682,24 +748,24 @@ function ppom_admin_delete_selected_meta() { global $wpdb; - $del_ids = []; - $del_ids_ph = []; + $del_ids = array(); + $del_ids_ph = array(); // for the performance wise, prefer to use foreach instead of array_map-array_filter-array_fill stack. - foreach( $_POST['productmeta_ids'] as $id ) { + foreach ( $_POST['productmeta_ids'] as $id ) { $id = absint( $id ); - if( 0 === $id ) { + if ( 0 === $id ) { continue; } - $del_ids[] = $id; + $del_ids[] = $id; $del_ids_ph[] = '%d'; } $del_ids_ph = implode( ', ', $del_ids_ph ); - $tbl_name = $wpdb->prefix . PPOM_TABLE_META; + $tbl_name = $wpdb->prefix . PPOM_TABLE_META; $res = $wpdb->query( $wpdb->prepare( "DELETE FROM {$tbl_name} WHERE productmeta_id IN ({$del_ids_ph})", $del_ids ) ); @@ -766,7 +832,19 @@ function ppom_admin_simplify_meta( $meta ) { return $html; } -// Showing PPOM Edit on Product Page +// Admin product-page shortcuts. + +/** + * Adds PPOM edit and attach shortcuts to the product admin bar. + * + * Builds links from the current product's resolved PPOM assignment so store + * managers can jump straight to the field-group editor or attach another group. + * + * @return void + * + * @see PPOM_Meta::__construct() + * @see ppom_attach_fields_to_product() + */ function ppom_admin_bar_menu() { if ( ! is_product() ) { @@ -867,4 +945,4 @@ function ppom_add_black_friday_data( $configs ) { return $configs; } -add_filter( 'themeisle_sdk_blackfriday_data', 'ppom_add_black_friday_data' ); \ No newline at end of file +add_filter( 'themeisle_sdk_blackfriday_data', 'ppom_add_black_friday_data' ); diff --git a/inc/arrays.php b/inc/arrays.php index 948cba4c..69faede0 100644 --- a/inc/arrays.php +++ b/inc/arrays.php @@ -287,7 +287,7 @@ function ppom_array_settings() { 'label' => __( 'Yes', 'woocommerce-product-addon' ), 'default' => 'no', 'id' => 'ppom_legacy_price', - 'desc' => __( 'Enable this option to use the legacy method for price calculations.', 'woocommerce-product-addon' ), + 'desc' => __( 'Enable this option to use the legacy method for price calculations.', 'woocommerce-product-addon' ), ), array( 'title' => __( 'PPOM Permissions', 'woocommerce-product-addon' ), @@ -307,7 +307,7 @@ function ppom_array_settings() { ), array( - 'name' => __( 'Advance Features', 'woocommerce-product-addon' ) . ' (' . __( 'PRO', 'woocommerce-product-addon' ) .')', + 'name' => __( 'Advance Features', 'woocommerce-product-addon' ) . ' (' . __( 'PRO', 'woocommerce-product-addon' ) . ')', 'type' => 'title', 'desc' => __( 'These options will work when PRO version is installed', 'woocommerce-product-addon' ), 'id' => 'ppom_pro_features', @@ -689,8 +689,8 @@ function ppom_array_get_addons_details() { 'title' => __( 'Demo', 'woocommerce-product-addon' ), 'link' => 'https://demo-ppom-lite.vertisite.cloud/', ), - ), - 'type' => 'field' + ), + 'type' => 'field', ), array( 'title' => __( 'Cart Edit', 'woocommerce-product-addon' ), @@ -701,7 +701,7 @@ function ppom_array_get_addons_details() { 'link' => 'https://docs.themeisle.com/article/1793-how-to-enable-the-cart-edit-in-ppom', ), ), - 'type' => 'feature' + 'type' => 'feature', ), array( 'title' => __( 'Conditional Field Repeater', 'woocommerce-product-addon' ), @@ -712,13 +712,13 @@ function ppom_array_get_addons_details() { 'link' => 'https://docs.themeisle.com/article/1700-personalized-product-meta-manager#conditional-repeater', ), ), - 'type' => 'feature' + 'type' => 'feature', ), array( 'title' => __( 'Enquiry Form', 'woocommerce-product-addon' ), 'desc' => __( 'Enquiry Form Add-on enhances your product pages by adding a customizable enquiry button. It allows customers to send inquiries directly to the admin about products with PPOM Fields via email. All associated PPOM Meta Fields are included in the customer\'s message.', 'woocommerce-product-addon' ), 'actions' => array(), - 'type' => 'feature' + 'type' => 'feature', ), array( 'title' => __( 'Fields Popup', 'woocommerce-product-addon' ), @@ -728,8 +728,8 @@ function ppom_array_get_addons_details() { 'title' => __( 'Documentation', 'woocommerce-product-addon' ), 'link' => 'https://docs.themeisle.com/article/1982-how-to-configure-the-field-popup-in-ppom', ), - ), - 'type' => 'feature' + ), + 'type' => 'feature', ), ); diff --git a/inc/files.php b/inc/files.php index db0bc1d1..a023c1da 100644 --- a/inc/files.php +++ b/inc/files.php @@ -1,12 +1,32 @@ '; - $ppom_html .= '
'; + $cropped_url = ppom_get_dir_url() . 'cropped/' . $cropped_file_name; + $ppom_html .= ''; + $ppom_html .= ''; // translators: $s: the size of the cropped image. $cropped_title = sprintf( __( 'Your image-%s', 'woocommerce-product-addon' ), $size ); @@ -142,25 +162,25 @@ function ppom_create_thumb_for_meta( $file_name, $product_id, $cropped = false, * @return string The new file name. */ function ppom_create_unique_file_name( $file_name, $file_ext ) { - $seed = $file_name . microtime( true ) . mt_rand(); - return $file_name . "." . wp_hash( $seed ) . "." . $file_ext; + $seed = $file_name . microtime( true ) . mt_rand(); + return $file_name . '.' . wp_hash( $seed ) . '.' . $file_ext; } final class UploadFileErrors { - const OPEN_INPUT = 'open_input'; - const OPEN_OUTPUT = 'open_output'; + const OPEN_INPUT = 'open_input'; + const OPEN_OUTPUT = 'open_output'; const MISSING_TEMP_FILE = 'missing_temp_file'; - const OPEN_DIR = 'open_dir'; + const OPEN_DIR = 'open_dir'; static function get_message_response( $error_slug ) { $msg = array( - self::OPEN_INPUT => '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}', - self::OPEN_OUTPUT => '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}', + self::OPEN_INPUT => '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}', + self::OPEN_OUTPUT => '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}', self::MISSING_TEMP_FILE => '{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}', - self::OPEN_DIR => '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}', + self::OPEN_DIR => '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}', ); - return isset( $msg[$error_slug] ) ? $msg[$error_slug] : false; + return isset( $msg[ $error_slug ] ) ? $msg[ $error_slug ] : false; } } @@ -195,6 +215,20 @@ function ppom_create_chunk_file( $file_path_to_read, $ppom_chunk_file_path, $mod return false; } +// Upload handlers. + +/** + * Handles AJAX uploads for PPOM file fields. + * + * Validates the request nonce, checks the file type, and stores the upload in + * the PPOM upload directory. + * + * @return void + * + * @see ppom_files_setup_get_directory() + * @see ppom_delete_file() + * @see ppom_woocommerce_rename_files() + */ function ppom_upload_file() { header( 'Expires: Mon, 26 Jul 1997 05:00:00 GMT' ); @@ -231,7 +265,13 @@ function ppom_upload_file() { $file_name = apply_filters( 'ppom_uploaded_filename', $file_name ); - $additional_mime_types = apply_filters( 'ppom_custom_allowed_mime_types', array( 'ai' => 'application/postscript', 'eps' => 'application/postscript' ) ); + $additional_mime_types = apply_filters( + 'ppom_custom_allowed_mime_types', + array( + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + ) + ); $allowed_mime_types = array_merge( get_allowed_mime_types(), $additional_mime_types ); @@ -275,8 +315,8 @@ function ppom_upload_file() { $file_name = strtolower( $file_name ); $file_ext = pathinfo( $file_name, PATHINFO_EXTENSION ); $original_name = $file_name; - $original_name = str_replace(".$file_ext", "", $original_name); - $file_hash = substr( hash('haval192,5', $file_name), 0, 8 ) . '-' . $ppom_nonce; + $original_name = str_replace( ".$file_ext", '', $original_name ); + $file_hash = substr( hash( 'haval192,5', $file_name ), 0, 8 ) . '-' . $ppom_nonce; $file_name = str_replace( ".$file_ext", ".$file_hash.$file_ext", $file_name ); $file_path = $file_dir_path . $file_name; @@ -288,7 +328,7 @@ function ppom_upload_file() { $count = 1; while ( file_exists( $file_dir_path . $file_name_a . '_' . $count . $file_name_b ) ) { - $count ++; + ++$count; } $file_name = $file_name_a . '_' . $count . $file_name_b; @@ -352,7 +392,7 @@ function ppom_upload_file() { if ( ! $chunks || $chunk === $chunks - 1 ) { // Give a unique name to prevent name collisions. - $file_name = ppom_create_unique_file_name( $original_name, $file_ext ); + $file_name = ppom_create_unique_file_name( $original_name, $file_ext ); $unique_file_path = $file_dir_path . $file_name; rename( $chunk_file_path, $unique_file_path ); @@ -396,7 +436,14 @@ function ppom_upload_file() { die( json_encode( apply_filters( 'ppom_file_upload', $response, $file_type, $file_dir_path, $file_name ) ) ); } -// Deleting file +/** + * Deletes a temporary PPOM upload. + * + * @return void + * + * @see ppom_upload_file() + * @see ppom_files_setup_get_directory() + */ function ppom_delete_file() { if ( ! isset( $_REQUEST ['file_name'] ) || ! isset( $_REQUEST['ppom_nonce'] ) ) { @@ -462,7 +509,20 @@ function ppom_create_image_thumb( $file_path, $image_name, $thumb_size ) { return $image_destination; } -// Get file download url after payment +// Download resolution. + +/** + * Resolves the download URL for a confirmed PPOM file. + * + * @param string $file_name Original file name. + * @param int $order_id Order ID. + * @param int $product_id Product ID. + * + * @return string + * + * @see ppom_woocommerce_rename_files() + * @see ppom_get_dir_path() + */ function ppom_get_file_download_url( $file_name, $order_id, $product_id ) { $base_dir_path = ppom_get_dir_path() . $file_name; @@ -488,7 +548,6 @@ function ppom_get_file_download_url( $file_name, $order_id, $product_id ) { } return apply_filters( 'ppom_file_download_url', $file_download_url_found, $file_name ); - } // Generate uploaded file preview @@ -657,7 +716,6 @@ function ppom_get_croppie_options( $settings ) { return apply_filters( 'ppom_croppie_options', $cropping_settigs, $settings ); - } function ppom_get_viewport_settings( $settings ) { @@ -695,7 +753,6 @@ function ppom_get_viewport_settings( $settings ) { $the_viewport['type'] = $viewport_type; return $the_viewport; - } /* diff --git a/inc/functions.php b/inc/functions.php index a7619c78..db3b4ea8 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -1,8 +1,13 @@ id ) ? $product->id : $product->ID; - } else { + } elseif ( is_a( $product, 'WC_Product' ) ) { - if ( is_a( $product, 'WC_Product' ) ) { - if ( $product->is_type( 'variation' ) ) { - $product_id = $product->get_parent_id(); - } else { + if ( $product->is_type( 'variation' ) ) { + $product_id = $product->get_parent_id(); + } else { - $product_id = $product->get_id(); - } + $product_id = $product->get_id(); } } @@ -284,7 +286,7 @@ function ppom_recursive_sanitization( $field_value ) { } if ( is_array( $field_value ) ) { - foreach ($field_value as $key => $val ) { + foreach ( $field_value as $key => $val ) { $field_value[ $key ] = ppom_recursive_sanitization( $val ); } } @@ -292,11 +294,25 @@ function ppom_recursive_sanitization( $field_value ) { return $field_value; } +// Cart and order metadata. + /** - * adding cart items to order + * Formats PPOM cart data into metadata rows. + * + * Sanitizes saved field values against the resolved field schema before + * delegating display formatting to `ppom_generate_cart_meta()`. + * + * @param array $cart_item Cart item data. + * @param string $context Metadata context. + * + * @return array * * @since 8.2 - **/ + * + * @see ppom_generate_cart_meta() + * @see ppom_get_field_meta_by_dataname() + * @see ppom_woocommerce_order_item_meta() + */ function ppom_make_meta_data( $cart_item, $context = 'cart' ) { if ( ! isset( $cart_item['ppom']['fields'] ) ) { @@ -316,15 +332,15 @@ function ppom_make_meta_data( $cart_item, $context = 'cart' ) { $variation_id = $cart_item['variation_id']; } - $product_id = ppom_get_product_id( $cart_item['data'] ); + $product_id = ppom_get_product_id( $cart_item['data'] ); // Fields sanitization. $ppom = new PPOM_Meta( $product_id ); if ( is_array( $ppom->fields ) ) { - foreach( $ppom->fields as $field ) { + foreach ( $ppom->fields as $field ) { $data_name = sanitize_key( $field['data_name'] ); - if ( isset( $cart_item['ppom']['fields'][$data_name] ) ) { - $cart_item['ppom']['fields'][$data_name] = ppom_recursive_sanitization( $cart_item['ppom']['fields'][$data_name] ); + if ( isset( $cart_item['ppom']['fields'][ $data_name ] ) ) { + $cart_item['ppom']['fields'][ $data_name ] = ppom_recursive_sanitization( $cart_item['ppom']['fields'][ $data_name ] ); } } } @@ -339,12 +355,19 @@ function ppom_make_meta_data( $cart_item, $context = 'cart' ) { } /** - * This function will process all fields in cart and return into - * readable form for cart meta + * Builds readable PPOM metadata from cart field values. * - * @params: $product_id - * @params: $ppom_meta_ids (if product_is not known) - **/ + * @param array $ppom_cart_items Cart PPOM payload. + * @param int $product_id Product ID. + * @param array|int|string|null $ppom_meta_ids Optional PPOM group IDs. + * @param string $context Metadata context. + * @param int|null $variation_id Variation ID. + * + * @return array + * + * @see ppom_make_meta_data() + * @see ppom_get_field_meta_by_dataname() + */ function ppom_generate_cart_meta( $ppom_cart_items, $product_id, $ppom_meta_ids = null, $context = 'cart', $variation_id = null ) { $ppom_meta = array(); // Return the empty array if the cart is empty. @@ -520,7 +543,7 @@ function ppom_generate_cart_meta( $ppom_cart_items, $product_id, $ppom_meta_ids if ( $total_qty > 0 ) { // translators: %d: the total quantity. - $meta_display[] = '' . sprintf( __( 'Total = %d', 'woocommerce-product-addon' ), $total_qty ) . '' ; + $meta_display[] = '' . sprintf( __( 'Total = %d', 'woocommerce-product-addon' ), $total_qty ) . ''; $meta_data = array( 'name' => $field_title, 'display' => implode( '
', $meta_display ), @@ -700,7 +723,7 @@ function ppom_generate_cart_meta( $ppom_cart_items, $product_id, $ppom_meta_ids $audio_meta = json_decode( stripslashes( $audio_meta ), true ); $audio_url = stripslashes( $audio_meta['link'] ); $audio_html = '' . $audio_meta['title'] . ''; - $meta_lable = $field_title . ': ' . $ppom_file_count ++; + $meta_lable = $field_title . ': ' . $ppom_file_count++; // $ppom_meta[$meta_lable] = $audio_html; $meta_data = array( 'name' => $meta_lable, @@ -1018,7 +1041,6 @@ function ppom_is_aviary_installed() { } else { return false; } - } function ppom_settings_link( $links ) { @@ -1029,7 +1051,23 @@ function ppom_settings_link( $links ) { return $links; } -// Get field type by data_name +// Field lookup. + +/** + * Resolves a visible PPOM field definition by data name. + * + * @param int $product_id Product ID. + * @param string $original_data_name Field data name from the request or stored payload. + * @param array|int|string|null $ppom_id Optional PPOM group ID or IDs. + * + * @return array|string + * + * @internal Maintains the legacy `ppom_get_field_by_dataname__field_meta` filter signature when required by Pro compatibility flags. + * + * @see PPOM_Meta::get_fields() + * @see ppom_make_meta_data() + * @see ppom_generate_cart_meta() + */ function ppom_get_field_meta_by_dataname( $product_id, $original_data_name, $ppom_id = null ) { $ppom = new PPOM_Meta( $product_id ); @@ -1060,7 +1098,7 @@ function ppom_get_field_meta_by_dataname( $product_id, $original_data_name, $ppo } // Keep the apply_filters call has wrong param (released with v32.0.0) - if( ppom_pro_is_installed() && ppom_check_pro_compatibility( 'cond_field_repeat' ) && ! ppom_check_pro_compatibility( 'pgfbdfm_wp_filter_param_fix' ) ) { + if ( ppom_pro_is_installed() && ppom_check_pro_compatibility( 'cond_field_repeat' ) && ! ppom_check_pro_compatibility( 'pgfbdfm_wp_filter_param_fix' ) ) { return apply_filters( 'ppom_get_field_by_dataname__field_meta', $ppom_fields, $field_meta, $original_data_name, $data_name ); } @@ -1267,7 +1305,7 @@ function ppom_convert_options_to_key_val( $options, $meta, $product ) { $option_label = ppom_generate_option_label( $option, $option_price, $meta ); // This filter change prices for Currency switcher - // $option_price = apply_filters('ppom_option_price', $option_price); + // $option_price = apply_filters('ppom_option_price', $option_price); // Price matrix discount $discount = isset( $meta['discount'] ) && $meta['discount'] == 'on' ? true : false; @@ -1287,7 +1325,7 @@ function ppom_convert_options_to_key_val( $options, $meta, $product ) { $option_price_without_tax = $option_price; $option_price = ppom_get_price_including_tax($option_price, $product); } - $option_label = ppom_generate_option_label($option, $option_price, $meta); + $option_label = ppom_generate_option_label($option, $option_price, $meta); $option_percent = $option['price']; } else { @@ -1578,7 +1616,6 @@ function ppom_get_filesize_in_kb( $file_name ) { return round( $size / 1024, 2 ) . ' KB'; } - } @@ -1591,7 +1628,7 @@ function ppom_generate_html_for_files( $file_names, $input_type, $item ) { $file_edit_path = ppom_get_dir_path( 'edits' ) . ppom_file_get_name( $file_name, $item->get_product_id() ); // Making file thumb download with new path - $ppom_file_url = ppom_get_file_download_url( $file_name, $item->get_order_id(), $item->get_product_id() ); + $ppom_file_url = ppom_get_file_download_url( $file_name, $item->get_order_id(), $item->get_product_id() ); $file_path = ppom_get_dir_url( true ) . $file_name; $is_image_file = ppom_is_file_image( $file_path ); @@ -1968,7 +2005,6 @@ function ppom_is_field_visible( $field ) { } return apply_filters( 'ppom_is_field_visible', $is_visible, $field ); - } // Get logged in user role @@ -2199,7 +2235,7 @@ function ppom_get_price_table_js_dependencies() { if ( version_compare( WC_VERSION, '10.3.0', '<' ) ) { $dependencies[] = 'accounting'; - } else{ + } else { $dependencies[] = 'wc-accounting'; } @@ -2236,10 +2272,10 @@ function ppom_is_cart_quantity_updatable( $product_id ) { $unlinked = isset( $field['unlink_order_qty'] ) ? true : false; if ( ( $field['type'] == 'quantities' && ! $unlinked ) || - $field['type'] == 'eventcalendar' || - $field['type'] == 'vm' || - ( $field['type'] == 'vqmatrix' && ppom_is_field_has_price( $field ) ) || - $field['type'] == 'bulkquantity_zzz' // bulkquantity should not be in there ... TESTING. + $field['type'] == 'eventcalendar' || + $field['type'] == 'vm' || + ( $field['type'] == 'vqmatrix' && ppom_is_field_has_price( $field ) ) || + $field['type'] == 'bulkquantity_zzz' // bulkquantity should not be in there ... TESTING. ) { $qty_updatable = false; @@ -2274,8 +2310,8 @@ function ppom_reset_cart_quantity_to_one( $product_id ) { $unlinked = isset( $field['unlink_order_qty'] ) ? true : false; if ( $field['type'] == 'vm' || - ( $field['type'] == 'quantities' && ppom_is_field_has_price( $field ) && ! $unlinked ) || - ( $field['type'] == 'vqmatrix' && ppom_is_field_has_price( $field ) ) + ( $field['type'] == 'quantities' && ppom_is_field_has_price( $field ) && ! $unlinked ) || + ( $field['type'] == 'vqmatrix' && ppom_is_field_has_price( $field ) ) ) { $reset_qty = true; @@ -2371,16 +2407,16 @@ function ppom_get_conditional_data_attributes( $meta ) { $conditions['rules'] = array_filter( $conditions['rules'], - function( $rule ) { - if ( empty( $rule['operators'] ) || ! is_string( $rule['operators'] ) ) { + function ( $rule ) { + if ( empty( $rule['operators'] ) || ! is_string( $rule['operators'] ) ) { return ! empty( $rule['element_values'] ); } - if ( in_array( $rule['operators'], array( 'any', 'empty', 'even-number', 'odd-number', 'regex') ) ) { + if ( in_array( $rule['operators'], array( 'any', 'empty', 'even-number', 'odd-number', 'regex' ) ) ) { return true; } - if ( in_array( $rule['operators'], array( 'number-multiplier', 'regex', 'contains', 'not-contains') ) ) { + if ( in_array( $rule['operators'], array( 'number-multiplier', 'regex', 'contains', 'not-contains' ) ) ) { return ! empty( $rule['element_constant'] ); } @@ -2390,7 +2426,7 @@ function( $rule ) { if ( 'between' === $rule['operators'] ) { return ( - ! empty( $rule['cond-between-interval']) && is_array( $rule['cond-between-interval'] ) + ! empty( $rule['cond-between-interval'] ) && is_array( $rule['cond-between-interval'] ) && isset( $rule['cond-between-interval']['to'] ) && isset( $rule['cond-between-interval']['from'] ) ); @@ -2410,7 +2446,7 @@ function( $rule ) { foreach ( $conditions['rules'] as $rule ) { - $counter = ++ $index; + $counter = ++$index; $input = 'input' . $counter; $value = 'val' . $counter; $opr = 'operator' . $counter; @@ -2475,8 +2511,8 @@ function ppom_settings_migrated() { * @param string $feature_slug That represents feature key to look the if there is a compatibility. * @return bool */ -function ppom_check_pro_compatibility($feature_slug) { - if( ! defined( 'PPOM_PRO_COMPATIBILITY_FEATURES' ) || ! is_array( PPOM_PRO_COMPATIBILITY_FEATURES ) ) { +function ppom_check_pro_compatibility( $feature_slug ) { + if ( ! defined( 'PPOM_PRO_COMPATIBILITY_FEATURES' ) || ! is_array( PPOM_PRO_COMPATIBILITY_FEATURES ) ) { return false; } @@ -2529,7 +2565,7 @@ function ppom_posted_field_max_min_value_validation( $posted_fields, $field ) { ? sprintf( '%1$s: %2$s', $title, $error_message ) : sprintf( // translators: %1$s is minimum checked value, %2$s is Field label. - __('You must select at least %1$s options for %2$s field.', 'woocommerce-product-addon'), + __( 'You must select at least %1$s options for %2$s field.', 'woocommerce-product-addon' ), $min_check, $title ); @@ -2571,6 +2607,7 @@ function ppom_posted_field_max_min_value_validation( $posted_fields, $field ) { /** * Get image name from images array. + * * @param mixed $images * @return string */ @@ -2593,4 +2630,4 @@ function ppom_get_image_name( $images ) { } return $updated_value; -} \ No newline at end of file +} diff --git a/inc/hooks.php b/inc/hooks.php index bdea3e04..6c118d05 100644 --- a/inc/hooks.php +++ b/inc/hooks.php @@ -1,19 +1,35 @@ admin_url( 'admin-ajax.php', (is_ssl() ? 'https' : 'http') ), - 'ppom_inputs' => $ppom_meta_fields, - 'field_meta' => $ppom_meta_fields);*/ + 'ppom_inputs' => $ppom_meta_fields, + 'field_meta' => $ppom_meta_fields);*/ $vars_args = array( @@ -466,9 +537,19 @@ function ppom_hooks_load_input_scripts( $product, $ppom_id = null ) { ); wp_localize_script( "ppom-{$ppom_conditions_script}", 'ppom_input_vars', $ppom_input_vars ); } - } +/** + * Normalizes per-field input args before the field template renders. + * + * @param array $field_setting Prepared input settings. + * @param array $field_meta Stored field definition. + * @param WC_Product $product Product being rendered. + * + * @return array + * + * @see PPOM_Form::ppom_fields_render() + */ function ppom_hooks_input_args( $field_setting, $field_meta, $product ) { if ( $field_setting['type'] == 'date' && isset( $field_meta['jquery_dp'] ) && $field_meta['jquery_dp'] == 'on' ) { @@ -493,6 +574,17 @@ function ppom_hooks_input_args( $field_setting, $field_meta, $product ) { return $field_setting; } +/** + * Validates checkbox minimum and maximum selections. + * + * @param bool $has_value Current validation result. + * @param array $posted_fields Normalized posted field values. + * @param array $field Stored field definition. + * + * @return bool + * + * @see ppom_check_validation() + */ function ppom_hooks_checkbox_valided( $has_value, $posted_fields, $field ) { if ( $field['type'] != 'checkbox' ) { @@ -541,11 +633,19 @@ function ppom_hooks_show_option_price_pricematrix( $show_price, $meta ) { return $show_price; } +// Input metadata normalization hooks. + /** - * registration meta in wmp for translation + * Registers translatable strings and normalized option IDs for a field group. + * + * @param array $meta_data Field definitions being stored. + * @param int $ppom_id Field-group ID being written. + * + * @return array * - * @since 7.0 - **/ + * @see ppom_admin_save_form_meta() + * @see ppom_admin_update_form_meta() + */ function ppom_hooks_register_wpml( $meta_data, $ppom_id ) { @@ -598,7 +698,6 @@ function ( $option ) { nm_wpml_register( $option['title'], 'PPOM' ); return $option; - }, $data['images'] ); @@ -625,7 +724,6 @@ function ( $option ) { $option['id'] = ppom_get_option_id( $option ); return $option; - }, $data['options'] ); @@ -647,8 +745,17 @@ function ( $option ) { return $meta_data; } +// Conditional wrapper hooks. -/** The input wrapper class, it is NOT the main wrapper */ + +/** + * Adds conditional-visibility classes to the legacy field wrapper element. + * + * @param string $input_wrapper_class Existing wrapper classes. + * @param array $field_meta Stored field definition. + * + * @return string + */ function ppom_hooks_input_wrapper_class( $input_wrapper_class, $field_meta ) { $input_wrapper_class .= ' ppom-input-' . $field_meta['id']; @@ -681,16 +788,32 @@ function ppom_hooks_input_wrapper_class( $input_wrapper_class, $field_meta ) { return $input_wrapper_class; } -/** The input wrapper class, it is NOT the main wrapper: WHEN NEW CONDITTIONS */ +/** + * Adds stable wrapper classes for the newer conditional-logic renderer. + * + * @param string $input_wrapper_class Existing wrapper classes. + * @param array $field_meta Stored field definition. + * + * @return string + * + * @see ppom_get_conditions_mode() + */ function ppom_hooks_input_wrapper_class_new( $input_wrapper_class, $field_meta ) { // var_dump($input_wrapper_class); $input_wrapper_class .= ' ppom-input-' . $field_meta['id']; return $input_wrapper_class; - } -/** The input MAIN wrapper class */ +/** + * Adds conditional-logic classes to the main field wrapper element. + * + * @param string $wrapper_class Existing wrapper classes. + * @param array $classes_array Wrapper class array passed by the caller. + * @param array $field_meta Stored field definition. + * + * @return string + */ function ppom_hooks_input_main_wrapper_class( $wrapper_class, $classes_array, $field_meta ) { $logic = ( isset( $field_meta['logic'] ) ? $field_meta['logic'] : '' ); @@ -714,7 +837,7 @@ function ppom_hooks_input_main_wrapper_class( $wrapper_class, $classes_array, $f foreach ( $conditions['rules'] as $index => $rule ) { - $element = isset( $rule['elements'] ) ? strtolower( $rule['elements'] ): ''; + $element = isset( $rule['elements'] ) ? strtolower( $rule['elements'] ) : ''; $wrapper_class .= " ppom-cond-{$element}"; $wrapper_class .= " ppom-locked-{$element}"; } @@ -739,6 +862,19 @@ function ppom_hooks_convert_option_json_to_string( $row, $order ) { return $row; } +// Cart and order integration hooks. + +/** + * Adds PPOM option weights to the cart item's product object. + * + * @param array $ppom_field_prices Calculated PPOM pricing rows. + * @param array $ppom_fields_post Normalized posted PPOM field payload. + * @param array $cart_items WooCommerce cart item data. + * + * @return void + * + * @see ppom_get_field_prices() + */ function ppom_hooks_update_cart_weight( $ppom_field_prices, $ppom_fields_post, $cart_items ) { if ( ppom_pro_is_installed() ) { @@ -811,7 +947,22 @@ function ppom_hooks_set_option_operator( $operator, $price, $meta ) { return $operator; } -// PPOM shortcode +// Shortcode and template override hooks. + +/** + * Renders PPOM fields inside the `[ppom]` shortcode form. + * + * Loads the same product-scoped assets used on the single-product template and + * then renders either the legacy or template-based PPOM field output. + * + * @param array $attr Shortcode attributes. + * + * @return string|null + * + * @see PPOM_FRONTEND_SCRIPTS::load_scripts_by_product_id() + * @see ppom_woocommerce_show_fields_on_product() + * @see ppom_woocommerce_template_base_inputs_rendering() + */ function ppom_hooks_render_shortcode( $attr ) { $params = shortcode_atts( @@ -840,8 +991,8 @@ function ppom_hooks_render_shortcode( $attr ) { PPOM_FRONTEND_SCRIPTS::load_scripts_by_product_id( $params['product_id'], '', 'shortcode' ); ?>
+ action="get_permalink() ) ); ?>" + method="post" enctype='multipart/form-data'> "> + value=""> @@ -877,14 +1028,30 @@ class="single_add_to_cart_button button alt">sing } } -// redirecting to cart directly if being called from shortcode +/** + * Redirects shortcode submissions to the cart after add-to-cart completes. + * + * @param string $url Default WooCommerce add-to-cart redirect URL. + * + * @return string + */ function ppom_hooks_redirect_to_cart_if_shortcode( $url ) { $url = isset( $_POST['ppom']['ppom_shorcode_product_id'] ) ? wc_get_cart_url() : $url; return $url; } -// Check if the PPOM field template inside the theme +/** + * Resolves theme-level template overrides for PPOM field templates. + * + * @param string $full_path Resolved plugin template path. + * @param string $template_path Relative template path within PPOM. + * @param array|null $vars Template vars available during resolution. + * + * @return string + * + * @see PPOM_Form::render_input_template() + */ function ppom_hooks_check_theme_path( $full_path, $template_path, $vars ) { // Extract variable from array @@ -953,7 +1120,6 @@ function update_converted_option_keys( $new_option, $option_key, $option, $meta, } return $new_option; - } // search PPOM meta in order search @@ -974,7 +1140,7 @@ function ppom_hooks_search_in_order( $search_fields ) { $order = new WC_Order( $order_id ); $items = $order->get_items(); - $ppom_fields = []; + $ppom_fields = array(); foreach ( $order->get_items() as $item_id => $item_values ) { if ( version_compare( WC_VERSION, '3.0', '<' ) ) { $product_id = $item_values['product_id']; @@ -988,7 +1154,7 @@ function ppom_hooks_search_in_order( $search_fields ) { } $ppom_fields[] = $ppom_fields_post['fields']; // array_merge($ppom_fields, $ppom_fields_post['fields']); - // ppom_pa($ppom_fields ); + // ppom_pa($ppom_fields ); } add_post_meta( $order_id, '_ppom_fields', $ppom_fields, true ); } diff --git a/inc/nmInput.class.php b/inc/nmInput.class.php index d5ec4fb1..f7c9bd60 100644 --- a/inc/nmInput.class.php +++ b/inc/nmInput.class.php @@ -118,7 +118,6 @@ public function Input( $args, $default_value = '' ) { * 4. Number * 5. color **/ - public function Regular( $args, $default_value = '' ) { $product = isset( $args['product_id'] ) ? wc_get_product( $args['product_id'] ) : null; @@ -155,7 +154,16 @@ public function Regular( $args, $default_value = '' ) { $html = '
'; if ( $label ) { $html .= ''; + $html .= wp_kses( + $label, + array( + 'span' => array( + 'class' => true, + 'data-*' => true, + 'title' => true, + ), + ) + ) . ''; } if ( $price !== '' ) { @@ -235,7 +243,16 @@ public function Measure( $args, $default_value = '' ) { $html = '
'; if ( $label ) { $html .= ''; + $html .= wp_kses( + $label, + array( + 'span' => array( + 'class' => true, + 'data-*' => true, + 'title' => true, + ), + ) + ) . ''; } $classes .= ' ppom-measure-input'; @@ -350,7 +367,7 @@ function Textarea( $args, $default_value = '' ) { if ( $label ) { $html .= ''; + $html .= $label . ''; } if ( $rich_editor == 'on' ) { @@ -399,7 +416,6 @@ function Textarea( $args, $default_value = '' ) { // filter: nmforms_input_htmls return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - } /** @@ -819,7 +835,7 @@ public function Palettes( $args, $default_value = '' ) { $html .= '
'; if ( $label ) { $html .= ''; + $html .= $label . ''; } // ppom_pa($options); $html .= '
'; @@ -1120,17 +1136,15 @@ public function Image( $args, $default_value = '' ) { $image_url = wp_get_attachment_thumb_url( $image['image_id'] ); $html .= ''; } - } else { - if ( isset( $image['url'] ) && $image['url'] != '' ) { + } elseif ( isset( $image['url'] ) && $image['url'] != '' ) { $html .= ''; - } else { - $html .= ''; - } + } else { + $html .= ''; } $html .= '
'; - $img_index ++; + ++$img_index; } } @@ -1596,7 +1610,16 @@ public function Custom( $args, $default_value = '' ) { $html = '
'; if ( $label ) { $html .= ''; + $html .= wp_kses( + $label, + array( + 'span' => array( + 'class' => true, + 'data-*' => true, + 'title' => true, + ), + ) + ) . ''; } $html .= apply_filters( 'nmform_custom_input', $html, $args, $default_value ); @@ -1702,14 +1725,12 @@ private function get_property( $property ) { } return apply_filters( "nmform_property-{$property}", $value ); - } /** * ====================== FILTERS ===================================== * */ - public function adjust_attributes_values( $attr_value, $attr, $args ) { switch ( $attr ) { @@ -1747,7 +1768,6 @@ public function adjust_attributes_values( $attr_value, $attr, $args ) { /** * ====================== ENDs FILTERS ===================================== * */ - public static function get_instance() { // create a new object if it doesn't exist. is_null( self::$ins ) && self::$ins = new self(); diff --git a/inc/prices.php b/inc/prices.php index fc1231ea..c363eeb7 100644 --- a/inc/prices.php +++ b/inc/prices.php @@ -1,13 +1,30 @@ get_price(), $cart_item ); - // $context = 'cart'; - // $product_price = ppom_get_product_price( $product, $variation_id, $context); + // $context = 'cart'; + // $product_price = ppom_get_product_price( $product, $variation_id, $context); // return array with: price, source $price_info = ppom_price_get_product_base( @@ -125,6 +142,24 @@ function ppom_before_calculate_totals( $cart_items ) { } +/** + * Resolves price entries for posted PPOM fields. + * + * Uses submitted field selections to look up field definitions and build the + * normalized pricing rows used by cart line pricing and cart fees. + * + * @param array $ppom_fields_post Posted PPOM field values. + * @param int $product_id Product ID. + * @param float|int $product_quantity Product quantity. + * @param int|string $variation_id Variation ID. + * @param array|null $item Cart item context. + * + * @return array + * + * @see ppom_get_field_meta_by_dataname() + * @see ppom_price_get_product_base() + * @see ppom_price_cart_fee() + */ function ppom_get_field_prices( $ppom_fields_post, $product_id, &$product_quantity, $variation_id, $item = null ) { $field_prices = array(); @@ -575,7 +610,7 @@ function ppom_get_field_prices( $ppom_fields_post, $product_id, &$product_quanti $val = $value['option']; $quantity = $value['qty']; - // var_dump($val, $option); + // var_dump($val, $option); $quantity = intval( $quantity ); $quantities_total += $quantity; @@ -679,7 +714,7 @@ function ppom_get_field_prices( $ppom_fields_post, $product_id, &$product_quanti $product_quantity = $value['qty']; $bq_value = $value['option']; - // $option = null; + // $option = null; $bq_found = ppom_price_bulkquantity_chunk( $product, $options, $product_quantity ); $option_price = isset( $bq_found[ $bq_value ] ) ? floatval( $bq_found[ $bq_value ] ) : null; @@ -708,7 +743,7 @@ function ppom_get_field_prices( $ppom_fields_post, $product_id, &$product_quanti $measure_price_field['price-multiplier'] = floatval( $field_meta['price-multiplier'] ); } - $field_prices[] = $measure_price_field; + $field_prices[] = $measure_price_field; break; case 'eventcalendar': @@ -1019,18 +1054,42 @@ function ppom_price_get_total_measure( $price_array ) { return $total_measure; } -// Get product base price +/** + * Resolves the base product price for a PPOM cart item. + * + * Applies matrix, quantity, fixed-price, and measure pricing before returning + * the base amount used in the final cart line total. + * + * @param float $product_price Product price before PPOM adjustments. + * @param WC_Product $product Product object. + * @param array $ppom_fields_post Posted PPOM field values. + * @param float|int $product_quantity Product quantity. + * @param array $ppom_field_prices Normalized PPOM price rows. + * @param float|int $ppom_discount Discount accumulator passed by reference. + * @param array|null $ppom_pricematrix Matched price matrix data. + * + * @return array + * + * @see ppom_get_field_prices() + * @see ppom_parse_price_matrix() + * @see ppom_price_controller() + */ function ppom_price_get_product_base( - $product_price, $product, $ppom_fields_post, - $product_quantity, $ppom_field_prices, &$ppom_discount, $ppom_pricematrix = null + $product_price, + $product, + $ppom_fields_post, + $product_quantity, + $ppom_field_prices, + &$ppom_discount, + $ppom_pricematrix = null ) { // converting back to org price if Currency Switcher is used $base_price = ppom_hooks_convert_price_back( $product_price ); - // $base_price = $product->get_price(); + // $base_price = $product->get_price(); // $base_price = floatval($base_price); - // $base_price = $product->get_price(); - // ppom_pa($product); + // $base_price = $product->get_price(); + // ppom_pa($product); // var_dump($product_quantity); $product_id = ppom_get_product_id( $product ); // var_dump('varia',$product_id); @@ -1065,7 +1124,7 @@ function ppom_price_get_product_base( $base_price = ($base_price + $total_addon_price); } $base_price = floatval($base_price) - $matrix_discount; - $source = 'matrix_discount'; + $source = 'matrix_discount'; }*/ } @@ -1187,13 +1246,21 @@ function ppom_price_fixedprice_chunk( $product, $fixedprice_options, $product_qu } return apply_filters( 'ppom_price_fixedprice_chunk_cart', $fixedprice_found, $product ); - } -/** - * Calculating Fixed Fees - * **/ +// Cart fees. +/** + * Adds PPOM cart fees and discounts to the WooCommerce cart. + * + * @param WC_Cart $cart WooCommerce cart. + * + * @return void + * + * @see ppom_get_field_prices() + * @see ppom_price_has_discount_matrix() + * @see ppom_price_controller() + */ function ppom_price_cart_fee( $cart ) { $fee_no = 1; $cart_counter = 1; @@ -1217,7 +1284,7 @@ function ppom_price_cart_fee( $cart ) { if ( $matrix_found = ppom_price_has_discount_matrix( $product, $quantity ) ) { - // $price_tobe_discount = $cart_item_price * $quantity; NOTE: This has to be check + // $price_tobe_discount = $cart_item_price * $quantity; NOTE: This has to be check $native_product = wc_get_product( $product_id ); $price_tobe_discount = ppom_get_product_price( $native_product ) * $quantity; if ( $matrix_found['discount'] == 'both' ) { @@ -1271,19 +1338,19 @@ function ppom_price_cart_fee( $cart ) { $tax_class = $product->get_tax_class( 'unfiltered' ); // if wc prices include tax: substract the tax from additional fixed fee since already WC will add tax. - if( wc_prices_include_tax() ) { - $tax = WC_Tax::calc_tax( $fee_price, \WC_Tax::get_rates($tax_class), true ); + if ( wc_prices_include_tax() ) { + $tax = WC_Tax::calc_tax( $fee_price, \WC_Tax::get_rates( $tax_class ), true ); - $total_tax = array_sum($tax); + $total_tax = array_sum( $tax ); $fee_price = $fee_price - $total_tax; } $cart->add_fee( esc_html( $label ), $fee_price, $taxable, $tax_class ); - $fee_no ++; + ++$fee_no; } } - $cart_counter ++; + ++$cart_counter; } } @@ -1350,12 +1417,10 @@ function ppom_parse_price_matrix( $ppom_pricematrix, $product, $product_quantity } else { $matrix_discount = isset( $matrix_found['raw_price'] ) ? floatval( $matrix_found['raw_price'] ) : 0; } - } else { - if ( isset( $matrix_found['matrix_fixed'] ) ) { + } elseif ( isset( $matrix_found['matrix_fixed'] ) ) { $matrix_price = isset( $matrix_found['raw_price'] ) ? $matrix_found['raw_price'] / $product_quantity : $base_price; - } else { - $matrix_price = isset( $matrix_found['raw_price'] ) ? $matrix_found['raw_price'] : $base_price; - } + } else { + $matrix_price = isset( $matrix_found['raw_price'] ) ? $matrix_found['raw_price'] : $base_price; } $matrix = array( @@ -1470,7 +1535,7 @@ function ppom_price_check_price_matrix( $cart_items, $values ) { return $cart_items; } - $matrix_found = []; + $matrix_found = array(); foreach ( $pricematrix_field as $pm ) { $pm_dataname = isset( $pm['data_name'] ) ? $pm['data_name'] : ''; @@ -1504,10 +1569,10 @@ function ppom_option_price_handle_vat( $option_price, $product ) { if ( $option_price >= 0 && ( ! is_product() && apply_filters( 'ppom_handle_option_price_vat_in_cart', true ) === true ) ) { $vat_type = get_option( 'woocommerce_tax_display_cart' ); - $args = [ + $args = array( 'price' => $option_price, 'quantity' => 1, - ]; + ); if ( $vat_type == 'excl' ) { $option_price = wc_get_price_excluding_tax( $product, $args ); } else { @@ -1517,10 +1582,10 @@ function ppom_option_price_handle_vat( $option_price, $product ) { if ( $option_price >= 0 && ( is_product() && apply_filters( 'ppom_handle_option_price_vat_in_product', true ) === true ) ) { $vat_type = get_option( 'woocommerce_tax_display_shop' ); - $args = [ + $args = array( 'price' => $option_price, 'quantity' => 1, - ]; + ); if ( $vat_type == 'excl' ) { $option_price = wc_get_price_excluding_tax( $product, $args ); } else { diff --git a/inc/rest.class.php b/inc/rest.class.php index 324cba40..f055a4fb 100644 --- a/inc/rest.class.php +++ b/inc/rest.class.php @@ -1,27 +1,43 @@ set_headers(); @@ -156,7 +182,16 @@ function get_ppom_meta_info_product( WP_REST_Request $request ) { return $response; } - function get_ppom_meta_by_id( WP_REST_Request $request ) { + /** + * Returns a PPOM field schema by field-group ID. + * + * @param WP_REST_Request $request REST request containing `id`. + * + * @return WP_REST_Response + * + * @see PPOM_Meta::get_fields_by_id() + */ + public function get_ppom_meta_by_id( WP_REST_Request $request ) { $this->set_headers(); @@ -193,9 +228,18 @@ function get_ppom_meta_by_id( WP_REST_Request $request ) { return $response; } - // Save meta against product - // Getting ppom meta info - function ppom_save_meta_product( WP_REST_Request $request ) { + /** + * Creates or updates the PPOM field group attached to a product. + * + * The submitted `fields` payload is JSON-decoded, validated against the + * configured secret key, and then written into the PPOM field-group table. + * + * @param WP_REST_Request $request REST request containing `product_id`, + * `secret_key`, and `fields`. + * + * @return WP_REST_Response + */ + public function ppom_save_meta_product( WP_REST_Request $request ) { $this->set_headers(); @@ -251,18 +295,18 @@ function ppom_save_meta_product( WP_REST_Request $request ) { // ppom_pa($ppom_fields); return new WP_REST_Response( $meta_response ); - } /** - * Delete fields against product - * params: - * product_id: integer - * secret_key: string - * fields : array() - **/ - function delete_ppom_fields_product( WP_REST_Request $request ) { + * Deletes selected PPOM fields from the field group attached to a product. + * + * @param WP_REST_Request $request REST request containing `product_id`, + * `secret_key`, and `fields`. + * + * @return WP_REST_Response + */ + public function delete_ppom_fields_product( WP_REST_Request $request ) { $this->set_headers(); @@ -312,12 +356,17 @@ function delete_ppom_fields_product( WP_REST_Request $request ) { // ppom_pa($ppom_fields); return new WP_REST_Response( $meta_response ); - } - // Check if secret key is set and matched - function is_secret_key_valid( $secretkey ) { + /** + * Validates the shared PPOM REST secret key. + * + * @param string $secretkey Secret key submitted by the API client. + * + * @return bool + */ + public function is_secret_key_valid( $secretkey ) { $api_key = ppom_get_option( 'ppom_rest_secret_key', true ); @@ -330,8 +379,20 @@ function is_secret_key_valid( $secretkey ) { return $key_valide; } - // build new meta entry - function save_new_meta_data( $product_id, $ppom_fields ) { + /** + * Creates a new PPOM field group for a product through the REST API. + * + * Inserts the initial field-group row, writes the normalized field schema, + * and attaches the new PPOM ID to the product post meta. + * + * @param int $product_id Product ID receiving the new field group. + * @param array $ppom_fields Field definitions decoded from the request. + * + * @return array + * + * @see ppom_admin_update_ppom_meta_only() + */ + public function save_new_meta_data( $product_id, $ppom_fields ) { $product = new WC_Product( $product_id ); @@ -402,7 +463,16 @@ function save_new_meta_data( $product_id, $ppom_fields ) { return $resp; } - function update_meta_data( $ppom_meta, $ppom_fields, $product_id ) { + /** + * Merges submitted REST fields into an existing PPOM field group. + * + * @param object $ppom_meta Existing PPOM settings row. + * @param array $ppom_fields Field definitions decoded from the request. + * @param int $product_id Product ID attached to the field group. + * + * @return array + */ + public function update_meta_data( $ppom_meta, $ppom_fields, $product_id ) { $existing_fields = json_decode( $ppom_meta->the_meta, true ); // var_dump($ppom_meta); exit; @@ -450,7 +520,16 @@ function update_meta_data( $ppom_meta, $ppom_fields, $product_id ) { return $resp; } - function delete_meta_data( $ppom_meta, $delete_fields, $product_id ) { + /** + * Removes selected fields, or an entire field group, from a product. + * + * @param object $ppom_meta Existing PPOM settings row. + * @param array $delete_fields Field keys requested for deletion. + * @param int $product_id Product ID attached to the field group. + * + * @return array + */ + public function delete_meta_data( $ppom_meta, $delete_fields, $product_id ) { $existing_fields = json_decode( $ppom_meta->the_meta ); @@ -514,14 +593,16 @@ function delete_meta_data( $ppom_meta, $delete_fields, $product_id ) { return $resp; } - /** - * ==================================================================== - * ========================== ORDERS ================================== - * ==================================================================== - * */ + // Order meta routes. - // Getting ppom meta info - function get_ppom_meta_info_order( WP_REST_Request $request ) { + /** + * Returns formatted PPOM order item metadata for an order. + * + * @param WP_REST_Request $request REST request containing `order_id`. + * + * @return WP_REST_Response + */ + public function get_ppom_meta_info_order( WP_REST_Request $request ) { $this->set_headers(); @@ -558,8 +639,15 @@ function get_ppom_meta_info_order( WP_REST_Request $request ) { return $response; } - // update meta against order - function ppom_update_meta_order( WP_REST_Request $request ) { + /** + * Updates PPOM order item metadata for matching order products. + * + * @param WP_REST_Request $request REST request containing `order_id`, + * `secret_key`, and `fields`. + * + * @return WP_REST_Response + */ + public function ppom_update_meta_order( WP_REST_Request $request ) { $this->set_headers(); @@ -660,13 +748,14 @@ function ppom_update_meta_order( WP_REST_Request $request ) { } /** - * Delete fields against order - * params: - * order_id: integer - * secret_key: string - * fields : array() - **/ - function delete_ppom_fields_order( WP_REST_Request $request ) { + * Deletes selected PPOM order item metadata keys for an order. + * + * @param WP_REST_Request $request REST request containing `order_id`, + * `secret_key`, and `fields`. + * + * @return WP_REST_Response + */ + public function delete_ppom_fields_order( WP_REST_Request $request ) { $this->set_headers(); @@ -742,12 +831,22 @@ function delete_ppom_fields_order( WP_REST_Request $request ) { ); return new WP_REST_Response( $response_info ); - } - // Return all order items' meta - function get_order_item_meta( $order_id ) { + // Response formatting and transport helpers. + + /** + * Returns formatted PPOM metadata for every order item in an order. + * + * @param int $order_id WooCommerce order ID. + * + * @return array + * + * @see ppom_generate_cart_meta() + * @see ppom_get_field_meta_by_dataname() + */ + public function get_order_item_meta( $order_id ) { $order = wc_get_order( $order_id ); @@ -777,8 +876,9 @@ function get_order_item_meta( $order_id ) { $formatted_data['value'] = $meta_data->value; if ( isset( $ppom_meta[ $meta_data->key ] ) ) { - $formatted_data['display'] = $ppom_meta[ $meta_data->key ]['display']; - $formatted_data['value'] = $ppom_meta[ $meta_data->key ]['value']; + $formatted_value = isset( $ppom_meta[ $meta_data->key ]['value'] ) ? $ppom_meta[ $meta_data->key ]['value'] : $meta_data->value; + $formatted_data['display'] = isset( $ppom_meta[ $meta_data->key ]['display'] ) ? $ppom_meta[ $meta_data->key ]['display'] : $formatted_value; + $formatted_data['value'] = $formatted_value; } $meta_info[] = $formatted_data; @@ -795,7 +895,14 @@ function get_order_item_meta( $order_id ) { } - function filter_required_keys_only( $ppom_fields ) { + /** + * Reduces field definitions to the keys exposed by the REST read routes. + * + * @param array $ppom_fields Full PPOM field definitions. + * + * @return array + */ + public function filter_required_keys_only( $ppom_fields ) { $new_ppom_fields = array(); if ( $ppom_fields ) { @@ -835,7 +942,11 @@ function filter_required_keys_only( $ppom_fields ) { return apply_filters( 'ppom_rest_fields', $new_ppom_fields, $ppom_fields ); } - // settings headers + /** + * Sends CORS headers for PPOM REST responses and OPTIONS requests. + * + * @return void + */ public function set_headers() { if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) { @@ -857,7 +968,6 @@ public function set_headers() { exit( 0 ); } - } } diff --git a/inc/validation.php b/inc/validation.php index 59e45f2b..0006823a 100644 --- a/inc/validation.php +++ b/inc/validation.php @@ -1,9 +1,23 @@ &$value ) { + /** + * Recursively sanitizes PPOM field definitions before they are stored. + * + * Keys that allow stored HTML are sanitized with `ppom_esc_html()`. All other + * scalar values are reduced to plain text. + * + * @param array $data Untrusted field-definition array. + * + * @return array + * + * @see ppom_admin_save_form_meta() + * @see ppom_admin_update_form_meta() + */ +function ppom_sanitize_array_data( $data ) { + foreach ( $data as $key => &$value ) { if ( is_array( $value ) ) { $value = ppom_sanitize_array_data( $value ); + } elseif ( in_array( $key, ppom_fields_with_html(), true ) ) { + $value = ppom_esc_html( $value ); } else { - if ( in_array( $key, ppom_fields_with_html(), true ) ) { - $value = ppom_esc_html( $value ); - } else { - $value = sanitize_text_field( $value ); - } + $value = sanitize_text_field( $value ); } } + unset( $value ); - return $array; + return $data; } -// ppom_fields keys requires html + /** + * Returns field-definition keys that store sanitized HTML instead of plain text. + * + * @return array + */ function ppom_fields_with_html() { - $have_html = [ 'description', 'tooltip', 'heading', 'html', 'error_message', 'checked', 'disable_custom_dates' ]; + $have_html = array( 'description', 'tooltip', 'heading', 'html', 'error_message', 'checked', 'disable_custom_dates' ); return apply_filters( 'ppom_fields_with_html', $have_html ); } +// Quantity limit resolution. + /** - * Updates the quantity arguments. + * Applies PPOM quantity limits to WooCommerce quantity input arguments. + * + * Resolves the effective min, max, and step values from the current product's + * PPOM field definitions and merges them into WooCommerce's server-side + * quantity args. * * @param array $data List of data to update. * @param \WC_Product $product Product object. * * @return array */ - function ppom_validation_product_limits( $data, $product ) { if ( ppom_is_client_validation_enabled() ) { @@ -176,7 +210,7 @@ function ppom_validation_product_limits( $data, $product ) { /** - * Adds variation min max settings to be used by JS. + * Adds PPOM quantity limits to the variation data passed to the frontend. * * @param array $data Available variation data. * @param \WC_Product $product Product object. @@ -242,6 +276,20 @@ function ppom_validation_variation_limits( $data, $product, $variation ) { return $data; } +/** + * Derives quantity limits from PPOM quantity and price-matrix fields. + * + * Builds the canonical `min_qty`, `max_qty`, `step`, and `input_value` payload + * used by both server-side quantity validation and variation JS data. + * + * @param int $product_id Parent product ID. + * @param int $variation_id Variation ID when resolving variation-specific data. + * + * @return array + * + * @see PPOM_Meta::__construct() + * @see ppom_has_field_by_type() + */ function ppom_get_product_limits( $product_id, $variation_id ) { $product = wc_get_product( $product_id ); @@ -350,28 +398,23 @@ function ppom_get_product_limits( $product_id, $variation_id ) { return $limits; } +// Safe CSS overrides. + /** - * By default, WordPress strips CSS values that contain \ ( & } = or comments, such as rgb() - * and rgba(), because the core regex in safecss_filter_attr() flags them as unsafe. - * - * This filter overrides that behavior by checking if the CSS string contains - * "rgb(" or "rgba(" and explicitly allows it. All other CSS values still pass - * through the normal WordPress sanitization process. - * - * @since 1.0.0 + * Allows CSS declarations containing `rgb()` and `rgba()` values. * * @param bool $allow_css Whether the CSS in the string is considered safe. * @param string $css_string The full CSS declaration. * - * @return bool True if the CSS is safe and should be allowed, false otherwise. + * @return bool */ function ppom_safecss_filter_attr( $allow_css, $css_string ) { - // If the CSS string contains rgb() or rgba(), mark it as safe. - if ( stripos( $css_string, 'rgb(' ) !== false || stripos( $css_string, 'rgba(' ) !== false ) { - return true; - } + // If the CSS string contains rgb() or rgba(), mark it as safe. + if ( stripos( $css_string, 'rgb(' ) !== false || stripos( $css_string, 'rgba(' ) !== false ) { + return true; + } - return $allow_css; + return $allow_css; } add_filter( 'safecss_filter_attr_allow_css', 'ppom_safecss_filter_attr', 10, 2 ); diff --git a/inc/woocommerce.php b/inc/woocommerce.php index 8382c2b7..77cd94e8 100644 --- a/inc/woocommerce.php +++ b/inc/woocommerce.php @@ -1,14 +1,21 @@ has_unique_datanames() ) { - printf( '
' . __( "Some of your fields has duplicated datanames, please fix it", 'woocommerce-product-addon' ) . '
', 'ppom' ); + echo '
' . esc_html__( 'Some of your fields has duplicated datanames, please fix it', 'woocommerce-product-addon' ) . '
'; return; } @@ -80,11 +98,23 @@ function ppom_woocommerce_inputs_template_base() { $product_id = ppom_get_product_id( $product ); - $args = apply_filters( 'ppom_rendering_template_args', [ 'enable_add_to_cart_id' => false ], $product ); + $args = apply_filters( 'ppom_rendering_template_args', array( 'enable_add_to_cart_id' => false ), $product ); ppom_woocommerce_template_base_inputs_rendering( $product_id, $args ); } +/** + * Renders template-based PPOM fields for a product. + * + * @param int $product_id Product ID. + * @param array|null $args Rendering arguments. + * + * @return string|void + * + * @see PPOM_Form::ppom_fields_render() + * @see ppom_load_input_templates() + * @see ppom_woocommerce_show_fields_on_product() + */ function ppom_woocommerce_template_base_inputs_rendering( $product_id, $args = null ) { $product = wc_get_product( $product_id ); @@ -98,7 +128,7 @@ function ppom_woocommerce_template_base_inputs_rendering( $product_id, $args = n } $ppom_html = ''; - $template_vars = [ 'form_obj' => $form_obj ]; + $template_vars = array( 'form_obj' => $form_obj ); ob_start(); ppom_load_input_templates( 'frontend/ppom-fields.php', $template_vars ); @@ -130,6 +160,23 @@ function ppom_woocommerce_load_scripts() { } +// Validation. + +/** + * Validates PPOM fields during add to cart. + * + * Treats posted PPOM values as untrusted input and validates them against the + * resolved field schema for the product. + * + * @param bool $passed Validation state from earlier callbacks. + * @param int $product_id Product ID. + * @param int|float $qty Requested quantity. + * + * @return bool + * + * @see ppom_check_validation() + * @see PPOM_Meta::__construct() + */ function ppom_woocommerce_validate_product( $passed, $product_id, $qty ) { $ppom = new PPOM_Meta( $product_id ); @@ -140,7 +187,7 @@ function ppom_woocommerce_validate_product( $passed, $product_id, $qty ) { if ( ppom_get_price_mode() == 'legacy' && isset( $_POST['ppom']['fields'] ) ) { if ( ppom_is_price_attached_with_fields( $_POST['ppom']['fields'] ) && - empty( $_POST['ppom']['ppom_option_price'] ) + empty( $_POST['ppom']['ppom_option_price'] ) ) { $error_message = __( 'Sorry, an error has occurred. Please enable JavaScript or contact site owner.', 'woocommerce-product-addon' ); ppom_wc_add_notice( $error_message ); @@ -153,6 +200,14 @@ function ppom_woocommerce_validate_product( $passed, $product_id, $qty ) { return $passed; } +/** + * Validates PPOM fields through the AJAX validation endpoint. + * + * @return void + * + * @see ppom_check_validation() + * @see ppom_woocommerce_validate_product() + */ function ppom_woocommerce_ajax_validate() { // ppom_pa($_POST); exit; @@ -160,7 +215,7 @@ function ppom_woocommerce_ajax_validate() { $validate_nonce_action = 'ppom_validating_action'; if ( ! wp_verify_nonce( $ppom_nonce, $validate_nonce_action ) ) { - $message = sprintf( '', __( 'Error while validating, try again', 'woocommerce-product-addon' ) ); + $message = sprintf( '', __( 'Error while validating, try again', 'woocommerce-product-addon' ) ); $response = array( 'status' => 'error', 'message' => $message, @@ -206,6 +261,22 @@ function ppom_woocommerce_ajax_validate() { wp_send_json( $response ); } +/** + * Validates posted PPOM fields against the product field schema. + * + * Reads posted values from `ppom[fields]`, skips hidden fields, and adds + * WooCommerce notices for failed requirements. + * + * @param int $product_id Product ID. + * @param array $post_data Posted request payload. + * @param bool $passed Validation state from earlier checks. + * + * @return bool + * + * @see PPOM_Meta::get_fields() + * @see ppom_has_posted_field_value() + * @see ppom_woocommerce_add_cart_item_data() + */ function ppom_check_validation( $product_id, $post_data, $passed = true ) { $ppom = new PPOM_Meta( $product_id ); @@ -281,6 +352,23 @@ function ppom_check_validation( $product_id, $post_data, $passed = true ) { } +// Cart data and session pricing. + +/** + * Stores posted PPOM data on the WooCommerce cart item. + * + * The posted payload remains untrusted and is revalidated and repriced later in + * the cart and checkout lifecycle. + * + * @param array $cart Cart item data. + * @param int $product_id Product ID. + * + * @return array + * + * @see ppom_check_validation() + * @see ppom_price_controller() + * @see ppom_make_meta_data() + */ function ppom_woocommerce_add_cart_item_data( $cart, $product_id ) { if ( ! isset( $_POST['ppom'] ) ) { @@ -292,7 +380,9 @@ function ppom_woocommerce_add_cart_item_data( $cart, $product_id ) { return $cart; } - WC()->cart->remove_cart_item( $_POST['ppom_cart_key'] ); + if ( ! empty( $_POST['ppom_cart_key'] ) && WC()->cart ) { + WC()->cart->remove_cart_item( sanitize_key( $_POST['ppom_cart_key'] ) ); + } // ADDED WC BUNDLES COMPATIBILITY if ( function_exists( 'wc_pb_is_bundled_cart_item' ) && wc_pb_is_bundled_cart_item( $cart ) ) { @@ -328,7 +418,7 @@ function ppom_woocommerce_update_cart_fees( $cart_items, $values ) { // converting back to org price if Currency Switcher is used $ppom_item_org_price = ppom_hooks_convert_price_back( $wc_product->get_price() ); - // $ppom_item_org_price = $wc_product->get_price(); + // $ppom_item_org_price = $wc_product->get_price(); $ppom_item_order_qty = floatval( $cart_items['quantity'] ); @@ -441,7 +531,7 @@ function ppom_woocommerce_update_cart_fees( $cart_items, $values ) { case 'measure': $measer_qty = isset( $option['qty'] ) ? intval( $option['qty'] ) : 0; $price_multiplier = isset( $option['price_multiplier'] ) ? floatval( $option['price_multiplier'] ) : 1; - $option_price = $option['price']; + $option_price = $option['price']; $ppomm_measures *= $measer_qty * $price_multiplier; @@ -591,7 +681,7 @@ function ppom_woocommerce_add_fixed_fee( $cart ) { if ( $fee_price != 0 ) { $cart->add_fee( esc_html( $label ), $fee_price, $taxable ); - $fee_no ++; + ++$fee_no; } } } @@ -777,7 +867,7 @@ function ppom_woocommerce_alter_price( $price, $product ) { function ppom_hide_variation_price_html($show, $parent, $variation) { $product_id = $parent->get_id(); - $ppom = new PPOM_Meta( $product_id ); + $ppom = new PPOM_Meta( $product_id ); if( $ppom->is_exists && $ppom->price_display != 'hide' ) { $show = false; @@ -955,7 +1045,7 @@ function ppom_woocommerce_control_cart_quantity_legacy( $quantity, $cart_item_ke // ppom_pa($cart_item) if ( ! isset( $cart_item['ppom']['ppom_option_price'] ) && - ! isset( $cart_item['ppom']['ppom_pricematrix'] ) ) { + ! isset( $cart_item['ppom']['ppom_pricematrix'] ) ) { return $quantity; } @@ -1178,6 +1268,25 @@ function ppom_woocommerce_cart_update_validate( $cart_validated, $cart_item_key, } +// Order persistence. + +/** + * Stores PPOM metadata on an order line item. + * + * Saves formatted display values as line-item meta and preserves the raw PPOM + * payload in `_ppom_fields` for later formatting and replay. + * + * @param WC_Order_Item_Product $item Order line item. + * @param string $cart_item_key Cart item key. + * @param array $values Cart item values. + * @param WC_Order $order Order being created. + * + * @return void + * + * @see ppom_make_meta_data() + * @see ppom_get_field_meta_by_dataname() + * @see ppom_woocommerce_order_value() + */ function ppom_woocommerce_order_item_meta( $item, $cart_item_key, $values, $order ) { if ( ! isset( $values ['ppom']['fields'] ) ) { @@ -1191,7 +1300,7 @@ function ppom_woocommerce_order_item_meta( $item, $cart_item_key, $values, $orde $ppom_meta = ppom_make_meta_data( $values, 'order' ); // ppom_pa($item->get_product_id()); exit; - $cropper_fields = []; + $cropper_fields = array(); foreach ( $ppom_meta as $key => $meta ) { if ( ! isset( $meta['value'] ) ) { @@ -1234,6 +1343,19 @@ function ppom_woocommerce_order_key( $display_key, $meta, $item ) { return $display_key; } +/** + * Formats PPOM order item meta for display. + * + * @param mixed $display_value Formatted value from WooCommerce. + * @param object|null $meta Order item meta object. + * @param WC_Order_Item|null $item Order item. + * + * @return mixed + * + * @see ppom_generate_html_for_files() + * @see ppom_get_field_meta_by_dataname() + * @see ppom_woocommerce_order_item_meta() + */ function ppom_woocommerce_order_value( $display_value, $meta = null, $item = null ) { if ( is_null( $item ) ) { @@ -1298,7 +1420,24 @@ function ppom_woocommerce_hide_order_meta( $formatted_meta, $order_item ) { return $formatted_meta; } -// When order paid update filename with order number +// Upload finalization. + +/** + * Moves uploaded PPOM files into the confirmed order directory. + * + * File names and destination paths are resolved on the server from the cart + * payload and the saved field schema. + * + * @param int $order_id Order ID. + * @param mixed $posted_data Posted checkout data. + * @param WC_Order $order Processed order. + * + * @return void + * + * @see ppom_get_dir_path() + * @see ppom_get_field_meta_by_dataname() + * @see ppom_get_file_download_url() + */ function ppom_woocommerce_rename_files( $order_id, $posted_data, $order ) { global $woocommerce; @@ -1413,15 +1552,15 @@ function ppom_woocommerce_rename_files( $order_id, $posted_data, $order ) { * Responsible from the adding a support for Order Again functionality in the WooCommerce My Account -> Order View page. * The method adds PPOM Fields to the given order item from the provided order. (Clones the PPOM data of data order item to the new cart) * - * @param array $cart_item_data Current custom item data. + * @param array $cart_item_data Current custom item data. * @param \WC_Order_Item_Product $item Order Item Product - * @param \WC_Order $order + * @param \WC_Order $order * @return void */ function ppom_wc_order_again_compatibility( $cart_item_data, $item, $order ) { - $ppom_data = $item->get_meta('_ppom_fields'); + $ppom_data = $item->get_meta( '_ppom_fields' ); - if( is_array($ppom_data) && array_key_exists( 'fields', $ppom_data ) ) { + if ( is_array( $ppom_data ) && array_key_exists( 'fields', $ppom_data ) ) { $cart_item_data['ppom'] = $ppom_data; } @@ -1431,15 +1570,16 @@ function ppom_wc_order_again_compatibility( $cart_item_data, $item, $order ) { /** * Outputs the formatted meta data for WooCommerce order items. * - * @param int $item_id The item ID. + * @param int $item_id The item ID. * @param \WC_Order_Item_Product $item The order item object. */ function ppom_woocommerce_order_item_meta_html( $item_id, $item ) { $formatted_meta = $item->get_formatted_meta_data(); - $strings = array(); + $strings = array(); $meta_item_html = ''; - $output_args = apply_filters( 'ppom_woocommerce_item_meta_args', + $output_args = apply_filters( + 'ppom_woocommerce_item_meta_args', array( 'before' => '
  • ', 'after' => '
', @@ -1470,9 +1610,9 @@ function ppom_wc_email_improvements_enabled() { /** * Outputs the formatted meta data for invoice or packing slips. * - * @param string $html HTML of the item meta data + * @param string $html HTML of the item meta data * @param \WC_Order_Item_Product $item The order item object. - * @param array $args arguments for display the html. + * @param array $args arguments for display the html. */ function ppom_invoice_packing_slips_html( $html, $item, $args = array() ) { $strings = array(); @@ -1500,4 +1640,4 @@ function ppom_invoice_packing_slips_html( $html, $item, $args = array() ) { } return $html; -} \ No newline at end of file +} diff --git a/js/admin/ppom-admin.js b/js/admin/ppom-admin.js index 415c2a97..8c486cd5 100644 --- a/js/admin/ppom-admin.js +++ b/js/admin/ppom-admin.js @@ -1,1824 +1,2625 @@ +/** + * Main admin field-builder controller for PPOM field groups. + * + * PHP renders hidden modal/template markup for each field type. This script + * turns those templates into the interactive builder by cloning field models, + * rewriting nested `ppom[...]` input names, refreshing the summary table, and + * keeping option/condition editors aligned with the current field list. + * + * @see populate_conditional_elements + * @see ppom_check_conditions in js/ppom-conditions-v2.js + * @see window.ppomPopup in js/popup.js + */ /* global ppom_vars */ -"use strict"; - -const FIELD_COMPATIBLE_WITH_SELECT_OPTIONS = [ 'select', 'radio', 'switcher', 'image', 'conditional_meta' ]; -const FIELDS_COMPATIBLE_WITH_TEXT = [ 'text', 'textarea', 'date', 'email' ] -const FIELDS_COMPATIBLE_WITH_NUMBERS = [ ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, 'number' ]; +'use strict'; + +const FIELD_COMPATIBLE_WITH_SELECT_OPTIONS = [ + 'select', + 'radio', + 'switcher', + 'image', + 'conditional_meta', +]; +const FIELDS_COMPATIBLE_WITH_TEXT = [ 'text', 'textarea', 'date', 'email' ]; +const FIELDS_COMPATIBLE_WITH_NUMBERS = [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + 'number', +]; const OPERATOR_COMPARISON_VALUE_FIELD_TYPE = { - 'select': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, 'checkbox', 'imageselect'], -} -const COMPARISON_VALUE_CAN_USE_SELECT = [ 'is', 'not', 'greater than', 'less than' ]; -const HIDE_COMPARISON_INPUT_FIELD = ['any', 'empty', 'odd-number', 'even-number']; + select: [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + 'checkbox', + 'imageselect', + ], +}; +const COMPARISON_VALUE_CAN_USE_SELECT = [ + 'is', + 'not', + 'greater than', + 'less than', +]; +const HIDE_COMPARISON_INPUT_FIELD = [ + 'any', + 'empty', + 'odd-number', + 'even-number', +]; const OPERATORS_FIELD_COMPATIBILITY = { - 'is': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, ...FIELDS_COMPATIBLE_WITH_TEXT, ...FIELDS_COMPATIBLE_WITH_NUMBERS, 'checkbox', 'imageselect'], - 'not': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, ...FIELDS_COMPATIBLE_WITH_TEXT, ...FIELDS_COMPATIBLE_WITH_NUMBERS, 'checkbox', 'imageselect'], - 'greater than': FIELDS_COMPATIBLE_WITH_NUMBERS, - 'less than': FIELDS_COMPATIBLE_WITH_NUMBERS, - 'even-number': FIELDS_COMPATIBLE_WITH_NUMBERS, - 'odd-number': FIELDS_COMPATIBLE_WITH_NUMBERS, - 'between': FIELDS_COMPATIBLE_WITH_NUMBERS, - 'number-multiplier': FIELDS_COMPATIBLE_WITH_NUMBERS, - 'any': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, ...FIELDS_COMPATIBLE_WITH_TEXT, ...FIELDS_COMPATIBLE_WITH_NUMBERS], - 'empty': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, ...FIELDS_COMPATIBLE_WITH_TEXT, ...FIELDS_COMPATIBLE_WITH_NUMBERS], - 'contains': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, ...FIELDS_COMPATIBLE_WITH_TEXT], - 'not-contains': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, ...FIELDS_COMPATIBLE_WITH_TEXT], - 'regex': [...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, ...FIELDS_COMPATIBLE_WITH_TEXT] -} + is: [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + ...FIELDS_COMPATIBLE_WITH_TEXT, + ...FIELDS_COMPATIBLE_WITH_NUMBERS, + 'checkbox', + 'imageselect', + ], + not: [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + ...FIELDS_COMPATIBLE_WITH_TEXT, + ...FIELDS_COMPATIBLE_WITH_NUMBERS, + 'checkbox', + 'imageselect', + ], + 'greater than': FIELDS_COMPATIBLE_WITH_NUMBERS, + 'less than': FIELDS_COMPATIBLE_WITH_NUMBERS, + 'even-number': FIELDS_COMPATIBLE_WITH_NUMBERS, + 'odd-number': FIELDS_COMPATIBLE_WITH_NUMBERS, + between: FIELDS_COMPATIBLE_WITH_NUMBERS, + 'number-multiplier': FIELDS_COMPATIBLE_WITH_NUMBERS, + any: [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + ...FIELDS_COMPATIBLE_WITH_TEXT, + ...FIELDS_COMPATIBLE_WITH_NUMBERS, + ], + empty: [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + ...FIELDS_COMPATIBLE_WITH_TEXT, + ...FIELDS_COMPATIBLE_WITH_NUMBERS, + ], + contains: [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + ...FIELDS_COMPATIBLE_WITH_TEXT, + ], + 'not-contains': [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + ...FIELDS_COMPATIBLE_WITH_TEXT, + ], + regex: [ + ...FIELD_COMPATIBLE_WITH_SELECT_OPTIONS, + ...FIELDS_COMPATIBLE_WITH_TEXT, + ], +}; const proOperatorOptionsToLock = new Set(); /** - * An array to store available condition targets. - * - * @type {Array<{fieldLabel?: string, fieldId?: string, fieldType?: string, canUse: boolean}>} + * Minimal condition-target description derived from the current field models. + * + * @typedef {Object} PPOMConditionTarget + * @property {string} fieldLabel + * @property {string} fieldId + * @property {string} fieldType + * @property {boolean} canUse */ + +/** @type {PPOMConditionTarget[]} */ const availableConditionTargets = []; -jQuery(function($) { - - var loader = new ImageLoader(ppom_vars.loader); - // define your 'onreadystatechange' - loader.loadEvent = function(url, imageAsDom) { - - $("#ppom-pre-loading").hide(); - $(".ppom-admin-wrap").show(); - } - loader.load(); - - /********************************* - * PPOM Form Design JS * - **********************************/ - - - /*------------------------------------------------------- - - ------ Its Include Following Function ----- - - 1- Submit PPOM Form Fields - 2- Hide And Show Import & Export & Product Meta blocks - 3- Get Last Field Index - 4- Show And Hide Visibility Role Field - 5- Remove Unsaved Fields - 6- Check And Uncheck All Fields - 7- Remove Check Fields - 8- On Fields Options Handle Add Option Last - 9- Edit Existing Fields - 10- Add New Fields - 11- Update Existing Fields - 12- Clone New Fields - 13- Clone Existing Fields - 14- Saving PPOM IDs In Existing Meta File - 15- Open Product Modal In Existing Meta File (removed) - 16- Handle Fields Tabs - 17- Handle Media Images Of Following Inputs Types - 18- Add Fields Conditions - 19- Add Fields Options - 20- Auto Generate Option IDs - 21- Create Field data_name By Thier Title - 22- Fields Sortable - 23- Fields Option Sortable - 24- Fields Dataname Must Be Required - 25- Fields Add Option Index Controle Funtion - 26- Fields Add Condition Index Controle Function - 27- Get All Fields Title On Condition Element Value After Click On Condition Tab - 28- validate API WooCommerce Product - ------------------------------------------------------------*/ - - - /** - PPOM Model - **/ - var append_overly_model = ("
"); - - $(document).on('click', '[data-modal-id]:not(.ppom-is-pro-field)', function(e) { - e.preventDefault(); - $("body").append(append_overly_model); - var modalBox = $(this).attr('data-modal-id'); - lockedDataName = false; - $('#' + modalBox).fadeIn(); - }); - - ppom_close_popup(); - - function ppom_close_popup() { - - $(".ppom-js-modal-close, .ppom-modal-overlay").click(function(e) { - - var target = $(e.target); - if (target.hasClass("ppom-modal-overlay")) { - return false; - } - $(".ppom-modal-box, .ppom-modal-overlay").fadeOut('fast', function() { - $(".ppom-modal-overlay").remove(); - }); - - }); - } - - - $('.ppom-color-picker-init').wpColorPicker(); - - - /** - 1- Submit PPOM Form Fields - **/ - $(".ppom-save-fields-meta").on('submit', function(e) { - e.preventDefault(); - - jQuery(".ppom-meta-save-notice").html('').show(); - - $('.ppom-unsave-data').remove(); - - const formData = new FormData(); - const ppomFields = new URLSearchParams(); - - /* - NOTE: since the request is to big for small values of `max_input_vars`, we will send the PPOM fields as a single string. - - INFO: some parts of the code use `\r\n` as delimiter for arrays in textarea. `serializeArray` respect this convention while native JS Form value access sanitize it to just `\n`. - */ - $( this ).serializeArray().forEach(({ value, name }) => { - if ( name.startsWith('ppom[') && typeof value === 'string' ) { - ppomFields.append( name, value ); - } else { - formData.append(name, value); - } - }); - - formData.append('ppom', ppomFields.toString()); - - fetch(ajaxurl, { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(resp => { - const bg_color = resp.status == 'success' ? '#4e694859' : '#ee8b94'; - jQuery(".ppom-meta-save-notice").html(resp.message).css({ 'background-color': bg_color, 'padding': '8px', 'border-left': '5px solid #008c00' }); - if (resp.status == 'success') { - if (resp.redirect_to != '') { - window.location = resp.redirect_to; - } else { - window.location.reload(); - } - } - }) - .catch(() => { - jQuery(".ppom-meta-save-notice") - .html( ppom_vars.i18n.errorOccurred ) - .css({ 'background-color': '#ee8b94', 'padding': '8px', 'border-left': '5px solid #c00' }); - }); - }); - - - /** - 2- Hide And Show Import & Export & Product Meta blocks - **/ - $('.ppom-import-export-btn').on('click', function(event) { - event.preventDefault(); - if ( $(".ppom-import-export-block").length === 0 ) { - $('#ppom-import-upsell').fadeIn(); - return; - } - $('.ppom-more-plugins-block').hide(); - $(".ppom-import-export-block").show(); - $(".ppom-product-meta-block").hide(); - }); - - $('.ppom-cancle-import-export-btn').on('click', function(event) { - event.preventDefault(); - $('.ppom-more-plugins-block').show(); - $(".ppom-import-export-block").hide(); - $(".ppom-product-meta-block").show(); - }); - - - /** - 3- Get Last Field Index - **/ - var field_no = $('#field_index').val(); - - - /** - 4- Show And Hide Visibility Role Field - **/ - $('.ppom-slider').find('[data-meta-id="visibility_role"]').hide(); - $('.ppom_save_fields_model .ppom-slider').each(function(i, div) { - var visibility_value = $(div).find('[data-meta-id="visibility"] select').val(); - if (visibility_value == 'roles') { - $(div).find('[data-meta-id="visibility_role"]').show(); - } - }); - $(document).on('change', '[data-meta-id="visibility"] select', function(e) { - e.preventDefault(); - - var div = $(this).closest('.ppom-slider'); - var visibility_value = $(this).val(); - // console.log(visibility_value); - if (visibility_value == 'roles') { - div.find('[data-meta-id="visibility_role"]').show(); - } - else { - div.find('[data-meta-id="visibility_role"]').hide(); - } - }); - - - /** - 5- Remove Unsaved Fields - **/ - $(document).on('click', '.ppom-close-fields', function(event) { - event.preventDefault(); - - $(this).closest('.ppom-slider').addClass('ppom-unsave-data'); - }); - - - /** - 6- Check And Uncheck All Fields - **/ - $('.ppom-main-field-wrapper').on('change', '.onoffswitch-checkbox', function(event) { - var div = $(this).closest('div'); - if ($(this).prop('checked')) { - div.find('input[type="hidden"]').val('on'); - } - else { - div.find('input[type="hidden"]').val('off'); - } - }); - - $('.ppom-main-field-wrapper').on('click', '.ppom-check-all-field input', function(event) { - if ($(this).prop('checked')) { - $('.ppom_field_table .ppom-checkboxe-style input[type="checkbox"]').prop('checked', true); - } - else { - $('.ppom_field_table .ppom-checkboxe-style input[type="checkbox"]').prop('checked', false); - } - }); - $('.ppom-main-field-wrapper').on('change', '.ppom_field_table tbody .ppom-checkboxe-style input[type="checkbox"]', function(event) { - if ($('.ppom_field_table tbody .ppom-checkboxe-style input[type="checkbox"]:checked').length == $('.ppom_field_table tbody .ppom-checkboxe-style input[type="checkbox"]').length) { - $('.ppom-check-all-field input').prop('checked', true); - } - else { - $('.ppom-check-all-field input').prop('checked', false); - } - }); - - - /** - 7- Remove Check Fields - **/ - $('.ppom-main-field-wrapper').on('click', '.ppom_remove_field', function(e) { - e.preventDefault(); - - const inputsToRemove = document.querySelectorAll('.ppom_field_table .ppom-check-one-field input:checked'); - - if ( inputsToRemove.length > 0 ) { - window?.ppomPopup?.open({ - title: window?.ppom_vars?.i18n.popup.confirmTitle, - onConfirmation: () => { - inputsToRemove.forEach(( meta_field ) => { - const field_id = meta_field.value; - meta_field.closest(`.row_no_${field_id}`)?.remove(); - document.querySelector(`#ppom_field_model_${field_id}`)?.remove(); - }); - } - }) - } - else { - window?.ppomPopup?.open({ - title: window.ppom_vars.i18n.popup.checkFieldTitle, - type: "error", - hideCloseBtn: true - }) - } - }); - - /** - 9- Edit Existing Fields - **/ - var lockedDataName = false; - $(document).on('click', '.ppom-edit-field:not(.ppom-is-pro-field)', function(event) { - event.preventDefault(); - - var the_id = $(this).attr('id'); - $('#ppom_field_model_' + the_id).find('.ppom-close-checker').removeClass('ppom-close-fields'); - lockedDataName = true; - }); - - - /** - 10- Add New Fields - **/ - $(document).on('click', '.ppom-add-field', function(event) { - event.preventDefault(); - - var $this = $(this); - var ui = ppom_required_data_name($this, 'new'); - if (ui == false) { - return; - } - - var copy_model_id = $(this).attr('data-copy-model-id'); - var id = $(this).attr('data-field-index'); - id = Number(id); - // console.log(id); - - var field_title = $('#ppom_field_model_' + id + '').find('.ppom-modal-body .ppom-fields-actions').attr('data-table-id'); - var data_name = $('#ppom_field_model_' + id + '').find('[data-meta-id="data_name"] input').val(); - var title = $('#ppom_field_model_' + id + '').find('[data-meta-id="title"] input').val(); - var placeholder = $('#ppom_field_model_' + id + '').find('[data-meta-id="placeholder"] input').val(); - var required = $('#ppom_field_model_' + id + '').find('[data-meta-id="required"] input').prop('checked'); - var type = $(this).attr('data-field-type'); - - // console.log(field_title); - - if (required == true) { - var _ok = ppom_vars.i18n.yes; - } - else { - _ok = ppom_vars.i18n.no; - } - if (placeholder == null) { - placeholder = '-'; - } - - var html = '
'; - html += ''; - html += ''; - - html += ''; - - // html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html += ''; - html = $.parseHTML(html); - // console.log(copy_model_id); - if (copy_model_id != '' && copy_model_id != undefined) { - $(html).find('.ppom_field_table tbody').end().insertAfter('#ppom_sort_id_' + copy_model_id + ''); - } - else { - $(html).appendTo('.ppom_field_table tbody'); - } - - $(".ppom-modal-box, .ppom-modal-overlay").fadeOut('fast', function() { - $(".ppom-modal-overlay").remove(); - }); - - $(this).removeClass('ppom-add-field').addClass('ppom-update-field'); - $(this).html( ppom_vars.i18n.updatedField ); - - }); - - - /** - 11- Update Existing Fields - **/ - $(document).on('click', '.ppom-update-field', function(event) { - event.preventDefault(); - - var id = $(this).attr('data-field-index'); - id = Number(id); - - var $this = $(this); - var ui = ppom_required_data_name($this, 'update'); - - if (ui == false) { - return; - } - - var data_name = $('#ppom_field_model_' + id + '').find('[data-meta-id="data_name"] input').val(); - var title = $('#ppom_field_model_' + id + '').find('[data-meta-id="title"] input').val(); - var placeholder = $('#ppom_field_model_' + id + '').find('[data-meta-id="placeholder"] input').val(); - var required = $('#ppom_field_model_' + id + '').find('[data-meta-id="required"] input').prop('checked'); - var type = $(this).attr('data-field-type'); - - if (required == true) { - var _ok = ppom_vars.i18n.yes; - } - else { - _ok = ppom_vars.i18n.no; - } - - var row = $('.ppom_field_table tbody').find('.row_no_' + id); - - row.find(".ppom_meta_field_title").text(title); - row.find(".ppom_meta_field_id").text(data_name); - row.find(".ppom_meta_field_type").text(type); - row.find(".ppom_meta_field_plchlder").text(placeholder); - row.find(".ppom_meta_field_req").text(_ok); - - $(".ppom-modal-box, .ppom-modal-overlay").fadeOut('fast', function() { - $(".ppom-modal-overlay").remove(); - }); - }); - - - /** - 12- Clone New Fields - **/ - var option_index = 0; - $(document).on('click', '.ppom_select_field', function(event) { - if( $(this).hasClass('ppom-locked-field') ) { - return; - } - - event.preventDefault(); - - $('#ppom_fields_model_id').find('.ppom-js-modal-close').trigger('click'); - - var field_type = $(this).data('field-type'); - var clone_new_field = $(".ppom-field-" + field_type + ":last").clone(); - - // field attr name apply on all fields meta with ppom-meta-field class - clone_new_field.find('.ppom-meta-field').each(function(i, meta_field) { - var field_name = 'ppom[' + field_no + '][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - - // fields options sortable - clone_new_field.find(".ppom-options-sortable").sortable(); - - // add fields index in data-field-no - clone_new_field.find(".ppom-fields-actions").attr('data-field-no', field_no); - - // fields conditions handle name attr - clone_new_field.find('.ppom-condition-visible-bound').each(function(i, meta_field) { - var field_name = 'ppom[' + field_no + '][conditions][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - - clone_new_field.find('.ppom-fields-actions [data-meta-id="visibility_role"]').hide(); - - var field_model_id = 'ppom_field_model_' + field_no + ''; - - clone_new_field.find('.ppom_save_fields_model') - .end() - .appendTo('.ppom_save_fields_model') - .attr('id', field_model_id) - .find( '.ppom-tabs-header label.ppom-tabs-label' ) - .each( function( index, item ) { - var tabId = $(item).attr('id'); - var hasTabOptions = $( '#' + field_model_id, document )?.find( '.ppom_handle_' + tabId )?.length > 0; - if ( ! hasTabOptions ) { - $(item).hide(); - } - } ); - clone_new_field.find('.ppom-field-checker').attr('data-field-index', field_no); - clone_new_field.find('.ppom-field-checker').addClass('ppom-add-fields-js-action'); - - // var color_picker_input = clone_new_field.find('.ppom-color-picker-init').clone(); - // clone_new_field.find('.ppom-color-picker-cloner').html(color_picker_input); - // clone_new_field.find('.ppom-color-picker-init').wpColorPicker(); - // $('.ppom-color-picker-init').wpColorPicker(); - // $('.ppom-color-picker-init').wpColorPicker(); - - // $('.ppom-color-picker-init').wpColorPicker('destroy'); - - - clone_new_field.addClass('ppom_sort_id_' + field_no + ''); - var field_index = field_no; - - // handle multiple options - var ppom_option_type = ''; - var option_selector = clone_new_field.find('.ppom-option-keys'); - var option_controller = clone_new_field.find('.ppom-fields-option'); - var add_cond_selector = clone_new_field.find('.ppom-conditional-keys'); - - // for address addon - var address_selector = clone_new_field.find('.ppom-checkout-field'); - var address_table_id = clone_new_field.find('.ppom_address_table'); - ppom_create_address_index(address_selector, field_index, address_table_id); - - var wpcolor_selector = clone_new_field.find('.ppom-color-picker-cloner'); - ppom_wp_color_handler(wpcolor_selector, field_index, option_index); - - ppom_create_option_index(option_selector, field_index, option_index, ppom_option_type); - ppom_option_controller(option_controller, field_index, option_index, ppom_option_type); - ppom_add_condition_set_index(add_cond_selector, field_index, field_type, option_index); - - // popup fields on model - ppom_close_popup(); - $('#ppom_field_model_' + field_no + '').fadeIn(); - - $( document ).trigger( 'ppom_new_field_created', [ clone_new_field, field_no, field_type ] ); - - field_no++; - }); - - - /** - 13- Clone Existing Fields - **/ - var copy_no = 0; - $('.ppom-main-field-wrapper').on('click', '.ppom_copy_field:not(.ppom-is-pro-field)', function(e) { - e.preventDefault(); - - var model_id_no = $(this).attr('id'); - - var field_type = $(this).data('field-type'); - // console.log(model_id_no); - - var clone_new_field = $('.ppom_save_fields_model #ppom_field_model_' + model_id_no + '').clone(true); - var dataTitleField = clone_new_field.find( '[data-metatype="title"]' ); - var dataNameField = clone_new_field.find( '[data-metatype="data_name"]' ); - var dataNameOldVal = dataNameField.val(); - var duplicateFields = jQuery(document).find('.ppom_field_table .ppom_meta_field_title:contains(' + dataTitleField.val() + ')'); - var dataNameNewVal = ''; - var reg = '/_copy_/'; - if ( duplicateFields && duplicateFields.length >= 1 ) { - dataNameOldVal = dataNameOldVal.split( '_copy_' ).shift(); - dataNameNewVal = dataNameOldVal + '_copy_' + duplicateFields.length++; - } else { - dataNameNewVal = dataNameOldVal + '_copy'; - } - dataNameField.val( dataNameNewVal ); - // clone_new_field.find('.ppom_save_fields_model').end().appendTo('.ppom_save_fields_model').attr('id','ppom_field_model_'+field_no+''); - clone_new_field.find('.ppom_save_fields_model').end().insertAfter('#ppom_field_model_' + model_id_no + '').attr('id', 'ppom_field_model_' + field_no + ''); - clone_new_field.find('.ppom-add-fields-js-action').attr('data-field-index', field_no); - clone_new_field.find('.ppom-close-fields').attr('data-field-index', field_no); - clone_new_field.find('.ppom-js-modal-close').addClass('ppom-close-fields'); - clone_new_field.find('.ppom-add-fields-js-action').removeClass('ppom-update-field'); - clone_new_field.find('.ppom-add-fields-js-action').attr('data-copy-model-id', model_id_no); - clone_new_field.find('.ppom-add-fields-js-action').addClass('ppom-add-field'); - clone_new_field.find('.ppom-add-fields-js-action').addClass('ppom-insertafter-field'); - clone_new_field.find('.ppom-add-fields-js-action').html('Add Field'); - clone_new_field.removeClass('ppom_sort_id_' + model_id_no + ''); - clone_new_field.addClass('ppom_sort_id_' + field_no + ''); - - // field attr name apply on all fields meta with ppom-meta-field class - clone_new_field.find('.ppom-meta-field').each(function(i, meta_field) { - var field_name = 'ppom[' + field_no + '][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - - // fields options sortable - clone_new_field.find(".ppom-options-sortable").sortable(); - - // add fields index in data-field-no - clone_new_field.find(".ppom-fields-actions").attr('data-field-no', field_no); - - // fields conditions handle name attr - clone_new_field.find('.ppom-condition-visible-bound').each(function(i, meta_field) { - var field_name = 'ppom[' + field_no + '][conditions][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - - clone_new_field.find('.ppom-fields-actions [data-meta-id="visibility_role"]').hide(); - - - var field_index = field_no; - - // handle multiple options - var ppom_option_type = 'ppom_copy_option'; - var option_selector = clone_new_field.find('.ppom-option-keys'); - var eventcalendar_selector = clone_new_field.find('.ppom-eventcalendar-field'); - var image_option_selector = clone_new_field.find('[data-table-id="image"] .data-options, [data-table-id="imageselect"] .data-options'); - - // reset option to one - // clone_new_field.find('[data-table-id="image"] .data-options').remove(); - clone_new_field.find('[data-table-id="audio"] .pre-upload-box li').remove(); - // clone_new_field.find('[data-table-id="imageselect"] .pre-upload-box li').remove(); - // clone_new_field.find('.data-options').not(':last').remove(); - - // set existing conditions meta - $(clone_new_field).find('select[data-metatype="elements"]').each(function(i, condition_element) { - - var existing_value1 = $(condition_element).attr("data-existingvalue"); - if ($.trim(existing_value1) !== '') { - jQuery(condition_element).val(existing_value1); - } - }); - $(clone_new_field).find('select[data-metatype="element_values"]').each(function(i, condition_element) { - - var div = $(this).closest('.webcontact-rules'); - var existing_value1 = $(condition_element).attr("data-existingvalue"); - - if ($.trim(existing_value1) !== '') { - jQuery(condition_element).val(existing_value1); - } - }); - - var wpcolor_selector = clone_new_field.find('.ppom-color-picker-cloner'); - ppom_wp_color_handler(wpcolor_selector, field_index, option_index); - - ppom_create_option_index(option_selector, field_index, option_index, ppom_option_type); - var option_controller = clone_new_field.find('.ppom-fields-option'); - - ppom_option_controller(option_controller, field_index, option_index, ppom_option_type); - clone_new_field.find('.webcontact-rules').each(function(index, rule_row) { - const rule_inputs = $(rule_row).find('.ppom-conditional-keys'); - ppom_add_condition_set_index(rule_inputs, field_index, field_type, index); - }); - - // for eventcalendar changing index - ppom_eventcalendar_set_index(eventcalendar_selector, field_index); - - // set index for all images fields - image_option_selector.find('input').each(function(img_index, img_meta) { - - var opt_in = $(img_meta).attr('data-opt-index'); - var field_name = 'ppom[' + field_index + '][images][' + opt_in + '][' + $(img_meta).attr('data-metatype') + ']'; - $(img_meta).attr('name', field_name); - }); - - // popup fields on model - $("body").append(append_overly_model); - ppom_close_popup(); - $('#ppom_field_model_' + field_no + '').fadeIn(); - - field_no++; - }); - - - /** - 14- Saving PPOM IDs In Existing Meta File - **/ - - /** - * @type {HTMLFormElement} - */ - const popupForm = document.querySelector('#ppom-product-form'); - popupForm?.addEventListener('submit', (e) => { - e.preventDefault(); - - const formContent = new FormData( document.querySelector('#ppom-product-form') ); - - formContent.append( 'action','ppom_attach_ppoms' ); - formContent.append( 'ppom_id', $("#ppom_id").val() ); - - fetch( ajaxurl, { - method: 'POST', - body: formContent - }) - .then(response => response.json()) - .then(resp => { - alert(resp.message); - - const openModalBtn = document.querySelector('.ppom-products-modal'); - if ( openModalBtn && openModalBtn.dataset?.reload ) { - window.location.reload(); - } else { - document.querySelector('.ppom-js-modal-close').dispatchEvent(new Event('click')); - } - }) - .catch(error => console.error('Error:', error)); - }) - - /** - 16- Handle Fields Tabs - **/ - $('.ppom-control-all-fields-tabs').hide(); - $('.ppom_handle_fields_tab').show(); - $(document).on('click', '.ppom-tabs-label', function() { - - var id = $(this).attr('id'); - var selectedTab = $(this).parent(); - var fields_wrap = selectedTab.parent(); - selectedTab.find('.ppom-tabs-label').removeClass('ppom-active-tab'); - $(this).addClass('ppom-active-tab'); - var content_box = fields_wrap.find('.ppom-control-all-fields-tabs'); - content_box.hide(); - - const handler = fields_wrap.find('.ppom_handle_' + id); - handler.fadeIn(200); - - $(fields_wrap).trigger('ppom_fields_tab_changed', [id, handler]); - }); - - - /** - 17- Handle Media Images Of Following Inputs Types - 17.1- Pre-Images Type - 17.2- Audio Type - 17.3- Imageselect Type - **/ - var $uploaded_image_container; - $(document).on('click', '.ppom-pre-upload-image-btn', function(e) { - e.preventDefault(); - - var meta_type = $(this).attr('data-metatype'); - $uploaded_image_container = $(this).closest('div'); - var image_append = $uploaded_image_container.find('ul'); - var option_index = parseInt($uploaded_image_container.find('#ppom-meta-opt-index').val()); - var main_wrapper = $(this).closest('.ppom-slider'); - var field_index = main_wrapper.find('.ppom-fields-actions').attr('data-field-no'); - var price_placeholder = ppom_vars.i18n.pricePlaceholder; - - var wp_media_type = 'image'; - if (meta_type == 'audio') { - wp_media_type = 'audio,video'; - } - - var button = $(this), - custom_uploader = wp.media({ - title: ppom_vars.i18n.choseFile, - library: { - type: wp_media_type - }, - button: { - text: ppom_vars.i18n.upload - }, - multiple: true - }).on('select', function() { - - var attachments = custom_uploader.state().get('selection').toJSON(); - - attachments.map((meta, index) => { - // console.log(meta); - var fileurl = meta.url; - var fileid = meta.id; - var filename = meta.filename; - var file_title = meta.title; - - - var img_icon = ''; - - var price_metatype = 'price'; - var stock_metatype = 'stock'; - var stock_placeholder = ppom_vars.i18n.stock; - let url_field = ''; - - // Set name key for imageselect addon - if (meta_type == 'imageselect') { - var class_name = 'data-options ui-sortable-handle'; - var condidtion_attr = 'image_options'; - meta_type = 'images'; - price_placeholder = 'Price'; - url_field = ''; - } - else if (meta_type == 'images') { - var class_name = 'data-options ui-sortable-handle'; - var condidtion_attr = 'image_options'; - price_metatype = 'meta_id'; - } - else if (meta_type == 'conditional_meta') { - meta_type = 'images'; - var class_name = 'data-options ui-sortable-handle'; - var condidtion_attr = 'image_options'; - price_placeholder = ppom_vars.i18n.metaIds; - price_metatype = 'meta_id'; - url_field = ''; - } - else { - var class_name = ''; - var condidtion_attr = ''; - } - - if (meta.type !== 'image') { - img_icon = ''; - url_field = ''; - } - - if (fileurl) { - var image_box = ''; - image_box += '
  • '; - image_box += ''; - image_box += ''; - image_box += '
    '; - image_box += '
    '; - image_box += img_icon; - image_box += '
    '; - image_box += ''; - image_box += ''; - image_box += ''; - image_box += ''; - - if (meta_type != 'audio') { - image_box += ''; - } - - image_box += url_field; - image_box += ''; - image_box += '
    '; - image_box += '
  • '; - - $(image_box).appendTo(image_append); - - option_index++; - } - - }); - - $uploaded_image_container.find('#ppom-meta-opt-index').val(option_index); - - }).open(); - }); - - var $uploaded_image_container; - $(document).on('click', '.ppom-pre-upload-image-btnsd', function(e) { - - e.preventDefault(); - var meta_type = $(this).attr('data-metatype'); - $uploaded_image_container = $(this).closest('div'); - var image_append = $uploaded_image_container.find('ul'); - var option_index = parseInt($uploaded_image_container.find('#ppom-meta-opt-index').val()); - $uploaded_image_container.find('#ppom-meta-opt-index').val(option_index + 1); - var main_wrapper = $(this).closest('.ppom-slider'); - var field_index = main_wrapper.find('.ppom-fields-actions').attr('data-field-no'); - var price_placeholder = ppom_vars.i18n.pricePlaceholder; - wp.media.editor.send.attachment = function(props, attachment) { - // console.log(attachment); - var existing_images; - var fileurl = attachment.url; - var fileid = attachment.id; - var img_icon = ''; - - var price_metatype = 'price'; - var stock_metatype = 'stock'; - var stock_placeholder = ppom_vars.i18n.stock; - - // Set name key for imageselect addon - if (meta_type == 'imageselect') { - var class_name = 'data-options ui-sortable-handle'; - var condidtion_attr = 'image_options'; - meta_type = 'images'; - price_placeholder = 'Price'; - url_field = ''; - } - else if (meta_type == 'images') { - var class_name = 'data-options ui-sortable-handle'; - var condidtion_attr = 'image_options'; - price_metatype = 'meta_id'; - } - else if (meta_type == 'conditional_meta') { - meta_type = 'images'; - var class_name = 'data-options ui-sortable-handle'; - var condidtion_attr = 'image_options'; - price_placeholder = ppom_vars.i18n.metaIds; - price_metatype = 'meta_id'; - } - else { - var class_name = ''; - var condidtion_attr = ''; - } - - let url_field = ''; - - if (attachment.type !== 'image') { - img_icon = ''; - url_field = ''; - } - - if (fileurl) { - var image_box = ''; - image_box += '
  • '; - image_box += ''; - image_box += ''; - image_box += '
    '; - image_box += '
    '; - image_box += img_icon; - image_box += '
    '; - image_box += ''; - image_box += ''; - image_box += ''; - image_box += ''; - image_box += ''; - image_box += url_field; - image_box += ''; - image_box += '
    '; - image_box += '
  • '; - - $(image_box).appendTo(image_append); - } - } - - wp.media.editor.open(this); - - return false; - }); - $(document).on('click', '.ppom-pre-upload-delete', function(e) { - - e.preventDefault(); - $(this).closest('li').remove(); - }); - - - /** - 18- Add Fields Conditions - **/ - $(document).on('click', '.ppom-add-rule', function(e) { - - e.preventDefault(); - - var div = $(this).parents('.form-group'); - var option_index = parseInt(div.find('.ppom-condition-last-id').val()); - div.find('.ppom-condition-last-id').val(option_index + 1); - - var field_index = div.parents('.ppom-slider').find('.ppom-fields-actions').attr('data-field-no'); - var cloneElement = $('.webcontact-rules:last', div); - var condition_clone = cloneElement.clone(); - - var append_item = div.find('.ppom-condition-clone-js'); - condition_clone.find(append_item).end().appendTo(append_item); - - ppom_rename_rules_name(); - - var field_type = ''; - var add_cond_selector = condition_clone.find('.ppom-conditional-keys'); - ppom_add_condition_set_index(add_cond_selector, field_index, field_type, option_index); - - if ( $(this).next('.ppom-remove-rule')?.length > 0 ) { - $(this).remove(); - return; - } - $('.ppom-add-rule', cloneElement) - .removeClass('ppom-add-rule').addClass('ppom-remove-rule') - .removeClass('btn-success').addClass('btn-danger') - .html(''); - - // Last row. - var lastRow = jQuery('.webcontact-rules:visible:last'); - if ( lastRow?.find('.ppom-remove-rule')?.length === 0 ) { - var cloneButton = lastRow?.find('.ppom-add-rule').clone(); - cloneButton - .removeClass('ppom-add-rule').addClass('ppom-remove-rule ml-1') - .removeClass('btn-success').addClass('btn-danger') - .html(''); - cloneButton.insertAfter(lastRow?.find('.ppom-add-rule')); - } - - }).on('click', '.ppom-remove-rule', function(e) { - const div = $(this).parents('.form-group'); - const option_index = parseInt(div.find('.ppom-condition-last-id').val()); - div.find('.ppom-condition-last-id').val(option_index - 1); - - var removeButton = $(this ); - if (removeButton.parents('.ppom-condition-clone-js')?.find('.webcontact-rules')?.length === 1) { - removeButton - .parents('.ppom-condition-clone-js') - .find('.webcontact-rules') - .find('select') - .val('') - .attr( 'selected', false ) - .prop( 'selected', false ); - $(this).remove(); - ppom_rename_rules_name(); - return false; - } - removeButton.parents('.webcontact-rules:first').remove(); - // Last row. - var lastRow = jQuery('.webcontact-rules:visible:last'); - if ( lastRow?.find('.ppom-add-rule')?.length === 0 ) { - var cloneButton = lastRow?.find('.ppom-remove-rule').clone(); - cloneButton - .removeClass('ppom-remove-rule').addClass('ppom-add-rule') - .removeClass('btn-danger').addClass('btn-success') - .html(''); - cloneButton.insertBefore(lastRow?.find('.ppom-remove-rule')); - } - ppom_rename_rules_name(); - e.preventDefault(); - return false; - }); - - - /** - 19- Add Fields Options - **/ - $(document).on('click', '.ppom-add-option', function(e) { - - e.preventDefault(); - - var main_wrapper = $(this).closest('.ppom-slider'); - var ppom_option_type = 'ppom_new_option'; - - var li = $(this).closest('li'); - var ul = li.closest('ul'); - var clone_item = li.clone(); - - clone_item.find(ul).end().appendTo(ul); - - var option_index = parseInt(ul.find('#ppom-meta-opt-index').val()); - ul.find('#ppom-meta-opt-index').val(option_index + 1); - // console.log(option_index); - - var field_index = main_wrapper.find('.ppom-fields-actions').attr('data-field-no'); - var option_selector = clone_item.find('.ppom-option-keys'); - var option_controller = clone_item.find('.ppom-fields-option'); - - ppom_option_controller(option_controller, field_index, option_index, ppom_option_type); - ppom_create_option_index(option_selector, field_index, option_index, ppom_option_type); - - // $('.ppom-slider').find('.data-options:not(:last) .ppom-add-option') - // .removeClass('ppom-add-option').addClass('ppom-remove-option') - // .removeClass('btn-success').addClass('btn-danger') - // .html(''); - }).on('click', '.ppom-remove-option', function(e) { - - var selector_btn = $(this).closest('.ppom-slider'); - var option_num = selector_btn.find('.data-options').length; - - if (option_num > 1) { - $(this).parents('.data-options:first').remove(); - } - else { - alert( ppom_vars.i18n.cannotRemoveMoreOption ); - } - - e.preventDefault(); - return false; - }); - - - /** - 20- Auto Generate Option IDs - **/ - $(document).on('keyup', '.option-title', function() { - - var closes_id = $(this).closest('li').find('.option-id'); - var option_id = $(this).val().replace(/[^A-Z0-9]/ig, "_"); - option_id = option_id.toLowerCase(); - $(closes_id).val(option_id); - }); - - - /** - 21- Create Field data_name By Thier Title - **/ - $(document).on('keyup', '[data-meta-id="title"] input[type="text"]', function() { - - /** - * If auto generated data name starts with the "_", that causes the order item meta is recognized as - * "hidden" in {prefix}_woocommerce_order_ittemmeta table order_by WC Core. To prevent that, add "f" char - * to beginning of the dataname as auto. - * - * With the prefix, all fields that will be created from now on; will be shown on order thank you page anymore. - * - * "f" is a randomly chosen character. - */ - const START_CHAR_DISALLOW_BEING_HIDDEN_FIELD = 'f'; - - var $this = $(this); - var field_id = $this.val().toLowerCase().replace(/[^A-Za-z\d]/g, '_'); - - field_id = field_id.charAt(0) === '_' ? `${START_CHAR_DISALLOW_BEING_HIDDEN_FIELD}${field_id}` : field_id; - - var selector = $this.closest('.ppom-slider'); - - var wp_field = selector.find('.ppom-fields-actions').attr('data-table-id'); - if (wp_field == 'shipping_fields' || wp_field == 'billing_fields') { - return; - } - if ( true === lockedDataName ) { - return; - } - selector.find('[data-meta-id="data_name"] input[type="text"]:not([readonly])').val(field_id); - }); - - - /** - 22- Fields Sortable - **/ - function insertAt(parent, element, index, dir) { - var el = parent.children().eq(index); - - element[dir == 'top' ? 'insertBefore' : 'insertAfter'](el); - } - $(".ppom_field_table tbody").sortable({ - stop: function(evt, ui) { - - let parent = $('.ppom_save_fields_model'), - el = parent.find('.' + ui.item.attr('id')), - dir = 'top'; - if (ui.offset.top > ui.originalPosition.top) { - dir = 'bottom'; - } - insertAt(parent, el, ui.item.index(), dir); - } - }); - - - /** - 23- Fields Option Sortable - **/ - $(".ppom-options-sortable").sortable(); - - $("ul.ppom-options-container").sortable({ - revert: true - }); - - - /** - 24- Fields Dataname Must Be Required - **/ - function ppom_required_data_name($this, context) { - var selector = $this.closest('.ppom-slider'); - var data_name = selector.find('[data-meta-id="data_name"] input[type="text"]').val(); - var savedDataName = selector.find('[data-metatype="data_name"]').val(); - var allDataName = $(document).find( 'table.ppom_field_table td.ppom_meta_field_id' ).map(function(){ - var metaFieldId = $.trim(jQuery(this).text()); - if ( $this.hasClass( 'ppom-update-field' ) && data_name === metaFieldId ) { - return ''; - } - return metaFieldId; - }).get(); - if (data_name == '') { - var msg = ppom_vars.i18n.dataNameRequired; - var is_ok = false; - } else if (('new'===context || ( 'update'===context && savedDataName !== data_name ) ) && $.inArray(data_name, allDataName) != -1) { - var msg = ppom_vars.i18n.dataNameExists; - var is_ok = false; - } - else { - msg = ''; - is_ok = true; - } - selector.find('.ppom-req-field-id').html(msg); - return is_ok; - } - - - /** - WP Color Picker Controller - **/ - function ppom_wp_color_handler(wpcolor_selector, field_index, option_index) { - - wpcolor_selector.each(function(i, meta_field) { - var color_picker_input = $(meta_field).find('.ppom-color-picker-init').clone(); - $(meta_field).html(color_picker_input); - color_picker_input.wpColorPicker(); - }); - } - - - /** - 25- Fields Add Option Index Controle Funtion - **/ - function ppom_create_option_index(option_selector, field_index, option_index, ppom_option_type) { - - option_selector.each(function(i, meta_field) { - - - if (ppom_option_type == 'ppom_copy_option') { - var opt_in = $(meta_field).attr('data-opt-index'); - if (opt_in !== undefined) { - option_index = opt_in; - } - } - $(meta_field).attr('data-opt-index', option_index); - - - var field_name = 'ppom[' + field_index + '][options][' + option_index + '][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - } - - - function ppom_option_controller(option_selector, field_index, option_index, ppom_option_type) { - - option_selector.each(function(i, meta_field) { - - // console.log(ppom_option_type); - if (ppom_option_type == 'ppom_copy_option') { - var opt_in = $(meta_field).attr('data-opt-index'); - if (opt_in !== undefined) { - option_index = opt_in; - } - } - $(meta_field).attr('data-opt-index', option_index); - - - var field_name = 'ppom[' + field_index + '][' + $(meta_field).attr('data-optiontype') + '][' + option_index + '][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - } - - - /** - 26- Fields Add Condition Index Controle Function - **/ - function ppom_add_condition_set_index(add_c_selector, opt_field_no, field_type, opt_no) { - add_c_selector.each(function(i, meta_field) { - // var field_name = 'ppom['+field_no+']['+$(meta_field).attr('data-metatype')+']'; - var field_name = 'ppom[' + opt_field_no + '][conditions][rules][' + opt_no + '][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - } - - // address addon - function ppom_create_address_index(address_selector, field_index, address_table_id) { - address_selector.each(function(i, meta_field) { - var field_id = $(meta_field).attr('data-fieldtype'); - var core_field_type = $(address_table_id).attr('data-addresstype'); - var field_name = 'ppom[' + field_index + '][' + core_field_type + '][' + field_id + '][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - } - - - // eventcalendar inputs changing - function ppom_eventcalendar_set_index(add_c_selector, opt_field_no) { - - add_c_selector.each(function(i, meta_field) { - - var date = $(meta_field).attr('data-date'); - var field_name = 'ppom[' + opt_field_no + '][calendar][' + date + '][' + $(meta_field).attr('data-metatype') + ']'; - $(meta_field).attr('name', field_name); - }); - } - - // Rename rules name. - function ppom_rename_rules_name() { - $('.webcontact-rules:visible' ).each(function( index, item ){ - $(this).attr('id', 'rule-box-' + parseInt(index + 1)) - .find('label') - .text(function(i,txt) { - return txt.replace(/\d+/, parseInt(index + 1)); - }); - }); - } - - /** - * Filter the operator options list based on the target type field. - * - * @param {string?} fieldType The PPOM field type. - * @param {HTMLSelectElement} operatorSelectField The select input for condition operator. - * @returns - */ - function toggleOperatorFieldByTargetType( fieldType, operatorSelectField ) { - if ( ! operatorSelectField ) { - return; - } - - let shouldHideSelectInput = true; - const currentValue = operatorSelectField?.value; - - operatorSelectField.querySelectorAll('optgroup').forEach( optgroup => { - let shouldHideGroup = true; - optgroup.querySelectorAll('option').forEach( option => { - const isAvailable = option?.value && OPERATORS_FIELD_COMPATIBILITY[option.value] ? OPERATORS_FIELD_COMPATIBILITY[option.value].includes(fieldType) : option?.value; - if ( shouldHideGroup && isAvailable ) { - shouldHideGroup = false; - } - - if ( option.value === currentValue && !isAvailable ) { - operatorSelectField.value = 'any'; // NOTE: Default to 'any' if the current select value is unavailable. - } - - option.classList.toggle('ppom-hide-element', !isAvailable ); - }); - - if ( ! shouldHideGroup ) { - shouldHideSelectInput = false; - } - - optgroup.classList.toggle( 'ppom-hide-element', shouldHideGroup ); - }); - - operatorSelectField.classList.toggle( 'ppom-invisible-element', shouldHideSelectInput ); - tryToggleConditionInputFields( operatorSelectField ); - } - - /** - 27- Refresh the condition comparison option list for PPOM field target that are of type select. - **/ - function updateTargetComparisonValueSelect( targetSelect, conditionContainer, initialSelectedValue ) { - /** @type {string?} */ - const targetElementNameToPullOptions = targetSelect.value; - - /** @type {HTMLDivElement?} */ - conditionContainer ??= targetSelect.closest('.webcontact-rules'); - const targetSelectOptions = conditionContainer?.querySelector('select[data-metatype="element_values"]'); - - if ( !conditionContainer || !targetSelectOptions ) { - return; - } - - document.querySelectorAll('.ppom-slider').forEach(sliderItem => { - - const targetElementFieldId = sliderItem.querySelector('input[data-metatype="data_name"]')?.value; - if ( targetElementFieldId !== targetElementNameToPullOptions ) { - return; - } - - const operatorsInput = conditionContainer.querySelector('[data-metatype="operators"]'); - if ( ! operatorsInput ) { - return; - } - - // Reset the options lists based on the new selection. - const newOptions = []; - - sliderItem.querySelectorAll('.data-options').forEach(/** @type {HTMLDivElement} */conditionValueContainer => { - const condition_type = conditionValueContainer.getAttribute('data-condition-type'); - - const conditionValueId = conditionValueContainer - .querySelector( - condition_type === 'simple_options' - ? 'input[data-metatype="option"]' - : '.ppom-image-option-title' - )?.value?.trim(); - - if ( ! conditionValueId ) { - return; - } - - const optionElement = document.createElement('option'); - optionElement.value = ppom_escape_html(conditionValueId); - optionElement.textContent = conditionValueId; - - newOptions.push( optionElement ); - }); - targetSelectOptions.replaceChildren(...newOptions); - }); - - if ( initialSelectedValue ) { - targetSelectOptions.value = initialSelectedValue; - } - } - /** - * Toggle the visibility for input fields type based on operator current value. - * - * @param {HTMLSelectElement?} conditionOperatorInput - * @returns - */ - function tryToggleConditionInputFields( conditionOperatorInput ) { - if ( ! conditionOperatorInput ) { - return; - } - - const selectedOperator = conditionOperatorInput?.value; - - /** - * @type {HTMLDivElement|null} - */ - const container = conditionOperatorInput?.closest('.webcontact-rules'); - if ( !container) { - return; - } - - /** - * @type {HTMLSelectElement|null} - */ - const conditionTargetSelectOptionsInput = container.querySelector( 'select[data-metatype="element_values"]' ); - - /** - * @type {HTMLInputElement|null} - */ - const conditionConstantInput = container.querySelector( '[data-metatype="element_constant"]' ); - - /** - * @type {HTMLSelectElement|null} - */ - const conditionTargetSelectInput = container.querySelector( '[data-metatype="elements"]' ); - if ( !conditionConstantInput || !conditionTargetSelectInput ) { - return; - } - - /** - * @type {HTMLDivElement|null} - */ - const betweenInputs = container.querySelector('.ppom-between-input-container'); - - let shouldHideSelectInput = false; - let shouldHideTextInput = false; - let shouldHideBetweenInputs = false; - let shouldHideUpsell = true; - - if ( proOperatorOptionsToLock.has( selectedOperator ) ) { - shouldHideSelectInput = true; - shouldHideTextInput = true; - shouldHideBetweenInputs = true; - shouldHideUpsell = false; - } - else if ( 'between' === selectedOperator ) { - shouldHideSelectInput = true; - shouldHideTextInput = true; - shouldHideBetweenInputs = false; - } - else if ( HIDE_COMPARISON_INPUT_FIELD.includes( selectedOperator ) ) { - shouldHideSelectInput = true; - shouldHideTextInput = true; - shouldHideBetweenInputs = true; - } else { - shouldHideSelectInput = true; - shouldHideBetweenInputs = true; - - /** - * @type {HTMLOptionElement|null} - */ - const targetFieldTypeInput = conditionTargetSelectInput.querySelector(`option[value="${conditionTargetSelectInput.value}"]`); - if ( - conditionTargetSelectOptionsInput && - COMPARISON_VALUE_CAN_USE_SELECT.includes( selectedOperator ) && - targetFieldTypeInput?.dataset?.fieldtype && - OPERATOR_COMPARISON_VALUE_FIELD_TYPE['select'].includes( targetFieldTypeInput.dataset.fieldtype ) - ) { - shouldHideTextInput = true; - shouldHideSelectInput = false; - } - } - - if ( shouldHideSelectInput && shouldHideTextInput && shouldHideBetweenInputs && shouldHideUpsell ) { - conditionConstantInput.parentNode?.classList.add('ppom-invisible-element'); // NOTE: Make the entire container visible to preserve the space. - } else { - conditionTargetSelectOptionsInput?.classList.toggle("ppom-hide-element", shouldHideSelectInput ); - conditionConstantInput.classList.toggle("ppom-hide-element", shouldHideTextInput ); - betweenInputs?.classList.toggle("ppom-hide-element", shouldHideBetweenInputs ); - container.querySelector('.ppom-upsell-condition')?.classList.toggle("ppom-hide-element", shouldHideUpsell); - - conditionConstantInput.parentNode?.classList.remove('ppom-invisible-element'); - } - } - - // Apply actions on initialization based on operator value. - document.querySelectorAll('select[data-metatype="operators"]').forEach( conditionOperatorInput => { - tryToggleConditionInputFields( conditionOperatorInput ); - }); - - // Apply actions when operator value changes. - document.addEventListener('change', function (e) { - if ( ! e.target.matches('select[data-metatype="operators"]') ) { - return; - } - - e.preventDefault(); - tryToggleConditionInputFields( e.target ); - }); - - $(document).on('change', '[data-meta-id="conditions"] select[data-metatype="element_values"]', function(e) { - e.preventDefault(); - - var element_values = $(this).val(); - $(this).attr('data-existingvalue', element_values); - }); - - $(document).on('click', '.ppom-condition-tab-js', function(e) { - e.preventDefault(); - populate_conditional_elements(); - }); - - /** - * Populate the condition target select with eligible options based on the operator. - * - * @param {HTMLSelectElement?} selectInput - * @param {string?} conditionOperator - * @param {string[]} excludeIds - * @returns - */ - function populate_condition_target( selectInput, conditionOperator, excludeIds = [] ) { - if ( !selectInput ) { - return; - } - - const newOptions = availableConditionTargets - .filter( ({ fieldId, canUse }) => canUse && !excludeIds.includes( fieldId) ) - .map( target => { - - const option = document.createElement('option'); - option.value = target.fieldId; - option.textContent = target.fieldLabel; - option.dataset.fieldtype = target.fieldType; - - return option; - }); - - selectInput.replaceChildren( ...newOptions ); - } - - function findFieldTypeById( fieldId ) { - if ( !fieldId ) { - return undefined; - } - - for ( const target of availableConditionTargets ) { - if ( target.fieldId === fieldId ) { - return target.fieldType; - } - } - - return undefined; - } - - function can_use_field_type( fieldType ) { - if ( ! fieldType?.length ) { - return false; - } - - for ( const operatorCompatibleFields of Object.values( OPERATORS_FIELD_COMPATIBILITY ) ) { - if ( operatorCompatibleFields.includes( fieldType ) ) { - return true; - } - } - - return false; - } - - /** - * Populate the condition target select with eligible options based on the operator on initialization and value change. - * - */ - function populate_conditional_elements() { - - // Get all available PPOM fields. - availableConditionTargets.splice(0, availableConditionTargets.length); - document.querySelectorAll(".ppom-slider").forEach(item => { - const fieldLabel = item.querySelector('input[data-metatype="title"]')?.value; - const fieldId = item.querySelector('input[data-metatype="data_name"]')?.value?.trim(); - const fieldType = item.querySelector('input[data-metatype="type"]')?.value; - const canUse = can_use_field_type( fieldType ); - - if ( !fieldLabel || !fieldId || !fieldType ) { - return; - } - - availableConditionTargets.push({ fieldLabel, fieldId, fieldType, canUse }); - }); - - // Change the target options for all the rules. - document.querySelectorAll(".ppom-slider").forEach(item => { - if ( ! item.id ) { - return; - } - const conditionContainers = item.querySelector('div[data-meta-id="conditions"]')?.querySelectorAll('.webcontact-rules'); - - conditionContainers?.forEach(conditionContainer => { - const conditionTargetsSelect = conditionContainer.querySelector('[data-metatype="elements"]'); - if ( ! conditionTargetsSelect ) { - return; - } - - const conditionOperatorSelect = conditionContainer.querySelector('[data-metatype="operators"]'); - const fieldId = item.querySelector('input[data-metatype="data_name"]')?.value?.trim(); - - populate_condition_target( conditionTargetsSelect, conditionOperatorSelect?.value, [fieldId] ); - - if ( conditionTargetsSelect?.dataset?.existingvalue ) { - conditionTargetsSelect.value = conditionTargetsSelect?.dataset?.existingvalue; - } - - // NOTE: Get all the locked operators. Unlock them to be eligible to show the upsell. - conditionOperatorSelect?.querySelectorAll( 'option' ).forEach( option => { - if ( ! option.disabled ) { - return; - } - - proOperatorOptionsToLock.add( option.value ); - option.disabled = false; - }); - - toggleOperatorFieldByTargetType( findFieldTypeById( conditionTargetsSelect?.value ), conditionOperatorSelect ); - - const optionsInput = conditionContainer.querySelector('[data-metatype="element_values"]'); - - updateTargetComparisonValueSelect( - conditionTargetsSelect, - conditionContainer, - ppom_escape_html(optionsInput?.dataset?.existingvalue) - ); - }); - }); - } - - /** - * Update the values of the operators selector and the comparison fields. - * - * NOTE: We are using a global listener since some node are dinamically created/cloned. - */ - document.addEventListener('change', function(e) { - if ( ! e.target.matches('select[data-metatype="elements"]') ) { - return; - } - - e.preventDefault(); - const conditionContainer = e.target.closest('.webcontact-rules'); - const conditionOperatorSelect = conditionContainer?.querySelector('[data-metatype="operators"]'); - if ( ! conditionContainer || ! conditionOperatorSelect ) { - return; - } - - toggleOperatorFieldByTargetType( findFieldTypeById(e.target?.value), conditionOperatorSelect ); - updateTargetComparisonValueSelect( e.target, conditionContainer ); - - const optionsInput = conditionContainer.querySelector('[data-metatype="element_values"]'); - const constantInput = conditionContainer.querySelector('[data-metatype="element_constant"]'); - - // Reset values. - if ( constantInput ) { - constantInput.value = ''; - } - - if ( optionsInput ) { - optionsInput.value = ''; - } - }); - - /** - 28- validate API WooCommerce Product - **/ - function validate_api_wooproduct(form) { - - jQuery(form).find("#nm-sending-api").html( - ''); - - var data = jQuery(form).serialize(); - data = data + '&action=nm_personalizedproduct_validate_api'; - - jQuery.post(ajaxurl, data, function(resp) { - - //console.log(resp); - jQuery(form).find("#nm-sending-api").html(resp.message); - if (resp.status == 'success') { - window.location.reload(true); - } - }, 'json'); - - - return false; - } - - - function ppom_escape_html(unsafe) { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); - } - - $(document).on('ppom_fields_tab_changed', 'div.row.ppom-tabs', (e, id, tab)=>{ - if( ppom_vars.i18n.freemiumCFRTab !== id ) { - return; - } - - if( tab.find('.freemium-cfr-content').length > 0 ) { - return; - } - - $(`
    ${ppom_vars.i18n.freemiumCFRContent}
    `).insertAfter( tab.find('.form-group') ); - }); - - const toggleHandler = { - setDisabledFields: function(jQueryDP){ - const on = jQueryDP.is(':checked'); - const slider = jQueryDP.parents('.ppom-slider'); - - const JQUERY_DP_FIELD_MTYPES = [ - 'min_date', - 'max_date', - 'date_formats', - 'default_value', - 'first_day_of_week', - 'year_range', - 'no_weekends', - 'past_dates' - ]; - - JQUERY_DP_FIELD_MTYPES.forEach(function(type){ - slider.find('*[data-metatype="'+type+'"]').prop('disabled', !on); - }); - }, - activateHandler: function() { - toggleHandler.setDisabledFields($(this)); - } - } - - $('input[data-metatype="jquery_dp"]').change(toggleHandler.activateHandler); - - $( document ).on( 'ppom_new_field_created', function(e, clone_new_field) { - $(clone_new_field).find('input[data-metatype="jquery_dp"]').change(toggleHandler.activateHandler); - } ); - - // Unsaved form exit confirmation. - var unsaved = false; - $( '.ppom-main-field-wrapper :input' ).change(function () { - if ( $( this ).parents( '.ppom-checkboxe-style' )?.length > 0 ) { - unsaved = false; - return; - } - unsaved = true; - }); - $( document ).on( 'click', '.ppom-submit-btn input.btn, button.ppom_copy_field, button.ppom-add-fields-js-action, button.ppom-js-modal-close', function() { - if ( $(this).hasClass('ppom_copy_field') || $(this).hasClass( 'ppom-add-fields-js-action' ) ) { - unsaved = true; - return; - } - unsaved = false; - } ); - window.addEventListener( 'beforeunload', function( e ) { - if ( unsaved ) { - e.preventDefault(); - e.returnValue = ''; - } - }); - - $( document ).on( 'ppom_fields_tab_changed', function(e, id, tab) { - if ( 'condition_tab' !== id ) { - return; - } - - if ( ! $('input[data-metatype="logic"]', tab?.first() )?.is(':checked') ) { - tab?.last()?.addClass( 'ppom-disabled-overlay' ); - } - } ); - - $( document ).on( 'change', 'input[data-metatype="logic"]:visible', function() { - $(this) - .parents('.ppom_handle_condition_tab') - .next('.ppom_handle_condition_tab') - .toggleClass('ppom-disabled-overlay'); - } ); - - $( document ).on( 'click', 'button.ppom-edit-field.ppom-is-pro-field, button.ppom_copy_field.ppom-is-pro-field', function() { - $('#ppom-lock-fields-upsell').fadeIn(); - return false; - } ); - - $(document).ready(function(){ - $('.ppom-slider').each(function(i, item){ - const itemEl = $(item); - const value = itemEl.find( - 'input[data-metatype="data_name"]').val(); - - const type = itemEl.find('input[data-metatype="type"]').val(); - - if( $.trim( value ) === '' || type !== 'date' ) { - return; - } - - toggleHandler.setDisabledFields(itemEl.find('input[data-metatype="jquery_dp"]')); - }); - }) - $(document).on('click', '.postbox-header', function() { - var postbox = $(this).closest('.postbox'); - postbox.toggleClass('closed'); - }); -}); -document.querySelectorAll('.ppom-modal-shortcuts a')?.forEach(anchor => { - anchor.addEventListener('click', function(e) { - e.preventDefault(); - // Get the href attribute and use it as the ID to display the corresponding section - const targetId = this.getAttribute('href'); - if (targetId === '#all') { - // Show all sections - document.querySelectorAll('.ppom-fields-section').forEach(section => { - section.style.display = 'block'; - }); - } else { - // Hide all sections with the class 'ppom-fields-section' - document.querySelectorAll('.ppom-fields-section').forEach(section => { - section.style.display = 'none'; - }); - - const targetSection = document.querySelector(targetId + '-ppom-fields'); - if (targetSection) { - targetSection.style.display = 'block'; - } - } - }); -}); +jQuery( function ( $ ) { + const loader = new ImageLoader( ppom_vars.loader ); + // Keep the admin shell hidden until shared assets are ready. + // define your 'onreadystatechange' + loader.loadEvent = function ( url, imageAsDom ) { + $( '#ppom-pre-loading' ).hide(); + $( '.ppom-admin-wrap' ).show(); + }; + loader.load(); + + /* + * The builder is template-driven: hidden modal markup is cloned per field, + * then all nested names/ids are rewritten to the current field index before + * the field is added to the summary table and save payload. + */ + + // Shared overlay injected behind the builder's inline modal dialogs. + const append_overly_model = + "
    "; + + $( document ).on( + 'click', + '[data-modal-id]:not(.ppom-is-pro-field)', + function ( e ) { + e.preventDefault(); + $( 'body' ).append( append_overly_model ); + const modalBox = $( this ).attr( 'data-modal-id' ); + lockedDataName = false; + $( '#' + modalBox ).fadeIn(); + } + ); + + ppom_close_popup(); + + function ppom_close_popup() { + // The admin builder uses lightweight inline modals, not WordPress media + // modals, so closing must also clean up the injected overlay element. + $( '.ppom-js-modal-close, .ppom-modal-overlay' ).click( function ( e ) { + const target = $( e.target ); + if ( target.hasClass( 'ppom-modal-overlay' ) ) { + return false; + } + $( '.ppom-modal-box, .ppom-modal-overlay' ).fadeOut( + 'fast', + function () { + $( '.ppom-modal-overlay' ).remove(); + } + ); + } ); + } + + $( '.ppom-color-picker-init' ).wpColorPicker(); + + /** + 1- Submit PPOM Form Fields + */ + $( '.ppom-save-fields-meta' ).on( 'submit', function ( e ) { + e.preventDefault(); + + jQuery( '.ppom-meta-save-notice' ) + .html( '' ) + .show(); + + $( '.ppom-unsave-data' ).remove(); + + const formData = new FormData(); + const ppomFields = new URLSearchParams(); + + /* + * PPOM field builders can exceed `max_input_vars`, so all `ppom[...]` + * values are collapsed into a single encoded string before POSTing. + * + * `serializeArray()` is used on purpose because some textarea-backed + * settings depend on preserving `\r\n` separators. + */ + $( this ) + .serializeArray() + .forEach( ( { value, name } ) => { + if ( name.startsWith( 'ppom[' ) && typeof value === 'string' ) { + ppomFields.append( name, value ); + } else { + formData.append( name, value ); + } + } ); + + formData.append( 'ppom', ppomFields.toString() ); + + fetch( ajaxurl, { + method: 'POST', + body: formData, + } ) + .then( ( response ) => response.json() ) + .then( ( resp ) => { + const bg_color = + resp.status == 'success' ? '#4e694859' : '#ee8b94'; + jQuery( '.ppom-meta-save-notice' ).html( resp.message ).css( { + 'background-color': bg_color, + padding: '8px', + 'border-left': '5px solid #008c00', + } ); + if ( resp.status == 'success' ) { + if ( resp.redirect_to != '' ) { + window.location = resp.redirect_to; + } else { + window.location.reload(); + } + } + } ) + .catch( () => { + jQuery( '.ppom-meta-save-notice' ) + .html( ppom_vars.i18n.errorOccurred ) + .css( { + 'background-color': '#ee8b94', + padding: '8px', + 'border-left': '5px solid #c00', + } ); + } ); + } ); + + /** + 2- Hide And Show Import & Export & Product Meta blocks + */ + $( '.ppom-import-export-btn' ).on( 'click', function ( event ) { + event.preventDefault(); + if ( $( '.ppom-import-export-block' ).length === 0 ) { + $( '#ppom-import-upsell' ).fadeIn(); + return; + } + $( '.ppom-more-plugins-block' ).hide(); + $( '.ppom-import-export-block' ).show(); + $( '.ppom-product-meta-block' ).hide(); + } ); + + $( '.ppom-cancle-import-export-btn' ).on( 'click', function ( event ) { + event.preventDefault(); + $( '.ppom-more-plugins-block' ).show(); + $( '.ppom-import-export-block' ).hide(); + $( '.ppom-product-meta-block' ).show(); + } ); + + /** + 3- Get Last Field Index + */ + let field_no = $( '#field_index' ).val(); + + /** + 4- Show And Hide Visibility Role Field + */ + $( '.ppom-slider' ) + .find( '[data-meta-id="visibility_role"]' ) + .removeClass( 'ppom_handle_fields_tab' ) + .hide(); + $( '.ppom_save_fields_model .ppom-slider' ).each( function ( i, div ) { + const visibility_value = $( div ) + .find( '[data-meta-id="visibility"] select' ) + .val(); + if ( visibility_value == 'roles' ) { + $( div ).find( '[data-meta-id="visibility_role"]' ).show(); + } + } ); + $( document ).on( + 'change', + '[data-meta-id="visibility"] select', + function ( e ) { + e.preventDefault(); + + const div = $( this ).closest( '.ppom-slider' ); + const visibility_value = $( this ).val(); + // console.log(visibility_value); + if ( visibility_value == 'roles' ) { + div.find( '[data-meta-id="visibility_role"]' ).show(); + } else { + div.find( '[data-meta-id="visibility_role"]' ).hide(); + } + } + ); + + /** + 5- Remove Unsaved Fields + */ + $( document ).on( 'click', '.ppom-close-fields', function ( event ) { + event.preventDefault(); + + $( this ).closest( '.ppom-slider' ).addClass( 'ppom-unsave-data' ); + } ); + + /** + 6- Check And Uncheck All Fields + */ + $( '.ppom-main-field-wrapper' ).on( + 'change', + '.onoffswitch-checkbox', + function ( event ) { + const div = $( this ).closest( 'div' ); + if ( $( this ).prop( 'checked' ) ) { + div.find( 'input[type="hidden"]' ).val( 'on' ); + } else { + div.find( 'input[type="hidden"]' ).val( 'off' ); + } + } + ); + + $( '.ppom-main-field-wrapper' ).on( + 'click', + '.ppom-check-all-field input', + function ( event ) { + if ( $( this ).prop( 'checked' ) ) { + $( + '.ppom_field_table .ppom-checkboxe-style input[type="checkbox"]' + ).prop( 'checked', true ); + } else { + $( + '.ppom_field_table .ppom-checkboxe-style input[type="checkbox"]' + ).prop( 'checked', false ); + } + } + ); + $( '.ppom-main-field-wrapper' ).on( + 'change', + '.ppom_field_table tbody .ppom-checkboxe-style input[type="checkbox"]', + function ( event ) { + if ( + $( + '.ppom_field_table tbody .ppom-checkboxe-style input[type="checkbox"]:checked' + ).length == + $( + '.ppom_field_table tbody .ppom-checkboxe-style input[type="checkbox"]' + ).length + ) { + $( '.ppom-check-all-field input' ).prop( 'checked', true ); + } else { + $( '.ppom-check-all-field input' ).prop( 'checked', false ); + } + } + ); + + /** + 7- Remove Check Fields + */ + $( '.ppom-main-field-wrapper' ).on( + 'click', + '.ppom_remove_field', + function ( e ) { + e.preventDefault(); + + const inputsToRemove = document.querySelectorAll( + '.ppom_field_table .ppom-check-one-field input:checked' + ); + + if ( inputsToRemove.length > 0 ) { + window?.ppomPopup?.open( { + title: window?.ppom_vars?.i18n.popup.confirmTitle, + onConfirmation: () => { + inputsToRemove.forEach( ( meta_field ) => { + const field_id = meta_field.value; + meta_field + .closest( `.row_no_${ field_id }` ) + ?.remove(); + document + .querySelector( + `#ppom_field_model_${ field_id }` + ) + ?.remove(); + } ); + }, + } ); + } else { + window?.ppomPopup?.open( { + title: window.ppom_vars.i18n.popup.checkFieldTitle, + type: 'error', + hideCloseBtn: true, + } ); + } + } + ); + + /** + 9- Edit Existing Fields + */ + var lockedDataName = false; + $( document ).on( + 'click', + '.ppom-edit-field:not(.ppom-is-pro-field)', + function ( event ) { + event.preventDefault(); + + const the_id = $( this ).attr( 'id' ); + $( '#ppom_field_model_' + the_id ) + .find( '.ppom-close-checker' ) + .removeClass( 'ppom-close-fields' ); + lockedDataName = true; + } + ); + + /** + 10- Add New Fields + */ + $( document ).on( 'click', '.ppom-add-field', function ( event ) { + event.preventDefault(); + + const $this = $( this ); + const ui = ppom_required_data_name( $this, 'new' ); + if ( ui == false ) { + return; + } + + const copy_model_id = $( this ).attr( 'data-copy-model-id' ); + let id = $( this ).attr( 'data-field-index' ); + id = Number( id ); + // console.log(id); + + const field_title = $( '#ppom_field_model_' + id + '' ) + .find( '.ppom-modal-body .ppom-fields-actions' ) + .attr( 'data-table-id' ); + const data_name = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="data_name"] input' ) + .val(); + const title = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="title"] input' ) + .val(); + let placeholder = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="placeholder"] input' ) + .val(); + const required = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="required"] input' ) + .prop( 'checked' ); + const type = $( this ).attr( 'data-field-type' ); + + // console.log(field_title); + + if ( required == true ) { + var _ok = ppom_vars.i18n.yes; + } else { + _ok = ppom_vars.i18n.no; + } + if ( placeholder == null ) { + placeholder = '-'; + } + + let html = + '
    '; + html += + ''; + html += ''; + + html += ''; + + // html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html = $.parseHTML( html ); + // console.log(copy_model_id); + if ( copy_model_id != '' && copy_model_id != undefined ) { + $( html ) + .find( '.ppom_field_table tbody' ) + .end() + .insertAfter( '#ppom_sort_id_' + copy_model_id + '' ); + } else { + $( html ).appendTo( '.ppom_field_table tbody' ); + } + + $( '.ppom-modal-box, .ppom-modal-overlay' ).fadeOut( + 'fast', + function () { + $( '.ppom-modal-overlay' ).remove(); + } + ); + + $( this ) + .removeClass( 'ppom-add-field' ) + .addClass( 'ppom-update-field' ); + $( this ).html( ppom_vars.i18n.updatedField ); + } ); + + /** + 11- Update Existing Fields + */ + $( document ).on( 'click', '.ppom-update-field', function ( event ) { + event.preventDefault(); + + let id = $( this ).attr( 'data-field-index' ); + id = Number( id ); + + const $this = $( this ); + const ui = ppom_required_data_name( $this, 'update' ); + + if ( ui == false ) { + return; + } + + const data_name = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="data_name"] input' ) + .val(); + const title = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="title"] input' ) + .val(); + const placeholder = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="placeholder"] input' ) + .val(); + const required = $( '#ppom_field_model_' + id + '' ) + .find( '[data-meta-id="required"] input' ) + .prop( 'checked' ); + const type = $( this ).attr( 'data-field-type' ); + + if ( required == true ) { + var _ok = ppom_vars.i18n.yes; + } else { + _ok = ppom_vars.i18n.no; + } + + const row = $( '.ppom_field_table tbody' ).find( '.row_no_' + id ); + + row.find( '.ppom_meta_field_title' ).text( title ); + row.find( '.ppom_meta_field_id' ).text( data_name ); + row.find( '.ppom_meta_field_type' ).text( type ); + row.find( '.ppom_meta_field_plchlder' ).text( placeholder ); + row.find( '.ppom_meta_field_req' ).text( _ok ); + + $( '.ppom-modal-box, .ppom-modal-overlay' ).fadeOut( + 'fast', + function () { + $( '.ppom-modal-overlay' ).remove(); + } + ); + } ); + + const option_index = 0; + $( document ).on( 'click', '.ppom_select_field', function ( event ) { + if ( $( this ).hasClass( 'ppom-locked-field' ) ) { + return; + } + + event.preventDefault(); + + $( '#ppom_fields_model_id' ) + .find( '.ppom-js-modal-close' ) + .trigger( 'click' ); + + const field_type = $( this ).data( 'field-type' ); + const clone_new_field = $( + '.ppom-field-' + field_type + ':last' + ).clone(); + + // Every cloned template starts life with placeholder names. Before the + // field can be saved, each nested input must be rebound to its new + // `ppom[field_no][...]` namespace. + clone_new_field + .find( '.ppom-meta-field' ) + .each( function ( i, meta_field ) { + const field_name = + 'ppom[' + + field_no + + '][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + + // fields options sortable + clone_new_field.find( '.ppom-options-sortable' ).sortable(); + + // add fields index in data-field-no + clone_new_field + .find( '.ppom-fields-actions' ) + .attr( 'data-field-no', field_no ); + + // Conditions have their own nested payload shape under the field entry. + clone_new_field + .find( '.ppom-condition-visible-bound' ) + .each( function ( i, meta_field ) { + const field_name = + 'ppom[' + + field_no + + '][conditions][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + + clone_new_field + .find( '.ppom-fields-actions [data-meta-id="visibility_role"]' ) + .hide(); + + const field_model_id = 'ppom_field_model_' + field_no + ''; + + clone_new_field + .find( '.ppom_save_fields_model' ) + .end() + .appendTo( '.ppom_save_fields_model' ) + .attr( 'id', field_model_id ) + .find( '.ppom-tabs-header label.ppom-tabs-label' ) + .each( function ( index, item ) { + const tabId = $( item ).attr( 'id' ); + const hasTabOptions = + $( '#' + field_model_id, document )?.find( + '.ppom_handle_' + tabId + )?.length > 0; + if ( ! hasTabOptions ) { + $( item ).hide(); + } + } ); + clone_new_field + .find( '.ppom-field-checker' ) + .attr( 'data-field-index', field_no ); + clone_new_field + .find( '.ppom-field-checker' ) + .addClass( 'ppom-add-fields-js-action' ); + + // var color_picker_input = clone_new_field.find('.ppom-color-picker-init').clone(); + // clone_new_field.find('.ppom-color-picker-cloner').html(color_picker_input); + // clone_new_field.find('.ppom-color-picker-init').wpColorPicker(); + // $('.ppom-color-picker-init').wpColorPicker(); + // $('.ppom-color-picker-init').wpColorPicker(); + + // $('.ppom-color-picker-init').wpColorPicker('destroy'); + + clone_new_field.addClass( 'ppom_sort_id_' + field_no + '' ); + const field_index = field_no; + + // handle multiple options + const ppom_option_type = ''; + const option_selector = clone_new_field.find( '.ppom-option-keys' ); + const option_controller = clone_new_field.find( '.ppom-fields-option' ); + const add_cond_selector = clone_new_field.find( + '.ppom-conditional-keys' + ); + + // for address addon + const address_selector = clone_new_field.find( '.ppom-checkout-field' ); + const address_table_id = clone_new_field.find( '.ppom_address_table' ); + ppom_create_address_index( + address_selector, + field_index, + address_table_id + ); + + const wpcolor_selector = clone_new_field.find( + '.ppom-color-picker-cloner' + ); + ppom_wp_color_handler( wpcolor_selector, field_index, option_index ); + + ppom_create_option_index( + option_selector, + field_index, + option_index, + ppom_option_type + ); + ppom_option_controller( + option_controller, + field_index, + option_index, + ppom_option_type + ); + ppom_add_condition_set_index( + add_cond_selector, + field_index, + field_type, + option_index + ); + + // popup fields on model + ppom_close_popup(); + $( '#ppom_field_model_' + field_no + '' ).fadeIn(); + + $( document ).trigger( 'ppom_new_field_created', [ + clone_new_field, + field_no, + field_type, + ] ); + + field_no++; + } ); + + const copy_no = 0; + $( '.ppom-main-field-wrapper' ).on( + 'click', + '.ppom_copy_field:not(.ppom-is-pro-field)', + function ( e ) { + e.preventDefault(); + + const model_id_no = $( this ).attr( 'id' ); + + const field_type = $( this ).data( 'field-type' ); + // console.log(model_id_no); + + const clone_new_field = $( + '.ppom_save_fields_model #ppom_field_model_' + model_id_no + '' + ).clone( true ); + const dataTitleField = clone_new_field.find( + '[data-metatype="title"]' + ); + const dataNameField = clone_new_field.find( + '[data-metatype="data_name"]' + ); + let dataNameOldVal = dataNameField.val(); + const duplicateFields = jQuery( document ).find( + '.ppom_field_table .ppom_meta_field_title:contains(' + + dataTitleField.val() + + ')' + ); + let dataNameNewVal = ''; + const reg = '/_copy_/'; + if ( duplicateFields && duplicateFields.length >= 1 ) { + dataNameOldVal = dataNameOldVal.split( '_copy_' ).shift(); + dataNameNewVal = + dataNameOldVal + '_copy_' + duplicateFields.length++; + } else { + dataNameNewVal = dataNameOldVal + '_copy'; + } + dataNameField.val( dataNameNewVal ); + // clone_new_field.find('.ppom_save_fields_model').end().appendTo('.ppom_save_fields_model').attr('id','ppom_field_model_'+field_no+''); + clone_new_field + .find( '.ppom_save_fields_model' ) + .end() + .insertAfter( '#ppom_field_model_' + model_id_no + '' ) + .attr( 'id', 'ppom_field_model_' + field_no + '' ); + clone_new_field + .find( '.ppom-add-fields-js-action' ) + .attr( 'data-field-index', field_no ); + clone_new_field + .find( '.ppom-close-fields' ) + .attr( 'data-field-index', field_no ); + clone_new_field + .find( '.ppom-js-modal-close' ) + .addClass( 'ppom-close-fields' ); + clone_new_field + .find( '.ppom-add-fields-js-action' ) + .removeClass( 'ppom-update-field' ); + clone_new_field + .find( '.ppom-add-fields-js-action' ) + .attr( 'data-copy-model-id', model_id_no ); + clone_new_field + .find( '.ppom-add-fields-js-action' ) + .addClass( 'ppom-add-field' ); + clone_new_field + .find( '.ppom-add-fields-js-action' ) + .addClass( 'ppom-insertafter-field' ); + clone_new_field + .find( '.ppom-add-fields-js-action' ) + .html( 'Add Field' ); + clone_new_field.removeClass( 'ppom_sort_id_' + model_id_no + '' ); + clone_new_field.addClass( 'ppom_sort_id_' + field_no + '' ); + + // Cloned existing fields need the same namespace rewrite as new ones, + // otherwise two field modals would submit into the same PHP array slot. + clone_new_field + .find( '.ppom-meta-field' ) + .each( function ( i, meta_field ) { + const field_name = + 'ppom[' + + field_no + + '][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + + // fields options sortable + clone_new_field.find( '.ppom-options-sortable' ).sortable(); + + // add fields index in data-field-no + clone_new_field + .find( '.ppom-fields-actions' ) + .attr( 'data-field-no', field_no ); + + // fields conditions handle name attr + clone_new_field + .find( '.ppom-condition-visible-bound' ) + .each( function ( i, meta_field ) { + const field_name = + 'ppom[' + + field_no + + '][conditions][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + + clone_new_field + .find( '.ppom-fields-actions [data-meta-id="visibility_role"]' ) + .hide(); + + const field_index = field_no; + + // handle multiple options + const ppom_option_type = 'ppom_copy_option'; + const option_selector = clone_new_field.find( '.ppom-option-keys' ); + const add_cond_selector = clone_new_field.find( + '.ppom-conditional-keys' + ); + const eventcalendar_selector = clone_new_field.find( + '.ppom-eventcalendar-field' + ); + const image_option_selector = clone_new_field.find( + '[data-table-id="image"] .data-options, [data-table-id="imageselect"] .data-options' + ); + + // reset option to one + // clone_new_field.find('[data-table-id="image"] .data-options').remove(); + clone_new_field + .find( '[data-table-id="audio"] .pre-upload-box li' ) + .remove(); + // clone_new_field.find('[data-table-id="imageselect"] .pre-upload-box li').remove(); + // clone_new_field.find('.data-options').not(':last').remove(); + clone_new_field.find( '.webcontact-rules' ).not( ':last' ).remove(); + + // set existing conditions meta + $( clone_new_field ) + .find( 'select[data-metatype="elements"]' ) + .each( function ( i, condition_element ) { + const existing_value1 = + $( condition_element ).attr( 'data-existingvalue' ); + if ( $.trim( existing_value1 ) !== '' ) { + jQuery( condition_element ).val( existing_value1 ); + } + } ); + $( clone_new_field ) + .find( 'select[data-metatype="element_values"]' ) + .each( function ( i, condition_element ) { + const div = $( this ).closest( '.webcontact-rules' ); + const existing_value1 = + $( condition_element ).attr( 'data-existingvalue' ); + + if ( $.trim( existing_value1 ) !== '' ) { + jQuery( condition_element ).val( existing_value1 ); + } + } ); + + const wpcolor_selector = clone_new_field.find( + '.ppom-color-picker-cloner' + ); + ppom_wp_color_handler( + wpcolor_selector, + field_index, + option_index + ); + + ppom_create_option_index( + option_selector, + field_index, + option_index, + ppom_option_type + ); + const option_controller = clone_new_field.find( + '.ppom-fields-option' + ); + + ppom_option_controller( + option_controller, + field_index, + option_index, + ppom_option_type + ); + ppom_add_condition_set_index( + add_cond_selector, + field_index, + field_type, + option_index + ); + + // for eventcalendar changing index + ppom_eventcalendar_set_index( eventcalendar_selector, field_index ); + + // set index for all images fields + image_option_selector + .find( 'input' ) + .each( function ( img_index, img_meta ) { + const opt_in = $( img_meta ).attr( 'data-opt-index' ); + const field_name = + 'ppom[' + + field_index + + '][images][' + + opt_in + + '][' + + $( img_meta ).attr( 'data-metatype' ) + + ']'; + $( img_meta ).attr( 'name', field_name ); + } ); + + // popup fields on model + $( 'body' ).append( append_overly_model ); + ppom_close_popup(); + $( '#ppom_field_model_' + field_no + '' ).fadeIn(); + + field_no++; + } + ); + + /** + 14- Saving PPOM IDs In Existing Meta File + */ + + /** + * @type {HTMLFormElement} + */ + const popupForm = document.querySelector( '#ppom-product-form' ); + popupForm?.addEventListener( 'submit', ( e ) => { + e.preventDefault(); + + const formContent = new FormData( + document.querySelector( '#ppom-product-form' ) + ); + + formContent.append( 'action', 'ppom_attach_ppoms' ); + formContent.append( 'ppom_id', $( '#ppom_id' ).val() ); + + fetch( ajaxurl, { + method: 'POST', + body: formContent, + } ) + .then( ( response ) => response.json() ) + .then( ( resp ) => { + alert( resp.message ); + + const openModalBtn = document.querySelector( + '.ppom-products-modal' + ); + if ( openModalBtn && openModalBtn.dataset?.reload ) { + window.location.reload(); + } else { + document + .querySelector( '.ppom-js-modal-close' ) + .dispatchEvent( new Event( 'click' ) ); + } + } ) + .catch( ( error ) => console.error( 'Error:', error ) ); + } ); + + /** + 16- Handle Fields Tabs + */ + $( '.ppom-control-all-fields-tabs' ).hide(); + $( '.ppom_handle_fields_tab' ).show(); + $( document ).on( 'click', '.ppom-tabs-label', function () { + const id = $( this ).attr( 'id' ); + const selectedTab = $( this ).parent(); + const fields_wrap = selectedTab.parent(); + selectedTab.find( '.ppom-tabs-label' ).removeClass( 'ppom-active-tab' ); + $( this ).addClass( 'ppom-active-tab' ); + const content_box = fields_wrap.find( '.ppom-control-all-fields-tabs' ); + content_box.hide(); + + const handler = fields_wrap.find( '.ppom_handle_' + id ); + handler.fadeIn( 200 ); + + $( fields_wrap ).trigger( 'ppom_fields_tab_changed', [ id, handler ] ); + } ); + + /** + 17- Handle Media Images Of Following Inputs Types + 17.1- Pre-Images Type + 17.2- Audio Type + 17.3- Imageselect Type + */ + var $uploaded_image_container; + $( document ).on( 'click', '.ppom-pre-upload-image-btn', function ( e ) { + e.preventDefault(); + + let meta_type = $( this ).attr( 'data-metatype' ); + $uploaded_image_container = $( this ).closest( 'div' ); + const image_append = $uploaded_image_container.find( 'ul' ); + let option_index = parseInt( + $uploaded_image_container.find( '#ppom-meta-opt-index' ).val() + ); + const main_wrapper = $( this ).closest( '.ppom-slider' ); + const field_index = main_wrapper + .find( '.ppom-fields-actions' ) + .attr( 'data-field-no' ); + let price_placeholder = ppom_vars.i18n.pricePlaceholder; + + let wp_media_type = 'image'; + if ( meta_type == 'audio' ) { + wp_media_type = 'audio,video'; + } + + var button = $( this ), + custom_uploader = wp + .media( { + title: ppom_vars.i18n.choseFile, + library: { + type: wp_media_type, + }, + button: { + text: ppom_vars.i18n.upload, + }, + multiple: true, + } ) + .on( 'select', function () { + const attachments = custom_uploader + .state() + .get( 'selection' ) + .toJSON(); + + attachments.map( ( meta, index ) => { + // console.log(meta); + const fileurl = meta.url; + const fileid = meta.id; + const filename = meta.filename; + const file_title = meta.title; + + let img_icon = + ''; + + let price_metatype = 'price'; + const stock_metatype = 'stock'; + const stock_placeholder = ppom_vars.i18n.stock; + let url_field = + ''; + + // Set name key for imageselect addon + if ( meta_type == 'imageselect' ) { + var class_name = 'data-options ui-sortable-handle'; + var condidtion_attr = 'image_options'; + meta_type = 'images'; + price_placeholder = 'Price'; + url_field = + ''; + } else if ( meta_type == 'images' ) { + var class_name = 'data-options ui-sortable-handle'; + var condidtion_attr = 'image_options'; + price_metatype = 'meta_id'; + } else if ( meta_type == 'conditional_meta' ) { + meta_type = 'images'; + var class_name = 'data-options ui-sortable-handle'; + var condidtion_attr = 'image_options'; + price_placeholder = ppom_vars.i18n.metaIds; + price_metatype = 'meta_id'; + url_field = + ''; + } else { + var class_name = ''; + var condidtion_attr = ''; + } + + if ( meta.type !== 'image' ) { + img_icon = + ''; + url_field = ''; + } + + if ( fileurl ) { + let image_box = ''; + image_box += + '
  • '; + image_box += + ''; + image_box += + ''; + image_box += '
    '; + image_box += + '
    '; + image_box += img_icon; + image_box += '
    '; + image_box += + ''; + image_box += + ''; + image_box += + ''; + image_box += + ''; + + if ( meta_type != 'audio' ) { + image_box += + ''; + } + + image_box += url_field; + image_box += + ''; + image_box += '
    '; + image_box += '
  • '; + + $( image_box ).appendTo( image_append ); + + option_index++; + } + } ); + + $uploaded_image_container + .find( '#ppom-meta-opt-index' ) + .val( option_index ); + } ) + .open(); + } ); + + var $uploaded_image_container; + $( document ).on( 'click', '.ppom-pre-upload-image-btnsd', function ( e ) { + e.preventDefault(); + let meta_type = $( this ).attr( 'data-metatype' ); + $uploaded_image_container = $( this ).closest( 'div' ); + const image_append = $uploaded_image_container.find( 'ul' ); + const option_index = parseInt( + $uploaded_image_container.find( '#ppom-meta-opt-index' ).val() + ); + $uploaded_image_container + .find( '#ppom-meta-opt-index' ) + .val( option_index + 1 ); + const main_wrapper = $( this ).closest( '.ppom-slider' ); + const field_index = main_wrapper + .find( '.ppom-fields-actions' ) + .attr( 'data-field-no' ); + let price_placeholder = ppom_vars.i18n.pricePlaceholder; + wp.media.editor.send.attachment = function ( props, attachment ) { + // console.log(attachment); + let existing_images; + const fileurl = attachment.url; + const fileid = attachment.id; + let img_icon = + ''; + + let price_metatype = 'price'; + const stock_metatype = 'stock'; + const stock_placeholder = ppom_vars.i18n.stock; + + // Set name key for imageselect addon + if ( meta_type == 'imageselect' ) { + var class_name = 'data-options ui-sortable-handle'; + var condidtion_attr = 'image_options'; + meta_type = 'images'; + price_placeholder = 'Price'; + url_field = + ''; + } else if ( meta_type == 'images' ) { + var class_name = 'data-options ui-sortable-handle'; + var condidtion_attr = 'image_options'; + price_metatype = 'meta_id'; + } else if ( meta_type == 'conditional_meta' ) { + meta_type = 'images'; + var class_name = 'data-options ui-sortable-handle'; + var condidtion_attr = 'image_options'; + price_placeholder = ppom_vars.i18n.metaIds; + price_metatype = 'meta_id'; + } else { + var class_name = ''; + var condidtion_attr = ''; + } + + let url_field = + ''; + + if ( attachment.type !== 'image' ) { + img_icon = + ''; + url_field = ''; + } + + if ( fileurl ) { + let image_box = ''; + image_box += + '
  • '; + image_box += + ''; + image_box += ''; + image_box += '
    '; + image_box += '
    '; + image_box += img_icon; + image_box += '
    '; + image_box += + ''; + image_box += + ''; + image_box += + ''; + image_box += + ''; + image_box += + ''; + image_box += url_field; + image_box += + ''; + image_box += '
    '; + image_box += '
  • '; + + $( image_box ).appendTo( image_append ); + } + }; + + wp.media.editor.open( this ); + + return false; + } ); + $( document ).on( 'click', '.ppom-pre-upload-delete', function ( e ) { + e.preventDefault(); + $( this ).closest( 'li' ).remove(); + } ); + + /** + 18- Add Fields Conditions + */ + $( document ) + .on( 'click', '.ppom-add-rule', function ( e ) { + e.preventDefault(); + + const div = $( this ).parents( '.form-group' ); + const option_index = parseInt( + div.find( '.ppom-condition-last-id' ).val() + ); + div.find( '.ppom-condition-last-id' ).val( option_index + 1 ); + + const field_index = div + .parents( '.ppom-slider' ) + .find( '.ppom-fields-actions' ) + .attr( 'data-field-no' ); + const cloneElement = $( '.webcontact-rules:last', div ); + const condition_clone = cloneElement.clone(); + + const append_item = div.find( '.ppom-condition-clone-js' ); + condition_clone.find( append_item ).end().appendTo( append_item ); + + ppom_rename_rules_name(); + + const field_type = ''; + const add_cond_selector = condition_clone.find( + '.ppom-conditional-keys' + ); + ppom_add_condition_set_index( + add_cond_selector, + field_index, + field_type, + option_index + ); + + if ( $( this ).next( '.ppom-remove-rule' )?.length > 0 ) { + $( this ).remove(); + return; + } + $( '.ppom-add-rule', cloneElement ) + .removeClass( 'ppom-add-rule' ) + .addClass( 'ppom-remove-rule' ) + .removeClass( 'btn-success' ) + .addClass( 'btn-danger' ) + .html( '' ); + + // Last row. + const lastRow = jQuery( '.webcontact-rules:visible:last' ); + if ( lastRow?.find( '.ppom-remove-rule' )?.length === 0 ) { + const cloneButton = lastRow?.find( '.ppom-add-rule' ).clone(); + cloneButton + .removeClass( 'ppom-add-rule' ) + .addClass( 'ppom-remove-rule ml-1' ) + .removeClass( 'btn-success' ) + .addClass( 'btn-danger' ) + .html( '' ); + cloneButton.insertAfter( lastRow?.find( '.ppom-add-rule' ) ); + } + } ) + .on( 'click', '.ppom-remove-rule', function ( e ) { + const removeButton = $( this ); + if ( + removeButton + .parents( '.ppom-condition-clone-js' ) + ?.find( '.webcontact-rules' )?.length === 1 + ) { + removeButton + .parents( '.ppom-condition-clone-js' ) + .find( '.webcontact-rules' ) + .find( 'select' ) + .val( '' ) + .attr( 'selected', false ) + .prop( 'selected', false ); + $( this ).remove(); + ppom_rename_rules_name(); + return false; + } + removeButton.parents( '.webcontact-rules:first' ).remove(); + // Last row. + const lastRow = jQuery( '.webcontact-rules:visible:last' ); + if ( lastRow?.find( '.ppom-add-rule' )?.length === 0 ) { + const cloneButton = lastRow + ?.find( '.ppom-remove-rule' ) + .clone(); + cloneButton + .removeClass( 'ppom-remove-rule' ) + .addClass( 'ppom-add-rule' ) + .removeClass( 'btn-danger' ) + .addClass( 'btn-success' ) + .html( '' ); + cloneButton.insertBefore( + lastRow?.find( '.ppom-remove-rule' ) + ); + } + ppom_rename_rules_name(); + e.preventDefault(); + return false; + } ); + + /** + 19- Add Fields Options + */ + $( document ) + .on( 'click', '.ppom-add-option', function ( e ) { + e.preventDefault(); + + const main_wrapper = $( this ).closest( '.ppom-slider' ); + const ppom_option_type = 'ppom_new_option'; + + const li = $( this ).closest( 'li' ); + const ul = li.closest( 'ul' ); + const clone_item = li.clone(); + + clone_item.find( ul ).end().appendTo( ul ); + + const option_index = parseInt( + ul.find( '#ppom-meta-opt-index' ).val() + ); + ul.find( '#ppom-meta-opt-index' ).val( option_index + 1 ); + // console.log(option_index); + + const field_index = main_wrapper + .find( '.ppom-fields-actions' ) + .attr( 'data-field-no' ); + const option_selector = clone_item.find( '.ppom-option-keys' ); + const option_controller = clone_item.find( '.ppom-fields-option' ); + + ppom_option_controller( + option_controller, + field_index, + option_index, + ppom_option_type + ); + ppom_create_option_index( + option_selector, + field_index, + option_index, + ppom_option_type + ); + + // $('.ppom-slider').find('.data-options:not(:last) .ppom-add-option') + // .removeClass('ppom-add-option').addClass('ppom-remove-option') + // .removeClass('btn-success').addClass('btn-danger') + // .html(''); + } ) + .on( 'click', '.ppom-remove-option', function ( e ) { + const selector_btn = $( this ).closest( '.ppom-slider' ); + const option_num = selector_btn.find( '.data-options' ).length; + + if ( option_num > 1 ) { + $( this ).parents( '.data-options:first' ).remove(); + } else { + alert( ppom_vars.i18n.cannotRemoveMoreOption ); + } + + e.preventDefault(); + return false; + } ); + + /** + 20- Auto Generate Option IDs + */ + $( document ).on( 'keyup', '.option-title', function () { + const closes_id = $( this ).closest( 'li' ).find( '.option-id' ); + let option_id = $( this ) + .val() + .replace( /[^A-Z0-9]/gi, '_' ); + option_id = option_id.toLowerCase(); + $( closes_id ).val( option_id ); + } ); + + /** + 21- Create Field data_name By Thier Title + */ + $( document ).on( + 'keyup', + '[data-meta-id="title"] input[type="text"]', + function () { + /** + * If auto generated data name starts with the "_", that causes the order item meta is recognized as + * "hidden" in {prefix}_woocommerce_order_ittemmeta table order_by WC Core. To prevent that, add "f" char + * to beginning of the dataname as auto. + * + * With the prefix, all fields that will be created from now on; will be shown on order thank you page anymore. + * + * "f" is a randomly chosen character. + */ + const START_CHAR_DISALLOW_BEING_HIDDEN_FIELD = 'f'; + + const $this = $( this ); + let field_id = $this + .val() + .toLowerCase() + .replace( /[^A-Za-z\d]/g, '_' ); + + field_id = + field_id.charAt( 0 ) === '_' + ? `${ START_CHAR_DISALLOW_BEING_HIDDEN_FIELD }${ field_id }` + : field_id; + + const selector = $this.closest( '.ppom-slider' ); + + const wp_field = selector + .find( '.ppom-fields-actions' ) + .attr( 'data-table-id' ); + if ( + wp_field == 'shipping_fields' || + wp_field == 'billing_fields' + ) { + return; + } + if ( true === lockedDataName ) { + return; + } + selector + .find( + '[data-meta-id="data_name"] input[type="text"]:not([readonly])' + ) + .val( field_id ); + } + ); + + /** + 22- Fields Sortable + * @param parent + * @param element + * @param index + * @param dir + */ + function insertAt( parent, element, index, dir ) { + const el = parent.children().eq( index ); + + element[ dir == 'top' ? 'insertBefore' : 'insertAfter' ]( el ); + } + $( '.ppom_field_table tbody' ).sortable( { + stop( evt, ui ) { + let parent = $( '.ppom_save_fields_model' ), + el = parent.find( '.' + ui.item.attr( 'id' ) ), + dir = 'top'; + if ( ui.offset.top > ui.originalPosition.top ) { + dir = 'bottom'; + } + insertAt( parent, el, ui.item.index(), dir ); + }, + } ); + + /** + 23- Fields Option Sortable + */ + $( '.ppom-options-sortable' ).sortable(); + + $( 'ul.ppom-options-container' ).sortable( { + revert: true, + } ); + + /** + 24- Fields Dataname Must Be Required + * @param $this + * @param context + */ + function ppom_required_data_name( $this, context ) { + const selector = $this.closest( '.ppom-slider' ); + const data_name = selector + .find( '[data-meta-id="data_name"] input[type="text"]' ) + .val(); + const savedDataName = selector + .find( '[data-metatype="data_name"]' ) + .val(); + const allDataName = $( document ) + .find( 'table.ppom_field_table td.ppom_meta_field_id' ) + .map( function () { + const metaFieldId = $.trim( jQuery( this ).text() ); + if ( + $this.hasClass( 'ppom-update-field' ) && + data_name === metaFieldId + ) { + return ''; + } + return metaFieldId; + } ) + .get(); + if ( data_name == '' ) { + var msg = ppom_vars.i18n.dataNameRequired; + var is_ok = false; + } else if ( + ( 'new' === context || + ( 'update' === context && savedDataName !== data_name ) ) && + $.inArray( data_name, allDataName ) != -1 + ) { + var msg = ppom_vars.i18n.dataNameExists; + var is_ok = false; + } else { + msg = ''; + is_ok = true; + } + selector.find( '.ppom-req-field-id' ).html( msg ); + return is_ok; + } + + /** + WP Color Picker Controller + * @param wpcolor_selector + * @param field_index + * @param option_index + */ + function ppom_wp_color_handler( + wpcolor_selector, + field_index, + option_index + ) { + wpcolor_selector.each( function ( i, meta_field ) { + const color_picker_input = $( meta_field ) + .find( '.ppom-color-picker-init' ) + .clone(); + $( meta_field ).html( color_picker_input ); + color_picker_input.wpColorPicker(); + } ); + } + + /** + 25- Fields Add Option Index Controle Funtion + * @param option_selector + * @param field_index + * @param option_index + * @param ppom_option_type + */ + function ppom_create_option_index( + option_selector, + field_index, + option_index, + ppom_option_type + ) { + option_selector.each( function ( i, meta_field ) { + if ( ppom_option_type == 'ppom_copy_option' ) { + const opt_in = $( meta_field ).attr( 'data-opt-index' ); + if ( opt_in !== undefined ) { + option_index = opt_in; + } + } + $( meta_field ).attr( 'data-opt-index', option_index ); + + const field_name = + 'ppom[' + + field_index + + '][options][' + + option_index + + '][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + } + + function ppom_option_controller( + option_selector, + field_index, + option_index, + ppom_option_type + ) { + option_selector.each( function ( i, meta_field ) { + // console.log(ppom_option_type); + if ( ppom_option_type == 'ppom_copy_option' ) { + const opt_in = $( meta_field ).attr( 'data-opt-index' ); + if ( opt_in !== undefined ) { + option_index = opt_in; + } + } + $( meta_field ).attr( 'data-opt-index', option_index ); + + const field_name = + 'ppom[' + + field_index + + '][' + + $( meta_field ).attr( 'data-optiontype' ) + + '][' + + option_index + + '][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + } + + /** + 26- Fields Add Condition Index Controle Function + * @param add_c_selector + * @param opt_field_no + * @param field_type + * @param opt_no + */ + function ppom_add_condition_set_index( + add_c_selector, + opt_field_no, + field_type, + opt_no + ) { + // Condition rows are saved under `ppom[field][conditions][rules][n]`. + // PHP later serializes that data back into the `data-cond-*` attributes + // consumed by the storefront condition engine. + add_c_selector.each( function ( i, meta_field ) { + // var field_name = 'ppom['+field_no+']['+$(meta_field).attr('data-metatype')+']'; + const field_name = + 'ppom[' + + opt_field_no + + '][conditions][rules][' + + opt_no + + '][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + } + + // address addon + function ppom_create_address_index( + address_selector, + field_index, + address_table_id + ) { + address_selector.each( function ( i, meta_field ) { + const field_id = $( meta_field ).attr( 'data-fieldtype' ); + const core_field_type = + $( address_table_id ).attr( 'data-addresstype' ); + const field_name = + 'ppom[' + + field_index + + '][' + + core_field_type + + '][' + + field_id + + '][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + } + + // eventcalendar inputs changing + function ppom_eventcalendar_set_index( add_c_selector, opt_field_no ) { + add_c_selector.each( function ( i, meta_field ) { + const date = $( meta_field ).attr( 'data-date' ); + const field_name = + 'ppom[' + + opt_field_no + + '][calendar][' + + date + + '][' + + $( meta_field ).attr( 'data-metatype' ) + + ']'; + $( meta_field ).attr( 'name', field_name ); + } ); + } + + // Rename rules name. + function ppom_rename_rules_name() { + $( '.webcontact-rules:visible' ).each( function ( index, item ) { + $( this ) + .attr( 'id', 'rule-box-' + parseInt( index + 1 ) ) + .find( 'label' ) + .text( function ( i, txt ) { + return txt.replace( /\d+/, parseInt( index + 1 ) ); + } ); + } ); + } + + /** + * Filter the operator options list based on the target type field. + * + * @param {string?} fieldType The PPOM field type. + * @param {HTMLSelectElement} operatorSelectField The select input for condition operator. + * @return + */ + function toggleOperatorFieldByTargetType( fieldType, operatorSelectField ) { + if ( ! operatorSelectField ) { + return; + } + + let shouldHideSelectInput = true; + const currentValue = operatorSelectField?.value; + + operatorSelectField + .querySelectorAll( 'optgroup' ) + .forEach( ( optgroup ) => { + let shouldHideGroup = true; + optgroup.querySelectorAll( 'option' ).forEach( ( option ) => { + const isAvailable = + option?.value && + OPERATORS_FIELD_COMPATIBILITY[ option.value ] + ? OPERATORS_FIELD_COMPATIBILITY[ + option.value + ].includes( fieldType ) + : option?.value; + if ( shouldHideGroup && isAvailable ) { + shouldHideGroup = false; + } + + if ( option.value === currentValue && ! isAvailable ) { + operatorSelectField.value = 'any'; // NOTE: Default to 'any' if the current select value is unavailable. + } + + option.classList.toggle( + 'ppom-hide-element', + ! isAvailable + ); + } ); + + if ( ! shouldHideGroup ) { + shouldHideSelectInput = false; + } + + optgroup.classList.toggle( + 'ppom-hide-element', + shouldHideGroup + ); + } ); + + operatorSelectField.classList.toggle( + 'ppom-invisible-element', + shouldHideSelectInput + ); + tryToggleConditionInputFields( operatorSelectField ); + } + + /** + * Refresh the comparison-value list for the selected condition target. + * + * @param {HTMLSelectElement} targetSelect + * @param {HTMLDivElement} [conditionContainer] + * @param {string} [initialSelectedValue] + * @see populate_conditional_elements + */ + function updateTargetComparisonValueSelect( + targetSelect, + conditionContainer, + initialSelectedValue + ) { + // Comparison values are not hard-coded. They are pulled from the current + // options/images configured on the target field so the condition builder + // stays synchronized with whatever the merchant has edited in this session. + /** @type {string?} */ + const targetElementNameToPullOptions = targetSelect.value; + + /** @type {HTMLDivElement?} */ + conditionContainer ??= targetSelect.closest( '.webcontact-rules' ); + const targetSelectOptions = conditionContainer?.querySelector( + 'select[data-metatype="element_values"]' + ); + + if ( ! conditionContainer || ! targetSelectOptions ) { + return; + } + + document.querySelectorAll( '.ppom-slider' ).forEach( ( sliderItem ) => { + const targetElementFieldId = sliderItem.querySelector( + 'input[data-metatype="data_name"]' + )?.value; + if ( targetElementFieldId !== targetElementNameToPullOptions ) { + return; + } + + const operatorsInput = conditionContainer.querySelector( + '[data-metatype="operators"]' + ); + if ( ! operatorsInput ) { + return; + } + + // Reset the options lists based on the new selection. + const newOptions = []; + + sliderItem.querySelectorAll( '.data-options' ).forEach( + /** @type {HTMLDivElement} */ ( conditionValueContainer ) => { + const condition_type = conditionValueContainer.getAttribute( + 'data-condition-type' + ); + + const conditionValueId = conditionValueContainer + .querySelector( + condition_type === 'simple_options' + ? 'input[data-metatype="option"]' + : '.ppom-image-option-title' + ) + ?.value?.trim(); + + if ( ! conditionValueId ) { + return; + } + + const optionElement = document.createElement( 'option' ); + optionElement.value = ppom_escape_html( conditionValueId ); + optionElement.textContent = conditionValueId; + + newOptions.push( optionElement ); + } + ); + targetSelectOptions.replaceChildren( ...newOptions ); + } ); + + if ( initialSelectedValue ) { + targetSelectOptions.value = initialSelectedValue; + } + } + /** + * Toggle the visibility for input fields type based on operator current value. + * + * @param {HTMLSelectElement?} conditionOperatorInput + * @return + */ + function tryToggleConditionInputFields( conditionOperatorInput ) { + if ( ! conditionOperatorInput ) { + return; + } + + const selectedOperator = conditionOperatorInput?.value; + + /** + * @type {HTMLDivElement|null} + */ + const container = + conditionOperatorInput?.closest( '.webcontact-rules' ); + if ( ! container ) { + return; + } + + /** + * @type {HTMLSelectElement|null} + */ + const conditionTargetSelectOptionsInput = container.querySelector( + 'select[data-metatype="element_values"]' + ); + + /** + * @type {HTMLInputElement|null} + */ + const conditionConstantInput = container.querySelector( + '[data-metatype="element_constant"]' + ); + + /** + * @type {HTMLSelectElement|null} + */ + const conditionTargetSelectInput = container.querySelector( + '[data-metatype="elements"]' + ); + if ( ! conditionConstantInput || ! conditionTargetSelectInput ) { + return; + } + + /** + * @type {HTMLDivElement|null} + */ + const betweenInputs = container.querySelector( + '.ppom-between-input-container' + ); + + let shouldHideSelectInput = false; + let shouldHideTextInput = false; + let shouldHideBetweenInputs = false; + let shouldHideUpsell = true; + + if ( proOperatorOptionsToLock.has( selectedOperator ) ) { + shouldHideSelectInput = true; + shouldHideTextInput = true; + shouldHideBetweenInputs = true; + shouldHideUpsell = false; + } else if ( 'between' === selectedOperator ) { + shouldHideSelectInput = true; + shouldHideTextInput = true; + shouldHideBetweenInputs = false; + } else if ( HIDE_COMPARISON_INPUT_FIELD.includes( selectedOperator ) ) { + shouldHideSelectInput = true; + shouldHideTextInput = true; + shouldHideBetweenInputs = true; + } else { + shouldHideSelectInput = true; + shouldHideBetweenInputs = true; + + /** + * @type {HTMLOptionElement|null} + */ + const targetFieldTypeInput = + conditionTargetSelectInput.querySelector( + `option[value="${ conditionTargetSelectInput.value }"]` + ); + if ( + conditionTargetSelectOptionsInput && + COMPARISON_VALUE_CAN_USE_SELECT.includes( selectedOperator ) && + targetFieldTypeInput?.dataset?.fieldtype && + OPERATOR_COMPARISON_VALUE_FIELD_TYPE.select.includes( + targetFieldTypeInput.dataset.fieldtype + ) + ) { + shouldHideTextInput = true; + shouldHideSelectInput = false; + } + } + + if ( + shouldHideSelectInput && + shouldHideTextInput && + shouldHideBetweenInputs && + shouldHideUpsell + ) { + conditionConstantInput.parentNode?.classList.add( + 'ppom-invisible-element' + ); // NOTE: Make the entire container visible to preserve the space. + } else { + conditionTargetSelectOptionsInput?.classList.toggle( + 'ppom-hide-element', + shouldHideSelectInput + ); + conditionConstantInput.classList.toggle( + 'ppom-hide-element', + shouldHideTextInput + ); + betweenInputs?.classList.toggle( + 'ppom-hide-element', + shouldHideBetweenInputs + ); + container + .querySelector( '.ppom-upsell-condition' ) + ?.classList.toggle( 'ppom-hide-element', shouldHideUpsell ); + + conditionConstantInput.parentNode?.classList.remove( + 'ppom-invisible-element' + ); + } + } + + // Apply actions on initialization based on operator value. + document + .querySelectorAll( 'select[data-metatype="operators"]' ) + .forEach( ( conditionOperatorInput ) => { + tryToggleConditionInputFields( conditionOperatorInput ); + } ); + + // Apply actions when operator value changes. + document.addEventListener( 'change', function ( e ) { + if ( ! e.target.matches( 'select[data-metatype="operators"]' ) ) { + return; + } + + e.preventDefault(); + tryToggleConditionInputFields( e.target ); + } ); + + $( document ).on( + 'change', + '[data-meta-id="conditions"] select[data-metatype="element_values"]', + function ( e ) { + e.preventDefault(); + + const element_values = $( this ).val(); + $( this ).attr( 'data-existingvalue', element_values ); + } + ); + + $( document ).on( 'click', '.ppom-condition-tab-js', function ( e ) { + e.preventDefault(); + populate_conditional_elements(); + } ); + + /** + * Populate the condition target select with eligible options based on the operator. + * + * @param {HTMLSelectElement?} selectInput + * @param {string[]} excludeIds + * @return + */ + function populate_condition_target( + selectInput, + conditionOperator, + excludeIds = [] + ) { + if ( ! selectInput ) { + return; + } + + const newOptions = availableConditionTargets + .filter( + ( { fieldId, canUse } ) => + canUse && ! excludeIds.includes( fieldId ) + ) + .map( ( target ) => { + const option = document.createElement( 'option' ); + option.value = target.fieldId; + option.textContent = target.fieldLabel; + option.dataset.fieldtype = target.fieldType; + + return option; + } ); + + selectInput.replaceChildren( ...newOptions ); + } + + function findFieldTypeById( fieldId ) { + if ( ! fieldId ) { + return undefined; + } + + for ( const target of availableConditionTargets ) { + if ( target.fieldId === fieldId ) { + return target.fieldType; + } + } + + return undefined; + } + + function can_use_field_type( fieldType ) { + if ( ! fieldType?.length ) { + return false; + } + + for ( const operatorCompatibleFields of Object.values( + OPERATORS_FIELD_COMPATIBILITY + ) ) { + if ( operatorCompatibleFields.includes( fieldType ) ) { + return true; + } + } + + return false; + } + + /** + * Rebuild condition targets from the current builder state. + * + * This keeps the condition tab in sync with renamed or newly cloned fields + * before the field group has been saved and re-rendered by PHP. + * + * @see ppom_add_condition_set_index + * @see ppom_check_conditions in js/ppom-conditions-v2.js + */ + function populate_conditional_elements() { + // Rebuild the list of condition targets from the current slider state so + // newly added, renamed, or cloned fields are immediately available as rule + // dependencies without reloading the admin page. + // Get all available PPOM fields. + availableConditionTargets.splice( 0, availableConditionTargets.length ); + document.querySelectorAll( '.ppom-slider' ).forEach( ( item ) => { + const fieldLabel = item.querySelector( + 'input[data-metatype="title"]' + )?.value; + const fieldId = item + .querySelector( 'input[data-metatype="data_name"]' ) + ?.value?.trim(); + const fieldType = item.querySelector( + 'input[data-metatype="type"]' + )?.value; + const canUse = can_use_field_type( fieldType ); + + if ( ! fieldLabel || ! fieldId || ! fieldType ) { + return; + } + + availableConditionTargets.push( { + fieldLabel, + fieldId, + fieldType, + canUse, + } ); + } ); + + // Change the target options for all the rules. + document.querySelectorAll( '.ppom-slider' ).forEach( ( item ) => { + if ( ! item.id ) { + return; + } + const conditionContainers = item + .querySelector( 'div[data-meta-id="conditions"]' ) + ?.querySelectorAll( '.webcontact-rules' ); + + conditionContainers?.forEach( ( conditionContainer ) => { + const conditionTargetsSelect = conditionContainer.querySelector( + '[data-metatype="elements"]' + ); + if ( ! conditionTargetsSelect ) { + return; + } + + const conditionOperatorSelect = + conditionContainer.querySelector( + '[data-metatype="operators"]' + ); + const fieldId = item + .querySelector( 'input[data-metatype="data_name"]' ) + ?.value?.trim(); + + populate_condition_target( + conditionTargetsSelect, + conditionOperatorSelect?.value, + [ fieldId ] + ); + + if ( conditionTargetsSelect?.dataset?.existingvalue ) { + conditionTargetsSelect.value = + conditionTargetsSelect?.dataset?.existingvalue; + } + + // NOTE: Get all the locked operators. Unlock them to be eligible to show the upsell. + conditionOperatorSelect + ?.querySelectorAll( 'option' ) + .forEach( ( option ) => { + if ( ! option.disabled ) { + return; + } + + proOperatorOptionsToLock.add( option.value ); + option.disabled = false; + } ); + + toggleOperatorFieldByTargetType( + findFieldTypeById( conditionTargetsSelect?.value ), + conditionOperatorSelect + ); + + const optionsInput = conditionContainer.querySelector( + '[data-metatype="element_values"]' + ); + + updateTargetComparisonValueSelect( + conditionTargetsSelect, + conditionContainer, + optionsInput?.dataset?.existingvalue + ); + } ); + } ); + } + + /** + * Update the values of the operators selector and the comparison fields. + * + * NOTE: We are using a global listener since some node are dinamically created/cloned. + */ + document.addEventListener( 'change', function ( e ) { + if ( ! e.target.matches( 'select[data-metatype="elements"]' ) ) { + return; + } + + e.preventDefault(); + const conditionContainer = e.target.closest( '.webcontact-rules' ); + const conditionOperatorSelect = conditionContainer?.querySelector( + '[data-metatype="operators"]' + ); + if ( ! conditionContainer || ! conditionOperatorSelect ) { + return; + } + + toggleOperatorFieldByTargetType( + findFieldTypeById( e.target?.value ), + conditionOperatorSelect + ); + updateTargetComparisonValueSelect( e.target, conditionContainer ); + + const optionsInput = conditionContainer.querySelector( + '[data-metatype="element_values"]' + ); + const constantInput = conditionContainer.querySelector( + '[data-metatype="element_constant"]' + ); + + // Reset values. + if ( constantInput ) { + constantInput.value = ''; + } + + if ( optionsInput ) { + optionsInput.value = ''; + } + } ); + + /** + 28- validate API WooCommerce Product + * @param form + */ + function validate_api_wooproduct( form ) { + jQuery( form ) + .find( '#nm-sending-api' ) + .html( '' ); + + let data = jQuery( form ).serialize(); + data = data + '&action=nm_personalizedproduct_validate_api'; + + jQuery.post( + ajaxurl, + data, + function ( resp ) { + //console.log(resp); + jQuery( form ).find( '#nm-sending-api' ).html( resp.message ); + if ( resp.status == 'success' ) { + window.location.reload( true ); + } + }, + 'json' + ); + + return false; + } + + function ppom_escape_html( unsafe ) { + return unsafe + .replace( /&/g, '&' ) + .replace( //g, '>' ) + .replace( /"/g, '"' ) + .replace( /'/g, ''' ); + } + + $( document ).on( + 'ppom_fields_tab_changed', + 'div.row.ppom-tabs', + ( e, id, tab ) => { + if ( ppom_vars.i18n.freemiumCFRTab !== id ) { + return; + } + + if ( tab.find( '.freemium-cfr-content' ).length > 0 ) { + return; + } + + $( + `
    ${ ppom_vars.i18n.freemiumCFRContent }
    ` + ).insertAfter( tab.find( '.form-group' ) ); + } + ); + + const toggleHandler = { + // jQuery datepicker-specific settings should only be editable when the + // field actually opts into the jQuery UI datepicker renderer. + setDisabledFields( jQueryDP ) { + const on = jQueryDP.is( ':checked' ); + const slider = jQueryDP.parents( '.ppom-slider' ); + + const JQUERY_DP_FIELD_MTYPES = [ + 'min_date', + 'max_date', + 'date_formats', + 'default_value', + 'first_day_of_week', + 'year_range', + 'no_weekends', + 'past_dates', + ]; + + JQUERY_DP_FIELD_MTYPES.forEach( function ( type ) { + slider + .find( '*[data-metatype="' + type + '"]' ) + .prop( 'disabled', ! on ); + } ); + }, + activateHandler() { + toggleHandler.setDisabledFields( $( this ) ); + }, + }; + + $( 'input[data-metatype="jquery_dp"]' ).change( + toggleHandler.activateHandler + ); + + $( document ).on( + 'ppom_new_field_created', + function ( e, clone_new_field ) { + $( clone_new_field ) + .find( 'input[data-metatype="jquery_dp"]' ) + .change( toggleHandler.activateHandler ); + } + ); + + // Track whether the builder has diverged from the last persisted state so + // accidental navigation does not discard a large field-group edit session. + let unsaved = false; + $( '.ppom-main-field-wrapper :input' ).change( function () { + if ( $( this ).parents( '.ppom-checkboxe-style' )?.length > 0 ) { + unsaved = false; + return; + } + unsaved = true; + } ); + $( document ).on( + 'click', + '.ppom-submit-btn input.btn, button.ppom_copy_field, button.ppom-add-fields-js-action, button.ppom-js-modal-close', + function () { + if ( + $( this ).hasClass( 'ppom_copy_field' ) || + $( this ).hasClass( 'ppom-add-fields-js-action' ) + ) { + unsaved = true; + return; + } + unsaved = false; + } + ); + window.addEventListener( 'beforeunload', function ( e ) { + if ( unsaved ) { + e.preventDefault(); + e.returnValue = ''; + } + } ); + + $( document ).on( 'ppom_fields_tab_changed', function ( e, id, tab ) { + if ( 'condition_tab' !== id ) { + return; + } + + if ( + ! $( 'input[data-metatype="logic"]', tab?.first() )?.is( + ':checked' + ) + ) { + tab?.last()?.addClass( 'ppom-disabled-overlay' ); + } + } ); + + $( document ).on( + 'change', + 'input[data-metatype="logic"]:visible', + function () { + $( this ) + .parents( '.ppom_handle_condition_tab' ) + .next( '.ppom_handle_condition_tab' ) + .toggleClass( 'ppom-disabled-overlay' ); + } + ); + + $( document ).on( + 'click', + 'button.ppom-edit-field.ppom-is-pro-field, button.ppom_copy_field.ppom-is-pro-field', + function () { + $( '#ppom-lock-fields-upsell' ).fadeIn(); + return false; + } + ); + + $( document ).ready( function () { + $( '.ppom-slider' ).each( function ( i, item ) { + const itemEl = $( item ); + const value = itemEl + .find( 'input[data-metatype="data_name"]' ) + .val(); + + const type = itemEl.find( 'input[data-metatype="type"]' ).val(); + + if ( $.trim( value ) === '' || type !== 'date' ) { + return; + } + + toggleHandler.setDisabledFields( + itemEl.find( 'input[data-metatype="jquery_dp"]' ) + ); + } ); + } ); + $( document ).on( 'click', '.postbox-header', function () { + const postbox = $( this ).closest( '.postbox' ); + postbox.toggleClass( 'closed' ); + } ); +} ); +document.querySelectorAll( '.ppom-modal-shortcuts a' )?.forEach( ( anchor ) => { + anchor.addEventListener( 'click', function ( e ) { + e.preventDefault(); + // Get the href attribute and use it as the ID to display the corresponding section + const targetId = this.getAttribute( 'href' ); + if ( targetId === '#all' ) { + // Show all sections + document + .querySelectorAll( '.ppom-fields-section' ) + .forEach( ( section ) => { + section.style.display = 'block'; + } ); + } else { + // Hide all sections with the class 'ppom-fields-section' + document + .querySelectorAll( '.ppom-fields-section' ) + .forEach( ( section ) => { + section.style.display = 'none'; + } ); + + const targetSection = document.querySelector( + targetId + '-ppom-fields' + ); + if ( targetSection ) { + targetSection.style.display = 'block'; + } + } + } ); +} ); /** * Search Field for Add Field modal. */ -document.querySelector('input[name="ppom-search-field"]')?.addEventListener('input', function() { - const query = this.value.toLowerCase(); - const sections = document.querySelectorAll('.ppom-fields-section'); - - sections.forEach(section => { - const buttons = section.querySelectorAll('.ppom-field-item'); - let hasVisibleButton = false; - - buttons.forEach(button => { - const text = button.textContent.toLowerCase(); - - if ( text.includes( query ) ) { - button.style.display = 'flex'; - hasVisibleButton = true; - } else { - button.style.display = 'none'; - } - }); - - // If no buttons in this section are visible, hide the section - if ( hasVisibleButton ) { - section.style.display = 'flex'; - } else { - section.style.display = 'none'; - } - }); -}); \ No newline at end of file +document + .querySelector( 'input[name="ppom-search-field"]' ) + ?.addEventListener( 'input', function () { + const query = this.value.toLowerCase(); + const sections = document.querySelectorAll( '.ppom-fields-section' ); + + sections.forEach( ( section ) => { + const buttons = section.querySelectorAll( '.ppom-field-item' ); + let hasVisibleButton = false; + + buttons.forEach( ( button ) => { + const text = button.textContent.toLowerCase(); + + if ( text.includes( query ) ) { + button.style.display = 'flex'; + hasVisibleButton = true; + } else { + button.style.display = 'none'; + } + } ); + + // If no buttons in this section are visible, hide the section + if ( hasVisibleButton ) { + section.style.display = 'flex'; + } else { + section.style.display = 'none'; + } + } ); + } ); diff --git a/js/admin/ppom-bulkquantity.js b/js/admin/ppom-bulkquantity.js index ad63578e..8053779f 100644 --- a/js/admin/ppom-bulkquantity.js +++ b/js/admin/ppom-bulkquantity.js @@ -1,243 +1,286 @@ -"use strict" -jQuery(function($){ - - /********************************* - * PPOM Bulk Quantity Addon JS * - **********************************/ - - - /*------------------------------------------------------- - - ------ Its Include Following Function ----- - - 1- Body Selector - 2- Add New Quantity Row - 3- Remove Quantity Row - 4- Remove Variation Colunm - 5- Add Bulk Variation Colunm - 6- Save Bulk Quantity Meta - 7- Edit Bulk Quantity Meta - --------------------------------------------------------*/ - - - /** - 1- Body Selector - **/ - var body = $('body'); - +'use strict'; + +/** + * Admin editor for the bulkquantity field type. + * + * The table built here is serialized into JSON and stored in the field meta. + * Frontend scripts later read that JSON to map entered quantities to the + * matching price/base-price row on the product page. + * + * @see ppom_bulkquantity_price_manager in js/ppom.inputs.js + */ + +/** + * Row shape produced by `tableToJSON()` for a bulkquantity matrix. + * + * The first two headers are expected to remain "Quantity Range" and + * "Base Price"; any additional headers represent variation labels whose cell + * values become the unit prices for that range. + * + * @typedef {{ + * 'Quantity Range': string, + * 'Base Price'?: string + * }} PPOMBulkQuantityRow + */ +jQuery( function ( $ ) { + const body = $( 'body' ); + + // Encapsulates the table editor so add/edit flows reuse the same validation + // and input-mask setup before the hidden JSON payload is updated. const ppomBQ = { + // Range inputs are kept as strings like `1-10` because `tableToJSON()` + // later serializes visible table cells, not hidden numeric inputs. setMaskRangeInput() { - $('.ppom-bulk-qty-val-picker,.ppom-bulk-qty-val').each((i, el)=>{ - let input = $(el); + $( '.ppom-bulk-qty-val-picker,.ppom-bulk-qty-val' ).each( + ( i, el ) => { + const input = $( el ); - if( input.inputmask('hasMaskedValue') ) { - return true; - } + if ( input.inputmask( 'hasMaskedValue' ) ) { + return true; + } - input.inputmask({regex: "[0-9]*-[0-9]*"}); - }); + input.inputmask( { regex: '[0-9]*-[0-9]*' } ); + } + ); }, - formValidation(formData) { - const pattern = new RegExp('^([0-9]+)-([0-9]+)$'); - const notification = (msgSlug, magicValues) => { - let msg = ppom_bq.i18n.validation[msgSlug]; - - for(const [key, value] of Object.entries(magicValues)){ - msg = msg.replace(`{${key}}`, value); + /** + * Validate the matrix before it is persisted into the hidden JSON field. + * + * The frontend price resolver assumes ranges are non-overlapping and + * ordered as `start-end`; allowing bad data here would make product-page + * price lookups ambiguous. + * + * @param {PPOMBulkQuantityRow[]} formData + * @return {boolean} + */ + formValidation( formData ) { + const pattern = new RegExp( '^([0-9]+)-([0-9]+)$' ); + const notification = ( msgSlug, magicValues ) => { + let msg = ppom_bq.i18n.validation[ msgSlug ]; + + for ( const [ key, value ] of Object.entries( magicValues ) ) { + msg = msg.replace( `{${ key }}`, value ); } - alert(msg); - } + alert( msg ); + }; const globalRanges = []; - for( const el of formData ) { - let range = el['Quantity Range']; + for ( const el of formData ) { + const range = el[ 'Quantity Range' ]; - if( ! pattern.test(range) ) { - notification('invalid_pattern', {range}); + if ( ! pattern.test( range ) ) { + notification( 'invalid_pattern', { range } ); return false; } - let rangeVals = range.split('-'); - let start = parseInt(rangeVals[0]); - let end = parseInt(rangeVals[1]); + const rangeVals = range.split( '-' ); + const start = parseInt( rangeVals[ 0 ] ); + const end = parseInt( rangeVals[ 1 ] ); // rule: start value should be lower than the end value - if( end= aStart && start <= aEnd ) || ( end >= aStart && end <= aEnd ) ) { - notification('range_intersection', { - range1:range, - range2:aRange - }) + for ( const anotherRange of globalRanges ) { + const aStart = parseInt( anotherRange.start ); + const aEnd = parseInt( anotherRange.end ); + const aRange = anotherRange.range; + + if ( + ( start >= aStart && start <= aEnd ) || + ( end >= aStart && end <= aEnd ) + ) { + notification( 'range_intersection', { + range1: range, + range2: aRange, + } ); return false; } } - globalRanges.push({start, end, range}); + globalRanges.push( { start, end, range } ); } return true; }, - showEditForm(el) { - var bulk_wrap = el.closest('.ppom-bulk-quantity-wrapper'); - bulk_wrap.find('table').find('tbody tr td').each(function(index, el) { - - var class_name = $(el).attr('id'); - var td_wrap = $(this); - var cross_icon = ''; - if (class_name == 'ppom-bulkqty-adjust-cross') { - var input = ''+cross_icon+''; - }else{ - var input = ''; - } + /** + * Turn the read-only preview table back into an editable grid. + * + * The saved state is rendered as plain table text for compactness inside + * the builder. Editing reverses that presentation step by replacing each + * cell with an input and restoring the row-removal affordance. + * + * @param {JQuery} el + * @return {void} + */ + showEditForm( el ) { + const bulk_wrap = el.closest( '.ppom-bulk-quantity-wrapper' ); + bulk_wrap + .find( 'table' ) + .find( 'tbody tr td' ) + .each( function ( index, el ) { + const class_name = $( el ).attr( 'id' ); + const td_wrap = $( this ); + const cross_icon = + ''; + if ( class_name == 'ppom-bulkqty-adjust-cross' ) { + var input = + '' + + cross_icon + + ''; + } else { + var input = + ''; + } - td_wrap.closest('td').html(input); - }); + td_wrap.closest( 'td' ).html( input ); + } ); // show action - $(this).hide(); - bulk_wrap.find('.ppom-bulk-action-wrap').show(); - bulk_wrap.find('.ppom-save-bulk-json').show(); + $( this ).hide(); + bulk_wrap.find( '.ppom-bulk-action-wrap' ).show(); + bulk_wrap.find( '.ppom-save-bulk-json' ).show(); this.setMaskRangeInput(); - } - } + }, + }; - body.ready(function(){ + body.ready( function () { ppomBQ.setMaskRangeInput(); - }); + } ); - $(document).on('ppom_new_field_created', (e, newField, fieldNo, fieldType)=>{ - if( fieldType !== 'bulkquantity' ) { - return; + $( document ).on( + 'ppom_new_field_created', + ( e, newField, fieldNo, fieldType ) => { + if ( fieldType !== 'bulkquantity' ) { + return; + } + + ppomBQ.setMaskRangeInput(); } + ); + + body.on( 'click', 'button.ppom-add-bulk-qty-row', function ( e ) { + e.preventDefault(); + + const main_wrapper = $( this ).closest( '.ppom-slider' ); + const field_index = main_wrapper + .find( '.ppom-fields-actions' ) + .attr( 'data-field-no' ); + const bulk_div = $( this ).closest( 'div' ); + const bulk_qty_val = bulk_div.find( '.ppom-bulk-qty-val' ).val(); + const table = $( this ).closest( 'div.table-content' ), + tbody = table.find( 'tbody' ), + thead = table.find( 'thead' ); + + // Clone the last visible row so merchants keep the same number of + // variation columns while defining the next quantity interval. + const clon_qty_section = tbody.find( 'tr:last-child' ).clone(); + clon_qty_section + .find( '.ppom-bulk-qty-val-picker' ) + .val( bulk_qty_val ); + clon_qty_section.appendTo( tbody ); ppomBQ.setMaskRangeInput(); - }); - - /** - 2- Add New Quantity Row - **/ - body.on('click', 'button.ppom-add-bulk-qty-row', function (e) { - e.preventDefault(); - - var main_wrapper = $(this).closest('.ppom-slider'); - var field_index = main_wrapper.find('.ppom-fields-actions').attr('data-field-no'); - var bulk_div = $(this).closest('div'); - var bulk_qty_val = bulk_div.find('.ppom-bulk-qty-val').val(); - var table = $(this).closest('div.table-content'), - tbody = table.find('tbody'), - thead = table.find('thead'); - - var clon_qty_section = tbody.find('tr:last-child').clone(); - clon_qty_section.find('.ppom-bulk-qty-val-picker').val(bulk_qty_val); - clon_qty_section.appendTo(tbody); + } ); - ppomBQ.setMaskRangeInput(); - }); - - - /** - 3- Remove Quantity Row - **/ - body.on('click', 'span.ppom-rm-bulk-qty', function (e) { - e.preventDefault(); - - var count = $(this).closest('tbody').find('tr').length; - if ( count < 2 ) { - alert('sorry! you can not remove more textbox'); - return; - } - $(this).closest('tr').remove(); - }); - - - /** - 4- Remove Variation Colunm - **/ - body.on('click', 'span.ppom-rm-bulk-variation', function (e) { - e.preventDefault(); - - var cell = $(this).closest('th'), - index = cell.index() + 1; - cell.closest('table').find('th, td').filter(':nth-child(' + index + ')').remove(); - }); - - - /** - 5- Add Bulk Variation Colunm - **/ - body.on('click', 'button.ppom-add-bulk-variation-col', function (e) { - e.preventDefault(); - - var buk_div = $(this).closest('div'); - var bulk_variation_val = buk_div.find('.ppom-bulk-variation-val').val(); - // console.log(bulk_variation_val); - var table = $(this).closest('div.table-content').find('table'), - thead = table.find('thead'), - lastTheadRow = thead.find('tr:last-child'), - tbody = table.find('tbody'); - var closest_td = tbody.find('td:last-child'); - - $('
    '; - $ppom_html .= ''; - $ppom_html .= '
    '; + $ppom_html .= ''; + $ppom_html .= '
    '; - html += ''; - html += ''; - html += '
    '; - html += ''; - html += ''; - html += '
    '; - html += '
    ' + data_name + '' + type + '' + title + '' + placeholder + '' + _ok + ''; - html += ''; - html += ''; - html += '
    '; + html += ''; + html += ''; + html += '
    '; + html += + ''; + html += + ''; + html += '
    '; + html += '
    ' + data_name + '' + type + '' + title + '' + placeholder + '' + _ok + ''; + html += + ''; + html += + ''; + html += '
    ', { - 'html': ' '+bulk_variation_val+' ' - }).appendTo(lastTheadRow); - $('', { - 'html': '' - }).insertAfter(closest_td); - }); - - - /** - 6- Save Bulk Quantity Meta - **/ - $('body').on('click', '.ppom-save-bulk-json', function(event) { - event.preventDefault(); - - const bulk_wrap = $(this).closest('.ppom-bulk-quantity-wrapper'); - bulk_wrap.find('table').find('input').each(function(index, el) { - const td_wrap = $(this); - td_wrap.closest('td').html(td_wrap.val()); - }); - const bulkData = bulk_wrap.find('table').tableToJSON(); - - if( ! ppomBQ.formValidation(bulkData) ) { - ppomBQ.showEditForm($(this)); + body.on( 'click', 'span.ppom-rm-bulk-qty', function ( e ) { + e.preventDefault(); + + const count = $( this ).closest( 'tbody' ).find( 'tr' ).length; + if ( count < 2 ) { + alert( 'sorry! you can not remove more textbox' ); + return; + } + $( this ).closest( 'tr' ).remove(); + } ); + + body.on( 'click', 'span.ppom-rm-bulk-variation', function ( e ) { + e.preventDefault(); + + const cell = $( this ).closest( 'th' ), + index = cell.index() + 1; + cell.closest( 'table' ) + .find( 'th, td' ) + .filter( ':nth-child(' + index + ')' ) + .remove(); + } ); + + body.on( 'click', 'button.ppom-add-bulk-variation-col', function ( e ) { + e.preventDefault(); + + const buk_div = $( this ).closest( 'div' ); + const bulk_variation_val = buk_div + .find( '.ppom-bulk-variation-val' ) + .val(); + // console.log(bulk_variation_val); + const table = $( this ).closest( 'div.table-content' ).find( 'table' ), + thead = table.find( 'thead' ), + lastTheadRow = thead.find( 'tr:last-child' ), + tbody = table.find( 'tbody' ); + const closest_td = tbody.find( 'td:last-child' ); + + // Every added column becomes a new object key when `tableToJSON()` + // serializes the matrix, so the header text is the persisted identifier. + $( '', { + html: + ' ' + + bulk_variation_val + + ' ', + } ).appendTo( lastTheadRow ); + $( '', { + html: '', + } ).insertAfter( closest_td ); + } ); + + // Convert the editable grid back into the JSON blob later consumed by + // `ppom_bulkquantity_price_manager()` on the product page. + $( 'body' ).on( 'click', '.ppom-save-bulk-json', function ( event ) { + event.preventDefault(); + + const bulk_wrap = $( this ).closest( '.ppom-bulk-quantity-wrapper' ); + bulk_wrap + .find( 'table' ) + .find( 'input' ) + .each( function ( index, el ) { + const td_wrap = $( this ); + td_wrap.closest( 'td' ).html( td_wrap.val() ); + } ); + const bulkData = bulk_wrap.find( 'table' ).tableToJSON(); + + if ( ! ppomBQ.formValidation( bulkData ) ) { + ppomBQ.showEditForm( $( this ) ); return; } - bulk_wrap.find('.ppom-saved-bulk-data').val(JSON.stringify(bulkData)); - - // hide action - $(this).hide(); - bulk_wrap.find('.ppom-bulk-action-wrap').hide(); - bulk_wrap.find('.ppom-edit-bulk-json').show(); - }); - - - /** - 7- Edit Bulk Quantity Meta - **/ - $('body').on('click', '.ppom-edit-bulk-json', function(event) { - event.preventDefault(); - ppomBQ.showEditForm($(this)); - }); - -}); \ No newline at end of file + bulk_wrap + .find( '.ppom-saved-bulk-data' ) + .val( JSON.stringify( bulkData ) ); + + // hide action + $( this ).hide(); + bulk_wrap.find( '.ppom-bulk-action-wrap' ).hide(); + bulk_wrap.find( '.ppom-edit-bulk-json' ).show(); + } ); + + // Rehydrate the read-only table into inputs so an existing matrix can be edited. + $( 'body' ).on( 'click', '.ppom-edit-bulk-json', function ( event ) { + event.preventDefault(); + ppomBQ.showEditForm( $( this ) ); + } ); +} ); diff --git a/js/admin/ppom-deactivate.js b/js/admin/ppom-deactivate.js index 97e756e4..23526f4a 100644 --- a/js/admin/ppom-deactivate.js +++ b/js/admin/ppom-deactivate.js @@ -1,70 +1,91 @@ /** - * Getting user response when deactivate plugin - * */ -"use strict" -jQuery(function($){ - - var modal = $('#ppom-deactivate-modal'); - var deactivateLink = ''; + * Deactivation survey modal for the plugin list screen. + * + * The collected reason is optional telemetry; the saved redirect target still + * drives the final plugin deactivation link after the modal flow completes. + */ +'use strict'; +jQuery( function ( $ ) { + const modal = $( '#ppom-deactivate-modal' ); + let deactivateLink = ''; + $( '#the-list' ).on( 'click', 'a.ppom-deactivate-link', function ( e ) { + e.preventDefault(); + modal.addClass( 'modal-active' ); + deactivateLink = $( this ).attr( 'href' ); + modal + .find( 'a.dont-bother-me' ) + .attr( 'href', deactivateLink ) + .css( 'float', 'left' ); + } ); - $('#the-list').on('click', 'a.ppom-deactivate-link', function (e) { - e.preventDefault(); - modal.addClass('modal-active'); - deactivateLink = $(this).attr('href'); - modal.find('a.dont-bother-me').attr('href', deactivateLink).css('float', 'left'); - }); + $( '#ppom-deactivate-modal' ).on( + 'click', + 'a.review-and-deactivate', + function ( e ) { + e.preventDefault(); + window.open( + 'https://wordpress.org/support/plugin/woocommerce-product-addon/reviews/#new-post' + ); + window.location.href = deactivateLink; + } + ); + modal.on( 'click', 'button.pipe-model-cancel', function ( e ) { + e.preventDefault(); + modal.removeClass( 'modal-active' ); + } ); + modal.on( 'click', 'input[type="radio"]', function () { + const parent = $( this ).parents( 'li:first' ); + modal.find( '.reason-input' ).remove(); + const inputType = parent.data( 'type' ), + inputPlaceholder = parent.data( 'placeholder' ); + if ( 'reviewhtml' === inputType ) { + var reasonInputHtml = + ''; + } else { + var reasonInputHtml = + '
    ' + + ( 'text' === inputType + ? '' + : '' ) + + '
    '; + } + if ( inputType !== '' ) { + parent.append( $( reasonInputHtml ) ); + parent + .find( 'input, textarea' ) + .attr( 'placeholder', inputPlaceholder ) + .focus(); + } + } ); - $('#ppom-deactivate-modal').on('click', 'a.review-and-deactivate', function (e) { - e.preventDefault(); - window.open("https://wordpress.org/support/plugin/woocommerce-product-addon/reviews/#new-post"); - window.location.href = deactivateLink; - }); - modal.on('click', 'button.pipe-model-cancel', function (e) { - e.preventDefault(); - modal.removeClass('modal-active'); - }); - modal.on('click', 'input[type="radio"]', function () { - var parent = $(this).parents('li:first'); - modal.find('.reason-input').remove(); - var inputType = parent.data('type'), - inputPlaceholder = parent.data('placeholder'); - if ('reviewhtml' === inputType) { - var reasonInputHtml = ''; - } else { - var reasonInputHtml = '
    ' + (('text' === inputType) ? '' : '') + '
    '; - } - if (inputType !== '') { - parent.append($(reasonInputHtml)); - parent.find('input, textarea').attr('placeholder', inputPlaceholder).focus(); - } - }); + modal.on( 'click', 'button.pipe-model-submit', function ( e ) { + e.preventDefault(); + // Submit the reason asynchronously, then continue to the original + // deactivation URL regardless of whether the response body is useful. + const button = $( this ); + if ( button.hasClass( 'disabled' ) ) { + return; + } + const $radio = $( 'input[type="radio"]:checked', modal ); + const $selected_reason = $radio.parents( 'li:first' ), + $input = $selected_reason.find( 'textarea, input[type="text"]' ); - modal.on('click', 'button.pipe-model-submit', function (e) { - e.preventDefault(); - var button = $(this); - if (button.hasClass('disabled')) { - return; - } - var $radio = $('input[type="radio"]:checked', modal); - var $selected_reason = $radio.parents('li:first'), - $input = $selected_reason.find('textarea, input[type="text"]'); - - $.ajax({ - url: ajaxurl, - type: 'POST', - data: { - action: 'pipe_submit_uninstall_reason', - reason_id: (0 === $radio.length) ? 'none' : $radio.val(), - reason_info: (0 !== $input.length) ? $input.val().trim() : '' - }, - beforeSend: function () { - button.addClass('disabled'); - button.text('Processing...'); - }, - complete: function (resp) { - window.location.href = deactivateLink; - } - }); - }); -}); \ No newline at end of file + $.ajax( { + url: ajaxurl, + type: 'POST', + data: { + action: 'pipe_submit_uninstall_reason', + reason_id: 0 === $radio.length ? 'none' : $radio.val(), + reason_info: 0 !== $input.length ? $input.val().trim() : '', + }, + beforeSend() { + button.addClass( 'disabled' ); + button.text( 'Processing...' ); + }, + complete( resp ) { + window.location.href = deactivateLink; + }, + } ); + } ); +} ); diff --git a/js/admin/ppom-meta-table.js b/js/admin/ppom-meta-table.js index 663627fa..da8c6ff1 100644 --- a/js/admin/ppom-meta-table.js +++ b/js/admin/ppom-meta-table.js @@ -1,199 +1,235 @@ -"use strict"; -jQuery(function($){ - - /********************************* - * PPOM Existing Table Meta JS * - **********************************/ - - /*------------------------------------------------------- - - ------ Its Include Following Function ----- - - 1- Apply DataTable JS Library To PPOM Meta List - 2- Delete Selected Products - 3- Check And Uncheck All Existing Product Meta List - 4- Loading Products In Modal DataTable - 5- Delete Single Product Meta - --------------------------------------------------------*/ - - +'use strict'; + +/** + * Admin list screen actions for saved PPOM field groups. + * + * This file wires DataTables, bulk actions, and the "attach to products" modal + * around the server-side group list rendered by PHP. + * + * @see window.ppomPopup in js/popup.js + */ +jQuery( function ( $ ) { /** - 1- Apply DataTable JS Library To PPOM Meta List - **/ - $('#ppom-meta-table').DataTable({ + * Initialize Select2 on the product search dropdowns in the "attach to products" modal. + * + * @return {void} + */ + function initAttachSelects() { + const attachSelects = $( '.ppom-attach-container-item select' ); + + if ( typeof $.fn.select2 === 'function' ) { + attachSelects.select2(); + } + } + + // DataTables provides the searchable/sortable shell, while PPOM injects its + // own action toolbar into the custom `ppom-toolbar` slot defined in `dom`. + $( '#ppom-meta-table' ).DataTable( { pageLength: 50, dom: 'f<"ppom-toolbar"><"top">rt<"bottom">lpi', - }); - var append_overly_model = ("
    "); - - /** - 2- Delete Selected Products - **/ - function deleteSelectedProducts(checkedProducts_ids) { - window?.ppomPopup?.open({ + } ); + const append_overlay_modal = + "
    "; + + // Bulk delete is confirmation-driven because it removes saved field groups, + // not just the rows in the current DataTable view. + /** + * Delete multiple saved PPOM groups after an explicit confirmation step. + * + * @param {number[]} checkedProducts_ids + * @return {void} + */ + function deleteSelectedProducts( checkedProducts_ids ) { + window?.ppomPopup?.open( { title: window?.ppom_vars?.i18n.popup.confirmTitle, onConfirmation: () => { - $('#ppom_delete_selected_products_btn').html('Deleting...'); - + $( '#ppom_delete_selected_products_btn' ).html( 'Deleting...' ); + const data = { - action : 'ppom_delete_selected_meta', - productmeta_ids : checkedProducts_ids, - ppom_meta_nonce : $("#ppom_meta_nonce").val() + action: 'ppom_delete_selected_meta', + productmeta_ids: checkedProducts_ids, + ppom_meta_nonce: $( '#ppom_meta_nonce' ).val(), }; - $.post(ajaxurl, data, function(resp){ - $('#ppom_delete_selected_products_btn').html('Delete'); - if (resp) { - window?.ppomPopup?.open({ + $.post( ajaxurl, data, function ( resp ) { + $( '#ppom_delete_selected_products_btn' ).html( 'Delete' ); + if ( resp ) { + window?.ppomPopup?.open( { title: window?.ppom_vars?.i18n.popup.finishTitle, hideCloseBtn: true, onConfirmation: () => location.reload(), - onClose: () => location.reload() - }); + onClose: () => location.reload(), + } ); } else { - window?.ppomPopup?.open({ + window?.ppomPopup?.open( { title: window.ppom_vars.i18n.popup.errorTitle, text: resp, - hideCloseBtn: true - }); + hideCloseBtn: true, + } ); } - }); - } - }) + } ); + }, + } ); } + $( '.ppom_product_checkbox' ).on( 'click', function ( event ) { + const checkboxProducts = $( '.ppom_product_checkbox' ) + .map( function () { + return this.value; + } ) + .get(); + + const checkedProducts = $( '.ppom_product_checkbox:checked' ) + .map( function () { + return this.value; + } ) + .get(); + + if ( checkboxProducts.length == checkedProducts.length ) { + $( + '#ppom-all-select-products-head-btn, #ppom-all-select-products-foot-btn' + ).prop( 'checked', true ); + } else { + $( + '#ppom-all-select-products-head-btn, #ppom-all-select-products-foot-btn' + ).prop( 'checked', false ); + } - /** - 3- Check And Uncheck All Existing Product Meta List - **/ - $('.ppom_product_checkbox').on('click', function(event){ - - var checkboxProducts = $('.ppom_product_checkbox').map(function() { - return this.value; - }).get(); - - var checkedProducts = $('.ppom_product_checkbox:checked').map(function() { - return this.value; - }).get(); - - if (checkboxProducts.length == checkedProducts.length ) { - $('#ppom-all-select-products-head-btn, #ppom-all-select-products-foot-btn').prop('checked', true); - }else{ - $('#ppom-all-select-products-head-btn, #ppom-all-select-products-foot-btn').prop('checked', false); - }; - - $('#selected_products_count').html(); - $('#selected_products_count').html(checkedProducts.length); - }); - $('#ppom-all-select-products-head-btn, #ppom-all-select-products-foot-btn').on('click', function(event){ - - $('#ppom-meta-table input:checkbox').not(this).prop('checked', this.checked); - var checkedProducts = $('.ppom_product_checkbox:checked').map(function() { - return this.value; - }).get(); - $('#selected_products_count').html(); - $('#selected_products_count').html(checkedProducts.length); - }); - + $( '#selected_products_count' ).html(); + $( '#selected_products_count' ).html( checkedProducts.length ); + } ); + $( + '#ppom-all-select-products-head-btn, #ppom-all-select-products-foot-btn' + ).on( 'click', function ( event ) { + $( '#ppom-meta-table input:checkbox' ) + .not( this ) + .prop( 'checked', this.checked ); + const checkedProducts = $( '.ppom_product_checkbox:checked' ) + .map( function () { + return this.value; + } ) + .get(); + $( '#selected_products_count' ).html(); + $( '#selected_products_count' ).html( checkedProducts.length ); + } ); + + // Load the product-assignment UI lazily so the heavy modal table is fetched + // only when the merchant asks to attach a group to products. + $( '#ppom-meta-table_wrapper, .ppom-basic-setting-section' ).on( + 'click', + 'a.ppom-products-modal', + function ( e ) { + e.preventDefault(); + + $( '.ppom-table' ).DataTable(); + const ppom_id = $( this ).data( 'ppom_id' ); + const get_url = + ajaxurl + '?action=ppom_get_products&ppom_id=' + ppom_id; + const model_id = $( this ).attr( 'data-formmodal-id' ); + + $.get( get_url, function ( html ) { + $( '#ppom-product-modal .ppom-modal-body' ).html( html ); + initAttachSelects(); + $( '#ppom_id' ).val( ppom_id ); + $( 'body' ).append( append_overlay_modal ); + $( '#' + model_id ).fadeIn(); + $( '#attach-to-products input' ).focus(); + } ); + } + ); - /** - 4- Loading Products In Modal DataTable - **/ - $('#ppom-meta-table_wrapper, .ppom-basic-setting-section').on('click','a.ppom-products-modal', function(e){ - - e.preventDefault(); - - $(".ppom-table").DataTable(); - var ppom_id = $(this).data('ppom_id'); - var get_url = ajaxurl+'?action=ppom_get_products&ppom_id='+ppom_id; - var model_id = $(this).attr('data-formmodal-id'); - - $.get( get_url, function(html){ - $('#ppom-product-modal .ppom-modal-body').html(html); - $('.ppom-attach-container-item select')?.select2(); - $("#ppom_id").val(ppom_id); - $("body").append(append_overly_model); - $('#'+model_id).fadeIn(); - $("#attach-to-products input").focus(); - }); - }); - - - /** - 5- Delete Single Product Meta - **/ - $('body').on('click','a.ppom-delete-single-product', function(e){ + $( 'body' ).on( 'click', 'a.ppom-delete-single-product', function ( e ) { e.preventDefault(); - const productmeta_id = $(this).attr('data-product-id'); + const productmeta_id = $( this ).attr( 'data-product-id' ); - window?.ppomPopup?.open({ + window?.ppomPopup?.open( { title: window?.ppom_vars?.i18n.popup.confirmTitle, onConfirmation: () => { - $("#del-file-" + productmeta_id).html(''); + $( '#del-file-' + productmeta_id ).html( + '' + ); const data = { - action : 'ppom_delete_meta', - productmeta_id : productmeta_id, - ppom_meta_nonce : $("#ppom_meta_nonce").val() + action: 'ppom_delete_meta', + productmeta_id, + ppom_meta_nonce: $( '#ppom_meta_nonce' ).val(), }; - $.post( ajaxurl, data, function(resp){ - $("#del-file-" + productmeta_id).html(''); - if ( resp.status === 'success' ) { - window?.ppomPopup?.open({ + $.post( ajaxurl, data, function ( resp ) { + $( '#del-file-' + productmeta_id ).html( + '' + ); + if ( resp.status === 'success' ) { + window?.ppomPopup?.open( { title: window?.ppom_vars?.i18n.popup.finishTitle, hideCloseBtn: true, onConfirmation: () => location.reload(), - onClose: () => location.reload() - }); - } else { - window?.ppomPopup?.open({ + onClose: () => location.reload(), + } ); + } else { + window?.ppomPopup?.open( { title: window.ppom_vars.i18n.popup.errorTitle, text: resp.message, hideCloseBtn: true, - }); - } - }); - } - }) - }); + } ); + } + } ); + }, + } ); + } ); - $(document).on( 'change', '#ppom-bulk-actions', function(){ - const type = $(this).val(); + $( document ).on( 'change', '#ppom-bulk-actions', function () { + const type = $( this ).val(); - const checkedProducts_ids = $('.ppom_product_checkbox:checked').map(function() { - return parseInt(this.value); - }).get(); + const checkedProducts_ids = $( '.ppom_product_checkbox:checked' ) + .map( function () { + return parseInt( this.value ); + } ) + .get(); if ( ! ( checkedProducts_ids.length > 0 ) ) { - window?.ppomPopup?.open({ + window?.ppomPopup?.open( { title: window?.ppom_vars?.i18n.popup.confirmTitle, type: 'error', - hideCloseBtn: true - }); + hideCloseBtn: true, + } ); return; } - if( 'delete' === type ) { - deleteSelectedProducts(checkedProducts_ids); - }else if( 'export' === type ) { - $('#ppom-groups-export-form').submit(); + // Only one action runs per selection. Resetting the select afterwards + // prevents DataTables redraws from accidentally replaying the last action. + if ( 'delete' === type ) { + deleteSelectedProducts( checkedProducts_ids ); + } else if ( 'export' === type ) { + $( '#ppom-groups-export-form' ).submit(); } - $(this).val(-1); - }); + $( this ).val( -1 ); + } ); - const exportOption = ppom_vars.ppomProActivated === 'yes' ? `` : ``; + // Import/export are always surfaced in the toolbar so the locked Pro state + // is visible even on Free installs; the markup changes between enabled and + // disabled variants based on the localized license flag. + const exportOption = + ppom_vars.ppomProActivated === 'yes' + ? `` + : ``; - const importBtn = `${ppom_vars.i18n.importLabel}`; + const importBtn = `${ ppom_vars.i18n.importLabel }`; const bulkActions = ``; - const btn = `${ppom_vars.i18n.addGroupLabel}`; + const btn = `${ ppom_vars.i18n.addGroupLabel }`; - $('div.ppom-toolbar').html(`
    ${bulkActions} ${importBtn} ${btn}
    `); -}); + // DataTables creates the placeholder container, then PPOM injects the + // toolbar HTML after initialization so the controls stay inside the table UI. + $( 'div.ppom-toolbar' ).html( + `
    ${ bulkActions } ${ importBtn } ${ btn }
    ` + ); +} ); diff --git a/js/admin/pre-load.js b/js/admin/pre-load.js index 489cfe83..85c7e79e 100644 --- a/js/admin/pre-load.js +++ b/js/admin/pre-load.js @@ -1,37 +1,49 @@ - /* Image loader */ -function addListener(element, type, expression, bubbling) { - bubbling = bubbling || false; - if (window.addEventListener) { // Standard - element.addEventListener(type, expression, bubbling); - return true; - } else if (window.attachEvent) { // IE - element.attachEvent('on' + type, expression); - return true; - } else return false; +/** + * Small image preloader used by the admin field builder shell. + * + * The main admin UI stays hidden until the loader asset is available so the + * screen does not flash half-rendered styles while the builder initializes. + */ +function addListener( element, type, expression, bubbling ) { + bubbling = bubbling || false; + if ( window.addEventListener ) { + // Standard + element.addEventListener( type, expression, bubbling ); + return true; + } else if ( window.attachEvent ) { + // IE + element.attachEvent( 'on' + type, expression ); + return true; + } + return false; } - -var ImageLoader = function (url) { - this.url = url; - this.image = null; - this.loadEvent = null; +const ImageLoader = function ( url ) { + this.url = url; + this.image = null; + this.loadEvent = null; }; ImageLoader.prototype = { - load: function () { - this.image = document.createElement('img'); - var url = this.url; - var image = this.image; - var loadEvent = this.loadEvent; - addListener(this.image, 'load', function (e) { - if (loadEvent != null) { - loadEvent(url, image); - } - }, false); - this.image.src = this.url; - }, - getImage: function () { - return this.image; - } + load() { + this.image = document.createElement( 'img' ); + const url = this.url; + const image = this.image; + const loadEvent = this.loadEvent; + addListener( + this.image, + 'load', + function ( e ) { + if ( loadEvent != null ) { + loadEvent( url, image ); + } + }, + false + ); + this.image.src = this.url; + }, + getImage() { + return this.image; + }, }; -/* End of image loader */ \ No newline at end of file +/* End of image loader */ diff --git a/js/admin/serializejson.js b/js/admin/serializejson.js index 22eb2074..6161367c 100644 --- a/js/admin/serializejson.js +++ b/js/admin/serializejson.js @@ -7,4 +7,288 @@ Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. */ -!function(e){if("function"==typeof define&&define.amd)define(["jquery"],e);else if("object"==typeof exports){var n=require("jquery");module.exports=e(n)}else e(window.jQuery||window.Zepto||window.$)}(function(e){"use strict";var n=/\r?\n/g,r=/^(?:submit|button|image|reset|file)$/i,t=/^(?:input|select|textarea|keygen)/i,i=/^(?:checkbox|radio)$/i;e.fn.serializeJSON=function(n){var r=e.serializeJSON,t=r.setupOpts(n),i=e.extend({},t.defaultTypes,t.customTypes),a=r.serializeArray(this,t),u={};return e.each(a,function(n,a){var s=a.name,l=e(a.el).attr("data-value-type");if(!l&&!t.disableColonTypes){var o=r.splitType(a.name);s=o[0],l=o[1]}if("skip"!==l){l||(l=t.defaultType);var p=r.applyTypeFunc(a.name,a.value,l,a.el,i);if(p||!r.shouldSkipFalsy(a.name,s,l,a.el,t)){var f=r.splitInputNameIntoKeysArray(s);r.deepSet(u,f,p,t)}}}),u},e.serializeJSON={defaultOptions:{},defaultBaseOptions:{checkboxUncheckedValue:void 0,useIntKeysAsArrayIndex:!1,skipFalsyValuesForTypes:[],skipFalsyValuesForFields:[],disableColonTypes:!1,customTypes:{},defaultTypes:{string:function(e){return String(e)},number:function(e){return Number(e)},boolean:function(e){return-1===["false","null","undefined","","0"].indexOf(e)},null:function(e){return-1===["false","null","undefined","","0"].indexOf(e)?e:null},array:function(e){return JSON.parse(e)},object:function(e){return JSON.parse(e)},skip:null},defaultType:"string"},setupOpts:function(n){null==n&&(n={});var r=e.serializeJSON,t=["checkboxUncheckedValue","useIntKeysAsArrayIndex","skipFalsyValuesForTypes","skipFalsyValuesForFields","disableColonTypes","customTypes","defaultTypes","defaultType"];for(var i in n)if(-1===t.indexOf(i))throw new Error("serializeJSON ERROR: invalid option '"+i+"'. Please use one of "+t.join(", "));return e.extend({},r.defaultBaseOptions,r.defaultOptions,n)},serializeArray:function(a,u){null==u&&(u={});var s=e.serializeJSON;return a.map(function(){var n=e.prop(this,"elements");return n?e.makeArray(n):this}).filter(function(){var n=e(this),a=this.type;return this.name&&!n.is(":disabled")&&t.test(this.nodeName)&&!r.test(a)&&(this.checked||!i.test(a)||null!=s.getCheckboxUncheckedValue(n,u))}).map(function(r,t){var a=e(this),l=a.val(),p=this.type;return null==l?null:(i.test(p)&&!this.checked&&(l=s.getCheckboxUncheckedValue(a,u)),o(l)?e.map(l,function(e){return{name:t.name,value:e.replace(n,"\r\n"),el:t}}):{name:t.name,value:l.replace(n,"\r\n"),el:t})}).get()},getCheckboxUncheckedValue:function(e,n){var r=e.attr("data-unchecked-value");return null==r&&(r=n.checkboxUncheckedValue),r},applyTypeFunc:function(e,n,r,t,i){var u=i[r];if(!u)throw new Error("serializeJSON ERROR: Invalid type "+r+" found in input name '"+e+"', please use one of "+a(i).join(", "));return u(n,t)},splitType:function(e){var n=e.split(":");if(n.length>1){var r=n.pop();return[n.join(":"),r]}return[e,""]},shouldSkipFalsy:function(n,r,t,i,a){var u=e(i).attr("data-skip-falsy");if(null!=u)return"false"!==u;var s=a.skipFalsyValuesForFields;if(s&&(-1!==s.indexOf(r)||-1!==s.indexOf(n)))return!0;var l=a.skipFalsyValuesForTypes;return!(!l||-1===l.indexOf(t))},splitInputNameIntoKeysArray:function(n){var r=n.split("[");return""===(r=e.map(r,function(e){return e.replace(/\]/g,"")}))[0]&&r.shift(),r},deepSet:function(n,r,t,i){null==i&&(i={});var a=e.serializeJSON;if(s(n))throw new Error("ArgumentError: param 'o' expected to be an object or array, found undefined");if(!r||0===r.length)throw new Error("ArgumentError: param 'keys' expected to be an array with least one element");var p=r[0];if(1!==r.length){var f=r[1],c=r.slice(1);if(""===p){var d=n.length-1,y=n[d];p=u(y)&&s(a.deepGet(y,c))?d:d+1}""===f?!s(n[p])&&o(n[p])||(n[p]=[]):i.useIntKeysAsArrayIndex&&l(f)?!s(n[p])&&o(n[p])||(n[p]=[]):!s(n[p])&&u(n[p])||(n[p]={}),a.deepSet(n[p],c,t,i)}else""===p?n.push(t):n[p]=t},deepGet:function(n,r){var t=e.serializeJSON;if(s(n)||s(r)||0===r.length||!u(n)&&!o(n))return n;var i=r[0];if(""!==i){if(1===r.length)return n[i];var a=r.slice(1);return t.deepGet(n[i],a)}}};var a=function(e){if(Object.keys)return Object.keys(e);var n,r=[];for(n in e)r.push(n);return r},u=function(e){return e===Object(e)},s=function(e){return void 0===e},l=function(e){return/^[0-9]+$/.test(String(e))},o=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}}); \ No newline at end of file +! ( function ( e ) { + if ( 'function' === typeof define && define.amd ) { + define( [ 'jquery' ], e ); + } else if ( 'object' === typeof exports ) { + const n = require( 'jquery' ); + module.exports = e( n ); + } else { + e( window.jQuery || window.Zepto || window.$ ); + } +} )( function ( e ) { + 'use strict'; + const n = /\r?\n/g, + r = /^(?:submit|button|image|reset|file)$/i, + t = /^(?:input|select|textarea|keygen)/i, + i = /^(?:checkbox|radio)$/i; + ( e.fn.serializeJSON = function ( n ) { + const r = e.serializeJSON, + t = r.setupOpts( n ), + i = e.extend( {}, t.defaultTypes, t.customTypes ), + a = r.serializeArray( this, t ), + u = {}; + return ( + e.each( a, function ( n, a ) { + let s = a.name, + l = e( a.el ).attr( 'data-value-type' ); + if ( ! l && ! t.disableColonTypes ) { + const o = r.splitType( a.name ); + ( s = o[ 0 ] ), ( l = o[ 1 ] ); + } + if ( 'skip' !== l ) { + l || ( l = t.defaultType ); + const p = r.applyTypeFunc( a.name, a.value, l, a.el, i ); + if ( p || ! r.shouldSkipFalsy( a.name, s, l, a.el, t ) ) { + const f = r.splitInputNameIntoKeysArray( s ); + r.deepSet( u, f, p, t ); + } + } + } ), + u + ); + } ), + ( e.serializeJSON = { + defaultOptions: {}, + defaultBaseOptions: { + checkboxUncheckedValue: void 0, + useIntKeysAsArrayIndex: ! 1, + skipFalsyValuesForTypes: [], + skipFalsyValuesForFields: [], + disableColonTypes: ! 1, + customTypes: {}, + defaultTypes: { + string( e ) { + return String( e ); + }, + number( e ) { + return Number( e ); + }, + boolean( e ) { + return ( + -1 === + [ 'false', 'null', 'undefined', '', '0' ].indexOf( + e + ) + ); + }, + null( e ) { + return -1 === + [ 'false', 'null', 'undefined', '', '0' ].indexOf( + e + ) + ? e + : null; + }, + array( e ) { + return JSON.parse( e ); + }, + object( e ) { + return JSON.parse( e ); + }, + skip: null, + }, + defaultType: 'string', + }, + setupOpts( n ) { + null == n && ( n = {} ); + const r = e.serializeJSON, + t = [ + 'checkboxUncheckedValue', + 'useIntKeysAsArrayIndex', + 'skipFalsyValuesForTypes', + 'skipFalsyValuesForFields', + 'disableColonTypes', + 'customTypes', + 'defaultTypes', + 'defaultType', + ]; + for ( const i in n ) { + if ( -1 === t.indexOf( i ) ) { + throw new Error( + "serializeJSON ERROR: invalid option '" + + i + + "'. Please use one of " + + t.join( ', ' ) + ); + } + } + return e.extend( + {}, + r.defaultBaseOptions, + r.defaultOptions, + n + ); + }, + serializeArray( a, u ) { + null == u && ( u = {} ); + const s = e.serializeJSON; + return a + .map( function () { + const n = e.prop( this, 'elements' ); + return n ? e.makeArray( n ) : this; + } ) + .filter( function () { + const n = e( this ), + a = this.type; + return ( + this.name && + ! n.is( ':disabled' ) && + t.test( this.nodeName ) && + ! r.test( a ) && + ( this.checked || + ! i.test( a ) || + null != s.getCheckboxUncheckedValue( n, u ) ) + ); + } ) + .map( function ( r, t ) { + let a = e( this ), + l = a.val(), + p = this.type; + return null == l + ? null + : ( i.test( p ) && + ! this.checked && + ( l = s.getCheckboxUncheckedValue( a, u ) ), + o( l ) + ? e.map( l, function ( e ) { + return { + name: t.name, + value: e.replace( n, '\r\n' ), + el: t, + }; + } ) + : { + name: t.name, + value: l.replace( n, '\r\n' ), + el: t, + } ); + } ) + .get(); + }, + getCheckboxUncheckedValue( e, n ) { + let r = e.attr( 'data-unchecked-value' ); + return null == r && ( r = n.checkboxUncheckedValue ), r; + }, + applyTypeFunc( e, n, r, t, i ) { + const u = i[ r ]; + if ( ! u ) { + throw new Error( + 'serializeJSON ERROR: Invalid type ' + + r + + " found in input name '" + + e + + "', please use one of " + + a( i ).join( ', ' ) + ); + } + return u( n, t ); + }, + splitType( e ) { + const n = e.split( ':' ); + if ( n.length > 1 ) { + const r = n.pop(); + return [ n.join( ':' ), r ]; + } + return [ e, '' ]; + }, + shouldSkipFalsy( n, r, t, i, a ) { + const u = e( i ).attr( 'data-skip-falsy' ); + if ( null != u ) { + return 'false' !== u; + } + const s = a.skipFalsyValuesForFields; + if ( s && ( -1 !== s.indexOf( r ) || -1 !== s.indexOf( n ) ) ) { + return ! 0; + } + const l = a.skipFalsyValuesForTypes; + return ! ( ! l || -1 === l.indexOf( t ) ); + }, + splitInputNameIntoKeysArray( n ) { + let r = n.split( '[' ); + return ( + '' === + ( r = e.map( r, function ( e ) { + return e.replace( /\]/g, '' ); + } ) )[ 0 ] && r.shift(), + r + ); + }, + deepSet( n, r, t, i ) { + null == i && ( i = {} ); + const a = e.serializeJSON; + if ( s( n ) ) { + throw new Error( + "ArgumentError: param 'o' expected to be an object or array, found undefined" + ); + } + if ( ! r || 0 === r.length ) { + throw new Error( + "ArgumentError: param 'keys' expected to be an array with least one element" + ); + } + let p = r[ 0 ]; + if ( 1 !== r.length ) { + const f = r[ 1 ], + c = r.slice( 1 ); + if ( '' === p ) { + const d = n.length - 1, + y = n[ d ]; + p = u( y ) && s( a.deepGet( y, c ) ) ? d : d + 1; + } + '' === f + ? ( ! s( n[ p ] ) && o( n[ p ] ) ) || ( n[ p ] = [] ) + : i.useIntKeysAsArrayIndex && l( f ) + ? ( ! s( n[ p ] ) && o( n[ p ] ) ) || ( n[ p ] = [] ) + : ( ! s( n[ p ] ) && u( n[ p ] ) ) || ( n[ p ] = {} ), + a.deepSet( n[ p ], c, t, i ); + } else { + '' === p ? n.push( t ) : ( n[ p ] = t ); + } + }, + deepGet( n, r ) { + const t = e.serializeJSON; + if ( + s( n ) || + s( r ) || + 0 === r.length || + ( ! u( n ) && ! o( n ) ) + ) { + return n; + } + const i = r[ 0 ]; + if ( '' !== i ) { + if ( 1 === r.length ) { + return n[ i ]; + } + const a = r.slice( 1 ); + return t.deepGet( n[ i ], a ); + } + }, + } ); + var a = function ( e ) { + if ( Object.keys ) { + return Object.keys( e ); + } + let n, + r = []; + for ( n in e ) { + r.push( n ); + } + return r; + }, + u = function ( e ) { + return e === Object( e ); + }, + s = function ( e ) { + return void 0 === e; + }, + l = function ( e ) { + return /^[0-9]+$/.test( String( e ) ); + }, + o = + Array.isArray || + function ( e ) { + return '[object Array]' === Object.prototype.toString.call( e ); + }; +} ); diff --git a/js/exif.js b/js/exif.js index dfcb08a2..3f937a20 100644 --- a/js/exif.js +++ b/js/exif.js @@ -1,1058 +1,1229 @@ -(function() { - - var debug = false; - - var root = this; - - var EXIF = function(obj) { - if (obj instanceof EXIF) return obj; - if (!(this instanceof EXIF)) return new EXIF(obj); - this.EXIFwrapped = obj; - }; - - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - exports = module.exports = EXIF; - } - exports.EXIF = EXIF; - } else { - root.EXIF = EXIF; - } - - var ExifTags = EXIF.Tags = { - - // version tags - 0x9000 : "ExifVersion", // EXIF version - 0xA000 : "FlashpixVersion", // Flashpix format version - - // colorspace tags - 0xA001 : "ColorSpace", // Color space information tag - - // image configuration - 0xA002 : "PixelXDimension", // Valid width of meaningful image - 0xA003 : "PixelYDimension", // Valid height of meaningful image - 0x9101 : "ComponentsConfiguration", // Information about channels - 0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel - - // user information - 0x927C : "MakerNote", // Any desired information written by the manufacturer - 0x9286 : "UserComment", // Comments by user - - // related file - 0xA004 : "RelatedSoundFile", // Name of related sound file - - // date and time - 0x9003 : "DateTimeOriginal", // Date and time when the original image was generated - 0x9004 : "DateTimeDigitized", // Date and time when the image was stored digitally - 0x9290 : "SubsecTime", // Fractions of seconds for DateTime - 0x9291 : "SubsecTimeOriginal", // Fractions of seconds for DateTimeOriginal - 0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized - - // picture-taking conditions - 0x829A : "ExposureTime", // Exposure time (in seconds) - 0x829D : "FNumber", // F number - 0x8822 : "ExposureProgram", // Exposure program - 0x8824 : "SpectralSensitivity", // Spectral sensitivity - 0x8827 : "ISOSpeedRatings", // ISO speed rating - 0x8828 : "OECF", // Optoelectric conversion factor - 0x9201 : "ShutterSpeedValue", // Shutter speed - 0x9202 : "ApertureValue", // Lens aperture - 0x9203 : "BrightnessValue", // Value of brightness - 0x9204 : "ExposureBias", // Exposure bias - 0x9205 : "MaxApertureValue", // Smallest F number of lens - 0x9206 : "SubjectDistance", // Distance to subject in meters - 0x9207 : "MeteringMode", // Metering mode - 0x9208 : "LightSource", // Kind of light source - 0x9209 : "Flash", // Flash status - 0x9214 : "SubjectArea", // Location and area of main subject - 0x920A : "FocalLength", // Focal length of the lens in mm - 0xA20B : "FlashEnergy", // Strobe energy in BCPS - 0xA20C : "SpatialFrequencyResponse", // - 0xA20E : "FocalPlaneXResolution", // Number of pixels in width direction per FocalPlaneResolutionUnit - 0xA20F : "FocalPlaneYResolution", // Number of pixels in height direction per FocalPlaneResolutionUnit - 0xA210 : "FocalPlaneResolutionUnit", // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution - 0xA214 : "SubjectLocation", // Location of subject in image - 0xA215 : "ExposureIndex", // Exposure index selected on camera - 0xA217 : "SensingMethod", // Image sensor type - 0xA300 : "FileSource", // Image source (3 == DSC) - 0xA301 : "SceneType", // Scene type (1 == directly photographed) - 0xA302 : "CFAPattern", // Color filter array geometric pattern - 0xA401 : "CustomRendered", // Special processing - 0xA402 : "ExposureMode", // Exposure mode - 0xA403 : "WhiteBalance", // 1 = auto white balance, 2 = manual - 0xA404 : "DigitalZoomRation", // Digital zoom ratio - 0xA405 : "FocalLengthIn35mmFilm", // Equivalent foacl length assuming 35mm film camera (in mm) - 0xA406 : "SceneCaptureType", // Type of scene - 0xA407 : "GainControl", // Degree of overall image gain adjustment - 0xA408 : "Contrast", // Direction of contrast processing applied by camera - 0xA409 : "Saturation", // Direction of saturation processing applied by camera - 0xA40A : "Sharpness", // Direction of sharpness processing applied by camera - 0xA40B : "DeviceSettingDescription", // - 0xA40C : "SubjectDistanceRange", // Distance to subject - - // other tags - 0xA005 : "InteroperabilityIFDPointer", - 0xA420 : "ImageUniqueID" // Identifier assigned uniquely to each image - }; - - var TiffTags = EXIF.TiffTags = { - 0x0100 : "ImageWidth", - 0x0101 : "ImageHeight", - 0x8769 : "ExifIFDPointer", - 0x8825 : "GPSInfoIFDPointer", - 0xA005 : "InteroperabilityIFDPointer", - 0x0102 : "BitsPerSample", - 0x0103 : "Compression", - 0x0106 : "PhotometricInterpretation", - 0x0112 : "Orientation", - 0x0115 : "SamplesPerPixel", - 0x011C : "PlanarConfiguration", - 0x0212 : "YCbCrSubSampling", - 0x0213 : "YCbCrPositioning", - 0x011A : "XResolution", - 0x011B : "YResolution", - 0x0128 : "ResolutionUnit", - 0x0111 : "StripOffsets", - 0x0116 : "RowsPerStrip", - 0x0117 : "StripByteCounts", - 0x0201 : "JPEGInterchangeFormat", - 0x0202 : "JPEGInterchangeFormatLength", - 0x012D : "TransferFunction", - 0x013E : "WhitePoint", - 0x013F : "PrimaryChromaticities", - 0x0211 : "YCbCrCoefficients", - 0x0214 : "ReferenceBlackWhite", - 0x0132 : "DateTime", - 0x010E : "ImageDescription", - 0x010F : "Make", - 0x0110 : "Model", - 0x0131 : "Software", - 0x013B : "Artist", - 0x8298 : "Copyright" - }; - - var GPSTags = EXIF.GPSTags = { - 0x0000 : "GPSVersionID", - 0x0001 : "GPSLatitudeRef", - 0x0002 : "GPSLatitude", - 0x0003 : "GPSLongitudeRef", - 0x0004 : "GPSLongitude", - 0x0005 : "GPSAltitudeRef", - 0x0006 : "GPSAltitude", - 0x0007 : "GPSTimeStamp", - 0x0008 : "GPSSatellites", - 0x0009 : "GPSStatus", - 0x000A : "GPSMeasureMode", - 0x000B : "GPSDOP", - 0x000C : "GPSSpeedRef", - 0x000D : "GPSSpeed", - 0x000E : "GPSTrackRef", - 0x000F : "GPSTrack", - 0x0010 : "GPSImgDirectionRef", - 0x0011 : "GPSImgDirection", - 0x0012 : "GPSMapDatum", - 0x0013 : "GPSDestLatitudeRef", - 0x0014 : "GPSDestLatitude", - 0x0015 : "GPSDestLongitudeRef", - 0x0016 : "GPSDestLongitude", - 0x0017 : "GPSDestBearingRef", - 0x0018 : "GPSDestBearing", - 0x0019 : "GPSDestDistanceRef", - 0x001A : "GPSDestDistance", - 0x001B : "GPSProcessingMethod", - 0x001C : "GPSAreaInformation", - 0x001D : "GPSDateStamp", - 0x001E : "GPSDifferential" - }; - - // EXIF 2.3 Spec - var IFD1Tags = EXIF.IFD1Tags = { - 0x0100: "ImageWidth", - 0x0101: "ImageHeight", - 0x0102: "BitsPerSample", - 0x0103: "Compression", - 0x0106: "PhotometricInterpretation", - 0x0111: "StripOffsets", - 0x0112: "Orientation", - 0x0115: "SamplesPerPixel", - 0x0116: "RowsPerStrip", - 0x0117: "StripByteCounts", - 0x011A: "XResolution", - 0x011B: "YResolution", - 0x011C: "PlanarConfiguration", - 0x0128: "ResolutionUnit", - 0x0201: "JpegIFOffset", // When image format is JPEG, this value show offset to JPEG data stored.(aka "ThumbnailOffset" or "JPEGInterchangeFormat") - 0x0202: "JpegIFByteCount", // When image format is JPEG, this value shows data size of JPEG image (aka "ThumbnailLength" or "JPEGInterchangeFormatLength") - 0x0211: "YCbCrCoefficients", - 0x0212: "YCbCrSubSampling", - 0x0213: "YCbCrPositioning", - 0x0214: "ReferenceBlackWhite" - }; - - var StringValues = EXIF.StringValues = { - ExposureProgram : { - 0 : "Not defined", - 1 : "Manual", - 2 : "Normal program", - 3 : "Aperture priority", - 4 : "Shutter priority", - 5 : "Creative program", - 6 : "Action program", - 7 : "Portrait mode", - 8 : "Landscape mode" - }, - MeteringMode : { - 0 : "Unknown", - 1 : "Average", - 2 : "CenterWeightedAverage", - 3 : "Spot", - 4 : "MultiSpot", - 5 : "Pattern", - 6 : "Partial", - 255 : "Other" - }, - LightSource : { - 0 : "Unknown", - 1 : "Daylight", - 2 : "Fluorescent", - 3 : "Tungsten (incandescent light)", - 4 : "Flash", - 9 : "Fine weather", - 10 : "Cloudy weather", - 11 : "Shade", - 12 : "Daylight fluorescent (D 5700 - 7100K)", - 13 : "Day white fluorescent (N 4600 - 5400K)", - 14 : "Cool white fluorescent (W 3900 - 4500K)", - 15 : "White fluorescent (WW 3200 - 3700K)", - 17 : "Standard light A", - 18 : "Standard light B", - 19 : "Standard light C", - 20 : "D55", - 21 : "D65", - 22 : "D75", - 23 : "D50", - 24 : "ISO studio tungsten", - 255 : "Other" - }, - Flash : { - 0x0000 : "Flash did not fire", - 0x0001 : "Flash fired", - 0x0005 : "Strobe return light not detected", - 0x0007 : "Strobe return light detected", - 0x0009 : "Flash fired, compulsory flash mode", - 0x000D : "Flash fired, compulsory flash mode, return light not detected", - 0x000F : "Flash fired, compulsory flash mode, return light detected", - 0x0010 : "Flash did not fire, compulsory flash mode", - 0x0018 : "Flash did not fire, auto mode", - 0x0019 : "Flash fired, auto mode", - 0x001D : "Flash fired, auto mode, return light not detected", - 0x001F : "Flash fired, auto mode, return light detected", - 0x0020 : "No flash function", - 0x0041 : "Flash fired, red-eye reduction mode", - 0x0045 : "Flash fired, red-eye reduction mode, return light not detected", - 0x0047 : "Flash fired, red-eye reduction mode, return light detected", - 0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode", - 0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", - 0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", - 0x0059 : "Flash fired, auto mode, red-eye reduction mode", - 0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode", - 0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode" - }, - SensingMethod : { - 1 : "Not defined", - 2 : "One-chip color area sensor", - 3 : "Two-chip color area sensor", - 4 : "Three-chip color area sensor", - 5 : "Color sequential area sensor", - 7 : "Trilinear sensor", - 8 : "Color sequential linear sensor" - }, - SceneCaptureType : { - 0 : "Standard", - 1 : "Landscape", - 2 : "Portrait", - 3 : "Night scene" - }, - SceneType : { - 1 : "Directly photographed" - }, - CustomRendered : { - 0 : "Normal process", - 1 : "Custom process" - }, - WhiteBalance : { - 0 : "Auto white balance", - 1 : "Manual white balance" - }, - GainControl : { - 0 : "None", - 1 : "Low gain up", - 2 : "High gain up", - 3 : "Low gain down", - 4 : "High gain down" - }, - Contrast : { - 0 : "Normal", - 1 : "Soft", - 2 : "Hard" - }, - Saturation : { - 0 : "Normal", - 1 : "Low saturation", - 2 : "High saturation" - }, - Sharpness : { - 0 : "Normal", - 1 : "Soft", - 2 : "Hard" - }, - SubjectDistanceRange : { - 0 : "Unknown", - 1 : "Macro", - 2 : "Close view", - 3 : "Distant view" - }, - FileSource : { - 3 : "DSC" - }, - - Components : { - 0 : "", - 1 : "Y", - 2 : "Cb", - 3 : "Cr", - 4 : "R", - 5 : "G", - 6 : "B" - } - }; - - function addEvent(element, event, handler) { - if (element.addEventListener) { - element.addEventListener(event, handler, false); - } else if (element.attachEvent) { - element.attachEvent("on" + event, handler); - } - } - - function imageHasData(img) { - return !!(img.exifdata); - } - - - function base64ToArrayBuffer(base64, contentType) { - contentType = contentType || base64.match(/^data\:([^\;]+)\;base64,/mi)[1] || ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' - base64 = base64.replace(/^data\:([^\;]+)\;base64,/gmi, ''); - var binary = atob(base64); - var len = binary.length; - var buffer = new ArrayBuffer(len); - var view = new Uint8Array(buffer); - for (var i = 0; i < len; i++) { - view[i] = binary.charCodeAt(i); - } - return buffer; - } - - function objectURLToBlob(url, callback) { - var http = new XMLHttpRequest(); - http.open("GET", url, true); - http.responseType = "blob"; - http.onload = function(e) { - if (this.status == 200 || this.status === 0) { - callback(this.response); - } - }; - http.send(); - } - - function getImageData(img, callback) { - function handleBinaryFile(binFile) { - var data = findEXIFinJPEG(binFile); - img.exifdata = data || {}; - var iptcdata = findIPTCinJPEG(binFile); - img.iptcdata = iptcdata || {}; - if (EXIF.isXmpEnabled) { - var xmpdata= findXMPinJPEG(binFile); - img.xmpdata = xmpdata || {}; - } - if (callback) { - callback.call(img); - } - } - - if (img.src) { - if (/^data\:/i.test(img.src)) { // Data URI - var arrayBuffer = base64ToArrayBuffer(img.src); - handleBinaryFile(arrayBuffer); - - } else if (/^blob\:/i.test(img.src)) { // Object URL - var fileReader = new FileReader(); - fileReader.onload = function(e) { - handleBinaryFile(e.target.result); - }; - objectURLToBlob(img.src, function (blob) { - fileReader.readAsArrayBuffer(blob); - }); - } else { - var http = new XMLHttpRequest(); - http.onload = function() { - if (this.status == 200 || this.status === 0) { - handleBinaryFile(http.response); - } else { - throw "Could not load image"; - } - http = null; - }; - http.open("GET", img.src, true); - http.responseType = "arraybuffer"; - http.send(null); - } - } else if (self.FileReader && (img instanceof self.Blob || img instanceof self.File)) { - var fileReader = new FileReader(); - fileReader.onload = function(e) { - if (debug) console.log("Got file of length " + e.target.result.byteLength); - handleBinaryFile(e.target.result); - }; - - fileReader.readAsArrayBuffer(img); - } - } - - function findEXIFinJPEG(file) { - var dataView = new DataView(file); - - if (debug) console.log("Got file of length " + file.byteLength); - if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { - if (debug) console.log("Not a valid JPEG"); - return false; // not a valid jpeg - } - - var offset = 2, - length = file.byteLength, - marker; - - while (offset < length) { - if (dataView.getUint8(offset) != 0xFF) { - if (debug) console.log("Not a valid marker at offset " + offset + ", found: " + dataView.getUint8(offset)); - return false; // not a valid marker, something is wrong - } - - marker = dataView.getUint8(offset + 1); - if (debug) console.log(marker); - - // we could implement handling for other markers here, - // but we're only looking for 0xFFE1 for EXIF data - - if (marker == 225) { - if (debug) console.log("Found 0xFFE1 marker"); - - return readEXIFData(dataView, offset + 4, dataView.getUint16(offset + 2) - 2); - - // offset += 2 + file.getShortAt(offset+2, true); - - } else { - offset += 2 + dataView.getUint16(offset+2); - } - - } - - } - - function findIPTCinJPEG(file) { - var dataView = new DataView(file); - - if (debug) console.log("Got file of length " + file.byteLength); - if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { - if (debug) console.log("Not a valid JPEG"); - return false; // not a valid jpeg - } - - var offset = 2, - length = file.byteLength; - - - var isFieldSegmentStart = function(dataView, offset){ - return ( - dataView.getUint8(offset) === 0x38 && - dataView.getUint8(offset+1) === 0x42 && - dataView.getUint8(offset+2) === 0x49 && - dataView.getUint8(offset+3) === 0x4D && - dataView.getUint8(offset+4) === 0x04 && - dataView.getUint8(offset+5) === 0x04 - ); - }; - - while (offset < length) { - - if ( isFieldSegmentStart(dataView, offset )){ - - // Get the length of the name header (which is padded to an even number of bytes) - var nameHeaderLength = dataView.getUint8(offset+7); - if(nameHeaderLength % 2 !== 0) nameHeaderLength += 1; - // Check for pre photoshop 6 format - if(nameHeaderLength === 0) { - // Always 4 - nameHeaderLength = 4; - } - - var startOffset = offset + 8 + nameHeaderLength; - var sectionLength = dataView.getUint16(offset + 6 + nameHeaderLength); - - return readIPTCData(file, startOffset, sectionLength); - - break; - - } - - - // Not the marker, continue searching - offset++; - - } - - } - var IptcFieldMap = { - 0x78 : 'caption', - 0x6E : 'credit', - 0x19 : 'keywords', - 0x37 : 'dateCreated', - 0x50 : 'byline', - 0x55 : 'bylineTitle', - 0x7A : 'captionWriter', - 0x69 : 'headline', - 0x74 : 'copyright', - 0x0F : 'category' - }; - function readIPTCData(file, startOffset, sectionLength){ - var dataView = new DataView(file); - var data = {}; - var fieldValue, fieldName, dataSize, segmentType, segmentSize; - var segmentStartPos = startOffset; - while(segmentStartPos < startOffset+sectionLength) { - if(dataView.getUint8(segmentStartPos) === 0x1C && dataView.getUint8(segmentStartPos+1) === 0x02){ - segmentType = dataView.getUint8(segmentStartPos+2); - if(segmentType in IptcFieldMap) { - dataSize = dataView.getInt16(segmentStartPos+3); - segmentSize = dataSize + 5; - fieldName = IptcFieldMap[segmentType]; - fieldValue = getStringFromDB(dataView, segmentStartPos+5, dataSize); - // Check if we already stored a value with this name - if(data.hasOwnProperty(fieldName)) { - // Value already stored with this name, create multivalue field - if(data[fieldName] instanceof Array) { - data[fieldName].push(fieldValue); - } - else { - data[fieldName] = [data[fieldName], fieldValue]; - } - } - else { - data[fieldName] = fieldValue; - } - } - - } - segmentStartPos++; - } - return data; - } - - - - function readTags(file, tiffStart, dirStart, strings, bigEnd) { - var entries = file.getUint16(dirStart, !bigEnd), - tags = {}, - entryOffset, tag, - i; - - for (i=0;i 4 ? valueOffset : (entryOffset + 8); - vals = []; - for (n=0;n 4 ? valueOffset : (entryOffset + 8); - return getStringFromDB(file, offset, numValues-1); - - case 3: // short, 16 bit int - if (numValues == 1) { - return file.getUint16(entryOffset + 8, !bigEnd); - } else { - offset = numValues > 2 ? valueOffset : (entryOffset + 8); - vals = []; - for (n=0;n dataView.byteLength) { // this should not happen - // console.log('******** IFD1Offset is outside the bounds of the DataView ********'); - return {}; - } - // console.log('******* thumbnail IFD offset (IFD1) is: %s', IFD1OffsetPointer); - - var thumbTags = readTags(dataView, tiffStart, tiffStart + IFD1OffsetPointer, IFD1Tags, bigEnd) - - // EXIF 2.3 specification for JPEG format thumbnail - - // If the value of Compression(0x0103) Tag in IFD1 is '6', thumbnail image format is JPEG. - // Most of Exif image uses JPEG format for thumbnail. In that case, you can get offset of thumbnail - // by JpegIFOffset(0x0201) Tag in IFD1, size of thumbnail by JpegIFByteCount(0x0202) Tag. - // Data format is ordinary JPEG format, starts from 0xFFD8 and ends by 0xFFD9. It seems that - // JPEG format and 160x120pixels of size are recommended thumbnail format for Exif2.1 or later. - - if (thumbTags['Compression']) { - // console.log('Thumbnail image found!'); - - switch (thumbTags['Compression']) { - case 6: - // console.log('Thumbnail image format is JPEG'); - if (thumbTags.JpegIFOffset && thumbTags.JpegIFByteCount) { - // extract the thumbnail - var tOffset = tiffStart + thumbTags.JpegIFOffset; - var tLength = thumbTags.JpegIFByteCount; - thumbTags['blob'] = new Blob([new Uint8Array(dataView.buffer, tOffset, tLength)], { - type: 'image/jpeg' - }); - } - break; - - case 1: - console.log("Thumbnail image format is TIFF, which is not implemented."); - break; - default: - console.log("Unknown thumbnail image format '%s'", thumbTags['Compression']); - } - } - else if (thumbTags['PhotometricInterpretation'] == 2) { - console.log("Thumbnail image format is RGB, which is not implemented."); - } - return thumbTags; - } - - function getStringFromDB(buffer, start, length) { - var outstr = ""; - for (var n = start; n < start+length; n++) { - outstr += String.fromCharCode(buffer.getUint8(n)); - } - return outstr; - } - - function readEXIFData(file, start) { - if (getStringFromDB(file, start, 4) != "Exif") { - if (debug) console.log("Not valid EXIF data! " + getStringFromDB(file, start, 4)); - return false; - } - - var bigEnd, - tags, tag, - exifData, gpsData, - tiffOffset = start + 6; - - // test for TIFF validity and endianness - if (file.getUint16(tiffOffset) == 0x4949) { - bigEnd = false; - } else if (file.getUint16(tiffOffset) == 0x4D4D) { - bigEnd = true; - } else { - if (debug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)"); - return false; - } - - if (file.getUint16(tiffOffset+2, !bigEnd) != 0x002A) { - if (debug) console.log("Not valid TIFF data! (no 0x002A)"); - return false; - } - - var firstIFDOffset = file.getUint32(tiffOffset+4, !bigEnd); - - if (firstIFDOffset < 0x00000008) { - if (debug) console.log("Not valid TIFF data! (First offset less than 8)", file.getUint32(tiffOffset+4, !bigEnd)); - return false; - } - - tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd); - - if (tags.ExifIFDPointer) { - exifData = readTags(file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd); - for (tag in exifData) { - switch (tag) { - case "LightSource" : - case "Flash" : - case "MeteringMode" : - case "ExposureProgram" : - case "SensingMethod" : - case "SceneCaptureType" : - case "SceneType" : - case "CustomRendered" : - case "WhiteBalance" : - case "GainControl" : - case "Contrast" : - case "Saturation" : - case "Sharpness" : - case "SubjectDistanceRange" : - case "FileSource" : - exifData[tag] = StringValues[tag][exifData[tag]]; - break; - - case "ExifVersion" : - case "FlashpixVersion" : - exifData[tag] = String.fromCharCode(exifData[tag][0], exifData[tag][1], exifData[tag][2], exifData[tag][3]); - break; - - case "ComponentsConfiguration" : - exifData[tag] = - StringValues.Components[exifData[tag][0]] + - StringValues.Components[exifData[tag][1]] + - StringValues.Components[exifData[tag][2]] + - StringValues.Components[exifData[tag][3]]; - break; - } - tags[tag] = exifData[tag]; - } - } - - if (tags.GPSInfoIFDPointer) { - gpsData = readTags(file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd); - for (tag in gpsData) { - switch (tag) { - case "GPSVersionID" : - gpsData[tag] = gpsData[tag][0] + - "." + gpsData[tag][1] + - "." + gpsData[tag][2] + - "." + gpsData[tag][3]; - break; - } - tags[tag] = gpsData[tag]; - } - } - - // extract thumbnail - tags['thumbnail'] = readThumbnailImage(file, tiffOffset, firstIFDOffset, bigEnd); - - return tags; - } - - function findXMPinJPEG(file) { - - if (!('DOMParser' in self)) { - // console.warn('XML parsing not supported without DOMParser'); - return; - } - var dataView = new DataView(file); - - if (debug) console.log("Got file of length " + file.byteLength); - if ((dataView.getUint8(0) != 0xFF) || (dataView.getUint8(1) != 0xD8)) { - if (debug) console.log("Not a valid JPEG"); - return false; // not a valid jpeg - } - - var offset = 2, - length = file.byteLength, - dom = new DOMParser(); - - while (offset < (length-4)) { - if (getStringFromDB(dataView, offset, 4) == "http") { - var startOffset = offset - 1; - var sectionLength = dataView.getUint16(offset - 2) - 1; - var xmpString = getStringFromDB(dataView, startOffset, sectionLength) - var xmpEndIndex = xmpString.indexOf('xmpmeta>') + 8; - xmpString = xmpString.substring( xmpString.indexOf( ' 0) { - json['@attributes'] = {}; - for (var j = 0; j < xml.attributes.length; j++) { - var attribute = xml.attributes.item(j); - json['@attributes'][attribute.nodeName] = attribute.nodeValue; - } - } - } else if (xml.nodeType == 3) { // text node - return xml.nodeValue; - } - - // deal with children - if (xml.hasChildNodes()) { - for(var i = 0; i < xml.childNodes.length; i++) { - var child = xml.childNodes.item(i); - var nodeName = child.nodeName; - if (json[nodeName] == null) { - json[nodeName] = xml2json(child); - } else { - if (json[nodeName].push == null) { - var old = json[nodeName]; - json[nodeName] = []; - json[nodeName].push(old); - } - json[nodeName].push(xml2json(child)); - } - } - } - - return json; - } - - function xml2Object(xml) { - try { - var obj = {}; - if (xml.children.length > 0) { - for (var i = 0; i < xml.children.length; i++) { - var item = xml.children.item(i); - var attributes = item.attributes; - for(var idx in attributes) { - var itemAtt = attributes[idx]; - var dataKey = itemAtt.nodeName; - var dataValue = itemAtt.nodeValue; - - if(dataKey !== undefined) { - obj[dataKey] = dataValue; - } - } - var nodeName = item.nodeName; - - if (typeof (obj[nodeName]) == "undefined") { - obj[nodeName] = xml2json(item); - } else { - if (typeof (obj[nodeName].push) == "undefined") { - var old = obj[nodeName]; - - obj[nodeName] = []; - obj[nodeName].push(old); - } - obj[nodeName].push(xml2json(item)); - } - } - } else { - obj = xml.textContent; - } - return obj; - } catch (e) { - console.log(e.message); - } - } - - EXIF.enableXmp = function() { - EXIF.isXmpEnabled = true; - } - - EXIF.disableXmp = function() { - EXIF.isXmpEnabled = false; - } - - EXIF.getData = function(img, callback) { - if (((self.Image && img instanceof self.Image) - || (self.HTMLImageElement && img instanceof self.HTMLImageElement)) - && !img.complete) - return false; - - if (!imageHasData(img)) { - getImageData(img, callback); - } else { - if (callback) { - callback.call(img); - } - } - return true; - } - - EXIF.getTag = function(img, tag) { - if (!imageHasData(img)) return; - return img.exifdata[tag]; - } - - EXIF.getIptcTag = function(img, tag) { - if (!imageHasData(img)) return; - return img.iptcdata[tag]; - } - - EXIF.getAllTags = function(img) { - if (!imageHasData(img)) return {}; - var a, - data = img.exifdata, - tags = {}; - for (a in data) { - if (data.hasOwnProperty(a)) { - tags[a] = data[a]; - } - } - return tags; - } - - EXIF.getAllIptcTags = function(img) { - if (!imageHasData(img)) return {}; - var a, - data = img.iptcdata, - tags = {}; - for (a in data) { - if (data.hasOwnProperty(a)) { - tags[a] = data[a]; - } - } - return tags; - } - - EXIF.pretty = function(img) { - if (!imageHasData(img)) return ""; - var a, - data = img.exifdata, - strPretty = ""; - for (a in data) { - if (data.hasOwnProperty(a)) { - if (typeof data[a] == "object") { - if (data[a] instanceof Number) { - strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n"; - } else { - strPretty += a + " : [" + data[a].length + " values]\r\n"; - } - } else { - strPretty += a + " : " + data[a] + "\r\n"; - } - } - } - return strPretty; - } - - EXIF.readFromBinaryFile = function(file) { - return findEXIFinJPEG(file); - } - - if (typeof define === 'function' && define.amd) { - define('exif-js', [], function() { - return EXIF; - }); - } -}.call(this)); +( function () { + const debug = false; + + const root = this; + + const EXIF = function ( obj ) { + if ( obj instanceof EXIF ) { + return obj; + } + if ( ! ( this instanceof EXIF ) ) { + return new EXIF( obj ); + } + this.EXIFwrapped = obj; + }; + + if ( typeof exports !== 'undefined' ) { + if ( typeof module !== 'undefined' && module.exports ) { + exports = module.exports = EXIF; + } + exports.EXIF = EXIF; + } else { + root.EXIF = EXIF; + } + + const ExifTags = ( EXIF.Tags = { + // version tags + 0x9000: 'ExifVersion', // EXIF version + 0xa000: 'FlashpixVersion', // Flashpix format version + + // colorspace tags + 0xa001: 'ColorSpace', // Color space information tag + + // image configuration + 0xa002: 'PixelXDimension', // Valid width of meaningful image + 0xa003: 'PixelYDimension', // Valid height of meaningful image + 0x9101: 'ComponentsConfiguration', // Information about channels + 0x9102: 'CompressedBitsPerPixel', // Compressed bits per pixel + + // user information + 0x927c: 'MakerNote', // Any desired information written by the manufacturer + 0x9286: 'UserComment', // Comments by user + + // related file + 0xa004: 'RelatedSoundFile', // Name of related sound file + + // date and time + 0x9003: 'DateTimeOriginal', // Date and time when the original image was generated + 0x9004: 'DateTimeDigitized', // Date and time when the image was stored digitally + 0x9290: 'SubsecTime', // Fractions of seconds for DateTime + 0x9291: 'SubsecTimeOriginal', // Fractions of seconds for DateTimeOriginal + 0x9292: 'SubsecTimeDigitized', // Fractions of seconds for DateTimeDigitized + + // picture-taking conditions + 0x829a: 'ExposureTime', // Exposure time (in seconds) + 0x829d: 'FNumber', // F number + 0x8822: 'ExposureProgram', // Exposure program + 0x8824: 'SpectralSensitivity', // Spectral sensitivity + 0x8827: 'ISOSpeedRatings', // ISO speed rating + 0x8828: 'OECF', // Optoelectric conversion factor + 0x9201: 'ShutterSpeedValue', // Shutter speed + 0x9202: 'ApertureValue', // Lens aperture + 0x9203: 'BrightnessValue', // Value of brightness + 0x9204: 'ExposureBias', // Exposure bias + 0x9205: 'MaxApertureValue', // Smallest F number of lens + 0x9206: 'SubjectDistance', // Distance to subject in meters + 0x9207: 'MeteringMode', // Metering mode + 0x9208: 'LightSource', // Kind of light source + 0x9209: 'Flash', // Flash status + 0x9214: 'SubjectArea', // Location and area of main subject + 0x920a: 'FocalLength', // Focal length of the lens in mm + 0xa20b: 'FlashEnergy', // Strobe energy in BCPS + 0xa20c: 'SpatialFrequencyResponse', // + 0xa20e: 'FocalPlaneXResolution', // Number of pixels in width direction per FocalPlaneResolutionUnit + 0xa20f: 'FocalPlaneYResolution', // Number of pixels in height direction per FocalPlaneResolutionUnit + 0xa210: 'FocalPlaneResolutionUnit', // Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution + 0xa214: 'SubjectLocation', // Location of subject in image + 0xa215: 'ExposureIndex', // Exposure index selected on camera + 0xa217: 'SensingMethod', // Image sensor type + 0xa300: 'FileSource', // Image source (3 == DSC) + 0xa301: 'SceneType', // Scene type (1 == directly photographed) + 0xa302: 'CFAPattern', // Color filter array geometric pattern + 0xa401: 'CustomRendered', // Special processing + 0xa402: 'ExposureMode', // Exposure mode + 0xa403: 'WhiteBalance', // 1 = auto white balance, 2 = manual + 0xa404: 'DigitalZoomRation', // Digital zoom ratio + 0xa405: 'FocalLengthIn35mmFilm', // Equivalent foacl length assuming 35mm film camera (in mm) + 0xa406: 'SceneCaptureType', // Type of scene + 0xa407: 'GainControl', // Degree of overall image gain adjustment + 0xa408: 'Contrast', // Direction of contrast processing applied by camera + 0xa409: 'Saturation', // Direction of saturation processing applied by camera + 0xa40a: 'Sharpness', // Direction of sharpness processing applied by camera + 0xa40b: 'DeviceSettingDescription', // + 0xa40c: 'SubjectDistanceRange', // Distance to subject + + // other tags + 0xa005: 'InteroperabilityIFDPointer', + 0xa420: 'ImageUniqueID', // Identifier assigned uniquely to each image + } ); + + const TiffTags = ( EXIF.TiffTags = { + 0x0100: 'ImageWidth', + 0x0101: 'ImageHeight', + 0x8769: 'ExifIFDPointer', + 0x8825: 'GPSInfoIFDPointer', + 0xa005: 'InteroperabilityIFDPointer', + 0x0102: 'BitsPerSample', + 0x0103: 'Compression', + 0x0106: 'PhotometricInterpretation', + 0x0112: 'Orientation', + 0x0115: 'SamplesPerPixel', + 0x011c: 'PlanarConfiguration', + 0x0212: 'YCbCrSubSampling', + 0x0213: 'YCbCrPositioning', + 0x011a: 'XResolution', + 0x011b: 'YResolution', + 0x0128: 'ResolutionUnit', + 0x0111: 'StripOffsets', + 0x0116: 'RowsPerStrip', + 0x0117: 'StripByteCounts', + 0x0201: 'JPEGInterchangeFormat', + 0x0202: 'JPEGInterchangeFormatLength', + 0x012d: 'TransferFunction', + 0x013e: 'WhitePoint', + 0x013f: 'PrimaryChromaticities', + 0x0211: 'YCbCrCoefficients', + 0x0214: 'ReferenceBlackWhite', + 0x0132: 'DateTime', + 0x010e: 'ImageDescription', + 0x010f: 'Make', + 0x0110: 'Model', + 0x0131: 'Software', + 0x013b: 'Artist', + 0x8298: 'Copyright', + } ); + + const GPSTags = ( EXIF.GPSTags = { + 0x0000: 'GPSVersionID', + 0x0001: 'GPSLatitudeRef', + 0x0002: 'GPSLatitude', + 0x0003: 'GPSLongitudeRef', + 0x0004: 'GPSLongitude', + 0x0005: 'GPSAltitudeRef', + 0x0006: 'GPSAltitude', + 0x0007: 'GPSTimeStamp', + 0x0008: 'GPSSatellites', + 0x0009: 'GPSStatus', + 0x000a: 'GPSMeasureMode', + 0x000b: 'GPSDOP', + 0x000c: 'GPSSpeedRef', + 0x000d: 'GPSSpeed', + 0x000e: 'GPSTrackRef', + 0x000f: 'GPSTrack', + 0x0010: 'GPSImgDirectionRef', + 0x0011: 'GPSImgDirection', + 0x0012: 'GPSMapDatum', + 0x0013: 'GPSDestLatitudeRef', + 0x0014: 'GPSDestLatitude', + 0x0015: 'GPSDestLongitudeRef', + 0x0016: 'GPSDestLongitude', + 0x0017: 'GPSDestBearingRef', + 0x0018: 'GPSDestBearing', + 0x0019: 'GPSDestDistanceRef', + 0x001a: 'GPSDestDistance', + 0x001b: 'GPSProcessingMethod', + 0x001c: 'GPSAreaInformation', + 0x001d: 'GPSDateStamp', + 0x001e: 'GPSDifferential', + } ); + + // EXIF 2.3 Spec + const IFD1Tags = ( EXIF.IFD1Tags = { + 0x0100: 'ImageWidth', + 0x0101: 'ImageHeight', + 0x0102: 'BitsPerSample', + 0x0103: 'Compression', + 0x0106: 'PhotometricInterpretation', + 0x0111: 'StripOffsets', + 0x0112: 'Orientation', + 0x0115: 'SamplesPerPixel', + 0x0116: 'RowsPerStrip', + 0x0117: 'StripByteCounts', + 0x011a: 'XResolution', + 0x011b: 'YResolution', + 0x011c: 'PlanarConfiguration', + 0x0128: 'ResolutionUnit', + 0x0201: 'JpegIFOffset', // When image format is JPEG, this value show offset to JPEG data stored.(aka "ThumbnailOffset" or "JPEGInterchangeFormat") + 0x0202: 'JpegIFByteCount', // When image format is JPEG, this value shows data size of JPEG image (aka "ThumbnailLength" or "JPEGInterchangeFormatLength") + 0x0211: 'YCbCrCoefficients', + 0x0212: 'YCbCrSubSampling', + 0x0213: 'YCbCrPositioning', + 0x0214: 'ReferenceBlackWhite', + } ); + + const StringValues = ( EXIF.StringValues = { + ExposureProgram: { + 0: 'Not defined', + 1: 'Manual', + 2: 'Normal program', + 3: 'Aperture priority', + 4: 'Shutter priority', + 5: 'Creative program', + 6: 'Action program', + 7: 'Portrait mode', + 8: 'Landscape mode', + }, + MeteringMode: { + 0: 'Unknown', + 1: 'Average', + 2: 'CenterWeightedAverage', + 3: 'Spot', + 4: 'MultiSpot', + 5: 'Pattern', + 6: 'Partial', + 255: 'Other', + }, + LightSource: { + 0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten (incandescent light)', + 4: 'Flash', + 9: 'Fine weather', + 10: 'Cloudy weather', + 11: 'Shade', + 12: 'Daylight fluorescent (D 5700 - 7100K)', + 13: 'Day white fluorescent (N 4600 - 5400K)', + 14: 'Cool white fluorescent (W 3900 - 4500K)', + 15: 'White fluorescent (WW 3200 - 3700K)', + 17: 'Standard light A', + 18: 'Standard light B', + 19: 'Standard light C', + 20: 'D55', + 21: 'D65', + 22: 'D75', + 23: 'D50', + 24: 'ISO studio tungsten', + 255: 'Other', + }, + Flash: { + 0x0000: 'Flash did not fire', + 0x0001: 'Flash fired', + 0x0005: 'Strobe return light not detected', + 0x0007: 'Strobe return light detected', + 0x0009: 'Flash fired, compulsory flash mode', + 0x000d: 'Flash fired, compulsory flash mode, return light not detected', + 0x000f: 'Flash fired, compulsory flash mode, return light detected', + 0x0010: 'Flash did not fire, compulsory flash mode', + 0x0018: 'Flash did not fire, auto mode', + 0x0019: 'Flash fired, auto mode', + 0x001d: 'Flash fired, auto mode, return light not detected', + 0x001f: 'Flash fired, auto mode, return light detected', + 0x0020: 'No flash function', + 0x0041: 'Flash fired, red-eye reduction mode', + 0x0045: 'Flash fired, red-eye reduction mode, return light not detected', + 0x0047: 'Flash fired, red-eye reduction mode, return light detected', + 0x0049: 'Flash fired, compulsory flash mode, red-eye reduction mode', + 0x004d: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', + 0x004f: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', + 0x0059: 'Flash fired, auto mode, red-eye reduction mode', + 0x005d: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', + 0x005f: 'Flash fired, auto mode, return light detected, red-eye reduction mode', + }, + SensingMethod: { + 1: 'Not defined', + 2: 'One-chip color area sensor', + 3: 'Two-chip color area sensor', + 4: 'Three-chip color area sensor', + 5: 'Color sequential area sensor', + 7: 'Trilinear sensor', + 8: 'Color sequential linear sensor', + }, + SceneCaptureType: { + 0: 'Standard', + 1: 'Landscape', + 2: 'Portrait', + 3: 'Night scene', + }, + SceneType: { + 1: 'Directly photographed', + }, + CustomRendered: { + 0: 'Normal process', + 1: 'Custom process', + }, + WhiteBalance: { + 0: 'Auto white balance', + 1: 'Manual white balance', + }, + GainControl: { + 0: 'None', + 1: 'Low gain up', + 2: 'High gain up', + 3: 'Low gain down', + 4: 'High gain down', + }, + Contrast: { + 0: 'Normal', + 1: 'Soft', + 2: 'Hard', + }, + Saturation: { + 0: 'Normal', + 1: 'Low saturation', + 2: 'High saturation', + }, + Sharpness: { + 0: 'Normal', + 1: 'Soft', + 2: 'Hard', + }, + SubjectDistanceRange: { + 0: 'Unknown', + 1: 'Macro', + 2: 'Close view', + 3: 'Distant view', + }, + FileSource: { + 3: 'DSC', + }, + + Components: { + 0: '', + 1: 'Y', + 2: 'Cb', + 3: 'Cr', + 4: 'R', + 5: 'G', + 6: 'B', + }, + } ); + + function addEvent( element, event, handler ) { + if ( element.addEventListener ) { + element.addEventListener( event, handler, false ); + } else if ( element.attachEvent ) { + element.attachEvent( 'on' + event, handler ); + } + } + + function imageHasData( img ) { + return !! img.exifdata; + } + + function base64ToArrayBuffer( base64, contentType ) { + contentType = + contentType || + base64.match( /^data\:([^\;]+)\;base64,/im )[ 1 ] || + ''; // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg' + base64 = base64.replace( /^data\:([^\;]+)\;base64,/gim, '' ); + const binary = atob( base64 ); + const len = binary.length; + const buffer = new ArrayBuffer( len ); + const view = new Uint8Array( buffer ); + for ( let i = 0; i < len; i++ ) { + view[ i ] = binary.charCodeAt( i ); + } + return buffer; + } + + function objectURLToBlob( url, callback ) { + const http = new XMLHttpRequest(); + http.open( 'GET', url, true ); + http.responseType = 'blob'; + http.onload = function ( e ) { + if ( this.status == 200 || this.status === 0 ) { + callback( this.response ); + } + }; + http.send(); + } + + function getImageData( img, callback ) { + function handleBinaryFile( binFile ) { + const data = findEXIFinJPEG( binFile ); + img.exifdata = data || {}; + const iptcdata = findIPTCinJPEG( binFile ); + img.iptcdata = iptcdata || {}; + if ( EXIF.isXmpEnabled ) { + const xmpdata = findXMPinJPEG( binFile ); + img.xmpdata = xmpdata || {}; + } + if ( callback ) { + callback.call( img ); + } + } + + if ( img.src ) { + if ( /^data\:/i.test( img.src ) ) { + // Data URI + const arrayBuffer = base64ToArrayBuffer( img.src ); + handleBinaryFile( arrayBuffer ); + } else if ( /^blob\:/i.test( img.src ) ) { + // Object URL + var fileReader = new FileReader(); + fileReader.onload = function ( e ) { + handleBinaryFile( e.target.result ); + }; + objectURLToBlob( img.src, function ( blob ) { + fileReader.readAsArrayBuffer( blob ); + } ); + } else { + let http = new XMLHttpRequest(); + http.onload = function () { + if ( this.status == 200 || this.status === 0 ) { + handleBinaryFile( http.response ); + } else { + throw 'Could not load image'; + } + http = null; + }; + http.open( 'GET', img.src, true ); + http.responseType = 'arraybuffer'; + http.send( null ); + } + } else if ( + self.FileReader && + ( img instanceof self.Blob || img instanceof self.File ) + ) { + var fileReader = new FileReader(); + fileReader.onload = function ( e ) { + if ( debug ) { + console.log( + 'Got file of length ' + e.target.result.byteLength + ); + } + handleBinaryFile( e.target.result ); + }; + + fileReader.readAsArrayBuffer( img ); + } + } + + function findEXIFinJPEG( file ) { + const dataView = new DataView( file ); + + if ( debug ) { + console.log( 'Got file of length ' + file.byteLength ); + } + if ( + dataView.getUint8( 0 ) != 0xff || + dataView.getUint8( 1 ) != 0xd8 + ) { + if ( debug ) { + console.log( 'Not a valid JPEG' ); + } + return false; // not a valid jpeg + } + + let offset = 2, + length = file.byteLength, + marker; + + while ( offset < length ) { + if ( dataView.getUint8( offset ) != 0xff ) { + if ( debug ) { + console.log( + 'Not a valid marker at offset ' + + offset + + ', found: ' + + dataView.getUint8( offset ) + ); + } + return false; // not a valid marker, something is wrong + } + + marker = dataView.getUint8( offset + 1 ); + if ( debug ) { + console.log( marker ); + } + + // we could implement handling for other markers here, + // but we're only looking for 0xFFE1 for EXIF data + + if ( marker == 225 ) { + if ( debug ) { + console.log( 'Found 0xFFE1 marker' ); + } + + return readEXIFData( + dataView, + offset + 4, + dataView.getUint16( offset + 2 ) - 2 + ); + + // offset += 2 + file.getShortAt(offset+2, true); + } + offset += 2 + dataView.getUint16( offset + 2 ); + } + } + + function findIPTCinJPEG( file ) { + const dataView = new DataView( file ); + + if ( debug ) { + console.log( 'Got file of length ' + file.byteLength ); + } + if ( + dataView.getUint8( 0 ) != 0xff || + dataView.getUint8( 1 ) != 0xd8 + ) { + if ( debug ) { + console.log( 'Not a valid JPEG' ); + } + return false; // not a valid jpeg + } + + let offset = 2, + length = file.byteLength; + + const isFieldSegmentStart = function ( dataView, offset ) { + return ( + dataView.getUint8( offset ) === 0x38 && + dataView.getUint8( offset + 1 ) === 0x42 && + dataView.getUint8( offset + 2 ) === 0x49 && + dataView.getUint8( offset + 3 ) === 0x4d && + dataView.getUint8( offset + 4 ) === 0x04 && + dataView.getUint8( offset + 5 ) === 0x04 + ); + }; + + while ( offset < length ) { + if ( isFieldSegmentStart( dataView, offset ) ) { + // Get the length of the name header (which is padded to an even number of bytes) + let nameHeaderLength = dataView.getUint8( offset + 7 ); + if ( nameHeaderLength % 2 !== 0 ) { + nameHeaderLength += 1; + } + // Check for pre photoshop 6 format + if ( nameHeaderLength === 0 ) { + // Always 4 + nameHeaderLength = 4; + } + + const startOffset = offset + 8 + nameHeaderLength; + const sectionLength = dataView.getUint16( + offset + 6 + nameHeaderLength + ); + + return readIPTCData( file, startOffset, sectionLength ); + + break; + } + + // Not the marker, continue searching + offset++; + } + } + const IptcFieldMap = { + 0x78: 'caption', + 0x6e: 'credit', + 0x19: 'keywords', + 0x37: 'dateCreated', + 0x50: 'byline', + 0x55: 'bylineTitle', + 0x7a: 'captionWriter', + 0x69: 'headline', + 0x74: 'copyright', + 0x0f: 'category', + }; + function readIPTCData( file, startOffset, sectionLength ) { + const dataView = new DataView( file ); + const data = {}; + let fieldValue, fieldName, dataSize, segmentType, segmentSize; + let segmentStartPos = startOffset; + while ( segmentStartPos < startOffset + sectionLength ) { + if ( + dataView.getUint8( segmentStartPos ) === 0x1c && + dataView.getUint8( segmentStartPos + 1 ) === 0x02 + ) { + segmentType = dataView.getUint8( segmentStartPos + 2 ); + if ( segmentType in IptcFieldMap ) { + dataSize = dataView.getInt16( segmentStartPos + 3 ); + segmentSize = dataSize + 5; + fieldName = IptcFieldMap[ segmentType ]; + fieldValue = getStringFromDB( + dataView, + segmentStartPos + 5, + dataSize + ); + // Check if we already stored a value with this name + if ( data.hasOwnProperty( fieldName ) ) { + // Value already stored with this name, create multivalue field + if ( data[ fieldName ] instanceof Array ) { + data[ fieldName ].push( fieldValue ); + } else { + data[ fieldName ] = [ + data[ fieldName ], + fieldValue, + ]; + } + } else { + data[ fieldName ] = fieldValue; + } + } + } + segmentStartPos++; + } + return data; + } + + function readTags( file, tiffStart, dirStart, strings, bigEnd ) { + let entries = file.getUint16( dirStart, ! bigEnd ), + tags = {}, + entryOffset, + tag, + i; + + for ( i = 0; i < entries; i++ ) { + entryOffset = dirStart + i * 12 + 2; + tag = strings[ file.getUint16( entryOffset, ! bigEnd ) ]; + if ( ! tag && debug ) { + console.log( + 'Unknown tag: ' + file.getUint16( entryOffset, ! bigEnd ) + ); + } + tags[ tag ] = readTagValue( + file, + entryOffset, + tiffStart, + dirStart, + bigEnd + ); + } + return tags; + } + + function readTagValue( file, entryOffset, tiffStart, dirStart, bigEnd ) { + let type = file.getUint16( entryOffset + 2, ! bigEnd ), + numValues = file.getUint32( entryOffset + 4, ! bigEnd ), + valueOffset = + file.getUint32( entryOffset + 8, ! bigEnd ) + tiffStart, + offset, + vals, + val, + n, + numerator, + denominator; + + switch ( type ) { + case 1: // byte, 8-bit unsigned int + case 7: // undefined, 8-bit byte, value depending on field + if ( numValues == 1 ) { + return file.getUint8( entryOffset + 8, ! bigEnd ); + } + offset = numValues > 4 ? valueOffset : entryOffset + 8; + vals = []; + for ( n = 0; n < numValues; n++ ) { + vals[ n ] = file.getUint8( offset + n ); + } + return vals; + + case 2: // ascii, 8-bit byte + offset = numValues > 4 ? valueOffset : entryOffset + 8; + return getStringFromDB( file, offset, numValues - 1 ); + + case 3: // short, 16 bit int + if ( numValues == 1 ) { + return file.getUint16( entryOffset + 8, ! bigEnd ); + } + offset = numValues > 2 ? valueOffset : entryOffset + 8; + vals = []; + for ( n = 0; n < numValues; n++ ) { + vals[ n ] = file.getUint16( offset + 2 * n, ! bigEnd ); + } + return vals; + + case 4: // long, 32 bit int + if ( numValues == 1 ) { + return file.getUint32( entryOffset + 8, ! bigEnd ); + } + vals = []; + for ( n = 0; n < numValues; n++ ) { + vals[ n ] = file.getUint32( valueOffset + 4 * n, ! bigEnd ); + } + return vals; + + case 5: // rational = two long values, first is numerator, second is denominator + if ( numValues == 1 ) { + numerator = file.getUint32( valueOffset, ! bigEnd ); + denominator = file.getUint32( valueOffset + 4, ! bigEnd ); + val = new Number( numerator / denominator ); + val.numerator = numerator; + val.denominator = denominator; + return val; + } + vals = []; + for ( n = 0; n < numValues; n++ ) { + numerator = file.getUint32( valueOffset + 8 * n, ! bigEnd ); + denominator = file.getUint32( + valueOffset + 4 + 8 * n, + ! bigEnd + ); + vals[ n ] = new Number( numerator / denominator ); + vals[ n ].numerator = numerator; + vals[ n ].denominator = denominator; + } + return vals; + + case 9: // slong, 32 bit signed int + if ( numValues == 1 ) { + return file.getInt32( entryOffset + 8, ! bigEnd ); + } + vals = []; + for ( n = 0; n < numValues; n++ ) { + vals[ n ] = file.getInt32( valueOffset + 4 * n, ! bigEnd ); + } + return vals; + + case 10: // signed rational, two slongs, first is numerator, second is denominator + if ( numValues == 1 ) { + return ( + file.getInt32( valueOffset, ! bigEnd ) / + file.getInt32( valueOffset + 4, ! bigEnd ) + ); + } + vals = []; + for ( n = 0; n < numValues; n++ ) { + vals[ n ] = + file.getInt32( valueOffset + 8 * n, ! bigEnd ) / + file.getInt32( valueOffset + 4 + 8 * n, ! bigEnd ); + } + return vals; + } + } + + /** + * Given an IFD (Image File Directory) start offset + * returns an offset to next IFD or 0 if it's the last IFD. + * @param dataView + * @param dirStart + * @param bigEnd + */ + function getNextIFDOffset( dataView, dirStart, bigEnd ) { + //the first 2bytes means the number of directory entries contains in this IFD + const entries = dataView.getUint16( dirStart, ! bigEnd ); + + // After last directory entry, there is a 4bytes of data, + // it means an offset to next IFD. + // If its value is '0x00000000', it means this is the last IFD and there is no linked IFD. + + return dataView.getUint32( dirStart + 2 + entries * 12, ! bigEnd ); // each entry is 12 bytes long + } + + function readThumbnailImage( dataView, tiffStart, firstIFDOffset, bigEnd ) { + // get the IFD1 offset + const IFD1OffsetPointer = getNextIFDOffset( + dataView, + tiffStart + firstIFDOffset, + bigEnd + ); + + if ( ! IFD1OffsetPointer ) { + // console.log('******** IFD1Offset is empty, image thumb not found ********'); + return {}; + } else if ( IFD1OffsetPointer > dataView.byteLength ) { + // this should not happen + // console.log('******** IFD1Offset is outside the bounds of the DataView ********'); + return {}; + } + // console.log('******* thumbnail IFD offset (IFD1) is: %s', IFD1OffsetPointer); + + const thumbTags = readTags( + dataView, + tiffStart, + tiffStart + IFD1OffsetPointer, + IFD1Tags, + bigEnd + ); + + // EXIF 2.3 specification for JPEG format thumbnail + + // If the value of Compression(0x0103) Tag in IFD1 is '6', thumbnail image format is JPEG. + // Most of Exif image uses JPEG format for thumbnail. In that case, you can get offset of thumbnail + // by JpegIFOffset(0x0201) Tag in IFD1, size of thumbnail by JpegIFByteCount(0x0202) Tag. + // Data format is ordinary JPEG format, starts from 0xFFD8 and ends by 0xFFD9. It seems that + // JPEG format and 160x120pixels of size are recommended thumbnail format for Exif2.1 or later. + + if ( thumbTags.Compression ) { + // console.log('Thumbnail image found!'); + + switch ( thumbTags.Compression ) { + case 6: + // console.log('Thumbnail image format is JPEG'); + if ( thumbTags.JpegIFOffset && thumbTags.JpegIFByteCount ) { + // extract the thumbnail + const tOffset = tiffStart + thumbTags.JpegIFOffset; + const tLength = thumbTags.JpegIFByteCount; + thumbTags.blob = new Blob( + [ + new Uint8Array( + dataView.buffer, + tOffset, + tLength + ), + ], + { + type: 'image/jpeg', + } + ); + } + break; + + case 1: + console.log( + 'Thumbnail image format is TIFF, which is not implemented.' + ); + break; + default: + console.log( + "Unknown thumbnail image format '%s'", + thumbTags.Compression + ); + } + } else if ( thumbTags.PhotometricInterpretation == 2 ) { + console.log( + 'Thumbnail image format is RGB, which is not implemented.' + ); + } + return thumbTags; + } + + function getStringFromDB( buffer, start, length ) { + let outstr = ''; + for ( let n = start; n < start + length; n++ ) { + outstr += String.fromCharCode( buffer.getUint8( n ) ); + } + return outstr; + } + + function readEXIFData( file, start ) { + if ( getStringFromDB( file, start, 4 ) != 'Exif' ) { + if ( debug ) { + console.log( + 'Not valid EXIF data! ' + getStringFromDB( file, start, 4 ) + ); + } + return false; + } + + let bigEnd, + tags, + tag, + exifData, + gpsData, + tiffOffset = start + 6; + + // test for TIFF validity and endianness + if ( file.getUint16( tiffOffset ) == 0x4949 ) { + bigEnd = false; + } else if ( file.getUint16( tiffOffset ) == 0x4d4d ) { + bigEnd = true; + } else { + if ( debug ) { + console.log( 'Not valid TIFF data! (no 0x4949 or 0x4D4D)' ); + } + return false; + } + + if ( file.getUint16( tiffOffset + 2, ! bigEnd ) != 0x002a ) { + if ( debug ) { + console.log( 'Not valid TIFF data! (no 0x002A)' ); + } + return false; + } + + const firstIFDOffset = file.getUint32( tiffOffset + 4, ! bigEnd ); + + if ( firstIFDOffset < 0x00000008 ) { + if ( debug ) { + console.log( + 'Not valid TIFF data! (First offset less than 8)', + file.getUint32( tiffOffset + 4, ! bigEnd ) + ); + } + return false; + } + + tags = readTags( + file, + tiffOffset, + tiffOffset + firstIFDOffset, + TiffTags, + bigEnd + ); + + if ( tags.ExifIFDPointer ) { + exifData = readTags( + file, + tiffOffset, + tiffOffset + tags.ExifIFDPointer, + ExifTags, + bigEnd + ); + for ( tag in exifData ) { + switch ( tag ) { + case 'LightSource': + case 'Flash': + case 'MeteringMode': + case 'ExposureProgram': + case 'SensingMethod': + case 'SceneCaptureType': + case 'SceneType': + case 'CustomRendered': + case 'WhiteBalance': + case 'GainControl': + case 'Contrast': + case 'Saturation': + case 'Sharpness': + case 'SubjectDistanceRange': + case 'FileSource': + exifData[ tag ] = + StringValues[ tag ][ exifData[ tag ] ]; + break; + + case 'ExifVersion': + case 'FlashpixVersion': + exifData[ tag ] = String.fromCharCode( + exifData[ tag ][ 0 ], + exifData[ tag ][ 1 ], + exifData[ tag ][ 2 ], + exifData[ tag ][ 3 ] + ); + break; + + case 'ComponentsConfiguration': + exifData[ tag ] = + StringValues.Components[ exifData[ tag ][ 0 ] ] + + StringValues.Components[ exifData[ tag ][ 1 ] ] + + StringValues.Components[ exifData[ tag ][ 2 ] ] + + StringValues.Components[ exifData[ tag ][ 3 ] ]; + break; + } + tags[ tag ] = exifData[ tag ]; + } + } + + if ( tags.GPSInfoIFDPointer ) { + gpsData = readTags( + file, + tiffOffset, + tiffOffset + tags.GPSInfoIFDPointer, + GPSTags, + bigEnd + ); + for ( tag in gpsData ) { + switch ( tag ) { + case 'GPSVersionID': + gpsData[ tag ] = + gpsData[ tag ][ 0 ] + + '.' + + gpsData[ tag ][ 1 ] + + '.' + + gpsData[ tag ][ 2 ] + + '.' + + gpsData[ tag ][ 3 ]; + break; + } + tags[ tag ] = gpsData[ tag ]; + } + } + + // extract thumbnail + tags.thumbnail = readThumbnailImage( + file, + tiffOffset, + firstIFDOffset, + bigEnd + ); + + return tags; + } + + function findXMPinJPEG( file ) { + if ( ! ( 'DOMParser' in self ) ) { + // console.warn('XML parsing not supported without DOMParser'); + return; + } + const dataView = new DataView( file ); + + if ( debug ) { + console.log( 'Got file of length ' + file.byteLength ); + } + if ( + dataView.getUint8( 0 ) != 0xff || + dataView.getUint8( 1 ) != 0xd8 + ) { + if ( debug ) { + console.log( 'Not a valid JPEG' ); + } + return false; // not a valid jpeg + } + + let offset = 2, + length = file.byteLength, + dom = new DOMParser(); + + while ( offset < length - 4 ) { + if ( getStringFromDB( dataView, offset, 4 ) == 'http' ) { + const startOffset = offset - 1; + const sectionLength = dataView.getUint16( offset - 2 ) - 1; + let xmpString = getStringFromDB( + dataView, + startOffset, + sectionLength + ); + const xmpEndIndex = xmpString.indexOf( 'xmpmeta>' ) + 8; + xmpString = xmpString.substring( + xmpString.indexOf( ' 0 ) { + json[ '@attributes' ] = {}; + for ( let j = 0; j < xml.attributes.length; j++ ) { + const attribute = xml.attributes.item( j ); + json[ '@attributes' ][ attribute.nodeName ] = + attribute.nodeValue; + } + } + } else if ( xml.nodeType == 3 ) { + // text node + return xml.nodeValue; + } + + // deal with children + if ( xml.hasChildNodes() ) { + for ( let i = 0; i < xml.childNodes.length; i++ ) { + const child = xml.childNodes.item( i ); + const nodeName = child.nodeName; + if ( json[ nodeName ] == null ) { + json[ nodeName ] = xml2json( child ); + } else { + if ( json[ nodeName ].push == null ) { + const old = json[ nodeName ]; + json[ nodeName ] = []; + json[ nodeName ].push( old ); + } + json[ nodeName ].push( xml2json( child ) ); + } + } + } + + return json; + } + + function xml2Object( xml ) { + try { + let obj = {}; + if ( xml.children.length > 0 ) { + for ( let i = 0; i < xml.children.length; i++ ) { + const item = xml.children.item( i ); + const attributes = item.attributes; + for ( const idx in attributes ) { + const itemAtt = attributes[ idx ]; + const dataKey = itemAtt.nodeName; + const dataValue = itemAtt.nodeValue; + + if ( dataKey !== undefined ) { + obj[ dataKey ] = dataValue; + } + } + const nodeName = item.nodeName; + + if ( typeof obj[ nodeName ] === 'undefined' ) { + obj[ nodeName ] = xml2json( item ); + } else { + if ( typeof obj[ nodeName ].push === 'undefined' ) { + const old = obj[ nodeName ]; + + obj[ nodeName ] = []; + obj[ nodeName ].push( old ); + } + obj[ nodeName ].push( xml2json( item ) ); + } + } + } else { + obj = xml.textContent; + } + return obj; + } catch ( e ) { + console.log( e.message ); + } + } + + EXIF.enableXmp = function () { + EXIF.isXmpEnabled = true; + }; + + EXIF.disableXmp = function () { + EXIF.isXmpEnabled = false; + }; + + EXIF.getData = function ( img, callback ) { + if ( + ( ( self.Image && img instanceof self.Image ) || + ( self.HTMLImageElement && + img instanceof self.HTMLImageElement ) ) && + ! img.complete + ) { + return false; + } + + if ( ! imageHasData( img ) ) { + getImageData( img, callback ); + } else if ( callback ) { + callback.call( img ); + } + return true; + }; + + EXIF.getTag = function ( img, tag ) { + if ( ! imageHasData( img ) ) { + return; + } + return img.exifdata[ tag ]; + }; + + EXIF.getIptcTag = function ( img, tag ) { + if ( ! imageHasData( img ) ) { + return; + } + return img.iptcdata[ tag ]; + }; + + EXIF.getAllTags = function ( img ) { + if ( ! imageHasData( img ) ) { + return {}; + } + let a, + data = img.exifdata, + tags = {}; + for ( a in data ) { + if ( data.hasOwnProperty( a ) ) { + tags[ a ] = data[ a ]; + } + } + return tags; + }; + + EXIF.getAllIptcTags = function ( img ) { + if ( ! imageHasData( img ) ) { + return {}; + } + let a, + data = img.iptcdata, + tags = {}; + for ( a in data ) { + if ( data.hasOwnProperty( a ) ) { + tags[ a ] = data[ a ]; + } + } + return tags; + }; + + EXIF.pretty = function ( img ) { + if ( ! imageHasData( img ) ) { + return ''; + } + let a, + data = img.exifdata, + strPretty = ''; + for ( a in data ) { + if ( data.hasOwnProperty( a ) ) { + if ( typeof data[ a ] === 'object' ) { + if ( data[ a ] instanceof Number ) { + strPretty += + a + + ' : ' + + data[ a ] + + ' [' + + data[ a ].numerator + + '/' + + data[ a ].denominator + + ']\r\n'; + } else { + strPretty += + a + ' : [' + data[ a ].length + ' values]\r\n'; + } + } else { + strPretty += a + ' : ' + data[ a ] + '\r\n'; + } + } + } + return strPretty; + }; + + EXIF.readFromBinaryFile = function ( file ) { + return findEXIFinJPEG( file ); + }; + + if ( typeof define === 'function' && define.amd ) { + define( 'exif-js', [], function () { + return EXIF; + } ); + } +} ).call( this ); diff --git a/js/file-upload.js b/js/file-upload.js index add8d6d7..791e706e 100644 --- a/js/file-upload.js +++ b/js/file-upload.js @@ -1,740 +1,900 @@ /** - * file upload js - * @since 8.4 - **/ + * File and cropper workflow for PPOM frontend fields. + * + * Each file/cropper field gets a dedicated Plupload instance. This script owns + * the upload UI, preview/croppie state, and the hidden inputs that eventually + * travel through add-to-cart, cart restore, and order meta persistence. + * + * @see ppom_get_field_meta_by_id in js/ppom.inputs.js + * @see ppom_update_option_prices in js/price/ppom-price.js + * @see ppom_generate_cropper_data_for_cart + */ + +/** + * Minimal localized metadata used by the upload/cropper bootstrap. + * + * @typedef {{ + * data_name: string, + * type: 'file'|'cropper', + * file_size: string, + * files_allowed: string, + * file_types: string, + * title?: string, + * required?: string, + * file_cost?: string, + * onetime?: string, + * max_img_w?: string, + * min_img_w?: string, + * max_img_h?: string, + * min_img_h?: string + * }} PPOMUploadFieldMeta + */ let isCartBlock = false; +// Runtime registries keyed by PPOM field data_name. const plupload_instances = Array(); const field_file_count = Array(); const file_list_preview_containers = Array(); -var ppom_file_progress = ''; -var featherEditor = ''; +const ppom_file_progress = ''; +const featherEditor = ''; const uploaderInstances = {}; -var Cropped_Data_Captured = false; - -jQuery(function($) { - - // If cropper input found in fields - // if (ppom_get_field_meta_by_type('cropper').length > 0) { - - // var wc_cart_form = $('form.cart'); - // $(wc_cart_form).on('submit', function(e) { - - // // e.preventDefault(); - // var cropper_fields = ppom_get_field_meta_by_type('cropper'); - // $.each(cropper_fields, function(i, cropper) { - - // if (cropper.legacy_cropper !== undefined) return; - - // var cropper_name = cropper.data_name; - // ppom_generate_cropper_data_for_cart(cropper.data_name); - - // }); - // }); - // } - - $(document).on('ppom_image_ready', function(e) { - - var image_url = e.image_url; - var image_id = e.image.id; - var data_name = e.data_name; - var input_type = e.input_type; - const file_input = e.file_input; - - if (input_type === 'cropper') { - - field_meta = ppom_get_field_meta_by_id(data_name); - // console.log('ppom',field_meta) - if (field_meta.legacy_cropper === undefined) { - ppom_show_cropped_preview(data_name, image_url, image_id, file_input); - // hiding the filelist-{data_name} when preview enabled - $(`#filelist-${data_name}`).hide(); - // hide the file upload area too - $(`.ppom-file-container.${data_name}`).hide(); - // also hide the crop ratio if only one option is provided - if( $(`#crop-size-${data_name} option`).length === 1){ - $(`#crop-size-${data_name}`).hide(); - } - } - } - - // moving modal to body end - $('.ppom-modals').appendTo('body'); - }); - - // On file removed - $(document).on('ppom_uploaded_file_removed', function(e) { - - var field_name = e.field_name; - // var fileid = e.fileid; - - ppom_reset_cropping_preview(field_name); - ppom_update_option_prices(); - }); - - - // Croppie update size - $('.ppom-croppie-preview').on('change', '.ppom-cropping-size', function(e) { - - var data_name = $(this).data('field_name'); - var cropp_preview_container = jQuery(".ppom-croppie-wrapper-" + data_name); - var v_width = $('option:selected', this).data('width'); - var v_height = $('option:selected', this).data('height'); - - cropp_preview_container.find('.croppie-container').each(function(i, croppie_dom) { - - var image_id = jQuery(croppie_dom).attr('data-image_id'); - const croppie_container = jQuery('.ppom-croppie-preview-' + image_id); - const image_url = jQuery(croppie_dom).find('img').attr('src'); - $(croppie_dom).croppie('destroy'); - const viewport = {'width': v_width, 'height': v_height}; - - file_list_preview_containers[data_name]['croppie'][image_id] = croppie_container; - file_list_preview_containers[data_name]['image_id'] = image_id; - file_list_preview_containers[data_name]['image_url'] = image_url; - - ppom_set_croppie_options(data_name, viewport, image_id); - }); - - }); - - // Deleting File - document.querySelector('.ppom-wrapper')?.addEventListener('click', async function(e) { - if ( - ! e.target.classList.contains('u_i_c_tools_del') || - ! plupload_instances - ) { - return; - } - - e.preventDefault(); - - const delMessage = ppom_file_vars.delete_file_msg; - if ( ! confirm( delMessage ) ) return; - - const ppomFileWrapper = e.target.closest('.ppom-file-wrapper'); - const fileId = ppomFileWrapper?.getAttribute("data-fileid"); - const ppomFieldWrapper = e.target.closest('div.ppom-field-wrapper'); - const fileDataName = ppomFieldWrapper?.getAttribute("data-data_name"); - - if ( !fileId || !fileDataName ) return; - - field_file_count[fileDataName] = 0; - - const uploaderInstance = plupload_instances[fileDataName]; - if ( uploaderInstance ) { - uploaderInstance.removeFile(fileId); - } - - const checkbox = document.querySelector(`input[name="ppom[fields][${fileDataName}][${fileId}][org]"]`); - const fileName = checkbox?.value; - - if ( ! fileName ) return; - - // Delete animation. - const imageElement = document.querySelector(`#u_i_c_${fileId} img`); - if ( imageElement ) { - imageElement.src = `${ppom_file_vars.plugin_url}/images/loading.gif`; - } - - const data = new URLSearchParams({ - action: 'ppom_delete_file', - file_name: fileName, - ppom_nonce: ppom_file_vars.ppom_file_delete_nonce - }); - - try { - const response = await fetch(ppom_file_vars.ajaxurl, { - method: 'POST', - body: data, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XMLHttpRequest' - } - }); - - const responseText = await response.text(); - if ( ! response.ok ) { - confirm(`Error: ${responseText}`); - return; - } - - // Update UI - const fileContainer = document.querySelector(`#u_i_c_${fileId}`); - if ( fileContainer ) { - fileContainer.remove(); - } - - if ( checkbox ) { - checkbox.remove(); - } - - const parentBox = e.target.closest('.u_i_c_box'); - if ( parentBox ) { - parentBox.remove(); - } - - const croppiePreview = document.querySelector(`.ppom-croppie-preview-${fileId}`); - if ( croppiePreview ) { - croppiePreview.remove(); - } - - // Send action to PPOM_Validate - document.dispatchEvent(new CustomEvent("ppom_uploaded_file_removed", { - detail: { - field_name: fileDataName, - fileid: fileId, - time: new Date() - } - })); - - // Decrease file count - field_file_count[fileDataName] -= 1; - - } catch (error) { - confirm(`Error: ${error.message}`); - } - }); - - $.each(ppom_input_vars.ppom_inputs, function(index, file_input) { - - - if (file_input.type === 'file' || file_input.type === 'cropper') { - - var file_data_name = file_input.data_name; - ppom_setup_file_upload_input(file_input); - } - - }); // $.each(ppom_file_vars - - -}); // jQuery(function($){}); - -// generate thumbbox -function add_thumb_box(file, $filelist_DIV) { - - let inner_html = '
    (' + plupload.formatSize(file.size) + ')
    '; - inner_html += '
    ' + file.name + '
    '; - - jQuery('
    ', { - 'id': 'u_i_c_' + file.id, - 'class': 'uk-text-center ppom-file-wrapper', - 'data-fileid': file.id, - 'html': inner_html, - - }).appendTo($filelist_DIV); - - // clearfix - // 1- removing last clearfix first - $filelist_DIV.find('.u_i_c_box_clearfix').remove(); - - jQuery('
    ', { - 'class': 'u_i_c_box_clearfix', - }).appendTo($filelist_DIV); - +const Cropped_Data_Captured = false; + +jQuery( function ( $ ) { + // Keep cropper previews, price recalculation, and modal placement aligned + // with the rest of the PPOM product form lifecycle. + // If cropper input found in fields + // if (ppom_get_field_meta_by_type('cropper').length > 0) { + + // var wc_cart_form = $('form.cart'); + // $(wc_cart_form).on('submit', function(e) { + + // // e.preventDefault(); + // var cropper_fields = ppom_get_field_meta_by_type('cropper'); + // $.each(cropper_fields, function(i, cropper) { + + // if (cropper.legacy_cropper !== undefined) return; + + // var cropper_name = cropper.data_name; + // ppom_generate_cropper_data_for_cart(cropper.data_name); + + // }); + // }); + // } + + $( document ).on( 'ppom_image_ready', function ( e ) { + const image_url = e.image_url; + const image_id = e.image.id; + const data_name = e.data_name; + const input_type = e.input_type; + const file_input = e.file_input; + + if ( input_type === 'cropper' ) { + field_meta = ppom_get_field_meta_by_id( data_name ); + // console.log('ppom',field_meta) + if ( field_meta.legacy_cropper === undefined ) { + ppom_show_cropped_preview( + data_name, + image_url, + image_id, + file_input + ); + // hiding the filelist-{data_name} when preview enabled + $( `#filelist-${ data_name }` ).hide(); + // hide the file upload area too + $( `.ppom-file-container.${ data_name }` ).hide(); + // also hide the crop ratio if only one option is provided + if ( $( `#crop-size-${ data_name } option` ).length === 1 ) { + $( `#crop-size-${ data_name }` ).hide(); + } + } + } + + // moving modal to body end + $( '.ppom-modals' ).appendTo( 'body' ); + } ); + + // On file removed + $( document ).on( 'ppom_uploaded_file_removed', function ( e ) { + const field_name = e.field_name; + // var fileid = e.fileid; + + ppom_reset_cropping_preview( field_name ); + ppom_update_option_prices(); + } ); + + // Croppie update size + $( '.ppom-croppie-preview' ).on( + 'change', + '.ppom-cropping-size', + function ( e ) { + const data_name = $( this ).data( 'field_name' ); + const cropp_preview_container = jQuery( + '.ppom-croppie-wrapper-' + data_name + ); + const v_width = $( 'option:selected', this ).data( 'width' ); + const v_height = $( 'option:selected', this ).data( 'height' ); + + cropp_preview_container + .find( '.croppie-container' ) + .each( function ( i, croppie_dom ) { + const image_id = jQuery( croppie_dom ).attr( + 'data-image_id' + ); + const croppie_container = jQuery( + '.ppom-croppie-preview-' + image_id + ); + const image_url = jQuery( croppie_dom ) + .find( 'img' ) + .attr( 'src' ); + $( croppie_dom ).croppie( 'destroy' ); + const viewport = { width: v_width, height: v_height }; + + file_list_preview_containers[ data_name ].croppie[ + image_id + ] = croppie_container; + file_list_preview_containers[ data_name ].image_id = image_id; + file_list_preview_containers[ data_name ].image_url = image_url; + + ppom_set_croppie_options( data_name, viewport, image_id ); + } ); + } + ); + + // Deleting File + document + .querySelector( '.ppom-wrapper' ) + ?.addEventListener( 'click', async function ( e ) { + if ( + ! e.target.classList.contains( 'u_i_c_tools_del' ) || + ! plupload_instances + ) { + return; + } + + e.preventDefault(); + + const delMessage = ppom_file_vars.delete_file_msg; + if ( ! confirm( delMessage ) ) { + return; + } + + const ppomFileWrapper = e.target.closest( '.ppom-file-wrapper' ); + const fileId = ppomFileWrapper?.getAttribute( 'data-fileid' ); + const ppomFieldWrapper = e.target.closest( + 'div.ppom-field-wrapper' + ); + const fileDataName = + ppomFieldWrapper?.getAttribute( 'data-data_name' ); + + if ( ! fileId || ! fileDataName ) { + return; + } + + field_file_count[ fileDataName ] = 0; + + const uploaderInstance = plupload_instances[ fileDataName ]; + if ( uploaderInstance ) { + uploaderInstance.removeFile( fileId ); + } + + const checkbox = document.querySelector( + `input[name="ppom[fields][${ fileDataName }][${ fileId }][org]"]` + ); + const fileName = checkbox?.value; + + if ( ! fileName ) { + return; + } + + // Delete animation. + const imageElement = document.querySelector( + `#u_i_c_${ fileId } img` + ); + if ( imageElement ) { + imageElement.src = `${ ppom_file_vars.plugin_url }/images/loading.gif`; + } + + const data = new URLSearchParams( { + action: 'ppom_delete_file', + file_name: fileName, + ppom_nonce: ppom_file_vars.ppom_file_delete_nonce, + } ); + + try { + const response = await fetch( ppom_file_vars.ajaxurl, { + method: 'POST', + body: data, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Requested-With': 'XMLHttpRequest', + }, + } ); + + const responseText = await response.text(); + if ( ! response.ok ) { + confirm( `Error: ${ responseText }` ); + return; + } + + // Update UI + const fileContainer = document.querySelector( + `#u_i_c_${ fileId }` + ); + if ( fileContainer ) { + fileContainer.remove(); + } + + if ( checkbox ) { + checkbox.remove(); + } + + const parentBox = e.target.closest( '.u_i_c_box' ); + if ( parentBox ) { + parentBox.remove(); + } + + const croppiePreview = document.querySelector( + `.ppom-croppie-preview-${ fileId }` + ); + if ( croppiePreview ) { + croppiePreview.remove(); + } + + // Send action to PPOM_Validate + document.dispatchEvent( + new CustomEvent( 'ppom_uploaded_file_removed', { + detail: { + field_name: fileDataName, + fileid: fileId, + time: new Date(), + }, + } ) + ); + + // Decrease file count + field_file_count[ fileDataName ] -= 1; + } catch ( error ) { + confirm( `Error: ${ error.message }` ); + } + } ); + + $.each( ppom_input_vars.ppom_inputs, function ( index, file_input ) { + if ( file_input.type === 'file' || file_input.type === 'cropper' ) { + const file_data_name = file_input.data_name; + ppom_setup_file_upload_input( file_input ); + } + } ); // $.each(ppom_file_vars +} ); // jQuery(function($){}); + +// Build the temporary thumbnail shell shown while a file is uploading. +function add_thumb_box( file, $filelist_DIV ) { + let inner_html = + '
    (' + + plupload.formatSize( file.size ) + + ')
    '; + inner_html += + '
    ' + file.name + '
    '; + + jQuery( '
    ', { + id: 'u_i_c_' + file.id, + class: 'uk-text-center ppom-file-wrapper', + 'data-fileid': file.id, + html: inner_html, + } ).appendTo( $filelist_DIV ); + + // clearfix + // 1- removing last clearfix first + $filelist_DIV.find( '.u_i_c_box_clearfix' ).remove(); + + jQuery( '
    ', { + class: 'u_i_c_box_clearfix', + } ).appendTo( $filelist_DIV ); } - // save croped/edited photo -function save_edited_photo(img_id, photo_url) { - - //console.log(img_id); - - //setting new image width to 75 - jQuery('#' + img_id).attr('width', 75); - - //disabling add to cart button for a while - jQuery('form.cart').block({ - message: null, - overlayCSS: { - background: "#fff", - opacity: .6 - } - }); - var post_data = { - action: 'ppom_save_edited_photo', - image_url: photo_url, - filename: jQuery('#' + img_id).attr('data-filename') - }; - - jQuery.post(ppom_file_vars.ajaxurl, post_data, function(resp) { - - //console.log( resp ); - jQuery('form.cart').unblock(); - - }); +function save_edited_photo( img_id, photo_url ) { + //console.log(img_id); + + //setting new image width to 75 + jQuery( '#' + img_id ).attr( 'width', 75 ); + + //disabling add to cart button for a while + jQuery( 'form.cart' ).block( { + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6, + }, + } ); + const post_data = { + action: 'ppom_save_edited_photo', + image_url: photo_url, + filename: jQuery( '#' + img_id ).attr( 'data-filename' ), + }; + + jQuery.post( ppom_file_vars.ajaxurl, post_data, function ( resp ) { + //console.log( resp ); + jQuery( 'form.cart' ).unblock(); + } ); } -// Cropping image with Croppie -function ppom_show_cropped_preview(file_name, image_url, image_id, file_input) { - - var cropp_preview_container = jQuery(".ppom-croppie-wrapper-" + file_name); - // Enable size option - cropp_preview_container.find('.ppom-cropping-size').prop('disabled', false); - cropp_preview_container.find('.ppom-cropping-size').show(); - - const croppie_container = jQuery('
    ') - .addClass('ppom-croppie-preview-' + image_id) - .attr('data-image_id', image_id) - .appendTo(cropp_preview_container); - - // Change preview image - jQuery('') - .addClass('btn ' + image_id) - .attr('href', 'javascript:;') - .attr('id', 'selectfiles-' + file_name + '-' + image_id) - .attr('data-field-name', file_name) - .attr('data-image-id', image_id) - .html('Change image') - .appendTo(cropp_preview_container) - .click(function(e){ - e.preventDefault(); - }); - - const file_inputs = { - ...file_input, - data_name: file_name + '-' + image_id, - is_change_image: true, - original_data_name: file_name, - } - ppom_setup_file_upload_input(file_inputs); - - - // file_list_preview_containers[file_name]['croppie'] = cropp_preview_container.find('.ppom-croppie-preview'); - - jQuery(croppie_container).on('update.croppie', function(ev, cropData) { - // console.log(cropData); - // croppie_container.croppie('result', 'rawcanvas').then(function(canvas) { - // console.log(canvas); - - ppom_generate_cropper_data_for_cart(file_name); - - jQuery.event.trigger({ - type: 'ppom_croppie_update', - img_id: image_id, - croppie_obj: croppie_container, - crop_data: cropData, - dataname: file_name, - time: new Date() - }); - - }); - - file_list_preview_containers[file_name]['croppie'][image_id] = croppie_container; - file_list_preview_containers[file_name]['image_id'] = image_id; - file_list_preview_containers[file_name]['image_url'] = image_url; - - ppom_set_croppie_options(file_name, undefined, image_id); +// Once an upload finishes, create the Croppie preview that feeds the hidden +// cropped-image payload later submitted with the add-to-cart request. +function ppom_show_cropped_preview( + file_name, + image_url, + image_id, + file_input +) { + const cropp_preview_container = jQuery( + '.ppom-croppie-wrapper-' + file_name + ); + // Enable size option + cropp_preview_container + .find( '.ppom-cropping-size' ) + .prop( 'disabled', false ); + cropp_preview_container.find( '.ppom-cropping-size' ).show(); + + const croppie_container = jQuery( '
    ' ) + .addClass( 'ppom-croppie-preview-' + image_id ) + .attr( 'data-image_id', image_id ) + .appendTo( cropp_preview_container ); + + // Change preview image + jQuery( '' ) + .addClass( 'btn ' + image_id ) + .attr( 'href', 'javascript:;' ) + .attr( 'id', 'selectfiles-' + file_name + '-' + image_id ) + .attr( 'data-field-name', file_name ) + .attr( 'data-image-id', image_id ) + .html( 'Change image' ) + .appendTo( cropp_preview_container ) + .click( function ( e ) { + e.preventDefault(); + } ); + + const file_inputs = { + ...file_input, + data_name: file_name + '-' + image_id, + is_change_image: true, + original_data_name: file_name, + }; + ppom_setup_file_upload_input( file_inputs ); + + // file_list_preview_containers[file_name]['croppie'] = cropp_preview_container.find('.ppom-croppie-preview'); + + jQuery( croppie_container ).on( + 'update.croppie', + function ( ev, cropData ) { + // console.log(cropData); + // croppie_container.croppie('result', 'rawcanvas').then(function(canvas) { + // console.log(canvas); + + ppom_generate_cropper_data_for_cart( file_name ); + + jQuery.event.trigger( { + type: 'ppom_croppie_update', + img_id: image_id, + croppie_obj: croppie_container, + crop_data: cropData, + dataname: file_name, + time: new Date(), + } ); + } + ); + + file_list_preview_containers[ file_name ].croppie[ image_id ] = + croppie_container; + file_list_preview_containers[ file_name ].image_id = image_id; + file_list_preview_containers[ file_name ].image_url = image_url; + + ppom_set_croppie_options( file_name, undefined, image_id ); } -function ppom_set_croppie_options(file_name, viewport, image_id) { - - const croppie_options = ppom_file_vars.croppie_options; - jQuery.each(croppie_options, function(field_name, option) { - - if (file_name === field_name) { - - option.url = file_list_preview_containers[file_name]['image_url']; - if (viewport !== undefined) { - viewport.type = option.viewport.type; - option.viewport = viewport; - } - - // console.log($filelist_DIV[file_name]['croppie'][image_id]); - file_list_preview_containers[file_name]['croppie'][image_id].croppie(option); - } - }); +function ppom_set_croppie_options( file_name, viewport, image_id ) { + const croppie_options = ppom_file_vars.croppie_options; + jQuery.each( croppie_options, function ( field_name, option ) { + if ( file_name === field_name ) { + option.url = file_list_preview_containers[ file_name ].image_url; + if ( viewport !== undefined ) { + viewport.type = option.viewport.type; + option.viewport = viewport; + } + + // console.log($filelist_DIV[file_name]['croppie'][image_id]); + file_list_preview_containers[ file_name ].croppie[ + image_id + ].croppie( option ); + } + } ); } // Reset cropping when image removed -function ppom_reset_cropping_preview(file_name) { - - var cropp_preview_container = jQuery(".ppom-croppie-wrapper-" + file_name); - // Reseting preview DOM - cropp_preview_container.find('.ppom-croppie-preview').html(''); +function ppom_reset_cropping_preview( file_name ) { + const cropp_preview_container = jQuery( + '.ppom-croppie-wrapper-' + file_name + ); + // Reseting preview DOM + cropp_preview_container.find( '.ppom-croppie-preview' ).html( '' ); } -// Attach FILE API with DOM -function ppom_setup_file_upload_input(file_input) { - - const file_inputs = file_input; - const parts = file_input.data_name.split('-'); - const [file_data_name, file_id ] = parts; - let data_name = file_data_name; - - if ( file_id !== undefined ) { - data_name = file_data_name + '-' + file_id; - } - - if ( plupload_instances[data_name] !== undefined ) { - return; - } - - if ( ! field_file_count.hasOwnProperty( file_data_name ) ) { - field_file_count[file_data_name] = 0; - } - file_list_preview_containers[file_data_name] = jQuery('#filelist-' + file_data_name); - - // Energy pack - const bar = window.document.getElementById(`ppom-progressbar-${file_data_name}`); - - const ppom_file_data = { - 'action': 'ppom_upload_file', - 'data_name': file_data_name, - 'ppom_nonce': ppom_file_vars.ppom_file_upload_nonce, - 'product_id': ppom_file_vars.product_id, - }; - - let img_dim_errormsg = 'Please upload correct image dimension'; - if (file_input.img_dimension_error) { - img_dim_errormsg = file_input.img_dimension_error; - } - - plupload_instances[file_data_name] = new plupload.Uploader({ - runtimes: ppom_file_vars.plupload_runtime, - browse_button: 'selectfiles-' + data_name, // you can pass in id... - container: 'ppom-file-container-' + file_data_name, // ... or DOM Element itself - drop_element: 'ppom-file-container-' + file_data_name, - url: ppom_file_vars.ajaxurl, - multipart_params: ppom_file_data, - max_file_size: file_input.file_size, - max_file_count: parseInt(file_input.files_allowed), - unique_names: ppom_file_vars.enable_file_rename, - chunk_size: '2mb', - unique_names: false, - - filters: { - mime_types: [ - { title: "Filetypes", extensions: file_input.file_types } - ] - }, - - init: { - PostInit: function() { - - // file_list_preview_containers[file_data_name].html(''); - if ( ! file_list_preview_containers[file_data_name].is(':visible') ) { - jQuery(document).on('ppom_field_shown', function() { - - jQuery.each(ppom_input_vars.ppom_inputs, function(index, file_input) { - if (file_input && (file_input.type === 'file' || file_input.type === 'cropper')) { - if ( - file_input.data_name && - file_input.files_allowed && - file_input.file_size && - file_input.files_allowed - ) { - ppom_setup_file_upload_input(file_input); - } - } - - }); - } ); - } - /*$('#uploadfiles-'+file_data_name).bind('click', function() { +/** + * Attach one Plupload instance per PPOM file/cropper field and keep the + * resulting hidden checkbox inputs compatible with the price/validation stack. + * + * @param {PPOMUploadFieldMeta} file_input + * @return {void} + */ +function ppom_setup_file_upload_input( file_input ) { + const file_inputs = file_input; + const parts = file_input.data_name.split( '-' ); + const [ file_data_name, file_id ] = parts; + let data_name = file_data_name; + + if ( file_id !== undefined ) { + data_name = file_data_name + '-' + file_id; + } + + if ( plupload_instances[ data_name ] !== undefined ) { + return; + } + + if ( ! Object.prototype.hasOwnProperty.call( field_file_count, file_data_name ) ) { + field_file_count[ file_data_name ] = 0; + } + file_list_preview_containers[ file_data_name ] = jQuery( + '#filelist-' + file_data_name + ); + + // Energy pack + const bar = window.document.getElementById( + `ppom-progressbar-${ file_data_name }` + ); + + const ppom_file_data = { + action: 'ppom_upload_file', + data_name: file_data_name, + ppom_nonce: ppom_file_vars.ppom_file_upload_nonce, + product_id: ppom_file_vars.product_id, + }; + + let img_dim_errormsg = 'Please upload correct image dimension'; + if ( file_input.img_dimension_error ) { + img_dim_errormsg = file_input.img_dimension_error; + } + + plupload_instances[ file_data_name ] = new plupload.Uploader( { + runtimes: ppom_file_vars.plupload_runtime, + browse_button: 'selectfiles-' + data_name, // you can pass in id... + container: 'ppom-file-container-' + file_data_name, // ... or DOM Element itself + drop_element: 'ppom-file-container-' + file_data_name, + url: ppom_file_vars.ajaxurl, + multipart_params: ppom_file_data, + max_file_size: file_input.file_size, + max_file_count: parseInt( file_input.files_allowed ), + unique_names: ppom_file_vars.enable_file_rename, + chunk_size: '2mb', + unique_names: false, + + filters: { + mime_types: [ + { title: 'Filetypes', extensions: file_input.file_types }, + ], + }, + + init: { + PostInit() { + // file_list_preview_containers[file_data_name].html(''); + if ( + ! file_list_preview_containers[ file_data_name ].is( + ':visible' + ) + ) { + jQuery( document ).on( 'ppom_field_shown', function () { + jQuery.each( + ppom_input_vars.ppom_inputs, + function ( index, file_input ) { + if ( + file_input && + ( file_input.type === 'file' || + file_input.type === 'cropper' ) + ) { + if ( + file_input.data_name && + file_input.files_allowed && + file_input.file_size && + file_input.files_allowed + ) { + ppom_setup_file_upload_input( + file_input + ); + } + } + } + ); + } ); + } + /*$('#uploadfiles-'+file_data_name).bind('click', function() { upload_instance[file_data_name].start(); return false; });*/ - }, - - FilesAdded: function(up, files) { - - // Adding progress bar - const file_pb = jQuery('
    ') - .addClass('progress') - .css('width', '100%') - .css('clear', 'both') - .css('margin', '5px auto') - .appendTo(file_list_preview_containers[file_data_name]); - const file_pb_runner = jQuery('
    ') - .addClass('progress-bar') - .attr('role', 'progressbar') - .attr('aria-valuenow', 0) - .attr('aria-valuemin', 0) - .attr('aria-valuemax', 100) - .css('height', '15px') - .css('width', 0) - .appendTo(file_pb); - - const files_added = files.length; - // return; - - // console.log('image w bac', files); - // plupload.each(files, function(file, i) { - // var img = new mOxie.Image; - // img.onload = function() { - // var img_height = this.height; - // var img_width = this.width; - // // if ((img_height >= 1024 || img_height <= 1100) && (img_width >= 750 || img_width <= 800)) { - // if ((img_width >= parseFloat(file_input.max_img_w) || img_width <= parseFloat(file_input.min_img_w))) { - // alert("Height and Width must not exceed 1100*800."); - // return false; - // } - // console.log('image h', parseFloat(file_input.max_img_w)); - // // access image size here using this.width and this.height - // }; - // img.load(file.getSource()); - // }); - - if ( file_id !== undefined ) { - --field_file_count[file_data_name]; - } - - if ((field_file_count[file_data_name] + files_added) > plupload_instances[file_data_name].settings.max_file_count) { - alert(plupload_instances[file_data_name].settings.max_file_count + ppom_file_vars.mesage_max_files_limit); - } - else { - - if ( file_id !== undefined ) { - - jQuery('.ppom-croppie-preview-' + file_id).hide(500).remove(); - jQuery(`.btn.${file_id}`).hide(500).remove(); - jQuery("#u_i_c_" + file_id).hide(500).remove(); - jQuery(`input[name="ppom[fields][${file_data_name}][${file_data_name}][cropped]"`).hide(500).remove(); - } - - plupload.each(files, function (file) { - - if (file.type.indexOf("image") !== -1 && file.type !== 'image/photoshop') { - const img = new moxie.image.Image(); - img.load = function() { - - const img_height = this.height; - const img_width = this.width; - - let aspect_ratio = Math.max(img_width, img_height) / Math.min(img_width, img_height); - - if ( - img_width >= parseFloat(file_input.max_img_w) || - img_width <= parseFloat(file_input.min_img_w) || - img_height >= parseFloat(file_input.max_img_h) || - img_height <= parseFloat(file_input.min_img_h) - ) { - up.removeFile(file); - alert(img_dim_errormsg); - } else { - field_file_count[file_data_name]++; - // Code to add pending file details, if you want - add_thumb_box(file, file_list_preview_containers[file_data_name], up); - up.start(); - } - }; - img.load(file.getSource()); - } else { - field_file_count[file_data_name]++; - // Code to add pending file details, if you want - add_thumb_box(file, file_list_preview_containers[file_data_name], up); - up.start(); - } - - // Energy pack - if ( bar ) { - bar.removeAttribute('hidden'); - bar.max = file.size; - bar.value = file.loaded; - } - }); - } - - - }, - - FileUploaded: function(up, file, info) { - - - const obj_resp = jQuery.parseJSON(info.response); - - if (obj_resp.file_name === 'ThumbNotFound') { - - plupload_instances[file_data_name].removeFile(file.id); - jQuery("#u_i_c_" + file.id).hide(500).remove(); - field_file_count[file_data_name]--; - - alert('There is some error please try again'); - return; - - } - else if (obj_resp.status === 'error') { - - plupload_instances[file_data_name].removeFile(file.id); - - jQuery("#u_i_c_" + file.id).hide(500).remove(); - - field_file_count[file_data_name]--; - alert(obj_resp.message); - return; - }; - - // var img_w = obj_resp.file_w - // var img_h = obj_resp.file_h - // if (img_w > parseFloat(file_input.max_img_w)) { - // upload_instance[file_data_name].removeFile(file.id); - // jQuery("#u_i_c_" + file.id).hide(500).remove(); - // file_count[file_data_name]--; - // alert('Image Dimension Error'); - // jQuery('form.cart').unblock(); - // return; - // } - - let file_thumb = ''; - - /*if( file_input.file_cost != "" ) { + }, + + FilesAdded( up, files ) { + // Adding progress bar + const file_pb = jQuery( '
    ' ) + .addClass( 'progress' ) + .css( 'width', '100%' ) + .css( 'clear', 'both' ) + .css( 'margin', '5px auto' ) + .appendTo( file_list_preview_containers[ file_data_name ] ); + const file_pb_runner = jQuery( '
    ' ) + .addClass( 'progress-bar' ) + .attr( 'role', 'progressbar' ) + .attr( 'aria-valuenow', 0 ) + .attr( 'aria-valuemin', 0 ) + .attr( 'aria-valuemax', 100 ) + .css( 'height', '15px' ) + .css( 'width', 0 ) + .appendTo( file_pb ); + + const files_added = files.length; + // return; + + // console.log('image w bac', files); + // plupload.each(files, function(file, i) { + // var img = new mOxie.Image; + // img.onload = function() { + // var img_height = this.height; + // var img_width = this.width; + // // if ((img_height >= 1024 || img_height <= 1100) && (img_width >= 750 || img_width <= 800)) { + // if ((img_width >= parseFloat(file_input.max_img_w) || img_width <= parseFloat(file_input.min_img_w))) { + // alert("Height and Width must not exceed 1100*800."); + // return false; + // } + // console.log('image h', parseFloat(file_input.max_img_w)); + // // access image size here using this.width and this.height + // }; + // img.load(file.getSource()); + // }); + + if ( file_id !== undefined ) { + --field_file_count[ file_data_name ]; + } + + if ( + field_file_count[ file_data_name ] + files_added > + plupload_instances[ file_data_name ].settings.max_file_count + ) { + alert( + plupload_instances[ file_data_name ].settings + .max_file_count + + ppom_file_vars.mesage_max_files_limit + ); + } else { + if ( file_id !== undefined ) { + jQuery( '.ppom-croppie-preview-' + file_id ) + .hide( 500 ) + .remove(); + jQuery( `.btn.${ file_id }` ).hide( 500 ).remove(); + jQuery( '#u_i_c_' + file_id ).hide( 500 ).remove(); + jQuery( + `input[name="ppom[fields][${ file_data_name }][${ file_data_name }][cropped]"]` + ) + .hide( 500 ) + .remove(); + } + + plupload.each( files, function ( file ) { + if ( + file.type.indexOf( 'image' ) !== -1 && + file.type !== 'image/photoshop' + ) { + const img = new moxie.image.Image(); + img.load = function () { + const img_height = this.height; + const img_width = this.width; + + const aspect_ratio = + Math.max( img_width, img_height ) / + Math.min( img_width, img_height ); + + if ( + img_width >= + parseFloat( file_input.max_img_w ) || + img_width <= + parseFloat( file_input.min_img_w ) || + img_height >= + parseFloat( file_input.max_img_h ) || + img_height <= + parseFloat( file_input.min_img_h ) + ) { + up.removeFile( file ); + alert( img_dim_errormsg ); + } else { + field_file_count[ file_data_name ]++; + // Code to add pending file details, if you want + add_thumb_box( + file, + file_list_preview_containers[ + file_data_name + ], + up + ); + up.start(); + } + }; + img.load( file.getSource() ); + } else { + field_file_count[ file_data_name ]++; + // Code to add pending file details, if you want + add_thumb_box( + file, + file_list_preview_containers[ file_data_name ], + up + ); + up.start(); + } + + // Energy pack + if ( bar ) { + bar.removeAttribute( 'hidden' ); + bar.max = file.size; + bar.value = file.loaded; + } + } ); + } + }, + + FileUploaded( up, file, info ) { + const obj_resp = jQuery.parseJSON( info.response ); + + if ( obj_resp.file_name === 'ThumbNotFound' ) { + plupload_instances[ file_data_name ].removeFile( file.id ); + jQuery( '#u_i_c_' + file.id ) + .hide( 500 ) + .remove(); + field_file_count[ file_data_name ]--; + + alert( 'There is some error please try again' ); + return; + } else if ( obj_resp.status === 'error' ) { + plupload_instances[ file_data_name ].removeFile( file.id ); + + jQuery( '#u_i_c_' + file.id ) + .hide( 500 ) + .remove(); + + field_file_count[ file_data_name ]--; + alert( obj_resp.message ); + return; + } + + // var img_w = obj_resp.file_w + // var img_h = obj_resp.file_h + // if (img_w > parseFloat(file_input.max_img_w)) { + // upload_instance[file_data_name].removeFile(file.id); + // jQuery("#u_i_c_" + file.id).hide(500).remove(); + // file_count[file_data_name]--; + // alert('Image Dimension Error'); + // jQuery('form.cart').unblock(); + // return; + // } + + let file_thumb = ''; + + /*if( file_input.file_cost != "" ) { jQuery('input[name="woo_file_cost"]').val( file_input.file_cost ); }*/ - file_list_preview_containers[file_data_name].find('#u_i_c_' + file.id).html(obj_resp.html) - .trigger({ - type: "ppom_image_ready", - image: file, - data_name: file_data_name, - input_type: file_input.type, - image_url: obj_resp.file_url, - image_resp: obj_resp, - time: new Date(), - file_input: file_inputs, - }); - - - // checking if uploaded file is thumb - const ext = obj_resp.file_name.substring(obj_resp.file_name.lastIndexOf('.') + 1).toLowerCase(); - - if ( - ext === 'png' || - ext === 'gif' || - ext === 'jpg' || - ext === 'jpeg' - ) { - const file_full = ppom_file_vars.file_upload_path + obj_resp.file_name; - // thumb thickbox only shown if it is image - file_list_preview_containers[file_data_name] - .find('#u_i_c_' + file.id) - .find('.u_i_c_thumb') - .append(''); - - // Aviary editing tools - if (file_input.photo_editing === 'on' && ppom_file_vars.aviary_api_key !== '') { - const editing_tools = file_input.editing_tools; - file_list_preview_containers[file_data_name] - .find('#u_i_c_' + file.id) - .find('.u_i_c_tools_edit') - .append(''); - } - } else { - file_thumb = ppom_file_vars.plugin_url + '/images/file.png'; - file_list_preview_containers[file_data_name].find('#u_i_c_' + file.id) - .find('.u_i_c_thumb') - .html('') - } - - // adding checkbox input to Hold uploaded file name as array - const file_container = file_list_preview_containers[file_data_name].find('#u_i_c_' + file.id); - let input_class = 'ppom-input'; - input_class += file_input.required === 'on' ? ' ppom-required' : ''; - - // Add file check - jQuery('') - .attr('data-price', file_input.file_cost) - .attr('data-label', obj_resp.file_name) - .attr('data-data_name', file_input.data_name) - .attr('data-title', file_input.title) - .attr('data-onetime', file_input.onetime) - .val(obj_resp.file_name) - .css('display', 'none') - .addClass('ppom-file-cb-' + file_data_name) - .addClass('ppom-file-cb') - .addClass(input_class) - .appendTo(file_container) - .trigger('change'); - - ppom_update_option_prices(); - - jQuery('form.cart').unblock(); - isCartBlock = false; - - // Removing progressbar - file_list_preview_containers[file_data_name].find('.progress').remove(); - - if ( bar ) { - setTimeout(function() { - bar.setAttribute('hidden', 'hidden'); - }, 1000); - bar.max = file.size; - bar.value = file.loaded; - } - - // Trigger - jQuery.event.trigger({ - type: "ppom_file_uploaded", - file: file, - file_meta: file_input, - file_resp: obj_resp, - time: new Date() - }); - }, - - UploadProgress: function(up, file) { - - // Energy pack - if ( bar ) { - bar.max = file.size; - bar.value = file.loaded; - } - - file_list_preview_containers[file_data_name].find('.progress-bar').css('width', file.percent + '%'); - - //disabling add to cart button for a while - if (!isCartBlock) { - jQuery('form.cart').block({ - message: null, - overlayCSS: { - background: "#fff", - opacity: .6, - onBlock: function() { - isCartBlock = true; - } - } - }); - } - }, - - Error: function(up, err) { - //document.getElementById('console').innerHTML += "\nError #" + err.code + ": " + err.message; - alert("\nError #" + err.code + ": " + err.message); - } - } - - - }); - - // console.log('running file', upload_instance[file_data_name]); - plupload_instances[file_data_name].init(); - uploaderInstances[file_data_name] = plupload_instances[file_data_name]; + file_list_preview_containers[ file_data_name ] + .find( '#u_i_c_' + file.id ) + .html( obj_resp.html ) + .trigger( { + type: 'ppom_image_ready', + image: file, + data_name: file_data_name, + input_type: file_input.type, + image_url: obj_resp.file_url, + image_resp: obj_resp, + file_input: file_inputs, + time: new Date(), + } ); + + // checking if uploaded file is thumb + const ext = obj_resp.file_name + .substring( obj_resp.file_name.lastIndexOf( '.' ) + 1 ) + .toLowerCase(); + + if ( + ext === 'png' || + ext === 'gif' || + ext === 'jpg' || + ext === 'jpeg' + ) { + const file_full = + ppom_file_vars.file_upload_path + obj_resp.file_name; + // thumb thickbox only shown if it is image + file_list_preview_containers[ file_data_name ] + .find( '#u_i_c_' + file.id ) + .find( '.u_i_c_thumb' ) + .append( + '' + ); + + // Aviary editing tools + if ( + file_input.photo_editing === 'on' && + ppom_file_vars.aviary_api_key !== '' + ) { + const editing_tools = file_input.editing_tools; + file_list_preview_containers[ file_data_name ] + .find( '#u_i_c_' + file.id ) + .find( '.u_i_c_tools_edit' ) + .append( + '' + ); + } + } else { + file_thumb = ppom_file_vars.plugin_url + '/images/file.png'; + file_list_preview_containers[ file_data_name ] + .find( '#u_i_c_' + file.id ) + .find( '.u_i_c_thumb' ) + .html( + '' + ); + } + + // adding checkbox input to Hold uploaded file name as array + const file_container = file_list_preview_containers[ + file_data_name + ].find( '#u_i_c_' + file.id ); + let input_class = 'ppom-input'; + input_class += + file_input.required === 'on' ? ' ppom-required' : ''; + + // Add file check + jQuery( + '' + ) + .attr( 'data-price', file_input.file_cost ) + .attr( 'data-label', obj_resp.file_name ) + .attr( 'data-data_name', file_input.data_name ) + .attr( 'data-title', file_input.title ) + .attr( 'data-onetime', file_input.onetime ) + .val( obj_resp.file_name ) + .css( 'display', 'none' ) + .addClass( 'ppom-file-cb-' + file_data_name ) + .addClass( 'ppom-file-cb' ) + .addClass( input_class ) + .appendTo( file_container ) + .trigger( 'change' ); + + ppom_update_option_prices(); + + jQuery( 'form.cart' ).unblock(); + isCartBlock = false; + + // Removing progressbar + file_list_preview_containers[ file_data_name ] + .find( '.progress' ) + .remove(); + + if ( bar ) { + setTimeout( function () { + bar.setAttribute( 'hidden', 'hidden' ); + }, 1000 ); + bar.max = file.size; + bar.value = file.loaded; + } + + // Trigger + jQuery.event.trigger( { + type: 'ppom_file_uploaded', + file, + file_meta: file_input, + file_resp: obj_resp, + time: new Date(), + } ); + }, + + UploadProgress( up, file ) { + // Energy pack + if ( bar ) { + bar.max = file.size; + bar.value = file.loaded; + } + + file_list_preview_containers[ file_data_name ] + .find( '.progress-bar' ) + .css( 'width', file.percent + '%' ); + + //disabling add to cart button for a while + if ( ! isCartBlock ) { + jQuery( 'form.cart' ).block( { + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6, + onBlock() { + isCartBlock = true; + }, + }, + } ); + } + }, + + Error( up, err ) { + //document.getElementById('console').innerHTML += "\nError #" + err.code + ": " + err.message; + alert( '\nError #' + err.code + ': ' + err.message ); + }, + }, + } ); + + // console.log('running file', upload_instance[file_data_name]); + plupload_instances[ file_data_name ].init(); + uploaderInstances[ file_data_name ] = plupload_instances[ file_data_name ]; } -// Generate Cropped image data for cart -function ppom_generate_cropper_data_for_cart(field_name) { - - const cropp_preview_container = jQuery(".ppom-croppie-wrapper-" + field_name); - - cropp_preview_container.find('.croppie-container').each(function(i, croppie_dom) { - - const image_id = jQuery(croppie_dom).attr('data-image_id'); - jQuery(croppie_dom).croppie('result', { - type: 'rawcanvas', - // size: { width: 300, height: 300 }, - size: 'original', - format: 'png' - }).then(function(canvas) { - const image_url = canvas.toDataURL(); - //console.log(image_url); - // remove first - jQuery(`input[name="ppom[fields][${field_name}][${image_id}][cropped]"`).remove(); - - // Add file check - jQuery('') - .val(image_url) - .css('display', 'none') - .appendTo(file_list_preview_containers[field_name]); - - }); - }); +// Persist the Croppie canvas output into hidden inputs so PHP can rebuild the +// edited image from the same request payload used for normal uploaded files. +function ppom_generate_cropper_data_for_cart( field_name ) { + const cropp_preview_container = jQuery( + '.ppom-croppie-wrapper-' + field_name + ); + + cropp_preview_container + .find( '.croppie-container' ) + .each( function ( i, croppie_dom ) { + const image_id = jQuery( croppie_dom ).attr( 'data-image_id' ); + jQuery( croppie_dom ) + .croppie( 'result', { + type: 'rawcanvas', + // size: { width: 300, height: 300 }, + size: 'original', + format: 'png', + } ) + .then( function ( canvas ) { + const image_url = canvas.toDataURL(); + //console.log(image_url); + // remove first + jQuery( + `input[name="ppom[fields][${ field_name }][${ image_id }][cropped]"]` + ).remove(); + + // Add file check + jQuery( + '' + ) + .val( image_url ) + .css( 'display', 'none' ) + .appendTo( file_list_preview_containers[ field_name ] ); + } ); + } ); } diff --git a/js/image-tooltip.js b/js/image-tooltip.js index 19b30542..623f0865 100644 --- a/js/image-tooltip.js +++ b/js/image-tooltip.js @@ -1,67 +1,91 @@ -(function ($) { - $.fn.imageTooltip = function (options) { +/** + * Lightweight hover preview used by PPOM image inputs. + * + * Product-page image choices can carry a larger image URL in + * `data-image-tooltip`; this plugin renders that preview near the cursor + * without opening the full media modal flow. + */ +( function ( $ ) { + $.fn.imageTooltip = function ( options ) { + const defaults = { + imgWidth: 'initial', + backgroundColor: '#fff', + }; - var defaults = { - imgWidth: 'initial', - backgroundColor: '#fff' - }; + if ( typeof options === 'object' ) { + options = $.extend( defaults, options ); + } else { + const tempOptions = {}; + tempOptions.imgWidth = arguments[ 0 ] || defaults.imgWidth; + tempOptions.backgroundColor = + arguments[ 1 ] || defaults.backgroundColor; + options = tempOptions; + } - if (typeof (options) === 'object') { - options = $.extend(defaults, options); - } else { - var tempOptions = {}; - tempOptions.imgWidth = arguments[0] || defaults.imgWidth; - tempOptions.backgroundColor = arguments[1] || defaults.backgroundColor; - options = tempOptions; - } + function calLeft( x, imgWidth ) { + return window.innerWidth - x > imgWidth ? x : x - imgWidth; + } - function calLeft(x, imgWidth) { - return window.innerWidth - x > imgWidth ? x : x - imgWidth; - } + function calTop( y, imgHeight ) { + return window.innerHeight - y > imgHeight + ? y + 25 + : y - imgHeight - 25; + } - function calTop(y, imgHeight) { - return window.innerHeight - y > imgHeight ? y + 25 : y - imgHeight - 25; - } + return this.each( function () { + const imgContainer = $( '

    ', { + css: { + display: 'none', + backgroundColor: options.backgroundColor, + padding: '5px', + position: 'fixed', + 'max-width': '350px', + 'z-index': '9999', + }, + } ); - return this.each(function () { + const img = $( '', { + src: + $( this ).data( 'image-tooltip' ) || + $( this ).attr( 'src' ), + alt: 'Image Not Available', + width: options.imgWidth, + } ); - var imgContainer = $('

    ', { - css: { - display: 'none', - backgroundColor: options.backgroundColor, - padding: '5px', - position: 'fixed', - 'max-width': '350px', - 'z-index': '9999' - } - }); + imgContainer.append( img ); - var img = $('', { - src: $(this).data('image-tooltip') || $(this).attr('src'), - alt: 'Image Not Available', - width: options.imgWidth - }); - - imgContainer.append(img); - - $(this).hover( - function (e) { - imgContainer.css({ - left: calLeft(e.clientX, imgContainer.outerWidth()) + 'px', - top: calTop(e.clientY, imgContainer.outerHeight()) + 'px' - }); - $('body').append(imgContainer); - imgContainer.fadeIn('fast'); - }, - function () { - imgContainer.remove(); - } - ).mousemove(function (e) { - imgContainer.css({ - left: calLeft(e.clientX, imgContainer.outerWidth()) + 'px', - top: calTop(e.clientY, imgContainer.outerHeight()) + 'px' - }); - }); - }); - }; -}(jQuery)); \ No newline at end of file + $( this ) + .hover( + function ( e ) { + imgContainer.css( { + left: + calLeft( + e.clientX, + imgContainer.outerWidth() + ) + 'px', + top: + calTop( + e.clientY, + imgContainer.outerHeight() + ) + 'px', + } ); + $( 'body' ).append( imgContainer ); + imgContainer.fadeIn( 'fast' ); + }, + function () { + imgContainer.remove(); + } + ) + .mousemove( function ( e ) { + imgContainer.css( { + left: + calLeft( e.clientX, imgContainer.outerWidth() ) + + 'px', + top: + calTop( e.clientY, imgContainer.outerHeight() ) + + 'px', + } ); + } ); + } ); + }; +} )( jQuery ); diff --git a/js/popup.js b/js/popup.js index b778cbae..4e977933 100644 --- a/js/popup.js +++ b/js/popup.js @@ -1,102 +1,140 @@ // @ts-check +/** + * Shared confirmation/error popup used by PPOM admin screens. + * + * Unlike the field-builder inline modals, this is a single programmatic popup + * instance exposed on `window.ppomPopup` so list-table and settings actions can + * reuse the same confirmation UX. + * + * @see js/admin/ppom-meta-table.js + * @see js/admin/ppom-admin.js + */ + +/** + * @typedef {{ + * title?: string, + * text?: string, + * hideCloseBtn?: boolean, + * type?: string, + * onConfirmation?: () => void, + * onClose?: () => void + * }} PpomPopupOptions + */ class PpomPopup { - constructor() { - this.overlay = document.createElement('div'); - this.overlay.classList.add('ppom-popup-overlay'); - - // Close on outside click. - this.overlay.addEventListener('click', (event) => { - if ( event.target === this.overlay ) { - this.close(); - } - }); - - this.popup = document.createElement('div'); - this.popup.classList.add('ppom-popup'); - - this.container = document.createElement('div'); - this.container.classList.add('ppom-popup-container'); - - this.title = document.createElement('h2'); - this.title.classList.add('ppom-popup-title'); - - this.text = document.createElement('p'); - this.text.classList.add('ppom-popup-text'); - - const containerActions = document.createElement('div'); - containerActions.classList.add('ppom-popup-actions'); - - this.confirmButton = document.createElement('button'); - this.confirmButton.classList.add('ppom-btn-confirm') - this.confirmButton.textContent = window.ppom_vars.i18n.popup.confirmationBtn; - this.confirmButton.addEventListener('click', this.confirm.bind(this)); - - this.cancelButton = document.createElement('button'); - this.cancelButton.classList.add('ppom-btn-cancel') - this.cancelButton.textContent = window.ppom_vars.i18n.popup.cancelBtn; - this.cancelButton.addEventListener('click', this.close.bind(this)); - - containerActions.appendChild(this.cancelButton); - containerActions.appendChild(this.confirmButton); - - this.container.appendChild(this.title); - this.container.appendChild(this.text); - this.container.appendChild(containerActions); - this.popup.appendChild(this.container); - this.overlay.appendChild(this.popup); - - this.onConfirmation = () => {} - this.onClose = () => {} - } - - open( options = {} ) { - - if ( options?.title ) { - this.title.innerHTML = options.title; - } - - if ( options?.text ) { - this.text.innerHTML = options.text; - } - - if ( options?.onConfirmation ) { - this.onConfirmation = options.onConfirmation; - } - - if ( options?.onClose ) { - this.onClose = options.onClose; - } - - this.cancelButton.classList.toggle('ppom-hide', Boolean( options?.hideCloseBtn ) ); - this.text.classList.toggle('ppom-hide', Boolean( options?.text?.length ) ); - this.popup.classList.toggle('ppom-error', 'error' === options?.type ); - - this.show(); - } - - close() { - this.hide(); - this.onClose?.(); - } - - confirm() { - this.hide(); - this.onConfirmation?.(); - } - - show() { - document.body.appendChild(this.overlay); - } - - hide() { - document.body.removeChild(this.overlay); - } + constructor() { + this.overlay = document.createElement( 'div' ); + this.overlay.classList.add( 'ppom-popup-overlay' ); + + // Close on outside click. + this.overlay.addEventListener( 'click', ( event ) => { + if ( event.target === this.overlay ) { + this.close(); + } + } ); + + this.popup = document.createElement( 'div' ); + this.popup.classList.add( 'ppom-popup' ); + + this.container = document.createElement( 'div' ); + this.container.classList.add( 'ppom-popup-container' ); + + this.title = document.createElement( 'h2' ); + this.title.classList.add( 'ppom-popup-title' ); + + this.text = document.createElement( 'p' ); + this.text.classList.add( 'ppom-popup-text' ); + + const containerActions = document.createElement( 'div' ); + containerActions.classList.add( 'ppom-popup-actions' ); + + this.confirmButton = document.createElement( 'button' ); + this.confirmButton.classList.add( 'ppom-btn-confirm' ); + this.confirmButton.textContent = + window.ppom_vars.i18n.popup.confirmationBtn; + this.confirmButton.addEventListener( + 'click', + this.confirm.bind( this ) + ); + + this.cancelButton = document.createElement( 'button' ); + this.cancelButton.classList.add( 'ppom-btn-cancel' ); + this.cancelButton.textContent = window.ppom_vars.i18n.popup.cancelBtn; + this.cancelButton.addEventListener( 'click', this.close.bind( this ) ); + + containerActions.appendChild( this.cancelButton ); + containerActions.appendChild( this.confirmButton ); + + this.container.appendChild( this.title ); + this.container.appendChild( this.text ); + this.container.appendChild( containerActions ); + this.popup.appendChild( this.container ); + this.overlay.appendChild( this.popup ); + + /** @type {() => void} */ + this.onConfirmation = () => {}; + /** @type {() => void} */ + this.onClose = () => {}; + } + + /** + * Open the shared admin popup with optional callbacks and copy. + * + * @param {PpomPopupOptions} [options] + * @return {void} + */ + open( options = {} ) { + if ( options?.title ) { + this.title.innerHTML = options.title; + } + + if ( options?.text ) { + this.text.innerHTML = options.text; + } + + if ( options?.onConfirmation ) { + this.onConfirmation = options.onConfirmation; + } + + if ( options?.onClose ) { + this.onClose = options.onClose; + } + + this.cancelButton.classList.toggle( + 'ppom-hide', + Boolean( options?.hideCloseBtn ) + ); + this.text.classList.toggle( + 'ppom-hide', + Boolean( options?.text?.length ) + ); + this.popup.classList.toggle( 'ppom-error', 'error' === options?.type ); + + this.show(); + } + + close() { + this.hide(); + this.onClose?.(); + } + + confirm() { + this.hide(); + this.onConfirmation?.(); + } + + show() { + document.body.appendChild( this.overlay ); + } + + hide() { + document.body.removeChild( this.overlay ); + } } -window.addEventListener('DOMContentLoaded', () => { - /** - * @type {import('../global.d.ts').Popup} - */ - window.ppomPopup = new PpomPopup(); -}); \ No newline at end of file +window.addEventListener( 'DOMContentLoaded', () => { + /** + * @type {import('../global.d.ts').Popup} + */ + window.ppomPopup = new PpomPopup(); +} ); diff --git a/js/ppom-conditions-v2.js b/js/ppom-conditions-v2.js index 5dee86ab..611dccbe 100644 --- a/js/ppom-conditions-v2.js +++ b/js/ppom-conditions-v2.js @@ -2,601 +2,775 @@ /** * PPOM Conditional Version 2 - * More Fast and Optimized - * April, 2020 in LockedDown (CORVID-19) - * */ - -var ppom_hidden_fields = []; - -jQuery(function($) { - - setTimeout(function() { - $('form.cart').find('select option:selected, input[type="radio"]:checked, input[type="checkbox"]:checked').each(function(i, field) { - - if ($(field).closest('div.ppom-field-wrapper').hasClass('ppom-c-hide')) return; - - const data_name = $(field).data('data_name'); - ppom_check_conditions(data_name, function(element_dataname, event_type) { - // console.log(data_name, event_type); - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - }); - - $('form.cart').find('div.ppom-c-show').each(function(i, field) { - - const data_name = $(field).data('data_name'); - ppom_check_conditions(data_name, function(element_dataname, event_type) { - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - }); - - $('form.cart').find('div.ppom-c-hide').each(function(i, field) { - const data_name = $(field).data('data_name'); - $.event.trigger({ - type: 'ppom_field_hidden', - field: data_name, - time: new Date() - }); - }); - - }, 100); - - // $('form.cart').on('change', 'select, input[type="radio"], input[type="checkbox"]', function(ev) { - - function trigger_check_conditions( modifiedElement ) { - let value = null; - if (modifiedElement.type === 'radio' || modifiedElement.type === 'checkbox') { - value = modifiedElement.checked ? modifiedElement.value : null; - } else { - value = modifiedElement.value; - } - - const data_name = modifiedElement.dataset?.data_name; - ppom_check_conditions(data_name, (element_dataname, event_type) => { - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - } - - $(".ppom-wrapper").on('change', 'select, input:radio, input:checkbox, input[type="date"]', function(_e) { - trigger_check_conditions( this ); - }); - - $(".ppom-wrapper").on('keyup', 'input:text, input[type="number"], input[type="email"]', function(_e) { - trigger_check_conditions( this ); - }); - - $(document).on('ppom_hidden_fields_updated', function(e) { - ppom_fields_hidden_conditionally(); - }); - - - $(document).on('ppom_field_hidden', function(e) { - - // console.log(e.field) - - var element_type = ppom_get_field_type_by_id(e.field); - switch (element_type) { - - case 'select': - $('select[name="ppom[fields][' + e.field + ']"]').val(''); - break; - - case 'multiple_select': - - var selector = $('select[name="ppom[fields][' + e.field + '][]"]'); - var selected_value = selector.val(); - var selected_options = selector.find('option:selected'); - - jQuery.each(selected_options, function(index, default_selected) { - - var option_id = jQuery(default_selected).attr('data-option_id'); - var the_id = 'ppom-multipleselect-' + e.field + '-' + option_id; - - $("#" + the_id).remove(); - }); - - if (selected_value) { - - $('select[name="ppom[fields][' + e.field + '][]"]').val(null).trigger("change"); - } - - break; - - case 'checkbox': - $('input[name="ppom[fields][' + e.field + '][]"]').prop('checked', false); - break; - - case 'radio': - $('input[name="ppom[fields][' + e.field + ']"]').prop('checked', false); - break; - - case 'file': - $('#filelist-' + e.field).find('.u_i_c_box').remove(); - break; - - case 'palettes': - case 'image': - $('input[name="ppom[fields][' + e.field + '][]"]').prop('checked', false); - break; - - case 'imageselect': - var the_id = 'ppom-imageselect' + e.field; - $("#" + the_id).remove(); - break; - - case 'quantityoption': - $('#' + e.field).val(''); - var the_id = 'ppom-quantityoption-rm' + e.field; - $("#" + the_id).remove(); - break; - - case 'pricematrix': - $(`input[data-dataname="ppom[fields][${e.field}]"]`).removeClass('active'); - break; - - case 'quantities': - $(`input[name^="ppom[fields][${e.field}]"]`).val(''); - break; - - case 'fixedprice': - // if select type is radio - $('input[name="ppom[fields][' + e.field + ']"]').prop('checked', false); - // if select type is select - $('select[name="ppom[fields][' + e.field + ']"]').val(''); - break; - - - default: - // Reset text/textarea/date/email etc types - $('#' + e.field).val(''); - break; - } + * + * This is the current conditional-logic engine used on the product page. PHP + * renders rule metadata into `data-cond-*` attributes; this file evaluates + * those rules and emits the shared `ppom_field_hidden` / `ppom_field_shown` + * events that pricing, uploads, validation, and default restoration rely on. + * + * @see populate_conditional_elements in js/admin/ppom-admin.js + * @see ppom_fields_hidden_conditionally + * @see ppom_update_option_prices in js/price/ppom-price.js + */ - $.event.trigger({ - type: "ppom_hidden_fields_updated", - field: e.field, - time: new Date() - }); +/** @type {string[]} */ +let ppom_hidden_fields = []; - ppom_check_conditions(e.field, function(element_dataname, event_type) { - // console.log(`${element_dataname} ===> ${event_type}`); - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - }); +/** + * @typedef {Object} PPOMConditionCompareArgs + * @property {string|string[]|undefined|null} valueToCompare + * @property {string|undefined} selectOptionToCompare + * @property {string|undefined} constantValueToCompare + * @property {{to: string, from: string}} betweenValueInterval + * @property {string} operator + */ - /*$(document).on('ppom_field_shown', function(e) { +jQuery( function ( $ ) { + // Replay the initial field state after the product form has rendered so + // defaults, preselected options, and conditionally hidden fields line up. + setTimeout( function () { + $( 'form.cart' ) + .find( + 'select option:selected, input[type="radio"]:checked, input[type="checkbox"]:checked' + ) + .each( function ( i, field ) { + if ( + $( field ) + .closest( 'div.ppom-field-wrapper' ) + .hasClass( 'ppom-c-hide' ) + ) { + return; + } + + const data_name = $( field ).data( 'data_name' ); + ppom_check_conditions( + data_name, + function ( element_dataname, event_type ) { + // console.log(data_name, event_type); + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + } ); + + $( 'form.cart' ) + .find( 'div.ppom-c-show' ) + .each( function ( i, field ) { + const data_name = $( field ).data( 'data_name' ); + ppom_check_conditions( + data_name, + function ( element_dataname, event_type ) { + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + } ); + + $( 'form.cart' ) + .find( 'div.ppom-c-hide' ) + .each( function ( i, field ) { + const data_name = $( field ).data( 'data_name' ); + $.event.trigger( { + type: 'ppom_field_hidden', + field: data_name, + time: new Date(), + } ); + } ); + }, 100 ); + + // $('form.cart').on('change', 'select, input[type="radio"], input[type="checkbox"]', function(ev) { + + /** + * Re-evaluate any condition tree that depends on the changed form control. + * + * @param {HTMLInputElement|HTMLSelectElement} modifiedElement + * @return {void} + */ + function trigger_check_conditions( modifiedElement ) { + let value = null; + if ( + modifiedElement.type === 'radio' || + modifiedElement.type === 'checkbox' + ) { + value = modifiedElement.checked ? modifiedElement.value : null; + } else { + value = modifiedElement.value; + } + + const data_name = modifiedElement.dataset?.data_name; + ppom_check_conditions( data_name, ( element_dataname, event_type ) => { + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } ); + } + + $( '.ppom-wrapper' ).on( + 'change', + 'select, input:radio, input:checkbox, input[type="date"]', + function ( _e ) { + trigger_check_conditions( this ); + } + ); + + $( '.ppom-wrapper' ).on( + 'keyup', + 'input:text, input[type="number"], input[type="email"]', + function ( _e ) { + trigger_check_conditions( this ); + } + ); + + $( document ).on( 'ppom_hidden_fields_updated', function ( e ) { + ppom_fields_hidden_conditionally(); + } ); + + $( document ).on( 'ppom_field_hidden', function ( e ) { + // console.log(e.field) + + const element_type = ppom_get_field_type_by_id( e.field ); + switch ( element_type ) { + case 'select': + $( 'select[name="ppom[fields][' + e.field + ']"]' ).val( '' ); + break; + + case 'multiple_select': + var selector = $( + 'select[name="ppom[fields][' + e.field + '][]"]' + ); + var selected_value = selector.val(); + var selected_options = selector.find( 'option:selected' ); + + jQuery.each( + selected_options, + function ( index, default_selected ) { + const option_id = + jQuery( default_selected ).attr( 'data-option_id' ); + const the_id = + 'ppom-multipleselect-' + e.field + '-' + option_id; + + $( '#' + the_id ).remove(); + } + ); + + if ( selected_value ) { + $( 'select[name="ppom[fields][' + e.field + '][]"]' ) + .val( null ) + .trigger( 'change' ); + } + + break; + + case 'checkbox': + $( 'input[name="ppom[fields][' + e.field + '][]"]' ).prop( + 'checked', + false + ); + break; + + case 'radio': + $( 'input[name="ppom[fields][' + e.field + ']"]' ).prop( + 'checked', + false + ); + break; + + case 'file': + $( '#filelist-' + e.field ) + .find( '.u_i_c_box' ) + .remove(); + break; + + case 'palettes': + case 'image': + $( 'input[name="ppom[fields][' + e.field + '][]"]' ).prop( + 'checked', + false + ); + break; + + case 'imageselect': + var the_id = 'ppom-imageselect' + e.field; + $( '#' + the_id ).remove(); + break; + + case 'quantityoption': + $( '#' + e.field ).val( '' ); + var the_id = 'ppom-quantityoption-rm' + e.field; + $( '#' + the_id ).remove(); + break; + + case 'pricematrix': + $( + `input[data-dataname="ppom[fields][${ e.field }]"]` + ).removeClass( 'active' ); + break; + + case 'quantities': + $( `input[name^="ppom[fields][${ e.field }]"]` ).val( '' ); + break; + + case 'fixedprice': + // if select type is radio + $( 'input[name="ppom[fields][' + e.field + ']"]' ).prop( + 'checked', + false + ); + // if select type is select + $( 'select[name="ppom[fields][' + e.field + ']"]' ).val( '' ); + break; + + default: + // Reset text/textarea/date/email etc types + $( '#' + e.field ).val( '' ); + break; + } + + $.event.trigger( { + type: 'ppom_hidden_fields_updated', + field: e.field, + time: new Date(), + } ); + + ppom_check_conditions( + e.field, + function ( element_dataname, event_type ) { + // console.log(`${element_dataname} ===> ${event_type}`); + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + } ); + + /*$(document).on('ppom_field_shown', function(e) { console.log(`shown event ${e.field}`); ppom_check_conditions(e.field); });*/ - $(document).on('ppom_field_shown', function(e) { - - ppom_fields_hidden_conditionally(); - - // Set checked/selected again - ppom_set_default_option(e.field); - - ppom_check_conditions(e.field, function(element_dataname, event_type) { - // console.log(`${element_dataname} ===> ${event_type}`); - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - - - var field_meta = ppom_get_field_meta_by_id(e.field); - - // Apply FileAPI to DOM - // PPOM version 22.0 has issue, commenting it so far by Najeeb April 4, 2021 - // if (field_meta.type === 'file' || field_meta.type === 'cropper') { - // ppom_setup_file_upload_input(field_meta); - // } - - // Price Matrix - if (field_meta.type == 'pricematrix') { - // Resettin - $(".ppom_pricematrix").removeClass('active'); - - // Set Active - var classname = "." + field_meta.data_name; - // console.log(field_meta.data_name, jQuery(`input[data-dataname="ppom[fields][${field_meta.data_name}]"]`)); - jQuery(`input[data-dataname="ppom[fields][${field_meta.data_name}]"]`).addClass('active') - // $(classname).find('.ppom_pricematrix').addClass('active') - } - - //Imageselect (Image dropdown) - if (field_meta.type === 'imageselect') { - - var dd_selector = 'ppom_imageselect_' + field_meta.data_name; - var ddData = $('#' + dd_selector).data('ppom_ddslick'); - var image_replace = field_meta.image_replace ? field_meta.image_replace : 'off'; - - ppom_create_hidden_input(ddData); - ppom_update_option_prices(); - setTimeout(function() { - ppom_image_selection(ddData, image_replace); - }, 100); - // $('#'+dd_selector).ddslick('select', {index: 0 }); - } - - - // Multiple Select Addon - if (field_meta.type === 'multiple_select') { - - var selector = jQuery('select[name="ppom[fields][' + field_meta.data_name + '][]"]'); - var selected_value = selector.val(); - var default_value = field_meta.selected; - - if (selected_value === null && default_value) { - - var selected_opt_arr = default_value.split(','); - - selector.val(selected_opt_arr).trigger('change'); - - var selected_options = selector.find('option:selected'); - jQuery.each(selected_options, function(index, default_selected) { - - var option_id = jQuery(default_selected).attr('data-option_id'); - var option_label = jQuery(default_selected).attr('data-optionlabel'); - var option_price = jQuery(default_selected).attr('data-optionprice'); - - ppom_multiple_select_create_hidden_input(field_meta.data_name, option_id, option_price, option_label, field_meta.title); - }); - } - } - - }); - - ppom_fields_hidden_conditionally(); - -}); - -function ppom_check_conditions(data_name, callback) { - - let is_matched = false; - let event_type, element_data_name; - - jQuery(`div.ppom-cond-${data_name}`).each(function() { - // return this.data('cond-val1').match(/\w*-Back/); - // console.log(jQuery(this)); - const total_cond = parseInt(jQuery(this).data('cond-total')); - const binding = jQuery(this).data(`cond-bind`); - const visibility = jQuery(this).data(`cond-visibility`); - element_data_name = jQuery(this).data('data_name'); - - let matched = 0; - var matched_conditions = []; - let cond_elements = []; - for (var t = 1; t <= total_cond; t++) { - const targetFieldToCompare = jQuery(this).data(`cond-input${t}`)?.toString()?.toLowerCase() - const targetFieldValue = ppom_get_element_value(targetFieldToCompare); - - const selectOptionValue = jQuery(this).data(`cond-val${t}`)?.toString(); - const operator = jQuery(this).data(`cond-operator${t}`); - const constantValue = jQuery(this).data(`cond-constant-val-${t}`)?.toString(); - const betweenValueTo = jQuery(this).data(`cond-between-to-${t}`); - const betweenValueFrom = jQuery(this).data(`cond-between-from-${t}`); - - - is_matched = ppom_compare_values({ - valueToCompare: targetFieldValue, - selectOptionToCompare: selectOptionValue, - constantValueToCompare: constantValue, - betweenValueInterval: { - from: betweenValueFrom, - to: betweenValueTo - }, - operator - }); - - if ( is_matched ) { - matched = ++matched; - cond_elements.push(targetFieldToCompare); - } - - matched_conditions[element_data_name] = matched; - - event_type = visibility === 'hide' ? 'ppom_field_hidden' : 'ppom_field_shown'; - // console.log(`${t} ***** ${element_data_name} total_cond ${total_cond} == matched ${matched} ==> ${matched_conditions[element_data_name]} ==> visibility ${event_type}`); - - if ( (matched_conditions[element_data_name] > 0 && binding === 'Any') || - (matched_conditions[element_data_name] == total_cond && binding === 'All') - ) { - - // remove/add locked classes for all dependent fields - cond_elements.forEach(cond_dataname => { - if( visibility === 'hide' ){ - jQuery(this).addClass(`ppom-locked-${cond_dataname} ppom-c-hide`).removeClass('ppom-c-show'); - }else{ - jQuery(this).removeClass(`ppom-locked-${cond_dataname} ppom-c-hide`); - } - }); - - if ( typeof callback == "function" ) { - callback(element_data_name, event_type); - } - } - else if ( ! is_matched || matched_conditions[element_data_name] !== total_cond) { - - if( visibility === 'hide' ){ - event_type = 'ppom_field_shown'; - jQuery(this).removeClass(`ppom-locked-${data_name} ppom-c-hide`); - }else{ - event_type = 'ppom_field_hidden'; - jQuery(this).addClass(`ppom-locked-${data_name} ppom-c-hide`); - } - - if ( typeof callback == "function" ) - callback(element_data_name, event_type); - } else { - - jQuery(this).removeClass(`ppom-locked-${data_name} ppom-c-hide`); - // console.log('event_type', event_type); - - if ( typeof callback == "function" ) - callback(element_data_name, event_type); - } - } - }); + $( document ).on( 'ppom_field_shown', function ( e ) { + ppom_fields_hidden_conditionally(); + + // Set checked/selected again + ppom_set_default_option( e.field ); + + ppom_check_conditions( + e.field, + function ( element_dataname, event_type ) { + // console.log(`${element_dataname} ===> ${event_type}`); + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + + const field_meta = ppom_get_field_meta_by_id( e.field ); + + // Apply FileAPI to DOM + // PPOM version 22.0 has issue, commenting it so far by Najeeb April 4, 2021 + // if (field_meta.type === 'file' || field_meta.type === 'cropper') { + // ppom_setup_file_upload_input(field_meta); + // } + + // Price Matrix + if ( field_meta.type == 'pricematrix' ) { + // Resettin + $( '.ppom_pricematrix' ).removeClass( 'active' ); + + // Set Active + const classname = '.' + field_meta.data_name; + // console.log(field_meta.data_name, jQuery(`input[data-dataname="ppom[fields][${field_meta.data_name}]"]`)); + jQuery( + `input[data-dataname="ppom[fields][${ field_meta.data_name }]"]` + ).addClass( 'active' ); + // $(classname).find('.ppom_pricematrix').addClass('active') + } + + //Imageselect (Image dropdown) + if ( field_meta.type === 'imageselect' ) { + const dd_selector = 'ppom_imageselect_' + field_meta.data_name; + const ddData = $( '#' + dd_selector ).data( 'ppom_ddslick' ); + const image_replace = field_meta.image_replace + ? field_meta.image_replace + : 'off'; + + ppom_create_hidden_input( ddData ); + ppom_update_option_prices(); + setTimeout( function () { + ppom_image_selection( ddData, image_replace ); + }, 100 ); + // $('#'+dd_selector).ddslick('select', {index: 0 }); + } + + // Multiple Select Addon + if ( field_meta.type === 'multiple_select' ) { + const selector = jQuery( + 'select[name="ppom[fields][' + field_meta.data_name + '][]"]' + ); + const selected_value = selector.val(); + const default_value = field_meta.selected; + + if ( selected_value === null && default_value ) { + const selected_opt_arr = default_value.split( ',' ); + + selector.val( selected_opt_arr ).trigger( 'change' ); + + const selected_options = selector.find( 'option:selected' ); + jQuery.each( + selected_options, + function ( index, default_selected ) { + const option_id = + jQuery( default_selected ).attr( 'data-option_id' ); + const option_label = + jQuery( default_selected ).attr( + 'data-optionlabel' + ); + const option_price = + jQuery( default_selected ).attr( + 'data-optionprice' + ); + + ppom_multiple_select_create_hidden_input( + field_meta.data_name, + option_id, + option_price, + option_label, + field_meta.title + ); + } + ); + } + } + } ); + + ppom_fields_hidden_conditionally(); +} ); + +function ppom_check_conditions( data_name, callback ) { + // Each `.ppom-cond-*` node describes one target field and its dependencies. + // We evaluate all rules for that target, then notify the rest of the stack + // through shared PPOM events instead of mutating unrelated features directly. + let is_matched = false; + let event_type, element_data_name; + + jQuery( `div.ppom-cond-${ data_name }` ).each( function () { + // return this.data('cond-val1').match(/\w*-Back/); + // console.log(jQuery(this)); + const total_cond = parseInt( jQuery( this ).data( 'cond-total' ) ); + const binding = jQuery( this ).data( `cond-bind` ); + const visibility = jQuery( this ).data( `cond-visibility` ); + element_data_name = jQuery( this ).data( 'data_name' ); + + let matched = 0; + const matched_conditions = []; + const cond_elements = []; + for ( let t = 1; t <= total_cond; t++ ) { + const targetFieldToCompare = jQuery( this ) + .data( `cond-input${ t }` ) + ?.toString() + ?.toLowerCase(); + const targetFieldValue = + ppom_get_element_value( targetFieldToCompare ); + + const selectOptionValue = jQuery( this ) + .data( `cond-val${ t }` ) + ?.toString(); + const operator = jQuery( this ).data( `cond-operator${ t }` ); + const constantValue = jQuery( this ) + .data( `cond-constant-val-${ t }` ) + ?.toString(); + const betweenValueTo = jQuery( this ).data( + `cond-between-to-${ t }` + ); + const betweenValueFrom = jQuery( this ).data( + `cond-between-from-${ t }` + ); + + is_matched = ppom_compare_values( { + valueToCompare: targetFieldValue, + selectOptionToCompare: selectOptionValue, + constantValueToCompare: constantValue, + betweenValueInterval: { + from: betweenValueFrom, + to: betweenValueTo, + }, + operator, + } ); + + if ( is_matched ) { + matched = ++matched; + cond_elements.push( targetFieldToCompare ); + } + + matched_conditions[ element_data_name ] = matched; + + event_type = + visibility === 'hide' + ? 'ppom_field_hidden' + : 'ppom_field_shown'; + // console.log(`${t} ***** ${element_data_name} total_cond ${total_cond} == matched ${matched} ==> ${matched_conditions[element_data_name]} ==> visibility ${event_type}`); + + if ( + ( matched_conditions[ element_data_name ] > 0 && + binding === 'Any' ) || + ( matched_conditions[ element_data_name ] == total_cond && + binding === 'All' ) + ) { + // remove/add locked classes for all dependent fields + cond_elements.forEach( ( cond_dataname ) => { + if ( visibility === 'hide' ) { + jQuery( this ) + .addClass( + `ppom-locked-${ cond_dataname } ppom-c-hide` + ) + .removeClass( 'ppom-c-show' ); + } else { + jQuery( this ).removeClass( + `ppom-locked-${ cond_dataname } ppom-c-hide` + ); + } + } ); + + if ( typeof callback === 'function' ) { + callback( element_data_name, event_type ); + } + } else if ( + ! is_matched || + matched_conditions[ element_data_name ] !== total_cond + ) { + if ( visibility === 'hide' ) { + event_type = 'ppom_field_shown'; + jQuery( this ).removeClass( + `ppom-locked-${ data_name } ppom-c-hide` + ); + } else { + event_type = 'ppom_field_hidden'; + jQuery( this ).addClass( + `ppom-locked-${ data_name } ppom-c-hide` + ); + } + + if ( typeof callback === 'function' ) { + callback( element_data_name, event_type ); + } + } else { + jQuery( this ).removeClass( + `ppom-locked-${ data_name } ppom-c-hide` + ); + // console.log('event_type', event_type); + + if ( typeof callback === 'function' ) { + callback( element_data_name, event_type ); + } + } + } + } ); } -function ppom_get_input_dom_type(data_name) { - // const field_obj = jQuery(`input[name="ppom[fields][${data_name}]"], input[name="ppom[fields][${data_name}[]]"], select[name="ppom[fields][${data_name}]"]`); - const field_obj = jQuery(`.ppom-input[data-data_name="${data_name}"]`); - return field_obj.closest('.ppom-field-wrapper').data('type'); +function ppom_get_input_dom_type( data_name ) { + // const field_obj = jQuery(`input[name="ppom[fields][${data_name}]"], input[name="ppom[fields][${data_name}[]]"], select[name="ppom[fields][${data_name}]"]`); + const field_obj = jQuery( `.ppom-input[data-data_name="${ data_name }"]` ); + return field_obj.closest( '.ppom-field-wrapper' ).data( 'type' ); } -function ppom_get_element_value(data_name) { - - const ppom_type = ppom_get_input_dom_type(data_name); - let element_value = ''; - var value_found_cb = []; - - switch (ppom_type) { - case 'switcher': - case 'radio': - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).val(); - break; - case 'palettes': - case 'checkbox': - jQuery('input[name="ppom[fields][' + data_name + '][]"]:checked').each(function(i) { - value_found_cb[i] = jQuery(this).val(); - }); - break; - case 'image': - case 'conditional_meta': - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).data('label'); - break; - case 'imageselect': - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).data('label'); - break; - case 'fixedprice': - var render_type = jQuery(`.ppom-input-${data_name}`).attr('data-input'); - if( render_type == 'radio' ){ - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).val(); - }else{ - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]`).val(); - } - break; - - default: - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]`).val(); - } - - if (ppom_type === 'checkbox' || ppom_type === 'palettes') { - // console.log(value_found_cb); - return value_found_cb; - } - - return element_value; +// Normalize values across PPOM field types so condition operators can stay +// unaware of the exact DOM structure used by each input renderer. +function ppom_get_element_value( data_name ) { + const ppom_type = ppom_get_input_dom_type( data_name ); + let element_value = ''; + const value_found_cb = []; + + switch ( ppom_type ) { + case 'switcher': + case 'radio': + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).val(); + break; + case 'palettes': + case 'checkbox': + jQuery( + 'input[name="ppom[fields][' + data_name + '][]"]:checked' + ).each( function ( i ) { + value_found_cb[ i ] = jQuery( this ).val(); + } ); + break; + case 'image': + case 'conditional_meta': + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).data( 'label' ); + break; + case 'imageselect': + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).data( 'label' ); + break; + case 'fixedprice': + var render_type = jQuery( `.ppom-input-${ data_name }` ).attr( + 'data-input' + ); + if ( render_type == 'radio' ) { + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).val(); + } else { + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]` + ).val(); + } + break; + + default: + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]` + ).val(); + } + + if ( ppom_type === 'checkbox' || ppom_type === 'palettes' ) { + // console.log(value_found_cb); + return value_found_cb; + } + + return element_value; } /** * Compares values based on the provided operator. * - * @param {Object} args - The arguments object containing comparison parameters. - * @param {string} args.valueToCompare - The target value to compare. - * @param {string} args.selectOptionToCompare - The select option value to compare. - * @param {string} args.constantValueToCompare - The constant value to compare. - * @param {{to: string, from: string}} args.betweenValueInterval - The between interval. - * @param {string} args.operator - The operator used for comparison. - * @returns {boolean} - The result of the comparison. + * @param {PPOMConditionCompareArgs} args - Comparison parameters taken from a + * rendered condition rule and the current state of its target field. + * @return {boolean} - The result of the comparison. */ function ppom_compare_values( args ) { - const { valueToCompare, selectOptionToCompare, constantValueToCompare, operator, betweenValueInterval } = args; - let result = false; - switch (operator) { - case 'is': - if ( Array.isArray(valueToCompare) ) { - result = valueToCompare.includes(selectOptionToCompare);; - } else { - result = valueToCompare === selectOptionToCompare; - if ( !selectOptionToCompare && constantValueToCompare ) { - result = valueToCompare === constantValueToCompare - } - } - break; - - case 'not': - if ( Array.isArray(valueToCompare) ) { - result = !valueToCompare.includes(selectOptionToCompare);; - } else { - result = valueToCompare !== selectOptionToCompare; - if ( !selectOptionToCompare && constantValueToCompare ) { - result = valueToCompare !== constantValueToCompare - } - } - break; - - case 'greater than': - result = parseFloat(valueToCompare) > parseFloat(selectOptionToCompare); - if ( !selectOptionToCompare && constantValueToCompare ) { - result = parseFloat(valueToCompare) > parseFloat(constantValueToCompare) - } - break; - - case 'less than': - result = parseFloat(valueToCompare) < parseFloat(selectOptionToCompare); - if ( !selectOptionToCompare && constantValueToCompare ) { - result = parseFloat(valueToCompare) < parseFloat(constantValueToCompare) - } - break; - - case 'any': - result = valueToCompare !== undefined && valueToCompare !== null && valueToCompare !== ''; - break; - - case 'empty': - result = valueToCompare === undefined || valueToCompare === null || valueToCompare === ''; - break; - - case 'between': - result = ( - parseFloat(valueToCompare) >= parseFloat( betweenValueInterval.from ) && - parseFloat(valueToCompare) <= parseFloat( betweenValueInterval.to ) - ); - break; - - case 'number-multiplier': - result = parseFloat(valueToCompare) % parseFloat(constantValueToCompare) === 0; - break; - - case 'even-number': - result = parseFloat(valueToCompare) % 2 === 0; - break; - - case 'odd-number': - result = parseFloat(valueToCompare) % 2 !== 0; - break; - - case 'contains': - result = valueToCompare?.includes(constantValueToCompare); - break; - - case 'not contains': - result = !valueToCompare?.includes(constantValueToCompare); - break; - - case 'regex': - if ( typeof constantValueToCompare === 'string' ) { - const [_, pattern, flags] = constantValueToCompare.split('/'); - const regex = new RegExp(pattern || constantValueToCompare, flags); - result = regex.test(valueToCompare); - } - break; - - default: - // code - } - - // console.log(`matching ${v1} ${operator} ${v2}`); - return result; + const { + valueToCompare, + selectOptionToCompare, + constantValueToCompare, + operator, + betweenValueInterval, + } = args; + let result = false; + switch ( operator ) { + case 'is': + if ( Array.isArray( valueToCompare ) ) { + result = valueToCompare.includes( selectOptionToCompare ); + } else { + result = valueToCompare === selectOptionToCompare; + if ( ! selectOptionToCompare && constantValueToCompare ) { + result = valueToCompare === constantValueToCompare; + } + } + break; + + case 'not': + if ( Array.isArray( valueToCompare ) ) { + result = ! valueToCompare.includes( selectOptionToCompare ); + } else { + result = valueToCompare !== selectOptionToCompare; + if ( ! selectOptionToCompare && constantValueToCompare ) { + result = valueToCompare !== constantValueToCompare; + } + } + break; + + case 'greater than': + result = + parseFloat( valueToCompare ) > + parseFloat( selectOptionToCompare ); + if ( ! selectOptionToCompare && constantValueToCompare ) { + result = + parseFloat( valueToCompare ) > + parseFloat( constantValueToCompare ); + } + break; + + case 'less than': + result = + parseFloat( valueToCompare ) < + parseFloat( selectOptionToCompare ); + if ( ! selectOptionToCompare && constantValueToCompare ) { + result = + parseFloat( valueToCompare ) < + parseFloat( constantValueToCompare ); + } + break; + + case 'any': + result = + valueToCompare !== undefined && + valueToCompare !== null && + valueToCompare !== ''; + break; + + case 'empty': + result = + valueToCompare === undefined || + valueToCompare === null || + valueToCompare === ''; + break; + + case 'between': + result = + parseFloat( valueToCompare ) >= + parseFloat( betweenValueInterval.from ) && + parseFloat( valueToCompare ) <= + parseFloat( betweenValueInterval.to ); + break; + + case 'number-multiplier': + result = + parseFloat( valueToCompare ) % + parseFloat( constantValueToCompare ) === + 0; + break; + + case 'even-number': + result = parseFloat( valueToCompare ) % 2 === 0; + break; + + case 'odd-number': + result = parseFloat( valueToCompare ) % 2 !== 0; + break; + + case 'contains': + result = valueToCompare?.includes( constantValueToCompare ); + break; + + case 'not contains': + result = ! valueToCompare?.includes( constantValueToCompare ); + break; + + case 'regex': + if ( typeof constantValueToCompare === 'string' ) { + const [ _, pattern, flags ] = + constantValueToCompare.split( '/' ); + const regex = new RegExp( + pattern || constantValueToCompare, + flags + ); + result = regex.test( valueToCompare ); + } + break; + + default: + // code + } + + // console.log(`matching ${v1} ${operator} ${v2}`); + return result; } -function ppom_set_default_option(field_id) { - - // get product id - var product_id = ppom_input_vars.product_id; - - var field = ppom_get_field_meta_by_id(field_id); - - switch (field.type) { - - // Check if field is - case 'switcher': - case 'radio': - jQuery.each(field.options, function(label, options) { - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - // console.log('optio nid ', opt_id); - - if (options.option == field.selected) { - jQuery("#" + opt_id).prop('checked', true).trigger('change'); - } - }); - break; - - case 'select': - if ( '' === jQuery("#" + field.data_name).val() ) { - jQuery("#" + field.data_name).val(field.selected); - } - break; - - case 'image': - jQuery.each(field.images, function(index, img) { - - if (img.title == field.selected) { - jQuery("#" + field.data_name + '-' + img.id).prop('checked', true); - } - }); - break; - - case 'checkbox': - jQuery.each(field.options, function(label, options) { - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - - var default_checked = field.checked.split('\r\n'); - if (jQuery.inArray(options.option, default_checked) > -1) { - jQuery("#" + opt_id).prop('checked', true); - - } - }); - break; - - case 'quantities': - jQuery.each(field.options, function(label, options) { - //console.log(options); - if( options.default === '' ) return; - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - jQuery("#" + opt_id).val(options.default).trigger('change'); - - }); - break; - - case 'text': - case 'date': - case 'number': - if ( '' === jQuery("#" + field.data_name).val() ) { - jQuery("#" + field.data_name).val(field.default_value); - } - break; - } +function ppom_set_default_option( field_id ) { + // When a field becomes visible again, restore its default state the same way + // the original PHP renderer would have populated it on first page load. + // get product id + const product_id = ppom_input_vars.product_id; + + const field = ppom_get_field_meta_by_id( field_id ); + + switch ( field.type ) { + // Check if field is + case 'switcher': + case 'radio': + jQuery.each( field.options, function ( label, options ) { + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + // console.log('optio nid ', opt_id); + + if ( options.option == field.selected ) { + jQuery( '#' + opt_id ) + .prop( 'checked', true ) + .trigger( 'change' ); + } + } ); + break; + + case 'select': + if ( '' === jQuery( '#' + field.data_name ).val() ) { + jQuery( '#' + field.data_name ).val( field.selected ); + } + break; + + case 'image': + jQuery.each( field.images, function ( index, img ) { + if ( img.title == field.selected ) { + jQuery( '#' + field.data_name + '-' + img.id ).prop( + 'checked', + true + ); + } + } ); + break; + + case 'checkbox': + jQuery.each( field.options, function ( label, options ) { + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + + const default_checked = field.checked.split( '\r\n' ); + if ( jQuery.inArray( options.option, default_checked ) > -1 ) { + jQuery( '#' + opt_id ).prop( 'checked', true ); + } + } ); + break; + + case 'quantities': + jQuery.each( field.options, function ( label, options ) { + //console.log(options); + if ( options.default === '' ) { + return; + } + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + jQuery( '#' + opt_id ) + .val( options.default ) + .trigger( 'change' ); + } ); + break; + + case 'text': + case 'date': + case 'number': + if ( '' === jQuery( '#' + field.data_name ).val() ) { + jQuery( '#' + field.data_name ).val( field.default_value ); + } + break; + } } -// Updating conditionally hidden fields +// Mirror the current hidden field list into the hidden input consumed by PHP. function ppom_fields_hidden_conditionally() { - - // Reset - ppom_hidden_fields = []; - // jQuery(`.ppom-field-wrapper.ppom-c-hide`).filter(function() { - - // const data_name = jQuery(this).data('data_name'); - // jQuery(`#${data_name}`).prop('required', false); - // // console.log(data_name); - // ppom_hidden_fields.push(data_name); - // }); - // console.log("Condionally Hidden", ppom_hidden_fields); - // jQuery("#conditionally_hidden").val(ppom_hidden_fields); - - var datanames = jQuery(`.ppom-field-wrapper[class*="ppom-locked-"]`).map( (i,h) => ppom_hidden_fields.push(jQuery(h).data('data_name')) ); - jQuery("#conditionally_hidden").val(ppom_hidden_fields); - // console.log(ppom_hidden_fields); + // Reset + ppom_hidden_fields = []; + // jQuery(`.ppom-field-wrapper.ppom-c-hide`).filter(function() { + + // const data_name = jQuery(this).data('data_name'); + // jQuery(`#${data_name}`).prop('required', false); + // // console.log(data_name); + // ppom_hidden_fields.push(data_name); + // }); + // console.log("Condionally Hidden", ppom_hidden_fields); + // jQuery("#conditionally_hidden").val(ppom_hidden_fields); + + const datanames = jQuery( + `.ppom-field-wrapper[class*="ppom-locked-"]` + ).map( ( i, h ) => + ppom_hidden_fields.push( jQuery( h ).data( 'data_name' ) ) + ); + jQuery( '#conditionally_hidden' ).val( ppom_hidden_fields ); + // console.log(ppom_hidden_fields); } diff --git a/js/ppom-conditions-v2bkp.js b/js/ppom-conditions-v2bkp.js index f89d3781..d51e97d3 100644 --- a/js/ppom-conditions-v2bkp.js +++ b/js/ppom-conditions-v2bkp.js @@ -2,505 +2,597 @@ * PPOM Conditional Version 2 * More Fast and Optimized * April, 2020 in LockedDown (CORVID-19) - * */ - -var ppom_hidden_fields = []; - -jQuery(function($) { - - setTimeout(function() { - $('form.cart').find('select option:selected, input[type="radio"]:checked, input[type="checkbox"]:checked').each(function(i, field) { - - if ($(field).closest('div.ppom-field-wrapper').hasClass('ppom-c-hide')) return; - - const data_name = $(field).data('data_name'); - ppom_check_conditions(data_name, function(element_dataname, event_type) { - // console.log(data_name, event_type); - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - }); - - $('form.cart').find('div.ppom-c-show').each(function(i, field) { - - const data_name = $(field).data('data_name'); - ppom_check_conditions(data_name, function(element_dataname, event_type) { - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - }); - - $('form.cart').find('div.ppom-c-hide').each(function(i, field) { - const data_name = $(field).data('data_name'); - $.event.trigger({ - type: 'ppom_field_hidden', - field: data_name, - time: new Date() - }); - }); - - }, 100); - - // $('form.cart').on('change', 'select, input[type="radio"], input[type="checkbox"]', function(ev) { - - $(".ppom-wrapper").on('change', 'select,input:radio,input:checkbox', function(e) { - - let value = null; - if (($(this).is(':radio') || $(this).is(':checkbox'))) { - value = this.checked ? $(this).val() : null; - } - else { - - value = $(this).val(); - } - - const data_name = $(this).data('data_name'); - // console.log("Checking condition for ", data_name); - - ppom_check_conditions(data_name, function(element_dataname, event_type) { - // console.log(`${element_dataname} ===> ${event_type}`); - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - }); - - $(document).on('ppom_hidden_fields_updated', function(e) { - ppom_fields_hidden_conditionally(); - - // $("#conditionally_hidden").val(ppom_hidden_fields); - // console.log(` hiddend field updated ==> ${e.field}`); - // $("#conditionally_hidden").val(ppom_hidden_fields); - // ppom_update_option_prices(); - }); - - - $(document).on('ppom_field_hidden', function(e) { - - // console.log(e.field) - - var element_type = ppom_get_field_type_by_id(e.field); - switch (element_type) { - - case 'select': - $('select[name="ppom[fields][' + e.field + ']"]').val(''); - break; - - case 'multiple_select': - - var selector = $('select[name="ppom[fields][' + e.field + '][]"]'); - var selected_value = selector.val(); - var selected_options = selector.find('option:selected'); - - jQuery.each(selected_options, function(index, default_selected) { - - var option_id = jQuery(default_selected).attr('data-option_id'); - var the_id = 'ppom-multipleselect-' + e.field + '-' + option_id; - - $("#" + the_id).remove(); - }); - - if (selected_value) { - - $('select[name="ppom[fields][' + e.field + '][]"]').val(null).trigger("change"); - } - - break; - - case 'checkbox': - $('input[name="ppom[fields][' + e.field + '][]"]').prop('checked', false); - break; - - case 'radio': - $('input[name="ppom[fields][' + e.field + ']"]').prop('checked', false); - break; - - case 'file': - $('#filelist-' + e.field).find('.u_i_c_box').remove(); - break; - - case 'palettes': - case 'image': - $('input[name="ppom[fields][' + e.field + '][]"]').prop('checked', false); - break; - - case 'imageselect': - var the_id = 'ppom-imageselect' + e.field; - $("#" + the_id).remove(); - break; - - case 'quantityoption': - $('#' + e.field).val(''); - var the_id = 'ppom-quantityoption-rm' + e.field; - $("#" + the_id).remove(); - break; - - case 'pricematrix': - $(`input[data-dataname="ppom[fields][${e.field}]"]`).removeClass('active'); - break; - - case 'quantities': - $(`input[name^="ppom[fields][${e.field}]"]`).val(''); - break; - - - default: - // Reset text/textarea/date/email etc types - $('#' + e.field).val(''); - break; - } - - $.event.trigger({ - type: "ppom_hidden_fields_updated", - field: e.field, - time: new Date() - }); - - ppom_check_conditions(e.field, function(element_dataname, event_type) { - // console.log(`${element_dataname} ===> ${event_type}`); - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - }); - - /*$(document).on('ppom_field_shown', function(e) { + */ + +let ppom_hidden_fields = []; + +jQuery( function ( $ ) { + setTimeout( function () { + $( 'form.cart' ) + .find( + 'select option:selected, input[type="radio"]:checked, input[type="checkbox"]:checked' + ) + .each( function ( i, field ) { + if ( + $( field ) + .closest( 'div.ppom-field-wrapper' ) + .hasClass( 'ppom-c-hide' ) + ) { + return; + } + + const data_name = $( field ).data( 'data_name' ); + ppom_check_conditions( + data_name, + function ( element_dataname, event_type ) { + // console.log(data_name, event_type); + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + } ); + + $( 'form.cart' ) + .find( 'div.ppom-c-show' ) + .each( function ( i, field ) { + const data_name = $( field ).data( 'data_name' ); + ppom_check_conditions( + data_name, + function ( element_dataname, event_type ) { + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + } ); + + $( 'form.cart' ) + .find( 'div.ppom-c-hide' ) + .each( function ( i, field ) { + const data_name = $( field ).data( 'data_name' ); + $.event.trigger( { + type: 'ppom_field_hidden', + field: data_name, + time: new Date(), + } ); + } ); + }, 100 ); + + // $('form.cart').on('change', 'select, input[type="radio"], input[type="checkbox"]', function(ev) { + + $( '.ppom-wrapper' ).on( + 'change', + 'select,input:radio,input:checkbox', + function ( e ) { + let value = null; + if ( $( this ).is( ':radio' ) || $( this ).is( ':checkbox' ) ) { + value = this.checked ? $( this ).val() : null; + } else { + value = $( this ).val(); + } + + const data_name = $( this ).data( 'data_name' ); + // console.log("Checking condition for ", data_name); + + ppom_check_conditions( + data_name, + function ( element_dataname, event_type ) { + // console.log(`${element_dataname} ===> ${event_type}`); + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + } + ); + + $( document ).on( 'ppom_hidden_fields_updated', function ( e ) { + ppom_fields_hidden_conditionally(); + + // $("#conditionally_hidden").val(ppom_hidden_fields); + // console.log(` hiddend field updated ==> ${e.field}`); + // $("#conditionally_hidden").val(ppom_hidden_fields); + // ppom_update_option_prices(); + } ); + + $( document ).on( 'ppom_field_hidden', function ( e ) { + // console.log(e.field) + + const element_type = ppom_get_field_type_by_id( e.field ); + switch ( element_type ) { + case 'select': + $( 'select[name="ppom[fields][' + e.field + ']"]' ).val( '' ); + break; + + case 'multiple_select': + var selector = $( + 'select[name="ppom[fields][' + e.field + '][]"]' + ); + var selected_value = selector.val(); + var selected_options = selector.find( 'option:selected' ); + + jQuery.each( + selected_options, + function ( index, default_selected ) { + const option_id = + jQuery( default_selected ).attr( 'data-option_id' ); + const the_id = + 'ppom-multipleselect-' + e.field + '-' + option_id; + + $( '#' + the_id ).remove(); + } + ); + + if ( selected_value ) { + $( 'select[name="ppom[fields][' + e.field + '][]"]' ) + .val( null ) + .trigger( 'change' ); + } + + break; + + case 'checkbox': + $( 'input[name="ppom[fields][' + e.field + '][]"]' ).prop( + 'checked', + false + ); + break; + + case 'radio': + $( 'input[name="ppom[fields][' + e.field + ']"]' ).prop( + 'checked', + false + ); + break; + + case 'file': + $( '#filelist-' + e.field ) + .find( '.u_i_c_box' ) + .remove(); + break; + + case 'palettes': + case 'image': + $( 'input[name="ppom[fields][' + e.field + '][]"]' ).prop( + 'checked', + false + ); + break; + + case 'imageselect': + var the_id = 'ppom-imageselect' + e.field; + $( '#' + the_id ).remove(); + break; + + case 'quantityoption': + $( '#' + e.field ).val( '' ); + var the_id = 'ppom-quantityoption-rm' + e.field; + $( '#' + the_id ).remove(); + break; + + case 'pricematrix': + $( + `input[data-dataname="ppom[fields][${ e.field }]"]` + ).removeClass( 'active' ); + break; + + case 'quantities': + $( `input[name^="ppom[fields][${ e.field }]"]` ).val( '' ); + break; + + default: + // Reset text/textarea/date/email etc types + $( '#' + e.field ).val( '' ); + break; + } + + $.event.trigger( { + type: 'ppom_hidden_fields_updated', + field: e.field, + time: new Date(), + } ); + + ppom_check_conditions( + e.field, + function ( element_dataname, event_type ) { + // console.log(`${element_dataname} ===> ${event_type}`); + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + } ); + + /*$(document).on('ppom_field_shown', function(e) { console.log(`shown event ${e.field}`); ppom_check_conditions(e.field); });*/ - $(document).on('ppom_field_shown', function(e) { - - ppom_fields_hidden_conditionally(); - - // Set checked/selected again - ppom_set_default_option(e.field); - - ppom_check_conditions(e.field, function(element_dataname, event_type) { - // console.log(`${element_dataname} ===> ${event_type}`); - $.event.trigger({ - type: event_type, - field: element_dataname, - time: new Date() - }); - }); - - - var field_meta = ppom_get_field_meta_by_id(e.field); - - // Apply FileAPI to DOM - // PPOM version 22.0 has issue, commenting it so far by Najeeb April 4, 2021 - // if (field_meta.type === 'file' || field_meta.type === 'cropper') { - // ppom_setup_file_upload_input(field_meta); - // } - - // Price Matrix - if (field_meta.type == 'pricematrix') { - // Resettin - $(".ppom_pricematrix").removeClass('active'); - - // Set Active - var classname = "." + field_meta.data_name; - // console.log(field_meta.data_name, jQuery(`input[data-dataname="ppom[fields][${field_meta.data_name}]"]`)); - jQuery(`input[data-dataname="ppom[fields][${field_meta.data_name}]"]`).addClass('active') - // $(classname).find('.ppom_pricematrix').addClass('active') - } - - //Imageselect (Image dropdown) - if (field_meta.type === 'imageselect') { - - var dd_selector = 'ppom_imageselect_' + field_meta.data_name; - var ddData = $('#' + dd_selector).data('ddslick'); - var image_replace = $('#' + dd_selector).attr('data-enable-rpimg'); - ppom_create_hidden_input(ddData); - ppom_update_option_prices(); - setTimeout(function() { - ppom_image_selection(ddData, image_replace); - }, 100); - // $('#'+dd_selector).ddslick('select', {index: 0 }); - } - - - // Multiple Select Addon - if (field_meta.type === 'multiple_select') { - - var selector = jQuery('select[name="ppom[fields][' + field_meta.data_name + '][]"]'); - var selected_value = selector.val(); - var default_value = field_meta.selected; - - if (selected_value === null && default_value) { - - var selected_opt_arr = default_value.split(','); - - selector.val(selected_opt_arr).trigger('change'); - - var selected_options = selector.find('option:selected'); - jQuery.each(selected_options, function(index, default_selected) { - - var option_id = jQuery(default_selected).attr('data-option_id'); - var option_label = jQuery(default_selected).attr('data-optionlabel'); - var option_price = jQuery(default_selected).attr('data-optionprice'); - - ppom_multiple_select_create_hidden_input(field_meta.data_name, option_id, option_price, option_label, field_meta.title); - }); - } - } - - }); - - ppom_fields_hidden_conditionally(); - -}); - -function ppom_check_conditions(data_name, callback) { - - let is_matched = false; - const ppom_type = jQuery(`.ppom-input[data-data_name="${data_name}"]`).data('type'); - let event_type, element_data_name; - const field_val = ppom_get_element_value(data_name); - // console.log('data_name',data_name); - jQuery(`div.ppom-cond-${data_name}`).each(function() { - // return this.data('cond-val1').match(/\w*-Back/); - // console.log(jQuery(this)); - const total_cond = parseInt(jQuery(this).data('cond-total')); - const binding = jQuery(this).data(`cond-bind`); - const visibility = jQuery(this).data(`cond-visibility`); - element_data_name = jQuery(this).data('data_name'); - - let matched = 0; - var matched_conditions = []; - for (var t = 1; t <= total_cond; t++) { - - const cond_element = jQuery(this).data(`cond-input${t}`); - const cond_val = jQuery(this).data(`cond-val${t}`).toString(); - const operator = jQuery(this).data(`cond-operator${t}`); - - // const field_val = ppom_get_field_type(field_obj); - if (cond_element !== data_name) continue; - is_matched = ppom_compare_values(field_val, cond_val, operator); - // console.log(`${data_name} TRIGGERS :: ${t} ***** ${element_data_name} ==> field value ${field_val} || cond_valu ${cond_val} || operator ${operator} || Binding ${binding} is_matched=>${is_matched}`); - // console.log(field_val,cond_val); - matched = is_matched ? ++matched : matched; - matched_conditions[element_data_name] = matched; - - event_type = visibility === 'hide' ? 'ppom_field_hidden' : 'ppom_field_shown'; - // console.log(`${t} ***** ${element_data_name} total_cond ${total_cond} == matched ${matched} ==> ${matched_conditions[element_data_name]} ==> visibility ${event_type}`); - - if ( (matched_conditions[element_data_name] > 0 && binding === 'Any') || - (matched_conditions[element_data_name] == total_cond && binding === 'All') - ) { - - if( visibility === 'hide' ){ - jQuery(this).addClass(`ppom-locked-${data_name} ppom-c-hide`).removeClass('ppom-c-show'); - }else{ - jQuery(this).removeClass(`ppom-locked-${data_name} ppom-c-hide`); - } - if (typeof callback == "function") - callback(element_data_name, event_type); - // return is_matched; - - - } - else if ( ! is_matched ) { - - if( visibility === 'hide' ){ - event_type = 'ppom_field_shown'; - jQuery(this).removeClass(`ppom-locked-${data_name} ppom-c-hide`); - }else{ - event_type = 'ppom_field_hidden'; - jQuery(this).addClass(`ppom-locked-${data_name} ppom-c-hide`); - } - - if (typeof callback == "function") - callback(element_data_name, event_type); - } else { - - jQuery(this).removeClass(`ppom-locked-${data_name} ppom-c-hide`); - // console.log('event_type', event_type); - if (typeof callback == "function") - callback(element_data_name, event_type); - } - } - - // return is_matched; - // return jQuery(this).data('cond-val1') === jQuery(this).val(); - }); + $( document ).on( 'ppom_field_shown', function ( e ) { + ppom_fields_hidden_conditionally(); + + // Set checked/selected again + ppom_set_default_option( e.field ); + + ppom_check_conditions( + e.field, + function ( element_dataname, event_type ) { + // console.log(`${element_dataname} ===> ${event_type}`); + $.event.trigger( { + type: event_type, + field: element_dataname, + time: new Date(), + } ); + } + ); + + const field_meta = ppom_get_field_meta_by_id( e.field ); + + // Apply FileAPI to DOM + // PPOM version 22.0 has issue, commenting it so far by Najeeb April 4, 2021 + // if (field_meta.type === 'file' || field_meta.type === 'cropper') { + // ppom_setup_file_upload_input(field_meta); + // } + + // Price Matrix + if ( field_meta.type == 'pricematrix' ) { + // Resettin + $( '.ppom_pricematrix' ).removeClass( 'active' ); + + // Set Active + const classname = '.' + field_meta.data_name; + // console.log(field_meta.data_name, jQuery(`input[data-dataname="ppom[fields][${field_meta.data_name}]"]`)); + jQuery( + `input[data-dataname="ppom[fields][${ field_meta.data_name }]"]` + ).addClass( 'active' ); + // $(classname).find('.ppom_pricematrix').addClass('active') + } + + //Imageselect (Image dropdown) + if ( field_meta.type === 'imageselect' ) { + const dd_selector = 'ppom_imageselect_' + field_meta.data_name; + const ddData = $( '#' + dd_selector ).data( 'ddslick' ); + const image_replace = $( '#' + dd_selector ).attr( + 'data-enable-rpimg' + ); + ppom_create_hidden_input( ddData ); + ppom_update_option_prices(); + setTimeout( function () { + ppom_image_selection( ddData, image_replace ); + }, 100 ); + // $('#'+dd_selector).ddslick('select', {index: 0 }); + } + + // Multiple Select Addon + if ( field_meta.type === 'multiple_select' ) { + const selector = jQuery( + 'select[name="ppom[fields][' + field_meta.data_name + '][]"]' + ); + const selected_value = selector.val(); + const default_value = field_meta.selected; + + if ( selected_value === null && default_value ) { + const selected_opt_arr = default_value.split( ',' ); + + selector.val( selected_opt_arr ).trigger( 'change' ); + + const selected_options = selector.find( 'option:selected' ); + jQuery.each( + selected_options, + function ( index, default_selected ) { + const option_id = + jQuery( default_selected ).attr( 'data-option_id' ); + const option_label = + jQuery( default_selected ).attr( + 'data-optionlabel' + ); + const option_price = + jQuery( default_selected ).attr( + 'data-optionprice' + ); + + ppom_multiple_select_create_hidden_input( + field_meta.data_name, + option_id, + option_price, + option_label, + field_meta.title + ); + } + ); + } + } + } ); + + ppom_fields_hidden_conditionally(); +} ); + +function ppom_check_conditions( data_name, callback ) { + let is_matched = false; + const ppom_type = jQuery( + `.ppom-input[data-data_name="${ data_name }"]` + ).data( 'type' ); + let event_type, element_data_name; + const field_val = ppom_get_element_value( data_name ); + // console.log('data_name',data_name); + jQuery( `div.ppom-cond-${ data_name }` ).each( function () { + // return this.data('cond-val1').match(/\w*-Back/); + // console.log(jQuery(this)); + const total_cond = parseInt( jQuery( this ).data( 'cond-total' ) ); + const binding = jQuery( this ).data( `cond-bind` ); + const visibility = jQuery( this ).data( `cond-visibility` ); + element_data_name = jQuery( this ).data( 'data_name' ); + + let matched = 0; + const matched_conditions = []; + for ( let t = 1; t <= total_cond; t++ ) { + const cond_element = jQuery( this ).data( `cond-input${ t }` ); + const cond_val = jQuery( this ).data( `cond-val${ t }` ).toString(); + const operator = jQuery( this ).data( `cond-operator${ t }` ); + + // const field_val = ppom_get_field_type(field_obj); + if ( cond_element !== data_name ) { + continue; + } + is_matched = ppom_compare_values( field_val, cond_val, operator ); + // console.log(`${data_name} TRIGGERS :: ${t} ***** ${element_data_name} ==> field value ${field_val} || cond_valu ${cond_val} || operator ${operator} || Binding ${binding} is_matched=>${is_matched}`); + // console.log(field_val,cond_val); + matched = is_matched ? ++matched : matched; + matched_conditions[ element_data_name ] = matched; + + event_type = + visibility === 'hide' + ? 'ppom_field_hidden' + : 'ppom_field_shown'; + // console.log(`${t} ***** ${element_data_name} total_cond ${total_cond} == matched ${matched} ==> ${matched_conditions[element_data_name]} ==> visibility ${event_type}`); + + if ( + ( matched_conditions[ element_data_name ] > 0 && + binding === 'Any' ) || + ( matched_conditions[ element_data_name ] == total_cond && + binding === 'All' ) + ) { + if ( visibility === 'hide' ) { + jQuery( this ) + .addClass( `ppom-locked-${ data_name } ppom-c-hide` ) + .removeClass( 'ppom-c-show' ); + } else { + jQuery( this ).removeClass( + `ppom-locked-${ data_name } ppom-c-hide` + ); + } + if ( typeof callback === 'function' ) { + callback( element_data_name, event_type ); + } + // return is_matched; + } else if ( ! is_matched ) { + if ( visibility === 'hide' ) { + event_type = 'ppom_field_shown'; + jQuery( this ).removeClass( + `ppom-locked-${ data_name } ppom-c-hide` + ); + } else { + event_type = 'ppom_field_hidden'; + jQuery( this ).addClass( + `ppom-locked-${ data_name } ppom-c-hide` + ); + } + + if ( typeof callback === 'function' ) { + callback( element_data_name, event_type ); + } + } else { + jQuery( this ).removeClass( + `ppom-locked-${ data_name } ppom-c-hide` + ); + // console.log('event_type', event_type); + if ( typeof callback === 'function' ) { + callback( element_data_name, event_type ); + } + } + } + + // return is_matched; + // return jQuery(this).data('cond-val1') === jQuery(this).val(); + } ); } -function ppom_get_input_dom_type(data_name) { - - // const field_obj = jQuery(`input[name="ppom[fields][${data_name}]"], input[name="ppom[fields][${data_name}[]]"], select[name="ppom[fields][${data_name}]"]`); - const field_obj = jQuery(`.ppom-input[data-data_name="${data_name}"]`); - const ppom_type = field_obj.closest('.ppom-field-wrapper').data('type'); - return ppom_type; +function ppom_get_input_dom_type( data_name ) { + // const field_obj = jQuery(`input[name="ppom[fields][${data_name}]"], input[name="ppom[fields][${data_name}[]]"], select[name="ppom[fields][${data_name}]"]`); + const field_obj = jQuery( `.ppom-input[data-data_name="${ data_name }"]` ); + const ppom_type = field_obj.closest( '.ppom-field-wrapper' ).data( 'type' ); + return ppom_type; } -function ppom_get_element_value(data_name) { - - const ppom_type = ppom_get_input_dom_type(data_name); - let element_value = ''; - var value_found_cb = []; - - switch (ppom_type) { - case 'switcher': - case 'radio': - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).val(); - break; - case 'palettes': - case 'checkbox': - jQuery('input[name="ppom[fields][' + data_name + '][]"]:checked').each(function(i) { - value_found_cb[i] = jQuery(this).val(); - }); - break; - case 'image': - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).data('label'); - break; - case 'imageselect': - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).data('label'); - break; - case 'fixedprice': - var render_type = jQuery(`.ppom-input-${data_name}`).attr('data-input'); - if( render_type == 'radio' ){ - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]:checked`).val(); - }else{ - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]`).val(); - } - break; - - default: - element_value = jQuery(`.ppom-input[data-data_name="${data_name}"]`).val(); - } - - if (ppom_type === 'checkbox' || ppom_type === 'palettes') { - // console.log(value_found_cb); - return value_found_cb; - } - - return element_value; +function ppom_get_element_value( data_name ) { + const ppom_type = ppom_get_input_dom_type( data_name ); + let element_value = ''; + const value_found_cb = []; + + switch ( ppom_type ) { + case 'switcher': + case 'radio': + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).val(); + break; + case 'palettes': + case 'checkbox': + jQuery( + 'input[name="ppom[fields][' + data_name + '][]"]:checked' + ).each( function ( i ) { + value_found_cb[ i ] = jQuery( this ).val(); + } ); + break; + case 'image': + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).data( 'label' ); + break; + case 'imageselect': + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).data( 'label' ); + break; + case 'fixedprice': + var render_type = jQuery( `.ppom-input-${ data_name }` ).attr( + 'data-input' + ); + if ( render_type == 'radio' ) { + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]:checked` + ).val(); + } else { + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]` + ).val(); + } + break; + + default: + element_value = jQuery( + `.ppom-input[data-data_name="${ data_name }"]` + ).val(); + } + + if ( ppom_type === 'checkbox' || ppom_type === 'palettes' ) { + // console.log(value_found_cb); + return value_found_cb; + } + + return element_value; } -function ppom_compare_values(v1, v2, operator) { - - let result = false; - switch (operator) { - case 'is': - if( Array.isArray(v1) ) { - result = jQuery.inArray(v2, v1) !== -1 ? true : false; - }else{ - result = v1 === v2 ? true : false; - } - break; - case 'not': - result = v1 !== v2 ? true : false; - break; - - case 'greater than': - result = parseFloat(v1) > parseFloat(v2) ? true : false; - break; - case 'less than': - result = parseFloat(v1) < parseFloat(v2) ? true : false; - break; - - default: - // code - } - - // console.log(`matching ${v1} ${operator} ${v2}`); - return result; +function ppom_compare_values( v1, v2, operator ) { + let result = false; + switch ( operator ) { + case 'is': + if ( Array.isArray( v1 ) ) { + result = jQuery.inArray( v2, v1 ) !== -1 ? true : false; + } else { + result = v1 === v2 ? true : false; + } + break; + case 'not': + result = v1 !== v2 ? true : false; + break; + + case 'greater than': + result = parseFloat( v1 ) > parseFloat( v2 ) ? true : false; + break; + case 'less than': + result = parseFloat( v1 ) < parseFloat( v2 ) ? true : false; + break; + + default: + // code + } + + // console.log(`matching ${v1} ${operator} ${v2}`); + return result; } -function ppom_set_default_option(field_id) { - - // get product id - var product_id = ppom_input_vars.product_id; - - var field = ppom_get_field_meta_by_id(field_id); - - switch (field.type) { - - // Check if field is - case 'switcher': - case 'radio': - jQuery.each(field.options, function(label, options) { - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - // console.log('optio nid ', opt_id); - - if (options.option == field.selected) { - jQuery("#" + opt_id).prop('checked', true).trigger('change'); - } - }); - break; - - case 'select': - jQuery("#" + field.data_name).val(field.selected); - break; - - case 'image': - jQuery.each(field.images, function(index, img) { - - if (img.title == field.selected) { - jQuery("#" + field.data_name + '-' + img.id).prop('checked', true); - } - }); - break; - - case 'checkbox': - jQuery.each(field.options, function(label, options) { - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - - var default_checked = field.checked.split('\r\n'); - if (jQuery.inArray(options.option, default_checked) > -1) { - jQuery("#" + opt_id).prop('checked', true); - - } - }); - break; - - case 'quantities': - jQuery.each(field.options, function(label, options) { - //console.log(options); - if( options.default === '' ) return; - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - jQuery("#" + opt_id).val(options.default).trigger('change'); - - }); - break; - - case 'text': - case 'date': - case 'number': - jQuery("#" + field.data_name).val(field.default_value); - break; - } +function ppom_set_default_option( field_id ) { + // get product id + const product_id = ppom_input_vars.product_id; + + const field = ppom_get_field_meta_by_id( field_id ); + + switch ( field.type ) { + // Check if field is + case 'switcher': + case 'radio': + jQuery.each( field.options, function ( label, options ) { + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + // console.log('optio nid ', opt_id); + + if ( options.option == field.selected ) { + jQuery( '#' + opt_id ) + .prop( 'checked', true ) + .trigger( 'change' ); + } + } ); + break; + + case 'select': + jQuery( '#' + field.data_name ).val( field.selected ); + break; + + case 'image': + jQuery.each( field.images, function ( index, img ) { + if ( img.title == field.selected ) { + jQuery( '#' + field.data_name + '-' + img.id ).prop( + 'checked', + true + ); + } + } ); + break; + + case 'checkbox': + jQuery.each( field.options, function ( label, options ) { + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + + const default_checked = field.checked.split( '\r\n' ); + if ( jQuery.inArray( options.option, default_checked ) > -1 ) { + jQuery( '#' + opt_id ).prop( 'checked', true ); + } + } ); + break; + + case 'quantities': + jQuery.each( field.options, function ( label, options ) { + //console.log(options); + if ( options.default === '' ) { + return; + } + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + jQuery( '#' + opt_id ) + .val( options.default ) + .trigger( 'change' ); + } ); + break; + + case 'text': + case 'date': + case 'number': + jQuery( '#' + field.data_name ).val( field.default_value ); + break; + } } // Updating conditionally hidden fields function ppom_fields_hidden_conditionally() { - - // Reset - ppom_hidden_fields = []; - // jQuery(`.ppom-field-wrapper.ppom-c-hide`).filter(function() { - - // const data_name = jQuery(this).data('data_name'); - // jQuery(`#${data_name}`).prop('required', false); - // // console.log(data_name); - // ppom_hidden_fields.push(data_name); - // }); - // console.log("Condionally Hidden", ppom_hidden_fields); - // jQuery("#conditionally_hidden").val(ppom_hidden_fields); - - var datanames = jQuery(`.ppom-field-wrapper[class*="ppom-locked-"]`).map( (i,h) => ppom_hidden_fields.push(jQuery(h).data('data_name')) ); - jQuery("#conditionally_hidden").val(ppom_hidden_fields); - // console.log(ppom_hidden_fields); -} \ No newline at end of file + // Reset + ppom_hidden_fields = []; + // jQuery(`.ppom-field-wrapper.ppom-c-hide`).filter(function() { + + // const data_name = jQuery(this).data('data_name'); + // jQuery(`#${data_name}`).prop('required', false); + // // console.log(data_name); + // ppom_hidden_fields.push(data_name); + // }); + // console.log("Condionally Hidden", ppom_hidden_fields); + // jQuery("#conditionally_hidden").val(ppom_hidden_fields); + + const datanames = jQuery( + `.ppom-field-wrapper[class*="ppom-locked-"]` + ).map( ( i, h ) => + ppom_hidden_fields.push( jQuery( h ).data( 'data_name' ) ) + ); + jQuery( '#conditionally_hidden' ).val( ppom_hidden_fields ); + // console.log(ppom_hidden_fields); +} diff --git a/js/ppom-conditions.js b/js/ppom-conditions.js index bea969d9..379dbfe8 100644 --- a/js/ppom-conditions.js +++ b/js/ppom-conditions.js @@ -1,424 +1,479 @@ /** - * PPOM Fields Conditions - **/ -"use strict" - -var ppom_field_matched_rules = {}; -var ppom_hidden_fields = []; -jQuery(function($) { - - $(".ppom-wrapper").on('change', 'select,input:radio,input:checkbox', function(e) { - - ppom_check_conditions(); - }); - - $(document).on('ppom_field_shown', function(e) { - - - // Remove from array - $.each(ppom_hidden_fields, function(i, item) { - if (item === e.field) { - - - // Set checked/selected again - ppom_set_default_option(item); - - ppom_hidden_fields.splice(i, 1); - $.event.trigger({ - type: "ppom_hidden_fields_updated", - field: e.field, - time: new Date() - }); - - } - }); - - // Apply FileAPI to DOM - var field_meta = ppom_get_field_meta_by_id(e.field); - if (field_meta.type === 'file' || field_meta.type === 'cropper') { - - ppom_setup_file_upload_input(field_meta); - } - - // Price Matrix - if (field_meta.type == 'pricematrix') { - // Resettin - $(".ppom_pricematrix").removeClass('active'); - - // Set Active - var classname = "." + field_meta.data_name; - // console.log(classname); - $(classname).find('.ppom_pricematrix').addClass('active') - } - - //Imageselect (Image dropdown) - if (field_meta.type === 'imageselect') { - - var dd_selector = 'ppom_imageselect_' + field_meta.data_name; - var ddData = $('#' + dd_selector).data('ddslick'); - var image_replace = $('#' + dd_selector).attr('data-enable-rpimg'); - ppom_create_hidden_input(ddData); - ppom_update_option_prices(); - setTimeout(function() { - ppom_image_selection(ddData, image_replace); - }, 100); - // $('#'+dd_selector).ddslick('select', {index: 0 }); - } - - - // Multiple Select Addon - if (field_meta.type === 'multiple_select') { - - var selector = jQuery('select[name="ppom[fields][' + field_meta.data_name + '][]"]'); - var selected_value = selector.val(); - var default_value = field_meta.selected; - - if (selected_value === null && default_value) { - - var selected_opt_arr = default_value.split(','); - - selector.val(selected_opt_arr).trigger('change'); - - var selected_options = selector.find('option:selected'); - jQuery.each(selected_options, function(index, default_selected) { - - var option_id = jQuery(default_selected).attr('data-option_id'); - var option_label = jQuery(default_selected).attr('data-optionlabel'); - var option_price = jQuery(default_selected).attr('data-optionprice'); - - ppom_multiple_select_create_hidden_input(field_meta.data_name, option_id, option_price, option_label, field_meta.title); - }); - } - } - - }); - - $(document).on('ppom_hidden_fields_updated', function(e) { - - - $("#conditionally_hidden").val(ppom_hidden_fields); - ppom_update_option_prices(); - }); - - $(document).on('ppom_field_hidden', function(e) { - - var element_type = ppom_get_field_type_by_id(e.field); - switch (element_type) { - - case 'select': - $('select[name="ppom[fields][' + e.field + ']"]').val(''); - break; - - case 'multiple_select': - - var selector = $('select[name="ppom[fields][' + e.field + '][]"]'); - var selected_value = selector.val(); - var selected_options = selector.find('option:selected'); - - jQuery.each(selected_options, function(index, default_selected) { - - var option_id = jQuery(default_selected).attr('data-option_id'); - var the_id = 'ppom-multipleselect-' + e.field + '-' + option_id; - - $("#" + the_id).remove(); - }); - - if (selected_value) { - - $('select[name="ppom[fields][' + e.field + '][]"]').val(null).trigger("change"); - } - - break; - - case 'checkbox': - $('input[name="ppom[fields][' + e.field + '][]"]').prop('checked', false); - break; - case 'radio': - $('input[name="ppom[fields][' + e.field + ']"]').prop('checked', false); - break; - - case 'file': - $('#filelist-' + e.field).find('.u_i_c_box').remove(); - break; - - case 'palettes': - case 'image': - $('input[name="ppom[fields][' + e.field + '][]"]').prop('checked', false); - break; - - case 'imageselect': - var the_id = 'ppom-imageselect' + e.field; - $("#" + the_id).remove(); - break; - - default: - // Reset text/textarea/date/email etc types - $('#' + e.field).val(''); - break; - } - - ppom_hidden_fields.push(e.field); - $.event.trigger({ - type: "ppom_hidden_fields_updated", - field: e.field, - time: new Date() - }); - }); - - - setTimeout(function() { - ppom_check_conditions(); - }, 500); - -}); - -function ppom_set_default_option(field_id) { - - // get product id - var product_id = ppom_input_vars.product_id; - - var field = ppom_get_field_meta_by_id(field_id); - switch (field.type) { - - // Check if field is - case 'radio': - jQuery.each(field.options, function(label, options) { - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - - if (options.option == field.selected) { - jQuery("#" + opt_id).prop('checked', true); - } - }); - - break; - - case 'select': - jQuery("#" + field.data_name).val(field.selected); - break; - - case 'image': - jQuery.each(field.images, function(index, img) { - - if (img.title == field.selected) { - jQuery("#" + field.data_name + '-' + img.id).prop('checked', true); - } - }); - break; - - case 'checkbox': - jQuery.each(field.options, function(label, options) { - var opt_id = product_id + '-' + field.data_name + '-' + options.id; - - var default_checked = field.checked.split('\r\n'); - if (jQuery.inArray(options.option, default_checked) > -1) { - jQuery("#" + opt_id).prop('checked', true); - - } - }); - break; - - case 'text': - case 'date': - case 'number': - jQuery("#" + field.data_name).val(field.default_value); - break; - } + * Legacy PPOM conditional-logic engine. + * + * This version works with the older `ppom_input_vars.conditions` structure. + * It still emits the same hidden/shown events so the rest of the frontend and + * the hidden `conditionally_hidden` payload remain compatible with PHP. + */ +'use strict'; + +const ppom_field_matched_rules = {}; +const ppom_hidden_fields = []; +jQuery( function ( $ ) { + // Legacy mode recalculates all conditions after each relevant field change. + $( '.ppom-wrapper' ).on( + 'change', + 'select,input:radio,input:checkbox', + function ( e ) { + ppom_check_conditions(); + } + ); + + $( document ).on( 'ppom_field_shown', function ( e ) { + // Remove from array + $.each( ppom_hidden_fields, function ( i, item ) { + if ( item === e.field ) { + // Set checked/selected again + ppom_set_default_option( item ); + + ppom_hidden_fields.splice( i, 1 ); + $.event.trigger( { + type: 'ppom_hidden_fields_updated', + field: e.field, + time: new Date(), + } ); + } + } ); + + // Apply FileAPI to DOM + const field_meta = ppom_get_field_meta_by_id( e.field ); + if ( field_meta.type === 'file' || field_meta.type === 'cropper' ) { + ppom_setup_file_upload_input( field_meta ); + } + + // Price Matrix + if ( field_meta.type == 'pricematrix' ) { + // Resettin + $( '.ppom_pricematrix' ).removeClass( 'active' ); + + // Set Active + const classname = '.' + field_meta.data_name; + // console.log(classname); + $( classname ).find( '.ppom_pricematrix' ).addClass( 'active' ); + } + + //Imageselect (Image dropdown) + if ( field_meta.type === 'imageselect' ) { + const dd_selector = 'ppom_imageselect_' + field_meta.data_name; + const ddData = $( '#' + dd_selector ).data( 'ddslick' ); + const image_replace = $( '#' + dd_selector ).attr( + 'data-enable-rpimg' + ); + ppom_create_hidden_input( ddData ); + ppom_update_option_prices(); + setTimeout( function () { + ppom_image_selection( ddData, image_replace ); + }, 100 ); + // $('#'+dd_selector).ddslick('select', {index: 0 }); + } + + // Multiple Select Addon + if ( field_meta.type === 'multiple_select' ) { + const selector = jQuery( + 'select[name="ppom[fields][' + field_meta.data_name + '][]"]' + ); + const selected_value = selector.val(); + const default_value = field_meta.selected; + + if ( selected_value === null && default_value ) { + const selected_opt_arr = default_value.split( ',' ); + + selector.val( selected_opt_arr ).trigger( 'change' ); + + const selected_options = selector.find( 'option:selected' ); + jQuery.each( + selected_options, + function ( index, default_selected ) { + const option_id = + jQuery( default_selected ).attr( 'data-option_id' ); + const option_label = + jQuery( default_selected ).attr( + 'data-optionlabel' + ); + const option_price = + jQuery( default_selected ).attr( + 'data-optionprice' + ); + + ppom_multiple_select_create_hidden_input( + field_meta.data_name, + option_id, + option_price, + option_label, + field_meta.title + ); + } + ); + } + } + } ); + + $( document ).on( 'ppom_hidden_fields_updated', function ( e ) { + $( '#conditionally_hidden' ).val( ppom_hidden_fields ); + ppom_update_option_prices(); + } ); + + $( document ).on( 'ppom_field_hidden', function ( e ) { + const element_type = ppom_get_field_type_by_id( e.field ); + switch ( element_type ) { + case 'select': + $( 'select[name="ppom[fields][' + e.field + ']"]' ).val( '' ); + break; + + case 'multiple_select': + var selector = $( + 'select[name="ppom[fields][' + e.field + '][]"]' + ); + var selected_value = selector.val(); + var selected_options = selector.find( 'option:selected' ); + + jQuery.each( + selected_options, + function ( index, default_selected ) { + const option_id = + jQuery( default_selected ).attr( 'data-option_id' ); + const the_id = + 'ppom-multipleselect-' + e.field + '-' + option_id; + + $( '#' + the_id ).remove(); + } + ); + + if ( selected_value ) { + $( 'select[name="ppom[fields][' + e.field + '][]"]' ) + .val( null ) + .trigger( 'change' ); + } + + break; + + case 'checkbox': + $( 'input[name="ppom[fields][' + e.field + '][]"]' ).prop( + 'checked', + false + ); + break; + case 'radio': + $( 'input[name="ppom[fields][' + e.field + ']"]' ).prop( + 'checked', + false + ); + break; + + case 'file': + $( '#filelist-' + e.field ) + .find( '.u_i_c_box' ) + .remove(); + break; + + case 'palettes': + case 'image': + $( 'input[name="ppom[fields][' + e.field + '][]"]' ).prop( + 'checked', + false + ); + break; + + case 'imageselect': + var the_id = 'ppom-imageselect' + e.field; + $( '#' + the_id ).remove(); + break; + + default: + // Reset text/textarea/date/email etc types + $( '#' + e.field ).val( '' ); + break; + } + + ppom_hidden_fields.push( e.field ); + $.event.trigger( { + type: 'ppom_hidden_fields_updated', + field: e.field, + time: new Date(), + } ); + } ); + + setTimeout( function () { + ppom_check_conditions(); + }, 500 ); +} ); + +function ppom_set_default_option( field_id ) { + // get product id + const product_id = ppom_input_vars.product_id; + + const field = ppom_get_field_meta_by_id( field_id ); + switch ( field.type ) { + // Check if field is + case 'radio': + jQuery.each( field.options, function ( label, options ) { + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + + if ( options.option == field.selected ) { + jQuery( '#' + opt_id ).prop( 'checked', true ); + } + } ); + + break; + + case 'select': + jQuery( '#' + field.data_name ).val( field.selected ); + break; + + case 'image': + jQuery.each( field.images, function ( index, img ) { + if ( img.title == field.selected ) { + jQuery( '#' + field.data_name + '-' + img.id ).prop( + 'checked', + true + ); + } + } ); + break; + + case 'checkbox': + jQuery.each( field.options, function ( label, options ) { + const opt_id = + product_id + '-' + field.data_name + '-' + options.id; + + const default_checked = field.checked.split( '\r\n' ); + if ( jQuery.inArray( options.option, default_checked ) > -1 ) { + jQuery( '#' + opt_id ).prop( 'checked', true ); + } + } ); + break; + + case 'text': + case 'date': + case 'number': + jQuery( '#' + field.data_name ).val( field.default_value ); + break; + } } +// Evaluate every configured field condition and toggle the target wrappers. function ppom_check_conditions() { - - jQuery.each(ppom_input_vars.conditions, function(field, condition) { - - - // It will return rules array with True or False - ppom_field_matched_rules[field] = ppom_get_field_rule_status(condition); - - // get length of condition - var obj_length = Object.keys(condition.rules).length; - - // Now check if all rules are valid - if (condition.bound === 'Any' && ppom_field_matched_rules[field] > 0) { - ppom_unlock_field_from_condition(field, condition.visibility); - } - else if (condition.bound === 'All' && ppom_field_matched_rules[field] == obj_length) { - ppom_unlock_field_from_condition(field, condition.visibility); - } - else { - ppom_lock_field_from_condition(field, condition.visibility); - } - - }); + jQuery.each( ppom_input_vars.conditions, function ( field, condition ) { + // It will return rules array with True or False + ppom_field_matched_rules[ field ] = + ppom_get_field_rule_status( condition ); + + // get length of condition + const obj_length = Object.keys( condition.rules ).length; + + // Now check if all rules are valid + if ( + condition.bound === 'Any' && + ppom_field_matched_rules[ field ] > 0 + ) { + ppom_unlock_field_from_condition( field, condition.visibility ); + } else if ( + condition.bound === 'All' && + ppom_field_matched_rules[ field ] == obj_length + ) { + ppom_unlock_field_from_condition( field, condition.visibility ); + } else { + ppom_lock_field_from_condition( field, condition.visibility ); + } + } ); } -function ppom_unlock_field_from_condition(field, unlock) { - - var classname = '.ppom-input-' + field; - if (unlock === 'Show') { - jQuery(classname).show().removeClass('ppom-locked').addClass('ppom-unlocked') - .trigger({ - type: "ppom_field_shown", - field: field, - time: new Date() - }); - } - else { - jQuery(classname).hide().removeClass('ppom-locked').addClass('ppom-unlocked') - .trigger({ - type: "ppom_field_hidden", - field: field, - time: new Date() - }); - } +// Showing/hiding a field also needs to broadcast the same lifecycle events +// used by uploads, pricing, and validation to keep their state consistent. +function ppom_unlock_field_from_condition( field, unlock ) { + const classname = '.ppom-input-' + field; + if ( unlock === 'Show' ) { + jQuery( classname ) + .show() + .removeClass( 'ppom-locked' ) + .addClass( 'ppom-unlocked' ) + .trigger( { + type: 'ppom_field_shown', + field, + time: new Date(), + } ); + } else { + jQuery( classname ) + .hide() + .removeClass( 'ppom-locked' ) + .addClass( 'ppom-unlocked' ) + .trigger( { + type: 'ppom_field_hidden', + field, + time: new Date(), + } ); + } } -function ppom_lock_field_from_condition(field, lock) { - - var classname = '.ppom-input-' + field; - if (lock === 'Show') { - jQuery(classname).hide().removeClass('ppom-unlocked').addClass('ppom-locked') - .trigger({ - type: "ppom_field_hidden", - field: field, - time: new Date() - }); - } - else { - jQuery(classname).show().removeClass('ppom-unlocked').addClass('ppom-locked') - .trigger({ - type: "ppom_field_shown", - field: field, - time: new Date() - }); - } - - jQuery.event.trigger({ - type: "ppom_field_locked", - field: field, - lock: lock, - time: new Date() - }); +function ppom_lock_field_from_condition( field, lock ) { + const classname = '.ppom-input-' + field; + if ( lock === 'Show' ) { + jQuery( classname ) + .hide() + .removeClass( 'ppom-unlocked' ) + .addClass( 'ppom-locked' ) + .trigger( { + type: 'ppom_field_hidden', + field, + time: new Date(), + } ); + } else { + jQuery( classname ) + .show() + .removeClass( 'ppom-unlocked' ) + .addClass( 'ppom-locked' ) + .trigger( { + type: 'ppom_field_shown', + field, + time: new Date(), + } ); + } + + jQuery.event.trigger( { + type: 'ppom_field_locked', + field, + lock, + time: new Date(), + } ); } -// It will return rules array with True or False -function ppom_get_field_rule_status(condition) { - - var ppom_rules_matched = 0; - jQuery.each(condition.rules, function(i, rule) { - - var element_type = ppom_get_field_type_by_id(rule.elements); - - // console.log(element_type); - switch (rule.operators) { - case 'is': - if (element_type === 'checkbox') { - var element_value = ppom_get_element_value(rule.elements); - jQuery(element_value).each(function(i, item) { - if (item === rule.element_values) { - ppom_rules_matched++; - } - }); - } - else if (element_type === 'image') { - var element_value = ppom_get_element_value(rule.elements); - jQuery(element_value).each(function(i, item) { - if (item === rule.element_values) { - ppom_rules_matched++; - } - }); - } - else if (ppom_get_element_value(rule.elements) === rule.element_values) { - ppom_rules_matched++; - } - break; - - case 'not': - if (element_type === 'checkbox') { - var element_value = ppom_get_element_value(rule.elements); - jQuery(element_value).each(function(i, item) { - if (item !== rule.element_values) { - ppom_rules_matched++; - } - }); - } - else if (ppom_get_element_value(rule.elements) !== rule.element_values) { - ppom_rules_matched++; - } - break; - - case 'greater than': - if (element_type === 'checkbox') { - var element_value = ppom_get_element_value(rule.elements); - jQuery(element_value).each(function(i, item) { - if (parseFloat(item) > parseFloat(rule.element_values)) { - ppom_rules_matched++; - } - }); - } - else if (parseFloat(ppom_get_element_value(rule.elements)) > parseFloat(rule.element_values)) { - ppom_rules_matched++; - } - break; - - case 'less than': - if (element_type === 'checkbox') { - var element_value = ppom_get_element_value(rule.elements); - jQuery(element_value).each(function(i, item) { - if (parseFloat(item) < parseFloat(rule.element_values)) { - ppom_rules_matched++; - } - }); - } - else if (parseFloat(ppom_get_element_value(rule.elements)) < parseFloat(rule.element_values)) { - ppom_rules_matched++; - } - break; - - - } - }); - - return ppom_rules_matched; +// Count how many rules match for a target field in the legacy condition format. +function ppom_get_field_rule_status( condition ) { + let ppom_rules_matched = 0; + jQuery.each( condition.rules, function ( i, rule ) { + const element_type = ppom_get_field_type_by_id( rule.elements ); + + // console.log(element_type); + switch ( rule.operators ) { + case 'is': + if ( element_type === 'checkbox' ) { + var element_value = ppom_get_element_value( rule.elements ); + jQuery( element_value ).each( function ( i, item ) { + if ( item === rule.element_values ) { + ppom_rules_matched++; + } + } ); + } else if ( element_type === 'image' ) { + var element_value = ppom_get_element_value( rule.elements ); + jQuery( element_value ).each( function ( i, item ) { + if ( item === rule.element_values ) { + ppom_rules_matched++; + } + } ); + } else if ( + ppom_get_element_value( rule.elements ) === + rule.element_values + ) { + ppom_rules_matched++; + } + break; + + case 'not': + if ( element_type === 'checkbox' ) { + var element_value = ppom_get_element_value( rule.elements ); + jQuery( element_value ).each( function ( i, item ) { + if ( item !== rule.element_values ) { + ppom_rules_matched++; + } + } ); + } else if ( + ppom_get_element_value( rule.elements ) !== + rule.element_values + ) { + ppom_rules_matched++; + } + break; + + case 'greater than': + if ( element_type === 'checkbox' ) { + var element_value = ppom_get_element_value( rule.elements ); + jQuery( element_value ).each( function ( i, item ) { + if ( + parseFloat( item ) > + parseFloat( rule.element_values ) + ) { + ppom_rules_matched++; + } + } ); + } else if ( + parseFloat( ppom_get_element_value( rule.elements ) ) > + parseFloat( rule.element_values ) + ) { + ppom_rules_matched++; + } + break; + + case 'less than': + if ( element_type === 'checkbox' ) { + var element_value = ppom_get_element_value( rule.elements ); + jQuery( element_value ).each( function ( i, item ) { + if ( + parseFloat( item ) < + parseFloat( rule.element_values ) + ) { + ppom_rules_matched++; + } + } ); + } else if ( + parseFloat( ppom_get_element_value( rule.elements ) ) < + parseFloat( rule.element_values ) + ) { + ppom_rules_matched++; + } + break; + } + } ); + + return ppom_rules_matched; } -// Getting rule element value -function ppom_get_element_value(field_name) { - - var element_type = ppom_get_field_type_by_id(field_name); - var value_found = ''; - var value_found_cb = []; - - switch (element_type) { - - case 'select': - value_found = jQuery('select[name="ppom[fields][' + field_name + ']"]').val(); - break; - - case 'radio': - value_found = jQuery('input[name="ppom[fields][' + field_name + ']"]:checked').val(); - break; - - case 'checkbox': - jQuery('input[name="ppom[fields][' + field_name + '][]"]:checked').each(function(i) { - value_found_cb[i] = jQuery(this).val(); - }); - break; - - case 'image': - // value_found = jQuery('input[name="ppom[fields]['+field_name+'][]"]:checked').attr('data-label'); - jQuery('input[name="ppom[fields][' + field_name + '][]"]:checked').each(function(i) { - value_found_cb[i] = jQuery(this).attr('data-label'); - }); - break; - - case 'imageselect': - value_found = jQuery('input[name="ppom[fields][' + field_name + ']"]:checked').attr('data-title'); - break; - - } - - if (element_type === 'checkbox' || element_type === 'image') { - return value_found_cb; - } - - return value_found; +// Normalize legacy field values before comparing them against rule operators. +function ppom_get_element_value( field_name ) { + const element_type = ppom_get_field_type_by_id( field_name ); + let value_found = ''; + const value_found_cb = []; + + switch ( element_type ) { + case 'select': + value_found = jQuery( + 'select[name="ppom[fields][' + field_name + ']"]' + ).val(); + break; + + case 'radio': + value_found = jQuery( + 'input[name="ppom[fields][' + field_name + ']"]:checked' + ).val(); + break; + + case 'checkbox': + jQuery( + 'input[name="ppom[fields][' + field_name + '][]"]:checked' + ).each( function ( i ) { + value_found_cb[ i ] = jQuery( this ).val(); + } ); + break; + + case 'image': + // value_found = jQuery('input[name="ppom[fields]['+field_name+'][]"]:checked').attr('data-label'); + jQuery( + 'input[name="ppom[fields][' + field_name + '][]"]:checked' + ).each( function ( i ) { + value_found_cb[ i ] = jQuery( this ).attr( 'data-label' ); + } ); + break; + + case 'imageselect': + value_found = jQuery( + 'input[name="ppom[fields][' + field_name + ']"]:checked' + ).attr( 'data-title' ); + break; + } + + if ( element_type === 'checkbox' || element_type === 'image' ) { + return value_found_cb; + } + + return value_found; } diff --git a/js/ppom-modal.js b/js/ppom-modal.js index 8fd17c67..0a723785 100644 --- a/js/ppom-modal.js +++ b/js/ppom-modal.js @@ -7,787 +7,890 @@ * Under MIT License */ -!(function(root, factory) { - if (typeof define === 'function' && define.amd) { - define(['jquery'], function($) { - return factory(root, $); - }); - } - else if (typeof exports === 'object') { - factory(root, require('jquery')); - } - else { - factory(root, root.jQuery || root.Zepto); - } -})(this, function(global, $) { - - 'use strict'; - - /** - * Name of the plugin - * @private - * @const - * @type {String} - */ - var PLUGIN_NAME = 'ppom_modal'; - - /** - * Namespace for CSS and events - * @private - * @const - * @type {String} - */ - var NAMESPACE = global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.NAMESPACE || PLUGIN_NAME; - - /** - * Animationstart event with vendor prefixes - * @private - * @const - * @type {String} - */ - var ANIMATIONSTART_EVENTS = $.map( - ['animationstart', 'webkitAnimationStart', 'MSAnimationStart', 'oAnimationStart'], - - function(eventName) { - return eventName + '.' + NAMESPACE; - } - - ).join(' '); - - /** - * Animationend event with vendor prefixes - * @private - * @const - * @type {String} - */ - var ANIMATIONEND_EVENTS = $.map( - ['animationend', 'webkitAnimationEnd', 'MSAnimationEnd', 'oAnimationEnd'], - - function(eventName) { - return eventName + '.' + NAMESPACE; - } - - ).join(' '); - - /** - * Default settings - * @private - * @const - * @type {Object} - */ - var DEFAULTS = $.extend({ - hashTracking: true, - closeOnConfirm: true, - closeOnCancel: true, - closeOnEscape: true, - closeOnOutsideClick: true, - modifier: '', - appendTo: '' - }, global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.DEFAULTS); - - /** - * States of the Remodal - * @private - * @const - * @enum {String} - */ - var STATES = { - CLOSING: 'closing', - CLOSED: 'closed', - OPENING: 'opening', - OPENED: 'opened' - }; - - /** - * Reasons of the state change. - * @private - * @const - * @enum {String} - */ - var STATE_CHANGE_REASONS = { - CONFIRMATION: 'confirmation', - CANCELLATION: 'cancellation' - }; - - /** - * Is animation supported? - * @private - * @const - * @type {Boolean} - */ - var IS_ANIMATION = (function() { - var style = document.createElement('div').style; - - return style.animationName !== undefined || - style.WebkitAnimationName !== undefined || - style.MozAnimationName !== undefined || - style.msAnimationName !== undefined || - style.OAnimationName !== undefined; - })(); - - /** - * Is iOS? - * @private - * @const - * @type {Boolean} - */ - var IS_IOS = /iPad|iPhone|iPod/.test(navigator.platform); - - /** - * Current modal - * @private - * @type {Remodal} - */ - var current; - - /** - * Scrollbar position - * @private - * @type {Number} - */ - var scrollTop; - - /** - * Returns an animation duration - * @private - * @param {jQuery} $elem - * @returns {Number} - */ - function getAnimationDuration($elem) { - if ( - IS_ANIMATION && - $elem.css('animation-name') === 'none' && - $elem.css('-webkit-animation-name') === 'none' && - $elem.css('-moz-animation-name') === 'none' && - $elem.css('-o-animation-name') === 'none' && - $elem.css('-ms-animation-name') === 'none' - ) { - return 0; - } - - var duration = $elem.css('animation-duration') || - $elem.css('-webkit-animation-duration') || - $elem.css('-moz-animation-duration') || - $elem.css('-o-animation-duration') || - $elem.css('-ms-animation-duration') || - '0s'; - - var delay = $elem.css('animation-delay') || - $elem.css('-webkit-animation-delay') || - $elem.css('-moz-animation-delay') || - $elem.css('-o-animation-delay') || - $elem.css('-ms-animation-delay') || - '0s'; - - var iterationCount = $elem.css('animation-iteration-count') || - $elem.css('-webkit-animation-iteration-count') || - $elem.css('-moz-animation-iteration-count') || - $elem.css('-o-animation-iteration-count') || - $elem.css('-ms-animation-iteration-count') || - '1'; - - var max; - var len; - var num; - var i; - - duration = duration.split(', '); - delay = delay.split(', '); - iterationCount = iterationCount.split(', '); - - // The 'duration' size is the same as the 'delay' size - for (i = 0, len = duration.length, max = Number.NEGATIVE_INFINITY; i < len; i++) { - num = parseFloat(duration[i]) * parseInt(iterationCount[i], 10) + parseFloat(delay[i]); - - if (num > max) { - max = num; - } - } - - return max; - } - - /** - * Returns a scrollbar width - * @private - * @returns {Number} - */ - function getScrollbarWidth() { - if ($(document).height() <= $(window).height()) { - return 0; - } - - var outer = document.createElement('div'); - var inner = document.createElement('div'); - var widthNoScroll; - var widthWithScroll; - - outer.style.visibility = 'hidden'; - outer.style.width = '100px'; - document.body.appendChild(outer); - - widthNoScroll = outer.offsetWidth; - - // Force scrollbars - outer.style.overflow = 'scroll'; - - // Add inner div - inner.style.width = '100%'; - outer.appendChild(inner); - - widthWithScroll = inner.offsetWidth; - - // Remove divs - outer.parentNode.removeChild(outer); - - return widthNoScroll - widthWithScroll; - } - - /** - * Locks the screen - * @private - */ - function lockScreen() { - if (IS_IOS) { - return; - } - - var $html = $('html'); - var lockedClass = namespacify('is-locked'); - var paddingRight; - var $body; - - if (!$html.hasClass(lockedClass)) { - $body = $(document.body); - - // Zepto does not support '-=', '+=' in the `css` method - paddingRight = parseInt($body.css('padding-right'), 10) + getScrollbarWidth(); - - $body.css('padding-right', paddingRight + 'px'); - $html.addClass(lockedClass); - } - } - - /** - * Unlocks the screen - * @private - */ - function unlockScreen() { - if (IS_IOS) { - return; - } - - var $html = $('html'); - var lockedClass = namespacify('is-locked'); - var paddingRight; - var $body; - - if ($html.hasClass(lockedClass)) { - $body = $(document.body); - - // Zepto does not support '-=', '+=' in the `css` method - paddingRight = parseInt($body.css('padding-right'), 10) - getScrollbarWidth(); - - $body.css('padding-right', paddingRight + 'px'); - $html.removeClass(lockedClass); - } - } - - /** - * Sets a state for an instance - * @private - * @param {Remodal} instance - * @param {STATES} state - * @param {Boolean} isSilent If true, Remodal does not trigger events - * @param {String} Reason of a state change. - */ - function setState(instance, state, isSilent, reason) { - - var newState = namespacify('is', state); - var allStates = [namespacify('is', STATES.CLOSING), - namespacify('is', STATES.OPENING), - namespacify('is', STATES.CLOSED), - namespacify('is', STATES.OPENED) - ].join(' '); - - instance.$bg - .removeClass(allStates) - .addClass(newState); - - instance.$overlay - .removeClass(allStates) - .addClass(newState); - - instance.$wrapper - .removeClass(allStates) - .addClass(newState); - - instance.$modal - .removeClass(allStates) - .addClass(newState); - - instance.state = state; - !isSilent && instance.$modal.trigger({ - type: state, - reason: reason - }, [{ reason: reason }]); - } - - /** - * Synchronizes with the animation - * @param {Function} doBeforeAnimation - * @param {Function} doAfterAnimation - * @param {Remodal} instance - */ - function syncWithAnimation(doBeforeAnimation, doAfterAnimation, instance) { - var runningAnimationsCount = 0; - - var handleAnimationStart = function(e) { - if (e.target !== this) { - return; - } - - runningAnimationsCount++; - }; - - var handleAnimationEnd = function(e) { - if (e.target !== this) { - return; - } - - if (--runningAnimationsCount === 0) { - - // Remove event listeners - $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { - instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); - }); - - doAfterAnimation(); - } - }; - - $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { - instance[elemName] - .on(ANIMATIONSTART_EVENTS, handleAnimationStart) - .on(ANIMATIONEND_EVENTS, handleAnimationEnd); - }); - - doBeforeAnimation(); - - // If the animation is not supported by a browser or its duration is 0 - if ( - getAnimationDuration(instance.$bg) === 0 && - getAnimationDuration(instance.$overlay) === 0 && - getAnimationDuration(instance.$wrapper) === 0 && - getAnimationDuration(instance.$modal) === 0 - ) { - - // Remove event listeners - $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { - instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); - }); - - doAfterAnimation(); - } - } - - /** - * Closes immediately - * @private - * @param {Remodal} instance - */ - function halt(instance) { - if (instance.state === STATES.CLOSED) { - return; - } - - $.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) { - instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS); - }); - - instance.$bg.removeClass(instance.settings.modifier); - instance.$overlay.removeClass(instance.settings.modifier).hide(); - instance.$wrapper.hide(); - unlockScreen(); - setState(instance, STATES.CLOSED, true); - } - - /** - * Parses a string with options - * @private - * @param str - * @returns {Object} - */ - function parseOptions(str) { - var obj = {}; - var arr; - var len; - var val; - var i; - - // Remove spaces before and after delimiters - str = str.replace(/\s*:\s*/g, ':').replace(/\s*,\s*/g, ','); - - // Parse a string - arr = str.split(','); - for (i = 0, len = arr.length; i < len; i++) { - arr[i] = arr[i].split(':'); - val = arr[i][1]; - - // Convert a string value if it is like a boolean - if (typeof val === 'string' || val instanceof String) { - val = val === 'true' || (val === 'false' ? false : val); - } - - // Convert a string value if it is like a number - if (typeof val === 'string' || val instanceof String) { - val = !isNaN(val) ? +val : val; - } - - obj[arr[i][0]] = val; - } - - return obj; - } - - /** - * Generates a string separated by dashes and prefixed with NAMESPACE - * @private - * @param {...String} - * @returns {String} - */ - function namespacify() { - var result = NAMESPACE; - - for (var i = 0; i < arguments.length; ++i) { - result += '-' + arguments[i]; - } - - return result; - } - - /** - * Handles the hashchange event - * @private - * @listens hashchange - */ - function handleHashChangeEvent() { - var id = location.hash.replace('#', ''); - var instance; - var $elem; - - if (!id) { - - // Check if we have currently opened modal and animation was completed - if (current && current.state === STATES.OPENED && current.settings.hashTracking) { - current.close(); - } - } - else { - - // Catch syntax error if your hash is bad - try { - $elem = $( - '[data-' + PLUGIN_NAME + '-id="' + id + '"]' - ); - } - catch (err) {} - - if ($elem && $elem.length) { - instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)]; - - if (instance && instance.settings.hashTracking) { - instance.open(); - } - } - - } - } - - /** - * Remodal constructor - * @constructor - * @param {jQuery} $modal - * @param {Object} options - */ - function Remodal($modal, options) { - // var $body = $(document.body); - var $body = $modal.parent(); - var $appendTo = $body; - var remodal = this; - - remodal.settings = $.extend({}, DEFAULTS, options); - remodal.index = $[PLUGIN_NAME].lookup.push(remodal) - 1; - remodal.state = STATES.CLOSED; - - remodal.$overlay = $('.' + namespacify('overlay')); - - if (remodal.settings.appendTo !== null && remodal.settings.appendTo.length) { - $appendTo = $(remodal.settings.appendTo); - } - - if (!remodal.$overlay.length) { - remodal.$overlay = $('

    ').addClass(namespacify('overlay') + ' ' + namespacify('is', STATES.CLOSED)).hide(); - $appendTo.append(remodal.$overlay); - } - - remodal.$bg = $('.' + namespacify('bg')).addClass(namespacify('is', STATES.CLOSED)); - - remodal.$modal = $modal - .addClass( - NAMESPACE + ' ' + - namespacify('is-initialized') + ' ' + - remodal.settings.modifier + ' ' + - namespacify('is', STATES.CLOSED)) - .attr('tabindex', '-1'); - - remodal.$wrapper = $('
    ') - .addClass( - namespacify('wrapper') + ' ' + - remodal.settings.modifier + ' ' + - namespacify('is', STATES.CLOSED)) - .hide() - .append(remodal.$modal); - $appendTo.append(remodal.$wrapper); - - // Add the event listener for the close button - remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="close"]', function(e) { - e.preventDefault(); - - remodal.close(); - }); - - // Add the event listener for the cancel button - remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="cancel"]', function(e) { - e.preventDefault(); - - remodal.$modal.trigger(STATE_CHANGE_REASONS.CANCELLATION); - - if (remodal.settings.closeOnCancel) { - remodal.close(STATE_CHANGE_REASONS.CANCELLATION); - } - }); - - // Add the event listener for the confirm button - remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="confirm"]', function(e) { - e.preventDefault(); - - remodal.$modal.trigger(STATE_CHANGE_REASONS.CONFIRMATION); - - if (remodal.settings.closeOnConfirm) { - remodal.close(STATE_CHANGE_REASONS.CONFIRMATION); - } - }); - - // Add the event listener for the overlay - remodal.$wrapper.on('click.' + NAMESPACE, function(e) { - var $target = $(e.target); - - if (!$target.hasClass(namespacify('wrapper'))) { - return; - } - - if (remodal.settings.closeOnOutsideClick) { - remodal.close(); - } - }); - } - - /** - * Opens a modal window - * @public - */ - Remodal.prototype.open = function() { - var remodal = this; - var id; - - // Check if the animation was completed - if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING) { - return; - } - - id = remodal.$modal.attr('data-' + PLUGIN_NAME + '-id'); - - if (id && remodal.settings.hashTracking) { - scrollTop = $(window).scrollTop(); - location.hash = id; - } - - if (current && current !== remodal) { - halt(current); - } - - current = remodal; - lockScreen(); - remodal.$bg.addClass(remodal.settings.modifier); - remodal.$overlay.addClass(remodal.settings.modifier).show(); - remodal.$wrapper.show().scrollTop(0); - remodal.$modal.focus(); - - syncWithAnimation( - function() { - setState(remodal, STATES.OPENING); - }, - - function() { - setState(remodal, STATES.OPENED); - }, - - remodal); - }; - - /** - * Closes a modal window - * @public - * @param {String} reason - */ - Remodal.prototype.close = function(reason) { - var remodal = this; - - // Check if the animation was completed - if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING || remodal.state === STATES.CLOSED) { - return; - } - - if ( - remodal.settings.hashTracking && - remodal.$modal.attr('data-' + PLUGIN_NAME + '-id') === location.hash.substr(1) - ) { - location.hash = ''; - $(window).scrollTop(scrollTop); - } - - syncWithAnimation( - function() { - setState(remodal, STATES.CLOSING, false, reason); - }, - - function() { - remodal.$bg.removeClass(remodal.settings.modifier); - remodal.$overlay.removeClass(remodal.settings.modifier).hide(); - remodal.$wrapper.hide(); - unlockScreen(); - - setState(remodal, STATES.CLOSED, false, reason); - }, - - remodal); - }; - - /** - * Returns a current state of a modal - * @public - * @returns {STATES} - */ - Remodal.prototype.getState = function() { - return this.state; - }; - - /** - * Destroys a modal - * @public - */ - Remodal.prototype.destroy = function() { - var lookup = $[PLUGIN_NAME].lookup; - var instanceCount; - - halt(this); - this.$wrapper.remove(); - - delete lookup[this.index]; - instanceCount = $.grep(lookup, function(instance) { - return !!instance; - }).length; - - if (instanceCount === 0) { - this.$overlay.remove(); - this.$bg.removeClass( - namespacify('is', STATES.CLOSING) + ' ' + - namespacify('is', STATES.OPENING) + ' ' + - namespacify('is', STATES.CLOSED) + ' ' + - namespacify('is', STATES.OPENED)); - } - }; - - /** - * Special plugin object for instances - * @public - * @type {Object} - */ - $[PLUGIN_NAME] = { - lookup: [] - }; - - /** - * Plugin constructor - * @constructor - * @param {Object} options - * @returns {JQuery} - */ - $.fn[PLUGIN_NAME] = function(opts) { - var instance; - var $elem; - - this.each(function(index, elem) { - $elem = $(elem); - - if ($elem.data(PLUGIN_NAME) == null) { - instance = new Remodal($elem, opts); - $elem.data(PLUGIN_NAME, instance.index); - - if ( - instance.settings.hashTracking && - $elem.attr('data-' + PLUGIN_NAME + '-id') === location.hash.substr(1) - ) { - instance.open(); - } - } - else { - instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)]; - } - }); - - return instance; - }; - - $(document).ready(function() { - - // data-remodal-target opens a modal window with the special Id - $(document).on('click', '[data-' + PLUGIN_NAME + '-target]', function(e) { - e.preventDefault(); - - var elem = e.currentTarget; - var id = elem.getAttribute('data-' + PLUGIN_NAME + '-target'); - var $target = $('[data-' + PLUGIN_NAME + '-id="' + id + '"]'); - - $[PLUGIN_NAME].lookup[$target.data(PLUGIN_NAME)].open(); - }); - - // Auto initialization of modal windows - // They should have the 'remodal' class attribute - // Also you can write the `data-remodal-options` attribute to pass params into the modal - $(document).find('.' + NAMESPACE).each(function(i, container) { - var $container = $(container); - var options = $container.data(PLUGIN_NAME + '-options'); - - if (!options) { - options = {}; - } - else if (typeof options === 'string' || options instanceof String) { - options = parseOptions(options); - } - - $container[PLUGIN_NAME](options); - }); - - // Handles the keydown event - $(document).on('keydown.' + NAMESPACE, function(e) { - if (current && current.settings.closeOnEscape && current.state === STATES.OPENED && e.keyCode === 27) { - current.close(); - } - }); - - // Handles the hashchange event - $(window).on('hashchange.' + NAMESPACE, handleHashChangeEvent); - }); -}); +! ( function ( root, factory ) { + if ( typeof define === 'function' && define.amd ) { + define( [ 'jquery' ], function ( $ ) { + return factory( root, $ ); + } ); + } else if ( typeof exports === 'object' ) { + factory( root, require( 'jquery' ) ); + } else { + factory( root, root.jQuery || root.Zepto ); + } +} )( this, function ( global, $ ) { + 'use strict'; + + /** + * Name of the plugin + * @private + * @constant + * @type {string} + */ + const PLUGIN_NAME = 'ppom_modal'; + + /** + * Namespace for CSS and events + * @private + * @constant + * @type {string} + */ + const NAMESPACE = + ( global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.NAMESPACE ) || + PLUGIN_NAME; + + /** + * Animationstart event with vendor prefixes + * @private + * @constant + * @type {string} + */ + const ANIMATIONSTART_EVENTS = $.map( + [ + 'animationstart', + 'webkitAnimationStart', + 'MSAnimationStart', + 'oAnimationStart', + ], + + function ( eventName ) { + return eventName + '.' + NAMESPACE; + } + ).join( ' ' ); + + /** + * Animationend event with vendor prefixes + * @private + * @constant + * @type {string} + */ + const ANIMATIONEND_EVENTS = $.map( + [ + 'animationend', + 'webkitAnimationEnd', + 'MSAnimationEnd', + 'oAnimationEnd', + ], + + function ( eventName ) { + return eventName + '.' + NAMESPACE; + } + ).join( ' ' ); + + /** + * Default settings + * @private + * @constant + * @type {Object} + */ + const DEFAULTS = $.extend( + { + hashTracking: true, + closeOnConfirm: true, + closeOnCancel: true, + closeOnEscape: true, + closeOnOutsideClick: true, + modifier: '', + appendTo: '', + }, + global.REMODAL_GLOBALS && global.REMODAL_GLOBALS.DEFAULTS + ); + + /** + * States of the Remodal + * @private + * @constant + * @enum {string} + */ + const STATES = { + CLOSING: 'closing', + CLOSED: 'closed', + OPENING: 'opening', + OPENED: 'opened', + }; + + /** + * Reasons of the state change. + * @private + * @constant + * @enum {string} + */ + const STATE_CHANGE_REASONS = { + CONFIRMATION: 'confirmation', + CANCELLATION: 'cancellation', + }; + + /** + * Is animation supported? + * @private + * @constant + * @type {boolean} + */ + const IS_ANIMATION = ( function () { + const style = document.createElement( 'div' ).style; + + return ( + style.animationName !== undefined || + style.WebkitAnimationName !== undefined || + style.MozAnimationName !== undefined || + style.msAnimationName !== undefined || + style.OAnimationName !== undefined + ); + } )(); + + /** + * Is iOS? + * @private + * @constant + * @type {boolean} + */ + const IS_IOS = /iPad|iPhone|iPod/.test( navigator.platform ); + + /** + * Current modal + * @private + * @type {Remodal} + */ + let current; + + /** + * Scrollbar position + * @private + * @type {number} + */ + let scrollTop; + + /** + * Returns an animation duration + * @private + * @param {jQuery} $elem + * @return {number} + */ + function getAnimationDuration( $elem ) { + if ( + IS_ANIMATION && + $elem.css( 'animation-name' ) === 'none' && + $elem.css( '-webkit-animation-name' ) === 'none' && + $elem.css( '-moz-animation-name' ) === 'none' && + $elem.css( '-o-animation-name' ) === 'none' && + $elem.css( '-ms-animation-name' ) === 'none' + ) { + return 0; + } + + let duration = + $elem.css( 'animation-duration' ) || + $elem.css( '-webkit-animation-duration' ) || + $elem.css( '-moz-animation-duration' ) || + $elem.css( '-o-animation-duration' ) || + $elem.css( '-ms-animation-duration' ) || + '0s'; + + let delay = + $elem.css( 'animation-delay' ) || + $elem.css( '-webkit-animation-delay' ) || + $elem.css( '-moz-animation-delay' ) || + $elem.css( '-o-animation-delay' ) || + $elem.css( '-ms-animation-delay' ) || + '0s'; + + let iterationCount = + $elem.css( 'animation-iteration-count' ) || + $elem.css( '-webkit-animation-iteration-count' ) || + $elem.css( '-moz-animation-iteration-count' ) || + $elem.css( '-o-animation-iteration-count' ) || + $elem.css( '-ms-animation-iteration-count' ) || + '1'; + + let max; + let len; + let num; + let i; + + duration = duration.split( ', ' ); + delay = delay.split( ', ' ); + iterationCount = iterationCount.split( ', ' ); + + // The 'duration' size is the same as the 'delay' size + for ( + i = 0, len = duration.length, max = Number.NEGATIVE_INFINITY; + i < len; + i++ + ) { + num = + parseFloat( duration[ i ] ) * + parseInt( iterationCount[ i ], 10 ) + + parseFloat( delay[ i ] ); + + if ( num > max ) { + max = num; + } + } + + return max; + } + + /** + * Returns a scrollbar width + * @private + * @return {number} + */ + function getScrollbarWidth() { + if ( $( document ).height() <= $( window ).height() ) { + return 0; + } + + const outer = document.createElement( 'div' ); + const inner = document.createElement( 'div' ); + let widthNoScroll; + let widthWithScroll; + + outer.style.visibility = 'hidden'; + outer.style.width = '100px'; + document.body.appendChild( outer ); + + widthNoScroll = outer.offsetWidth; + + // Force scrollbars + outer.style.overflow = 'scroll'; + + // Add inner div + inner.style.width = '100%'; + outer.appendChild( inner ); + + widthWithScroll = inner.offsetWidth; + + // Remove divs + outer.parentNode.removeChild( outer ); + + return widthNoScroll - widthWithScroll; + } + + /** + * Locks the screen + * @private + */ + function lockScreen() { + if ( IS_IOS ) { + return; + } + + const $html = $( 'html' ); + const lockedClass = namespacify( 'is-locked' ); + let paddingRight; + let $body; + + if ( ! $html.hasClass( lockedClass ) ) { + $body = $( document.body ); + + // Zepto does not support '-=', '+=' in the `css` method + paddingRight = + parseInt( $body.css( 'padding-right' ), 10 ) + + getScrollbarWidth(); + + $body.css( 'padding-right', paddingRight + 'px' ); + $html.addClass( lockedClass ); + } + } + + /** + * Unlocks the screen + * @private + */ + function unlockScreen() { + if ( IS_IOS ) { + return; + } + + const $html = $( 'html' ); + const lockedClass = namespacify( 'is-locked' ); + let paddingRight; + let $body; + + if ( $html.hasClass( lockedClass ) ) { + $body = $( document.body ); + + // Zepto does not support '-=', '+=' in the `css` method + paddingRight = + parseInt( $body.css( 'padding-right' ), 10 ) - + getScrollbarWidth(); + + $body.css( 'padding-right', paddingRight + 'px' ); + $html.removeClass( lockedClass ); + } + } + + /** + * Sets a state for an instance + * @private + * @param {Remodal} instance + * @param {STATES} state + * @param {boolean} isSilent If true, Remodal does not trigger events + * @param reason + * @param {string} Reason of a state change. + */ + function setState( instance, state, isSilent, reason ) { + const newState = namespacify( 'is', state ); + const allStates = [ + namespacify( 'is', STATES.CLOSING ), + namespacify( 'is', STATES.OPENING ), + namespacify( 'is', STATES.CLOSED ), + namespacify( 'is', STATES.OPENED ), + ].join( ' ' ); + + instance.$bg.removeClass( allStates ).addClass( newState ); + + instance.$overlay.removeClass( allStates ).addClass( newState ); + + instance.$wrapper.removeClass( allStates ).addClass( newState ); + + instance.$modal.removeClass( allStates ).addClass( newState ); + + instance.state = state; + ! isSilent && + instance.$modal.trigger( + { + type: state, + reason, + }, + [ { reason } ] + ); + } + + /** + * Synchronizes with the animation + * @param {Function} doBeforeAnimation + * @param {Function} doAfterAnimation + * @param {Remodal} instance + */ + function syncWithAnimation( + doBeforeAnimation, + doAfterAnimation, + instance + ) { + let runningAnimationsCount = 0; + + const handleAnimationStart = function ( e ) { + if ( e.target !== this ) { + return; + } + + runningAnimationsCount++; + }; + + const handleAnimationEnd = function ( e ) { + if ( e.target !== this ) { + return; + } + + if ( --runningAnimationsCount === 0 ) { + // Remove event listeners + $.each( + [ '$bg', '$overlay', '$wrapper', '$modal' ], + function ( index, elemName ) { + instance[ elemName ].off( + ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS + ); + } + ); + + doAfterAnimation(); + } + }; + + $.each( + [ '$bg', '$overlay', '$wrapper', '$modal' ], + function ( index, elemName ) { + instance[ elemName ] + .on( ANIMATIONSTART_EVENTS, handleAnimationStart ) + .on( ANIMATIONEND_EVENTS, handleAnimationEnd ); + } + ); + + doBeforeAnimation(); + + // If the animation is not supported by a browser or its duration is 0 + if ( + getAnimationDuration( instance.$bg ) === 0 && + getAnimationDuration( instance.$overlay ) === 0 && + getAnimationDuration( instance.$wrapper ) === 0 && + getAnimationDuration( instance.$modal ) === 0 + ) { + // Remove event listeners + $.each( + [ '$bg', '$overlay', '$wrapper', '$modal' ], + function ( index, elemName ) { + instance[ elemName ].off( + ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS + ); + } + ); + + doAfterAnimation(); + } + } + + /** + * Closes immediately + * @private + * @param {Remodal} instance + */ + function halt( instance ) { + if ( instance.state === STATES.CLOSED ) { + return; + } + + $.each( + [ '$bg', '$overlay', '$wrapper', '$modal' ], + function ( index, elemName ) { + instance[ elemName ].off( + ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS + ); + } + ); + + instance.$bg.removeClass( instance.settings.modifier ); + instance.$overlay.removeClass( instance.settings.modifier ).hide(); + instance.$wrapper.hide(); + unlockScreen(); + setState( instance, STATES.CLOSED, true ); + } + + /** + * Parses a string with options + * @private + * @param str + * @return {Object} + */ + function parseOptions( str ) { + const obj = {}; + let arr; + let len; + let val; + let i; + + // Remove spaces before and after delimiters + str = str.replace( /\s*:\s*/g, ':' ).replace( /\s*,\s*/g, ',' ); + + // Parse a string + arr = str.split( ',' ); + for ( i = 0, len = arr.length; i < len; i++ ) { + arr[ i ] = arr[ i ].split( ':' ); + val = arr[ i ][ 1 ]; + + // Convert a string value if it is like a boolean + if ( typeof val === 'string' || val instanceof String ) { + val = val === 'true' || ( val === 'false' ? false : val ); + } + + // Convert a string value if it is like a number + if ( typeof val === 'string' || val instanceof String ) { + val = ! isNaN( val ) ? +val : val; + } + + obj[ arr[ i ][ 0 ] ] = val; + } + + return obj; + } + + /** + * Generates a string separated by dashes and prefixed with NAMESPACE + * @private + * @param {...string} + * @return {string} + */ + function namespacify() { + let result = NAMESPACE; + + for ( let i = 0; i < arguments.length; ++i ) { + result += '-' + arguments[ i ]; + } + + return result; + } + + /** + * Handles the hashchange event + * @private + * @listens hashchange + */ + function handleHashChangeEvent() { + const id = location.hash.replace( '#', '' ); + let instance; + let $elem; + + if ( ! id ) { + // Check if we have currently opened modal and animation was completed + if ( + current && + current.state === STATES.OPENED && + current.settings.hashTracking + ) { + current.close(); + } + } else { + // Catch syntax error if your hash is bad + try { + $elem = $( '[data-' + PLUGIN_NAME + '-id="' + id + '"]' ); + } catch ( err ) {} + + if ( $elem && $elem.length ) { + instance = $[ PLUGIN_NAME ].lookup[ $elem.data( PLUGIN_NAME ) ]; + + if ( instance && instance.settings.hashTracking ) { + instance.open(); + } + } + } + } + + /** + * Remodal constructor + * @class + * @param {jQuery} $modal + * @param {Object} options + */ + function Remodal( $modal, options ) { + // var $body = $(document.body); + const $body = $modal.parent(); + let $appendTo = $body; + const remodal = this; + + remodal.settings = $.extend( {}, DEFAULTS, options ); + remodal.index = $[ PLUGIN_NAME ].lookup.push( remodal ) - 1; + remodal.state = STATES.CLOSED; + + remodal.$overlay = $( '.' + namespacify( 'overlay' ) ); + + if ( + remodal.settings.appendTo !== null && + remodal.settings.appendTo.length + ) { + $appendTo = $( remodal.settings.appendTo ); + } + + if ( ! remodal.$overlay.length ) { + remodal.$overlay = $( '
    ' ) + .addClass( + namespacify( 'overlay' ) + + ' ' + + namespacify( 'is', STATES.CLOSED ) + ) + .hide(); + $appendTo.append( remodal.$overlay ); + } + + remodal.$bg = $( '.' + namespacify( 'bg' ) ).addClass( + namespacify( 'is', STATES.CLOSED ) + ); + + remodal.$modal = $modal + .addClass( + NAMESPACE + + ' ' + + namespacify( 'is-initialized' ) + + ' ' + + remodal.settings.modifier + + ' ' + + namespacify( 'is', STATES.CLOSED ) + ) + .attr( 'tabindex', '-1' ); + + remodal.$wrapper = $( '
    ' ) + .addClass( + namespacify( 'wrapper' ) + + ' ' + + remodal.settings.modifier + + ' ' + + namespacify( 'is', STATES.CLOSED ) + ) + .hide() + .append( remodal.$modal ); + $appendTo.append( remodal.$wrapper ); + + // Add the event listener for the close button + remodal.$wrapper.on( + 'click.' + NAMESPACE, + '[data-' + PLUGIN_NAME + '-action="close"]', + function ( e ) { + e.preventDefault(); + + remodal.close(); + } + ); + + // Add the event listener for the cancel button + remodal.$wrapper.on( + 'click.' + NAMESPACE, + '[data-' + PLUGIN_NAME + '-action="cancel"]', + function ( e ) { + e.preventDefault(); + + remodal.$modal.trigger( STATE_CHANGE_REASONS.CANCELLATION ); + + if ( remodal.settings.closeOnCancel ) { + remodal.close( STATE_CHANGE_REASONS.CANCELLATION ); + } + } + ); + + // Add the event listener for the confirm button + remodal.$wrapper.on( + 'click.' + NAMESPACE, + '[data-' + PLUGIN_NAME + '-action="confirm"]', + function ( e ) { + e.preventDefault(); + + remodal.$modal.trigger( STATE_CHANGE_REASONS.CONFIRMATION ); + + if ( remodal.settings.closeOnConfirm ) { + remodal.close( STATE_CHANGE_REASONS.CONFIRMATION ); + } + } + ); + + // Add the event listener for the overlay + remodal.$wrapper.on( 'click.' + NAMESPACE, function ( e ) { + const $target = $( e.target ); + + if ( ! $target.hasClass( namespacify( 'wrapper' ) ) ) { + return; + } + + if ( remodal.settings.closeOnOutsideClick ) { + remodal.close(); + } + } ); + } + + /** + * Opens a modal window + * @public + */ + Remodal.prototype.open = function () { + const remodal = this; + let id; + + // Check if the animation was completed + if ( + remodal.state === STATES.OPENING || + remodal.state === STATES.CLOSING + ) { + return; + } + + id = remodal.$modal.attr( 'data-' + PLUGIN_NAME + '-id' ); + + if ( id && remodal.settings.hashTracking ) { + scrollTop = $( window ).scrollTop(); + location.hash = id; + } + + if ( current && current !== remodal ) { + halt( current ); + } + + current = remodal; + lockScreen(); + remodal.$bg.addClass( remodal.settings.modifier ); + remodal.$overlay.addClass( remodal.settings.modifier ).show(); + remodal.$wrapper.show().scrollTop( 0 ); + remodal.$modal.focus(); + + syncWithAnimation( + function () { + setState( remodal, STATES.OPENING ); + }, + + function () { + setState( remodal, STATES.OPENED ); + }, + + remodal + ); + }; + + /** + * Closes a modal window + * @public + * @param {string} reason + */ + Remodal.prototype.close = function ( reason ) { + const remodal = this; + + // Check if the animation was completed + if ( + remodal.state === STATES.OPENING || + remodal.state === STATES.CLOSING || + remodal.state === STATES.CLOSED + ) { + return; + } + + if ( + remodal.settings.hashTracking && + remodal.$modal.attr( 'data-' + PLUGIN_NAME + '-id' ) === + location.hash.substr( 1 ) + ) { + location.hash = ''; + $( window ).scrollTop( scrollTop ); + } + + syncWithAnimation( + function () { + setState( remodal, STATES.CLOSING, false, reason ); + }, + + function () { + remodal.$bg.removeClass( remodal.settings.modifier ); + remodal.$overlay + .removeClass( remodal.settings.modifier ) + .hide(); + remodal.$wrapper.hide(); + unlockScreen(); + + setState( remodal, STATES.CLOSED, false, reason ); + }, + + remodal + ); + }; + + /** + * Returns a current state of a modal + * @public + * @return {STATES} + */ + Remodal.prototype.getState = function () { + return this.state; + }; + + /** + * Destroys a modal + * @public + */ + Remodal.prototype.destroy = function () { + const lookup = $[ PLUGIN_NAME ].lookup; + let instanceCount; + + halt( this ); + this.$wrapper.remove(); + + delete lookup[ this.index ]; + instanceCount = $.grep( lookup, function ( instance ) { + return !! instance; + } ).length; + + if ( instanceCount === 0 ) { + this.$overlay.remove(); + this.$bg.removeClass( + namespacify( 'is', STATES.CLOSING ) + + ' ' + + namespacify( 'is', STATES.OPENING ) + + ' ' + + namespacify( 'is', STATES.CLOSED ) + + ' ' + + namespacify( 'is', STATES.OPENED ) + ); + } + }; + + /** + * Special plugin object for instances + * @public + * @type {Object} + */ + $[ PLUGIN_NAME ] = { + lookup: [], + }; + + /** + * Plugin constructor + * @class + * @param opts + * @param {Object} options + * @return {JQuery} + */ + $.fn[ PLUGIN_NAME ] = function ( opts ) { + let instance; + let $elem; + + this.each( function ( index, elem ) { + $elem = $( elem ); + + if ( $elem.data( PLUGIN_NAME ) == null ) { + instance = new Remodal( $elem, opts ); + $elem.data( PLUGIN_NAME, instance.index ); + + if ( + instance.settings.hashTracking && + $elem.attr( 'data-' + PLUGIN_NAME + '-id' ) === + location.hash.substr( 1 ) + ) { + instance.open(); + } + } else { + instance = $[ PLUGIN_NAME ].lookup[ $elem.data( PLUGIN_NAME ) ]; + } + } ); + + return instance; + }; + + $( document ).ready( function () { + // data-remodal-target opens a modal window with the special Id + $( document ).on( + 'click', + '[data-' + PLUGIN_NAME + '-target]', + function ( e ) { + e.preventDefault(); + + const elem = e.currentTarget; + const id = elem.getAttribute( + 'data-' + PLUGIN_NAME + '-target' + ); + const $target = $( + '[data-' + PLUGIN_NAME + '-id="' + id + '"]' + ); + + $[ PLUGIN_NAME ].lookup[ $target.data( PLUGIN_NAME ) ].open(); + } + ); + + // Auto initialization of modal windows + // They should have the 'remodal' class attribute + // Also you can write the `data-remodal-options` attribute to pass params into the modal + $( document ) + .find( '.' + NAMESPACE ) + .each( function ( i, container ) { + const $container = $( container ); + let options = $container.data( PLUGIN_NAME + '-options' ); + + if ( ! options ) { + options = {}; + } else if ( + typeof options === 'string' || + options instanceof String + ) { + options = parseOptions( options ); + } + + $container[ PLUGIN_NAME ]( options ); + } ); + + // Handles the keydown event + $( document ).on( 'keydown.' + NAMESPACE, function ( e ) { + if ( + current && + current.settings.closeOnEscape && + current.state === STATES.OPENED && + e.keyCode === 27 + ) { + current.close(); + } + } ); + + // Handles the hashchange event + $( window ).on( 'hashchange.' + NAMESPACE, handleHashChangeEvent ); + } ); +} ); diff --git a/js/ppom-plusminus.js b/js/ppom-plusminus.js index 12be5864..b632c92c 100644 --- a/js/ppom-plusminus.js +++ b/js/ppom-plusminus.js @@ -1,156 +1,163 @@ -(function ($) { - $.fn.niceNumber = function(options) { - var settings = $.extend({ - autoSize: true, - autoSizeBuffer: 1, - buttonDecrement: '-', - buttonIncrement: "+", - buttonPosition: 'around' - }, options); +( function ( $ ) { + $.fn.niceNumber = function ( options ) { + const settings = $.extend( + { + autoSize: true, + autoSizeBuffer: 1, + buttonDecrement: '-', + buttonIncrement: '+', + buttonPosition: 'around', + }, + options + ); - return this.each(function(){ - var currentInput = this, - $currentInput = $(currentInput), - attrMax = null, - attrMin = null; + return this.each( function () { + let currentInput = this, + $currentInput = $( currentInput ), + attrMax = null, + attrMin = null; - // Handle max and min values - if ( - typeof $currentInput.attr('max') !== typeof undefined - && $currentInput.attr('max') !== false - ) { - attrMax = parseFloat($currentInput.attr('max')); - } + // Handle max and min values + if ( + typeof $currentInput.attr( 'max' ) !== typeof undefined && + $currentInput.attr( 'max' ) !== false + ) { + attrMax = parseFloat( $currentInput.attr( 'max' ) ); + } - if ( - typeof $currentInput.attr('min') !== typeof undefined - && $currentInput.attr('min') !== false - ) { - attrMin = parseFloat($currentInput.attr('min')); - } + if ( + typeof $currentInput.attr( 'min' ) !== typeof undefined && + $currentInput.attr( 'min' ) !== false + ) { + attrMin = parseFloat( $currentInput.attr( 'min' ) ); + } - // if (isNaN(attrMax)) { - // attrMax = 44; - // console.log(attrMax); - // } + // if (isNaN(attrMax)) { + // attrMax = 44; + // console.log(attrMax); + // } - // if (isNaN(attrMin)) { - // attrMin = 0; - // console.log(attrMin); - // } + // if (isNaN(attrMin)) { + // attrMin = 0; + // console.log(attrMin); + // } - // Fix issue with initial value being < min - //console.log(currentInput.value); - if ( - attrMin - && !currentInput.value - ) { - $currentInput.val(attrMin); - } + // Fix issue with initial value being < min + //console.log(currentInput.value); + if ( attrMin && ! currentInput.value ) { + $currentInput.val( attrMin ); + } - // Generate container - var $inputContainer = $('
    ',{ - class: 'ppom-number-plusminus' - }) - .insertAfter(currentInput); + // Generate container + const $inputContainer = $( '
    ', { + class: 'ppom-number-plusminus', + } ).insertAfter( currentInput ); - // Generate interval (object so it is passed by reference) - var interval = {}; + // Generate interval (object so it is passed by reference) + const interval = {}; - // Generate buttons - var $minusButton = $('