diff --git a/.distignore b/.distignore index 73db87ff..9e3a99ca 100644 --- a/.distignore +++ b/.distignore @@ -26,4 +26,8 @@ phpstan-baseline.neon **/*.css.map .DS_Store .gitkeep -.wordpress-org \ No newline at end of file +.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/create-build-zip.yml b/.github/workflows/create-build-zip.yml index c94d80b9..311a39da 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')" @@ -66,6 +66,8 @@ jobs: pr_number: ${{ steps.get-pr-number.outputs.num }} comment_body: ${{ steps.get-comment-body.outputs.body }} steps: + - name: Check out source files + uses: actions/checkout@v6 - name: Get PR number id: get-pr-number run: echo "::set-output name=num::$(echo $GITHUB_REF | awk 'BEGIN { FS = "/" } ; { print $3 }')" @@ -80,8 +82,19 @@ jobs: - name: Get comment body id: get-comment-body + env: + GITHUB_REPO_NAME: ${{ github.event.pull_request.base.repo.name }} + DEV_ZIP_BRANCH_NAME: ${{ needs.dev-zip.outputs.branch-name }} + DEV_ZIP_GIT_SHA_8: ${{ needs.dev-zip.outputs.git-sha-8 }} + GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - body="Plugin build for ${{ github.event.pull_request.head.sha }} is ready :bellhop_bell:! + previewLink=$(node ./bin/generatePlaygroundPreviewLink.js) + + body=" + > [!TIP] + > You can preview the changes in the Playground + + Plugin build for ${{ github.event.pull_request.head.sha }} is ready :bellhop_bell:! - Download PPOM - [build πŸ”—](https://verti-artifacts.s3.amazonaws.com/${{ github.event.pull_request.base.repo.name }}-${{ needs.dev-zip.outputs.branch-name }}-${{ needs.dev-zip.outputs.git-sha-8 }}/woocommerce-product-addon.zip)" body="${body//$'\n'/'%0A'}" echo "::set-output name=body::$body" 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..d98e4d11 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,7 +1,17 @@ { - "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", + "." + ], + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "WP_DEBUG_DISPLAY": false, + "SCRIPT_DEBUG": true + }, + "mappings": { + "wp-content/mu-plugins": "./bin/wp-env/mu-plugins" + }, + "phpVersion": "8.1" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..b202233d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,174 @@ +# Agent Workflow + +## Project Overview + +PPOM (Personalized Product Option Manager) for WooCommerce β€” a WordPress plugin that lets store owners add custom product fields (text, date, file upload, color, image cropper, etc.) to WooCommerce product pages. Has a free version and a separate Pro add-on (`PPOM_PRO_PATH`). + +## Build & Development Commands + +```bash +# Install dependencies +composer install +npm install + +# Full build (i18n pot + grunt addtextdomain) +npm run build + +# Distribution zip +npm run dist +``` + +## Testing + +```bash +# PHPUnit β€” requires Docker (wp-env) +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) that runs on docker. +npm run test:e2e +npm run test:e2e:debug # Opens Playwright UI + +# Static analysis +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 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 + +| 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 + +Each input type has a class in `classes/inputs/` (e.g. `input.text.php`, `input.select.php`) and a corresponding frontend template in `templates/frontend/inputs/`. There are 23+ input types. Template paths are filterable via `ppom_input_templates_path`. + +### Include Files (`inc/`) + +Procedural utility code organized by concern: + +- `functions.php` β€” general helpers +- `hooks.php` β€” filter/action callbacks +- `validation.php` β€” server-side field validation +- `woocommerce.php` β€” WooCommerce integration hooks (cart, checkout, orders) +- `prices.php` β€” price calculation logic +- `files.php` β€” file upload handling +- `src/Rest/` β€” REST API (`Routes` facade, `ProductMetaController`, `OrderMetaController`, services) at `/wp-json/ppom/v1/` + +### Two Runtime Modes + +1. **Legacy mode** (`ppom_is_legacy_mode()`) β€” older rendering, cart fee-based pricing +2. **Modern mode** (default) β€” new template system, direct price modification + +Conditional logic also has two versions: old (`ppom-conditions.js`) and new (`ppom-conditions-v2.js`, enabled via `ppom_get_conditions_mode() === 'new'`). + +### Database + +Custom table `{prefix}_nm_personalized` (constant `PPOM_TABLE_META`). Field groups are linked to products via the `_product_meta_id` post meta key. + +### Key Constants + +```php +PPOM_PATH // Plugin directory path +PPOM_URL // Plugin URL +PPOM_VERSION // Current version string +PPOM_TABLE_META // 'nm_personalized' (custom DB table name, without prefix) +PPOM_PRODUCT_META_KEY // '_product_meta_id' +PPOM_UPLOAD_DIR_NAME // 'ppom_files' +``` + +### Frontend Assets + +JavaScript and CSS live in `js/` and `css/` (not compiled/bundled β€” loaded directly). Enqueuing handled by `classes/frontend-scripts.class.php`. Includes vendored libs: Bootstrap, Select2, Croppie, CodeMirror. + +### Pro Feature Gating + +Free vs Pro determined by `defined('PPOM_PRO_PATH')`. License tiers: `LICENSE_PLAN_FREE` (-1), `LICENSE_PLAN_1` (Essential), `LICENSE_PLAN_2` (Plus), `LICENSE_PLAN_3` (VIP). Freemium UI handled by `classes/freemium.class.php` and Themeisle SDK. + +### WooCommerce Hook Flow + +Product page β†’ `woocommerce_before_add_to_cart_button` (render fields) β†’ `woocommerce_add_to_cart_validation` (validate) β†’ `woocommerce_add_cart_item_data` (save to cart) β†’ `woocommerce_get_cart_item_from_session` (restore from session) β†’ `woocommerce_checkout_create_order_line_item` (persist to order). + +### HPOS Compatibility + +Declares WooCommerce Custom Order Tables compatibility via `FeaturesUtil::declare_compatibility('custom_order_tables', ...)`. + +## WooCommerce Security + Workflow + +### 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`. +- 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..57ae33f0 --- /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`](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/`](inc/) +- loads the class files under [`classes/`](classes/) and the settings framework under [`backend/`](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`](woocommerce-product-addon.php) | Defines constants, loads the plugin, registers top-level hooks | +| Main runtime | [`classes/plugin.class.php`](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`](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`](classes/form.class.php) | Renders modern template-based product fields and hidden runtime state | +| Input registry | [`classes/input.class.php`](classes/input.class.php) | Loads input-type classes and add-on input classes | +| Admin field UI | [`classes/fields.class.php`](classes/fields.class.php) | Powers the field-group builder UI and admin-side assets | +| Admin coordinator | [`classes/admin.class.php`](classes/admin.class.php) | Registers PPOM admin menus, settings integration, attach flows, and admin initialization hooks | +| Frontend asset loader | [`classes/frontend-scripts.class.php`](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`](classes/scripts.class.php) | Shared wrapper for registering, enqueuing, localizing, and inlining PPOM frontend assets | +| WooCommerce flow functions | [`inc/woocommerce.php`](inc/woocommerce.php) | Product-page rendering, validation, cart item payloads, order item metadata, file finalization | +| Pricing engine | [`inc/prices.php`](inc/prices.php) | Server-side option pricing, matrix pricing, cart fee calculation, line-item price updates | +| Upload subsystem | [`inc/files.php`](inc/files.php) | AJAX upload and delete handlers, thumbnails, cropped files, confirmed-file storage, cleanup cron | +| Admin CRUD | [`inc/admin.php`](inc/admin.php) | Field-group create/update/delete handlers and product-attachment UI | +| REST API | [`src/Rest/`](src/Rest/) (`Routes`, controllers, services) | 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/`](classes/inputs/) +- frontend templates in [`templates/frontend/inputs/`](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`](templates/render-fields.php) +- modern mode via `ppom_woocommerce_inputs_template_base()`, `PPOM_Form`, and [`templates/frontend/ppom-fields.php`](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`](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\Routes` (legacy alias `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/assets/settings.css b/backend/assets/settings.css index b3537e2f..cc82b419 100644 --- a/backend/assets/settings.css +++ b/backend/assets/settings.css @@ -436,7 +436,6 @@ div.nmsf-panels-area .nmsf-panels-content p { .nmsf-panel-table td, .nmsf-panel-table th { - vertical-align: middle; padding-left: 0px; } @@ -646,4 +645,418 @@ div.nmsf-panels-area .nmsf-panels-content p { .ppom-is-locked-field input, .ppom-is-locked-field select, .ppom-is-locked-panel select{ cursor: not-allowed; - pointer-events: none; } \ No newline at end of file + pointer-events: none; } + +h3.ppom-section-heading { + display: flex; + gap: 8px; + padding: 12px 16px; + margin: 0; + font-size: 0.95em; + color: #1d2327; +} + +.ppom-status-badge { + display: inline-flex; + align-items: center; + font-size: 0.72em; + font-weight: 600; + padding: 2px 9px; + border-radius: 12px; + margin-left: 8px; + vertical-align: middle; + line-height: 1.6; + white-space: nowrap; +} + +.ppom-badge-active { + background: #e6f9ed; + color: #1a7a40; + border: 1px solid #b4e8c5; +} + +.ppom-badge-inactive { + background: #f2f2f2; + color: #888; + border: 1px solid #d0d0d0; +} + +.ppom-section-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: #6576ff; + color: #fff; + font-size: 15px; + flex-shrink: 0; + margin-right: 4px; +} + +.ppom-rest-url-block { + display: flex; + align-items: center; + gap: 8px; + margin: 10px 0 6px; + flex-wrap: wrap; +} + +.ppom-rest-url-input { + flex: 1; + min-width: 200px; + background: #f0f0f1 !important; + border: 1px solid #ddd !important; + color: #3c434a !important; + font-family: monospace; + font-size: 0.85em !important; + cursor: default; +} + +.ppom-api-guide-link { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 12px; + color: #6576ff; + text-decoration: none; + font-weight: 500; +} + +.ppom-api-guide-link:hover { + color: #4a5be0; + text-decoration: underline; +} + +.ppom-endpoint-group { + border-top: 1px solid #f0f0f1; +} + +.ppom-endpoint-group-label { + display: block; + font-weight: 700; + color: #8c8f94; + margin-bottom: 4px; +} + +.ppom-endpoint-row { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; + flex-wrap: wrap; + justify-content: space-between; +} + +.ppom-method-badge { + display: inline-block; + font-size: 0.75em; + font-weight: 700; + padding: 1px 6px; + border-radius: 3px; + min-width: 36px; + text-align: center; + flex-shrink: 0; +} + +.ppom-method-get { + background: #e6f9ed; + color: #1a7a40; + border: 1px solid #b4e8c5; +} + +.ppom-method-post { + background: #fff3e0; + color: #b85d00; + border: 1px solid #ffd699; +} + +.ppom-endpoint-path { + background: transparent; + border: none; + padding: 0; + font-size: 12px; + color: #3c434a; + font-family: monospace; +} + +.ppom-endpoint-desc { + color: #514b4b; + font-size: 12px; + font-weight: 500; +} + +.ppom-endpoint-auth { + font-size: 13px !important; + width: 13px !important; + height: 13px !important; + color: #8c8f94; + flex-shrink: 0; +} + +.ppom-password-field-wrap { + display: flex; + align-items: center; + gap: 4px; +} + +.ppom-password-field-wrap input[type="password"], +.ppom-password-field-wrap input[type="text"] { + flex: 1; + min-width: 0; +} + +.ppom-eye-toggle .dashicons, +.ppom-copy-btn .dashicons { + font-size: 16px; + width: 16px; + height: 16px; + vertical-align: middle; +} + +.ppom-copy-btn.ppom-copied { + color: #1a7a40; +} + +.ppom-toggle { + display: inline-flex; + align-items: center; + gap: 10px; + cursor: pointer; + user-select: none; + line-height: 1; +} + +.ppom-toggle > input[type="checkbox"] { + position: absolute; + width: 1px; + height: 1px; + opacity: 0; + margin: 0; + pointer-events: none; +} + +.ppom-toggle-track { + position: relative; + display: inline-block; + width: 40px; + height: 22px; + border-radius: 11px; + background: #c0ccda; + transition: background 0.2s ease; + flex-shrink: 0; +} + +.ppom-toggle-track::before { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + top: 3px; + left: 3px; + transition: transform 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18); +} + +.ppom-toggle > input:checked + .ppom-toggle-track { + background: #6576ff; +} + +.ppom-toggle > input:checked + .ppom-toggle-track::before { + transform: translateX(18px); +} + +.ppom-toggle > input:disabled + .ppom-toggle-track { + opacity: 0.5; + cursor: not-allowed; +} + +.ppom-toggle > input:focus-visible + .ppom-toggle-track { + outline: 2px solid #6576ff; + outline-offset: 2px; +} + +.ppom-toggle-label { + font-size: 13px; + color: #3c4d62; + font-weight: 400; +} + +.ppom-is-locked-panel .ppom-toggle, +.ppom-is-locked-field .ppom-toggle { + pointer-events: none; +} + +.nmsf-panel-table.form-table { + border-collapse: separate; + border-spacing: 0; +} + +/* Spacer row between cards */ +.ppom-card-spacer-row > td { + height: 16px; + padding: 0 !important; + border: none !important; + background: transparent !important; +} + +/* Card header row */ +.ppom-card-header-row > td.nmsf-section-type { + border: 1px solid #e2e4e7; + border-bottom: 1px solid #e2e4e7; + border-radius: 6px 6px 0 0; + padding: 0 !important; + background: #fff; +} + +/* Card with no body rows β€” full border radius */ +.ppom-card-header-row.ppom-card-no-body > td.nmsf-section-type { + border-bottom: 1px solid #e2e4e7; + border-radius: 6px; +} + +/* Card body rows β€” left / right border only */ +.ppom-card-body-row > th { + border-left: 1px solid #e2e4e7; + background: #fff; + padding: 10px 10px 10px 16px !important; +} +.ppom-card-body-row > td { + border-right: 1px solid #e2e4e7; + background: #fff; + padding: 10px 16px 10px 10px !important; +} + +.ppom-available-endpoints-row > th { + border-left: 1px solid #e2e4e7; + border-bottom: 1px solid #e2e4e7; + border-radius: 0 0 0 6px; + background: #fff; + padding: 10px 10px 10px 16px !important; +} +.ppom-available-endpoints-row > td { + border-right: 1px solid #e2e4e7; + border-bottom: 1px solid #e2e4e7; + border-radius: 0 0 6px 0; + background: #fff; + padding: 10px 16px 10px 10px !important; +} + +.ppom-card-title-row { + display: flex; + flex-direction: column; + width: 100%; +} + +.ppom-card-title { + display: flex; + justify-content: space-between; + width: 100%; +} + +.ppom-card-title-text { + font-weight: 600; + font-size: 1em; + flex: 1; +} + +.ppom-card-desc { + margin: 4px 0 0 36px; + font-size: 0.82em; + color: #6c757d; + font-weight: 400; +} + +/* Collapse toggle button */ +.ppom-card-toggle { + background: transparent; + border: none; + cursor: pointer; + padding: 3px; + color: #8c8f94; + line-height: 1; + border-radius: 4px; + margin-left: auto; + flex-shrink: 0; +} + +.ppom-card-toggle:hover { + background: #f0f0f1; + color: #1d2327; +} + +.ppom-card-toggle .dashicons { + font-size: 18px; + width: 18px; + height: 18px; + transition: transform 0.2s ease; + display: block; +} + +.ppom-card-toggle[aria-expanded="false"] .dashicons { + transform: rotate(180deg); +} + +.ppom-endpoints-panel { + border: 1px solid #e2e4e7; + border-radius: 6px; + overflow: hidden; + background: #fff; + font-size: 12px; +} + +.ppom-endpoints-panel-header { + display: flex; + justify-content: flex-end; + padding: 6px 10px; + border-bottom: 1px solid #f0f0f1; + background: #fafafa; +} + +.ppom-endpoint-group { + border-top: 1px solid #f0f0f1; + padding: 4px 10px 6px; +} + +.ppom-endpoint-group:first-of-type { + border-top: none; +} + +.ppom-endpoint-group-label { + display: block; + font-weight: 700; + font-size: 10px; + letter-spacing: 0.05em; + color: #3e3e3e; + text-transform: uppercase; + margin-bottom: 2px; + padding: 4px 0 2px; +} + +.ppom-endpoint-row { + display: flex; + align-items: center; + gap: 8px; + padding: 3px 0; + flex-wrap: wrap; + justify-content: space-between; +} + +.ppom-endpoint-left { + display: flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.ppom-endpoint-right { + display: flex; + align-items: center; + gap: 4px; + color: #6c757d; + font-size: 11px; + flex-shrink: 0; +} diff --git a/backend/assets/settings.js b/backend/assets/settings.js index 1e9f41bf..626daf36 100644 --- a/backend/assets/settings.js +++ b/backend/assets/settings.js @@ -152,4 +152,135 @@ jQuery(function($) { } }); }); + + /** + * Toggle password visibility. + */ + $(document).on('click', '.ppom-eye-toggle', function () { + var targetId = $(this).data('target'); + var $input = $('#' + targetId); + var reveal = $input.attr('type') === 'password'; + $input.attr('type', reveal ? 'text' : 'password'); + $(this).find('.dashicons') + .toggleClass('dashicons-visibility', !reveal) + .toggleClass('dashicons-hidden', reveal); + }); + + /** + * Copy to clipboard and show feedback. + * @param {object} $btn + */ + function ppomCopyFeedback($btn) { + var $icon = $btn.find('.dashicons'); + if ($icon.length) { + $icon.removeClass('dashicons-clipboard').addClass('dashicons-yes'); + $btn.addClass('ppom-copied'); + setTimeout(function () { + $icon.removeClass('dashicons-yes').addClass('dashicons-clipboard'); + $btn.removeClass('ppom-copied'); + }, 2000); + } else { + var orig = $btn.text(); + $btn.addClass('ppom-copied').text(nmsf_vars.copied); + setTimeout(function () { $btn.removeClass('ppom-copied').text(orig); }, 2000); + } + } + + /** + * Execute copy command for the given text. + * + * @param {string} text + * @returns {boolean} + */ + function ppomExecCopy(text) { + var $tmp = $(''; - } - - $html .= ''; // form-group - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - - } - - /** - * Select options - * - * $options: array($key => $value) - **/ - public function Select( $args, $selected_value = '' ) { - - $product = isset( $args['product_id'] ) ? wc_get_product( $args['product_id'] ) : null; - - $type = $this->get_attribute_value( 'type', $args ); - $label = $this->get_attribute_value( 'label', $args ); - $classes = $this->get_attribute_value( 'classes', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $name = $this->get_attribute_value( 'name', $args ); - $multiple = $this->get_attribute_value( 'multiple', $args ); - $attributes = $this->get_attribute_value( 'attributes', $args ); - - // Only title without description for price calculation etc. - $title = $args['title']; - // One time fee - $onetime = $args['onetime']; - $taxable = $args['taxable']; - - // Options - $options = $this->get_attribute_value( 'options', $args ); - // ppom_pa($args); - - if ( ! $options ) { - return; - } - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - - if ( $label ) { - $html .= ''; - } - - $html .= ''; - $html .= '
'; // form-group - - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $selected_value ); - } - - /** - * Timezone - * - * $options: array($key => $value) - **/ - public function Timezone( $args, $selected_value = '' ) { - - $label = $this->get_attribute_value( 'label', $args ); - $classes = $this->get_attribute_value( 'classes', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $name = $this->get_attribute_value( 'name', $args ); - $multiple = $this->get_attribute_value( 'multiple', $args ); - $attributes = $this->get_attribute_value( 'attributes', $args ); - - // Only title withou description for price calculation etc. - $title = $args['title']; - - - // Options - $options = $this->get_attribute_value( 'options', $args ); - - - if ( ! $options ) { - return; - } - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - - if ( $label ) { - $html .= ''; - } - - $html .= ''; - $html .= '
'; // form-group - - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $selected_value ); - } - - - // Checkbox - public function Checkbox( $args, $checked_value = array() ) { - - $type = $this->get_attribute_value( 'type', $args ); - - $label = $this->get_attribute_value( 'label', $args ); - $classes = $this->get_attribute_value( 'classes', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $name = $this->get_attribute_value( 'name', $args ); - - // Only title withou description for price calculation etc. - $title = $args['title']; - // One time fee - $onetime = $args['onetime']; - $taxable = $args['taxable']; - - // Options - $options = $this->get_attribute_value( 'options', $args ); - - // Checkbox label class - $check_wrapper_class = apply_filters( 'ppom_checkbox_wrapper_class', 'form-check-inline' ); - $check_label_class = $this->get_attribute_value( 'check_label_class', $args ); - - if ( ! $options ) { - return; - } - - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - - if ( $label ) { - $html .= ''; - } - - if ( is_array( $checked_value ) ) { - $checked_value = array_map( 'trim', $checked_value ); - } - - foreach ( $options as $key => $value ) { - - $option_label = $value['label']; - $option_price = $value['price']; - $raw_label = $value['raw']; - $without_tax = $value['without_tax']; - $option_id = $value['option_id']; - $dom_id = apply_filters( 'ppom_dom_option_id', $option_id, $args ); - - $checked_option = ''; - if ( count( $checked_value ) > 0 && in_array( $key, $checked_value ) && ! empty( $key ) ) { - - $checked_option = checked( $key, $key, false ); - } - - // $option_id = sanitize_key( $id."-".$key ); - - $html .= '
'; - $html .= ''; // closing form-check - $html .= '
'; // closing form-check - } - - $html .= '
'; // form-group - - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $checked_value ); - } - - - // Radio - public function Radio( $args, $checked_value = '' ) { - - $type = $this->get_attribute_value( 'type', $args ); - - $label = $this->get_attribute_value( 'label', $args ); - $classes = $this->get_attribute_value( 'classes', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $name = $this->get_attribute_value( 'name', $args ); - - // Only title withou description for price calculation etc. - $title = $args['title']; - // One time fee - $onetime = $args['onetime']; - $taxable = $args['taxable']; - - // Options - $options = $this->get_attribute_value( 'options', $args ); - if ( ! $options ) { - return; - } - - // Radio label class - $radio_wrapper_class = apply_filters( 'ppom_radio_wrapper_class', 'form-check' ); - $radio_label_class = $this->get_attribute_value( 'radio_label_class', $args ); - - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - - if ( $label ) { - $html .= ''; - } - - foreach ( $options as $key => $value ) { - - $option_label = $value['label']; - $option_price = $value['price']; - $raw_label = $value['raw']; - $without_tax = $value['without_tax']; - $option_id = $value['option_id']; - $dom_id = apply_filters( 'ppom_dom_option_id', $option_id, $args ); - - $checked_option = ''; - if ( ! empty( $checked_value ) ) { - - $checked_value = stripcslashes( $checked_value ); - $checked_option = checked( $checked_value, $key, false ); - } - - - $html .= '
'; - $html .= ''; // closing form-check - $html .= '
'; // closing form-check - } - - $html .= '
'; // form-group - - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $checked_value ); - } - - // A custom input will be just some option html - public function Palettes( $args, $default_value = '' ) { - - - $product = isset( $args['product_id'] ) ? wc_get_product( $args['product_id'] ) : null; - - $type = $this->get_attribute_value( 'type', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $name = $this->get_attribute_value( 'name', $args ); - $label = $this->get_attribute_value( 'label', $args ); - $classes = isset( $args['classes'] ) ? $args['classes'] : ''; - - // Only title withou description for price calculation etc. - $title = $args['title']; - - // One time fee - $onetime = $args['onetime']; - $taxable = $args['taxable']; - - // ppom_pa($args); - - // Options - $options = isset( $args['options'] ) ? $args['options'] : ''; - if ( ! $options ) { - return ''; - } - - $html = ''; - - if ( ! is_array( $default_value ) ) { - - $default_value = explode( ',', $default_value ); - } - - $checked_value = array_map( 'trim', $default_value ); - - // apply for selected palette border color - $selected_palette_bcolor = isset( $args['selected_palette_bcolor'] ) ? $args['selected_palette_bcolor'] : ''; - $html .= ''; - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html .= '
'; - if ( $label ) { - $html .= ''; - } - // ppom_pa($options); - $html .= '
'; - foreach ( $options as $key => $value ) { - // First Separate color code and label - $color_label_arr = explode( '-', $key ); - $color_code = trim( $color_label_arr[0] ); - $color_label = ''; - if ( isset( $color_label_arr[1] ) ) { - $color_label = trim( $color_label_arr[1] ); - } - - $color_label = $value['label']; - $option_label = $value['label']; - $option_price = $value['price']; - $raw_label = $value['raw']; - $without_tax = $value['without_tax']; - - $option_id = $value['option_id']; - $dom_id = apply_filters( 'ppom_dom_option_id', $option_id, $args ); - - $checked_option = ''; - if ( count( $checked_value ) > 0 && in_array( $key, $checked_value ) && ! empty( $key ) ) { - - $checked_option = checked( $key, $key, false ); - } - - - $html .= '
'; // form-group - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - // return 'asd'; - } - - // A custom input will be just some option html - public function Pricematrix( $args, $default_value = '' ) { - - $id = $this->get_attribute_value( 'id', $args ); - $type = $this->get_attribute_value( 'type', $args ); - $label = $this->get_attribute_value( 'label', $args ); - $ranges = $args['ranges']; - $discount = $args['discount']; - $is_hidden = ( $args['hide_matrix'] == 'on' ) ? true : false; - - // ppom_pa($ranges); - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - if ( $label ) { - $html .= ''; - } - - // Check if price matrix table is not hidden by settings - if ( ! $is_hidden ) { - foreach ( $ranges as $opt ) { - $price = isset( $opt['raw_price'] ) ? trim( $opt['raw_price'] ) : 0; - $label = isset( $opt['label'] ) ? $opt['label'] : $opt['raw']; - - if ( ! empty( $opt['percent'] ) ) { - - $percent = $opt['percent']; - if ( $discount == 'on' ) { - $price = "-{$percent}"; - } else { - $price = "{$percent} (" . wc_price( $price ) . ')'; - } - } else { - $price = wc_price( $price ); - } - - - $range_id = isset( $opt['option_id'] ) ? $opt['option_id'] : ''; - - $html .= '
'; - $html .= '' . apply_filters( 'ppom_matrix_item_label', stripslashes( trim( $label ) ), $opt ) . ''; - $html .= '' . apply_filters( 'ppom_matrix_item_price', $price, $opt ) . ''; - $html .= '
'; - } - } - - // Showing Range Slider - if ( isset( $args['show_slider'] ) && $args['show_slider'] == 'on' ) { - - $first_range = reset( $ranges ); - $qty_ranges = explode( '-', $first_range['raw'] ); - $min_quantity = $qty_ranges[0] - 1; - - $last_range = end( $ranges ); - $qty_ranges = explode( '-', $last_range['raw'] ); - $max_quantity = $qty_ranges[1]; - $qty_step = ! empty( $args['qty_step'] ) ? $args['qty_step'] : 1; - - $html .= '
'; - - if ( apply_filters( 'ppom_range_slider_legacy', false, $args ) ) { - - $html .= ''; - } else { - $html .= ''; - } - $html .= '
'; - - } - - $html .= '
'; // form-group - - $html .= ''; - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - } - - // Variation Quantities - public function Quantities( $args, $default_value = '' ) { - - $product = isset( $args['product_id'] ) ? wc_get_product( $args['product_id'] ) : null; - // IMPORTANT - // if price matrix is used and quantities has price set or default price - // it will conflict. So to use with price matrix prices should not be set - $product_id = ppom_get_product_id( $product ); - $matrix_found = ppom_has_field_by_type( $product_id, 'pricematrix' ); - if ( ! empty( $matrix_found ) && ppom_is_field_has_price( $args ) ) { - $error_msg = __( 'Quantities cannot be used with Price Matrix, Remove prices from quantities input.', 'woocommerce-product-addon' ); - - return sprintf( '
%s
', $error_msg ); - } - - $type = $this->get_attribute_value( 'type', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $label = $this->get_attribute_value( 'label', $args ); - // ppom_pa($args); - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - if ( $label ) { - $html .= ''; - } - - - $template_vars = array( - 'args' => $args, - 'default_value' => $default_value, - ); - ob_start(); - ppom_load_template( 'input/quantities.php', $template_vars ); - $html .= ob_get_clean(); - - $html .= '
'; // form-group - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - } - - // HTML or Text (section) - public function Section( $args, $default_value = '' ) { - - $type = $this->get_attribute_value( 'type', $args ); - $name = $this->get_attribute_value( 'name', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $label = $this->get_attribute_value( 'label', $args ); - $field_html = $this->get_attribute_value( 'html', $args ); - - // var_dump($field_html); - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - - if ( $label ) { - - $field_html = $field_html . $label; - } - - $html_content = apply_filters( 'ppom_section_content', $field_html ); - - $html .= stripslashes( $html_content ); - - $html .= '
'; - - $html .= ''; - - $html .= '
'; // form-group - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - } - - // Audio/video - public function Audio_video( $args, $default_value = '' ) { - - $type = $this->get_attribute_value( 'type', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $name = $this->get_attribute_value( 'name', $args ); - $label = $this->get_attribute_value( 'label', $args ); - $classes = isset( $args['classes'] ) ? $args['classes'] : ''; - - // Only title withou description for price calculation etc. - $title = $args['title']; - - // Options - $audios = isset( $args['audios'] ) ? $args['audios'] : ''; - if ( ! $audios ) { - return __( 'audios not selected', 'woocommerce-product-addon' ); - } - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - if ( $label ) { - $html .= ''; - } - - // ppom_pa($audios); - $html .= '
'; - foreach ( $audios as $audio ) { - - - $audio_link = isset( $audio['link'] ) ? $audio['link'] : 0; - $audio_id = isset( $audio['id'] ) ? $audio['id'] : 0; - $audio_title = isset( $audio['title'] ) ? stripslashes( $audio['title'] ) : 0; - $audio_price = isset( $audio['price'] ) ? $audio['price'] : 0; - - // Actually image URL is link - $audio_url = wp_get_attachment_url( $audio_id ); - $audio_title_price = $audio_title . ' ' . ( $audio_price > 0 ? ppom_price( $audio_price ) : '' ); - - $checked_option = ''; - if ( ! empty( $default_value ) ) { - - $checked_option = checked( $default_value, $key, false ); - } - - $html .= '
'; - - if ( ! empty( $audio_url ) ) { - $html .= apply_filters( 'the_content', $audio_url ); - } - - ?> - - '; - if ( $args['multiple_allowed'] == 'on' ) { - $html .= ''; - } else { - - $html .= ''; - } - - $html .= '
' . $audio_title_price . '
'; - $html .= '
'; // input_image - - - $html .= '
'; // pre_upload_image - } - - $html .= '
'; // .ppom_upload_image_box - $html .= '
'; // form-group - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - } - - // File Upload - public function File( $args, $default_files = '' ) { - - $type = $this->get_attribute_value( 'type', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $label = $this->get_attribute_value( 'label', $args ); - - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - - $html = '
'; - if ( $label ) { - $html .= ''; - } - - - $container_height = isset( $args['dragdrop'] ) ? 'auto' : '30px'; - $html .= '
'; - $html .= ''; - $html .= $args['button_label'] . ''; - $html .= ''; - $html .= __( 'Drag File Here', 'woocommerce-product-addon' ); - $html .= ''; - $html .= '
'; // ppom-file-container - - if ( $args['dragdrop'] ) { - - $html .= '
'; - $html .= __( 'Drag file/directory here', 'woocommerce-product-addon' ); - $html .= '
'; - } - - $html .= '
'; - - - // Editing existing file - if ( ! empty( $default_files ) ) { - - // var_dump($default_files); - - foreach ( $default_files as $key => $file ) { - - $file_preview = ppom_uploaded_file_preview( $file['org'], $args ); - if ( ! isset( $file['org'] ) || $file_preview == '' ) { - continue; - } - - $html .= '
'; - - $html .= $file_preview; - - if ( $html != '' ) { - $file_name = $file['org']; - } - $data_name = 'ppom[fields][' . $args['id'] . '][' . $key . '][org]'; - $file_class = 'ppom-file-cb ppom-file-cb-' . $args['id']; - - // Adding CB for data handling - $html .= 'get_attribute_value( 'type', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $name = $this->get_attribute_value( 'name', $args ); - $label = $this->get_attribute_value( 'label', $args ); - $title = $this->get_attribute_value( 'title', $args ); - $classes = $this->get_attribute_value( 'classes', $args ); - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - - $html = '
'; - if ( $label ) { - $html .= ''; - } - - $container_height = isset( $args['dragdrop'] ) ? 'auto' : '30px'; - $html .= '
'; - $html .= ''; - $html .= $args['button_label'] . ''; - $html .= '' . __( 'Drag file/directory here', 'woocommerce-product-addon' ) . ''; - $html .= '
'; // ppom-file-container - - if ( $args['dragdrop'] ) { - - $html .= '
'; - $html .= __( 'Drag file/directory here', 'woocommerce-product-addon' ); - $html .= '
'; - } - - $html .= '
'; - - $html .= '
'; - $html .= '
'; - // ppom_pa($args); - - // @since: 12.8 - // Showing size option if more than one found. - if ( isset( $args['options'] ) && count( $args['options'] ) > 0 ) { - - $cropping_sizes = $args['options']; - - $select_css = 'width:' . $args['croppie_options']['boundary']['width'] . 'px;'; - $select_css .= 'margin:5px auto;display:none;'; - - $html .= ''; - - } - - $html .= '
'; // ppom-croppie-preview - $html .= '
'; // ppom-croppie-wrapper - - $html .= '
'; // form-group - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $selected_value ); - } - - // A custom input will be just some option html - public function Custom( $args, $default_value = '' ) { - - $type = $this->get_attribute_value( 'type', $args ); - $id = $this->get_attribute_value( 'id', $args ); - $label = $this->get_attribute_value( 'label', $args ); - - $input_wrapper_class = $this->get_default_setting_value( 'global', 'input_wrapper_class', $id ); - $input_wrapper_class = apply_filters( 'ppom_input_wrapper_class', $input_wrapper_class, $args ); - $html = '
'; - if ( $label ) { - $html .= ''; - } - - $html .= apply_filters( 'nmform_custom_input', $html, $args, $default_value ); - - $html .= '
'; // form-group - - // filter: nmforms_input_htmls - return apply_filters( 'nmforms_input_html', $html, $args, $default_value ); - } - - - /** - * this function return current or/else default attribute values - * - * filter: nmform_attribute_value - * */ - private function get_attribute_value( $attr, $args ) { - - $attr_value = ''; - $type = isset( $args['type'] ) ? $args['type'] : $this->get_default_setting_value( 'global', 'type' ); - - // if( $attr == 'type' ) return $type; - - if ( isset( $args[ $attr ] ) ) { - - $attr_value = $args[ $attr ]; - } else { - - $attr_value = $this->get_default_setting_value( $type, $attr ); - } - - return apply_filters( 'nmform_attribute_value', $attr_value, $attr, $args ); - } - - - /** - * this function return default value - * defined in class/config - * - * @params: $setting_type - * @params: $key - * filter: default_setting_value - * */ - function get_default_setting_value( $setting_type, $key, $field_id = '' ) { - - $defaults = $this->get_property( 'defaults' ); - - $default_value = isset( $defaults[ $setting_type ][ $key ] ) ? $defaults[ $setting_type ][ $key ] : ''; - - return apply_filters( 'default_setting_value', $default_value, $setting_type, $key, $field_id ); - } - - - /** - * function return class property values/settings - * - * filter: nmform_property-{$property} - * */ - private function get_property( $property ) { - - $value = ''; - switch ( $property ) { - - case 'echoable': - $value = ECHOABLE; - break; - - case 'defaults': - $value = array( - 'global' => array( - 'type' => 'text', - 'input_wrapper_class' => 'form-group', - 'label_class' => 'form-control-label', - ), - 'text' => array( - 'placeholder' => '', - 'attributes' => array(), - ), - 'date' => array(), - 'email' => array(), - 'number' => array(), - 'cropper' => array( 'classes' => array( 'ppom-cropping-size', 'form-control' ) ), - 'textarea' => array( - 'cols' => 6, - 'rows' => 3, - 'placeholder' => '', - ), - 'select' => array( 'multiple' => false ), - 'checkbox' => array( - 'label_class' => 'form-control-label', - 'check_wrapper_class' => 'form-check', - 'check_label_class' => 'form-check-label', - 'classes' => array( 'ppom-check-input' ), - ), - 'radio' => array( - 'label_class' => 'form-control-label', - 'radio_wrapper_class' => 'form-check', - 'radio_label_class' => 'form-check-label', - 'classes' => array( 'ppom-check-input' ), - ), - ); - break; - } - - return apply_filters( "nmform_property-{$property}", $value ); - - } - - - /** - * ====================== FILTERS ===================================== - * */ - - public function adjust_attributes_values( $attr_value, $attr, $args ) { - - switch ( $attr ) { - - // converting classes to string - case 'classes': - $type = $this->get_attribute_value( 'type', $args ); - - if ( $type != 'image' ) { - - $attr_value[] = 'ppom-input'; - } - // adding ppom-input class to all inputs - // {type} also added as class - $attr_value[] = $type; - $attr_value = implode( ' ', $attr_value ); - break; - - /** - * converting name to array for multiple:select - * */ - case 'name': - $type = $this->get_attribute_value( 'type', $args ); - $multiple = $this->get_attribute_value( 'multiple', $args ); - if ( $type == 'select' && $multiple ) { - - $attr_value .= '[]'; - } - break; - } - - return $attr_value; - } - - /** - * ====================== ENDs FILTERS ===================================== - * */ - - public static function get_instance() { - // create a new object if it doesn't exist. - is_null( self::$ins ) && self::$ins = new self(); + * Legacy bootstrap for field markup. + * + * The `NM_Form` class alias and the global `NMForm()` function are deprecated. New code + * should use {@see \PPOM\FieldMarkup\FieldMarkupRenderer::get_instance()} directly. + * + * Implementation lives in {@see \PPOM\FieldMarkup\FieldMarkupRenderer}, exposed for backward + * compatibility as the legacy `NM_Form` singleton via {@see NMForm()}. + * + * @package PPOM + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} - return self::$ins; - } +if ( ! defined( 'ECHOABLE' ) ) { + define( 'ECHOABLE', false ); } +class_alias( \PPOM\FieldMarkup\FieldMarkupRenderer::class, 'NM_Form' ); + +/** + * Singleton accessor for field markup rendering. + * + * @deprecated Use {@see \PPOM\FieldMarkup\FieldMarkupRenderer::get_instance()} instead. + * + * @return \PPOM\FieldMarkup\FieldMarkupRenderer + */ function NMForm() { - return NM_Form::get_instance(); + return \PPOM\FieldMarkup\FieldMarkupRenderer::get_instance(); } diff --git a/inc/ppom-meta-repository-functions.php b/inc/ppom-meta-repository-functions.php new file mode 100644 index 00000000..fd2ffc57 --- /dev/null +++ b/inc/ppom-meta-repository-functions.php @@ -0,0 +1,15 @@ +get_price(), $cart_item ); - - // return array with: price, source - $price_info = ppom_price_get_product_base( - $product_price, - $wc_product, - $ppom_fields_post, - $product_quantity, - $ppom_field_prices, - $ppom_discount, - $ppom_pricematrix - ); - $product_base_price = $price_info['price']; - // var_dump("product_base_price ==> ".$product_base_price."
"); - // ppom_pa($price_info); - // var_dump("total_addon_price ==> ".$total_addon_price."
"); - // var_dump("total_cart_fee_price ==> ".$total_cart_fee_price."
"); - // var_dump("ppom_discount ==> ".$ppom_discount."
"); - - $ppom_total_price = floatval( $total_addon_price ) + floatval( $product_base_price ) - $ppom_discount; - // var_dump("ppom_total_price ==> ".$ppom_total_price."
"); - - do_action( 'ppom_before_calculate_cart_total', $ppom_field_prices, $ppom_fields_post, $cart_item ); - - $ppom_total_price = apply_filters( 'ppom_cart_line_total', $ppom_total_price, $cart_item, $values ); - $wc_product->set_price( $ppom_total_price ); - - return $cart_item; + * Pricing engine β€” compatibility wrappers. + * + * @package PPOM + */ +function ppom_price_controller( ...$args ) { + return \PPOM\Pricing\Engine::price_controller( ...$args ); } -function ppom_before_calculate_totals( $cart_items ) { - - // ppom_pa($cart_items); - if ( empty( $cart_items ) ) { - return $cart_items; - } - - foreach ( $cart_items->cart_contents as $cart_item_key => $cart_item ) { - - - if ( ! isset( $cart_item['ppom']['fields'] ) ) { - continue; - } - $product_id = isset( $cart_item['product_id'] ) ? $cart_item['product_id'] : ''; - $variation_id = isset( $cart_item['variation_id'] ) ? $cart_item['variation_id'] : ''; - $wc_product = $cart_item['data']; - $ppom_fields_post = $cart_item['ppom']['fields']; - $product_quantity = floatval( $cart_item['quantity'] ); - $ppom_field_prices = ppom_get_field_prices( $ppom_fields_post, $product_id, $product_quantity, $variation_id ); - $ppom_discount = 0; - $ppom_pricematrix = isset( $cart_item['ppom']['price_matrix_found'] ) ? $cart_item['ppom']['price_matrix_found'] : null; - // ppom_pa($product_quantity); - // ppom_pa($ppom_fields_post); - // ppom_pa($ppom_field_prices); - - $total_addon_price = ppom_price_get_addon_total( $ppom_field_prices ); - $total_cart_fee_price = ppom_price_get_cart_fee_total( $ppom_field_prices ); - - $product_price = apply_filters( 'ppom_product_price_on_cart', $wc_product->get_price(), $cart_item ); - - // $context = 'cart'; - // $product_price = ppom_get_product_price( $product, $variation_id, $context); - - // return array with: price, source - $price_info = ppom_price_get_product_base( - $product_price, - $wc_product, - $ppom_fields_post, - $product_quantity, - $ppom_field_prices, - $ppom_discount, - $ppom_pricematrix - ); - - $product_base_price = $price_info['price']; - // var_dump("product_base_price ==> ".$product_base_price."
"); - // ppom_pa($cart_item['data']); - // var_dump("total_addon_price ==> ".$total_addon_price."
"); - // var_dump("total_cart_fee_price ==> ".$total_cart_fee_price."
"); - // var_dump("ppom_discount ==> ".$ppom_discount."
"); - - - $ppom_total_price = $total_addon_price + $product_base_price - $ppom_discount; - - do_action( 'ppom_before_calculate_cart_total', $ppom_field_prices, $ppom_fields_post, $cart_item ); - $ppom_total_price = apply_filters( 'ppom_cart_line_total', $ppom_total_price, $cart_items, $cart_item ); - $cart_item['data']->set_price( $ppom_total_price ); - } +function ppom_before_calculate_totals( ...$args ) { + return \PPOM\Pricing\Engine::before_calculate_totals( ...$args ); } - function ppom_get_field_prices( $ppom_fields_post, $product_id, &$product_quantity, $variation_id, $item = null ) { - - $field_prices = array(); - $ppom_meta_ids = apply_filters( 'ppom_meta_ids_in_cart', null, $item ); - - // ppom_pa($item); - foreach ( $ppom_fields_post as $data_name => $value ) { - - if ( $data_name == 'id' ) { - continue; - } - - if ( empty( $value ) ) { - continue; - } - - // var_dump($data_name); - - $value = ! is_array( $value ) ? stripcslashes( $value ) : $value; - - $field_meta = ppom_get_field_meta_by_dataname( $product_id, $data_name, $ppom_meta_ids ); - $product = wc_get_product( $product_id ); - - $field_type = isset( $field_meta['type'] ) ? $field_meta['type'] : ''; - $field_title = isset( $field_meta['title'] ) ? $field_meta['title'] : ''; - - $charge = isset( $field_meta['onetime'] ) ? $field_meta['onetime'] : ''; - $charge = $charge == 'on' ? 'cart_fee' : 'addon'; - - $context = 'cart'; - $product_price = ppom_get_product_price( $product, $variation_id, $context ); - - $options = array(); - - $field_type = apply_filters( 'ppom_field_type_price', $field_type, $ppom_fields_post, $product_id ); - - // ppom_pa($field_meta); - - // Getting options - switch ( $field_type ) { - - case 'bulkquantity': - $options = isset( $field_meta['options'] ) ? json_decode( stripcslashes( $field_meta['options'] ), true ) : array(); - break; - case 'image': - $options = isset( $field_meta['images'] ) ? ppom_convert_options_to_key_val( $field_meta['images'], $field_meta, $product ) : array(); - break; - case 'audio': - $options = isset( $field_meta['audio'] ) ? ppom_convert_options_to_key_val( $field_meta['audio'], $field_meta, $product ) : array(); - break; - case 'imageselect': - $options = isset( $field_meta['images'] ) ? ppom_convert_options_to_key_val( $field_meta['images'], $field_meta, $product ) : array(); - break; - case 'eventcalendar': - $disbl_global_price = isset( $field_meta['disabled_global_price'] ) ? $field_meta['disabled_global_price'] : ''; - - if ( isset( $field_meta['global_price'] ) && $field_meta['global_price'] != '' ) { - $global_price = $field_meta['global_price']; - } else { - $global_price = 0; - } - - $options = isset( $field_meta['options'] ) ? $field_meta['options'] : array(); - - $options[] = array( - 'price' => $global_price, - 'option' => 'Simple', - 'id' => 'simple', - ); - - break; - - // Note: Variation Matrix Use for only customized clients - case 'vm': - $vm_options = array(); - - $options = isset( $field_meta['options'] ) ? ppom_convert_options_to_key_val( $field_meta['options'], $field_meta, $product ) : array(); - - $row_options = isset( $field_meta['row_options'] ) ? explode( PHP_EOL, $field_meta['row_options'] ) : array(); - - foreach ( $options as $opt_name => $opt ) { - - foreach ( $row_options as $index => $r_opt ) { - - $color_code = apply_filters( 'ppom_vm_row_index', $r_opt, $field_meta ); - - $name_k = stripslashes( trim( $opt_name ) ) . '_' . stripslashes( trim( $color_code ) ); - - $vm_options[ $name_k ] = $opt['price']; - - } - } - - break; - - - case 'vqmatrix': - $vqmatrix_pricemeta = array(); - - $priced_options = isset( $field_meta['options'] ) ? $field_meta['options'] : array(); - $simple_options = isset( $field_meta['row_options'] ) ? $field_meta['row_options'] : array(); - $default_price = ! empty( $field_meta['default_price'] ) ? $field_meta['default_price'] : 0; - - foreach ( $priced_options as $index_1 => $opt_meta_1 ) { - - $the_price = isset( $opt_meta_1['price'] ) && $opt_meta_1['price'] != '' ? $opt_meta_1['price'] : $default_price; - $priced_opt_id = ! empty( $opt_meta_1['option_id'] ) ? stripslashes( trim( $opt_meta_1['option_id'] ) ) : ''; - - foreach ( $simple_options as $index_2 => $opt_meta_2 ) { - - $simple_opt_id = ! empty( $opt_meta_2['option_id'] ) ? stripslashes( trim( $opt_meta_2['option_id'] ) ) : ''; - - $options_key = ucfirst( $priced_opt_id ) . '-' . ucfirst( $simple_opt_id ); - - $vqmatrix_pricemeta[ $options_key ] = $the_price; - - } - } - - break; - default: - $options = isset( $field_meta['options'] ) ? ppom_convert_options_to_key_val( $field_meta['options'], $field_meta, $product ) : array(); - break; - } - - // ppom_pa($options); - - - $field_price = ''; - $option_quantity = 1; - - if ( ppom_reset_cart_quantity_to_one( $product_id ) ) { - $option_quantity = ppom_price_get_total_quantities( $ppom_fields_post, $product_id ); - } - - switch ( $field_type ) { - - case 'text': - case 'textarea': - case 'date': - case 'email': - $option = $value; - $field_price = isset( $field_meta['price'] ) ? $field_meta['price'] : ''; - if ( $field_price !== '' ) { - $field_prices[] = ppom_generate_field_price( $field_price, $field_meta, $charge, $option, $option_quantity ); - } - break; - - case 'select': - case 'radio': - foreach ( $options as $option ) { - - $option_raw = ppom_wpml_translate( $option['raw'], 'PPOM' ); - if ( $option_raw == stripcslashes( $value ) ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - $option_percent = isset( $option['percent'] ) ? $option['percent'] : ''; - - if ( $option_price !== '' ) { - - if ( $option_percent !== '' ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_percent ); - } - - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - - // weight - if ( ! empty( $option['option_weight'] ) ) { - $field_price = 0; - $field_prices[] = ppom_generate_field_price( $field_price, $field_meta, 'weight', $option ); - } - } - } - break; - - case 'checkbox': - foreach ( $options as $option ) { - - if ( $value ) { - foreach ( $value as $val ) { - - $option_raw = ppom_wpml_translate( $option['raw'], 'PPOM' ); - if ( $option_raw == stripcslashes( $val ) ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - - /** - * @since 24.6 discount price added - * */ - if ( $option['raw_discount'] > 0 ) { - $option_price = $option['raw_discount']; - } - - if ( $option_price !== '' ) { - if ( strpos( $option_price, '%' ) !== false ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_price ); - } - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - - // weight - if ( ! empty( $option['option_weight'] ) ) { - $field_price = 0; - $field_prices[] = ppom_generate_field_price( $field_price, $field_meta, 'weight', $option ); - } - } - } - } - } - break; - - case 'multiple_select': - foreach ( $value as $opt_index => $opt_label ) { - - foreach ( $options as $option ) { - - $option_raw = ppom_wpml_translate( $option['raw'], 'PPOM' ); - - if ( $option_raw == $opt_label ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - $option_percent = isset( $option['percent'] ) ? $option['percent'] : ''; - - if ( $option_price !== '' ) { - - if ( $option_percent !== '' ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_percent ); - } - - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - } - } - } - break; - - case 'cropper': - // ppom_pa($options); - - // Checking if ratio found with cropping - if ( isset( $value['ratio'] ) && $value['ratio'] !== '' ) { - $ratio_found = $value['ratio']; - // Getting option - foreach ( $options as $option ) { - if ( $option['option_id'] === $ratio_found ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - $option_percent = isset( $option['percent'] ) ? $option['percent'] : ''; - - if ( $option_price !== '' ) { - - if ( $option_percent !== '' ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_percent ); - } - - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - } - } - } - - break; - - case 'fancycropper': - foreach ( $value as $popupID => $image_data ) { - - $fomatted_data = json_decode( stripcslashes( $image_data ), true ); - - $cropped_price = isset( $fomatted_data['cartPrice'] ) ? $fomatted_data['cartPrice'] : 0; - if ( $cropped_price > 0 ) { - - $field_prices[] = ppom_generate_field_price( $cropped_price, $field_meta, $charge, $image_data, $option_quantity ); - } - } - - break; - - case 'palettes': - foreach ( $options as $option ) { - - if ( $value ) { - foreach ( $value as $color ) { - - if ( $option['raw'] == $color ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - $option_percent = isset( $option['percent'] ) ? $option['percent'] : ''; - - if ( $option_price !== '' ) { - - if ( $option_percent !== '' ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_percent ); - } - - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - } - } - } - } - break; - - case 'imageselect': - foreach ( $options as $option ) { - $image_id = isset( $option['image_id'] ) ? $option['image_id'] : 0; - if ( $image_id == $value ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - if ( $option_price !== '' ) { - if ( strpos( $option_price, '%' ) !== false ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_price ); - } - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - } - } - break; - - case 'image': - foreach ( $options as $option ) { - if ( $value ) { - foreach ( $value as $images_meta ) { - - $images_meta = json_decode( stripslashes( $images_meta ), true ); - $image_id = $images_meta['image_id']; - $image_price = $images_meta['price']; - $image_option_id = $images_meta['option_id']; - - - if ( $option['option_id'] == $image_option_id ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - $option_percent = isset( $option['percent'] ) ? $option['percent'] : ''; - - // var_dump($option_price); - if ( $option_price !== '' ) { - if ( strpos( $option_price, '%' ) !== false ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_price ); - } - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - } - } - } - } - // $field_prices[] = ppom_generate_field_price($option_price, $field_meta, $charge, $option); - break; - - case 'audio': - foreach ( $options as $option ) { - - if ( $value ) { - foreach ( $value as $images_meta ) { - - $images_meta = json_decode( stripslashes( $images_meta ), true ); - $image_id = $images_meta['id']; - $image_price = $images_meta['price']; - $image_title = $images_meta['title']; - - if ( $option['raw'] == $image_title ) { - - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - $option_percent = isset( $option['percent'] ) ? $option['percent'] : ''; - - if ( $option_price !== '' ) { - if ( strpos( $option_price, '%' ) !== false ) { - $option_price = ppom_get_amount_after_percentage( $product_price, $option_price ); - } - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $option_quantity ); - } - } - } - } - } - // $field_prices[] = ppom_generate_field_price($option_price, $field_meta, $charge, $option); - break; - - case 'file': - $file_cost = isset( $field_meta['file_cost'] ) ? floatval( $field_meta['file_cost'] ) : 0; - $total_files = count( $value ); - $field_price = $file_cost * $total_files; - - if ( $field_price > 0 ) { - $option = $value; // files array from post - $field_prices[] = ppom_generate_field_price( $field_price, $field_meta, $charge, $option, 1 ); - } - break; - - - case 'quantities': - case 'qtypack': - // ppom_pa(ppom_is_field_has_price($field_meta)); - - if ( ppom_is_field_has_price( $field_meta ) ) { - foreach ( $options as $option ) { - $quantities_total = 0; - - foreach ( $value as $val => $quantity ) { - - $quantity = intval( $quantity ); - $quantities_total += $quantity; - // Important: need to convert browser data into html_entity_decode - $val = html_entity_decode( $val ); - - if ( $option['raw'] == $val && $quantity > 0 ) { - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $quantity ); - - // weight - if ( ! empty( $option['option_weight'] ) ) { - $field_price = 0; - $field_prices[] = ppom_generate_field_price( $field_price, $field_meta, 'weight', $option, $quantity ); - } - } - } - } - - if ( $quantities_total > 0 ) { - $product_quantity = $quantities_total; - } - } else { - - // Check if matrix used - $option_price = $product_price; - $option = array(); - $pricematrix_field = ppom_has_field_by_type( ppom_get_product_id( $product ), 'pricematrix' ); - if ( $pricematrix_field ) { - $matrix_found = ppom_price_matrix_chunk( $product, $pricematrix_field, $product_quantity ); - - if ( $matrix_found ) { - // $option_price = $matrix_found['matrix_price']; - $charge = 'matrix_quantities'; - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $product_quantity ); - } - } - } - - break; - - - case 'selectqty': - if ( ppom_is_field_has_price( $field_meta ) ) { - foreach ( $options as $option ) { - $quantities_total = 0; - - $val = $value['option']; - $quantity = $value['qty']; - - // var_dump($val, $option); - $quantity = intval( $quantity ); - $quantities_total += $quantity; - - if ( $option['raw'] == $val && $quantity > 0 ) { - $option_price = isset( $option['raw_price'] ) ? $option['raw_price'] : ''; - - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $quantity ); - - // weight - if ( ! empty( $option['option_weight'] ) ) { - $field_price = 0; - $field_prices[] = ppom_generate_field_price( $field_price, $field_meta, 'weight', $option, $quantity ); - } - } - } - - if ( $quantities_total > 0 ) { - $product_quantity = $quantities_total; - } - } - break; - - - // Note: Variation Matrix Use for only customized clients - case 'vm': - if ( ppom_is_field_has_price( $field_meta ) ) { - - foreach ( $vm_options as $vm_key => $vm_price ) { - $quantities_total = 0; - - foreach ( $value as $val => $quantity ) { - - $quantities_total += $quantity; - // Important: need to convert browser data into html_entity_decode - $val = html_entity_decode( $val ); - - if ( $val === $vm_key && $quantity > 0 ) { - $option_price = $vm_price; - - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $options, $quantity ); - } - } - } - - if ( $quantities_total > 0 ) { - $product_quantity = $quantities_total; - } - } - - break; - - case 'vqmatrix': - if ( ppom_is_field_has_price( $field_meta ) ) { - - foreach ( $vqmatrix_pricemeta as $vm_key => $vm_price ) { - $quantities_total = 0; - - foreach ( $value as $val => $quantity ) { - - $quantities_total += $quantity; - - // Important: need to convert browser data into html_entity_decode - $val = html_entity_decode( $val ); - - if ( $val === $vm_key && $quantity > 0 ) { - $option_price = $vm_price; - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $options, $quantity ); - } - } - } - - if ( $quantities_total > 0 ) { - $product_quantity = $quantities_total; - } - } else { - - // Check if matrix used - $option_price = $product_price; - $option = array(); - $pricematrix_field = ppom_has_field_by_type( ppom_get_product_id( $product ), 'pricematrix' ); - if ( $pricematrix_field ) { - $matrix_found = ppom_price_matrix_chunk( $product, $pricematrix_field, $product_quantity ); - - if ( $matrix_found ) { - // $option_price = $matrix_found['matrix_price']; - $charge = 'matrix_quantities'; - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $option, $product_quantity ); - } - } - } - - break; - - - case 'bulkquantity': - // THIS SHOULD BE CHECK ON TOP AFTER FOREACH LOOP - $conditionally_hidden = $item['ppom']['conditionally_hidden']; - if ( ppom_is_field_hidden_by_condition( $data_name, $conditionally_hidden ) ) { - continue 2; - } - - $product_quantity = $value['qty']; - $bq_value = $value['option']; - // $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; - if ( $option_price ) { - $single_quantity = 1; - $field_prices[] = ppom_generate_field_price( $option_price, $field_meta, $charge, $bq_found, $single_quantity ); - } - // ppom_pa($bq_found); - break; - - case 'fixedprice': - $fp_found = ppom_price_fixedprice_chunk( $product, $options, $product_quantity ); - $unit_price = PPOM_FPP()->get_unit_price( $fp_found, $field_meta ); - if ( $unit_price ) { - $field_prices[] = ppom_generate_field_price( $unit_price, $field_meta, $charge, $fp_found, 1 ); - } - break; - - case 'measure': - // var_dump($value); - $product_quantity = $value; - $unit_price = 0; - $measure_price_field = ppom_generate_field_price( $unit_price, $field_meta, $charge, $options, $product_quantity ); - - if ( isset( $field_meta['price-multiplier'] ) && is_numeric( $field_meta['price-multiplier'] ) ) { - $measure_price_field['price-multiplier'] = floatval( $field_meta['price-multiplier'] ); - } - - $field_prices[] = $measure_price_field; - break; - - case 'eventcalendar': - // if( ppom_is_field_has_price($field_meta) ) { // Waite & Watch Party - if ( 1 ) { - foreach ( $options as $ticket ) { - - $quantities_total = 0; - $ticket_label = isset( $ticket['option'] ) ? $ticket['option'] : ''; - - foreach ( $value as $date => $ticket_meta ) { - - - foreach ( $ticket_meta as $label => $qty ) { - - $quantities_total += $qty; - - if ( $ticket_label == $label ) { - $ticket_price = isset( $ticket['raw_price'] ) ? $ticket['raw_price'] : ''; - $field_prices[] = ppom_generate_field_price( $ticket_price, $field_meta, $charge, $options, $qty ); - } - } - } - - if ( $quantities_total > 0 ) { - $product_quantity = $quantities_total; - } - } - } - - break; - - case 'textcounter': - // ppom_pa($field_meta); - $count_type = $field_meta['count_type']; - $enabled_space = isset( $field_meta['enabled_space'] ) ? $field_meta['enabled_space'] : false; - - switch ( $count_type ) { - case 'character': - if ( $enabled_space == 'on' ) { - $length = preg_match_all( '/[^\.]/', $value, $matches ); - } else { - $length = preg_match_all( '/[^ \.]/', $value, $matches ); - } - break; - case 'word': - default: - $length = str_word_count( $value ); - - } - - if ( $field_meta['count_price'] && $field_meta['count_price'] != '' ) { - - $price = $field_meta['count_price'] * $length; - $field_prices[] = ppom_generate_field_price( $price, $field_meta, $charge, $options, 1 ); - } - - break; - - } - } - - // ppom_pa($field_prices); - - return apply_filters( 'ppom_fields_prices', $field_prices, $ppom_fields_post, $product_id ); + return \PPOM\Pricing\Engine::get_field_prices( $ppom_fields_post, $product_id, $product_quantity, $variation_id, $item ); } -function ppom_generate_field_price( $field_price, $field_meta, $apply, $option = array(), $qty = 0 ) { - - // ppom_pa($option); - - $data_name = isset( $field_meta['data_name'] ) ? $field_meta['data_name'] : ''; - $field_title = isset( $field_meta['title'] ) ? stripslashes( $field_meta['title'] ) : ''; - $field_type = isset( $field_meta['type'] ) ? $field_meta['type'] : ''; - $taxable = ( isset( $field_meta['onetime_taxable'] ) && $field_meta['onetime_taxable'] == 'on' ) ? true : false; - $option_label = isset( $option['raw'] ) ? $option['raw'] : ''; - $without_tax = isset( $option['without_tax'] ) ? $option['without_tax'] : ''; - - $label_price = "{$field_title} - " . wc_price( $field_price ); - // For bulkquantity - $base_price = isset( $option['Base Price'] ) ? $option['Base Price'] : ''; - $option_id = isset( $option['option_id'] ) ? $option['option_id'] : ''; - - if ( $field_type == 'file' ) { - // translators: %d: the number of files options. - $option_label = sprintf( __( '%d Files', 'woocommerce-product-addon' ), count( $option ) ); - } - - return apply_filters( - 'ppom_price_option_meta', - array( - 'type' => $field_type, - 'option_id' => $option_id, - 'label' => $field_title, - 'label_price' => $label_price, - 'price' => $field_price, - 'apply' => $apply, - 'data_name' => $data_name, - 'taxable' => $taxable, - 'without_tax' => $without_tax, - 'option_label' => $option_label, - 'quantity' => $qty, - 'base_price' => $base_price, - ), - $field_meta, - $field_price, - $option, - $qty - ); +function ppom_generate_field_price( ...$args ) { + return \PPOM\Pricing\Engine::generate_field_price( ...$args ); } -// Get total addon price of given Price Array -function ppom_price_get_addon_total( $price_array ) { - - $total_addon = 0; - - if ( $price_array ) { - foreach ( $price_array as $price ) { - - if ( $price['apply'] != 'addon' ) { - continue; - } - if ( ! isset( $price['price'] ) ) { - continue; - } - - $the_price = floatval( $price['price'] ); - - $total_addon += ( $the_price * $price['quantity'] ); - - /* - if( $price['type'] == 'quantities' || $price['type'] == 'bulkquantity' ) { - $total_addon += ($price['price'] * $price['quantity']); - } else { - $total_addon += $price['price']; - }*/ - } - } - - return $total_addon; +function ppom_price_get_addon_total( ...$args ) { + return \PPOM\Pricing\Engine::price_get_addon_total( ...$args ); } -// Get total cart_fee price of given Price Array -function ppom_price_get_cart_fee_total( $price_array ) { - - $total_cart_fee = 0; - - if ( $price_array ) { - foreach ( $price_array as $price ) { - - if ( $price['apply'] == 'cart_fee' ) { - $total_cart_fee += $price['price']; - } - } - } - - return $total_cart_fee; +function ppom_price_get_cart_fee_total( ...$args ) { + return \PPOM\Pricing\Engine::price_get_cart_fee_total( ...$args ); } -// Get total quantities -function ppom_price_get_total_quantities( $ppom_fields_post, $product_id ) { - - $total_quantities = 0; - - foreach ( $ppom_fields_post as $data_name => $value ) { - - if ( $data_name == 'id' ) { - continue; - } - - if ( empty( $value ) ) { - continue; - } - - $field_meta = ppom_get_field_meta_by_dataname( $product_id, $data_name ); - $field_type = isset( $field_meta['type'] ) ? $field_meta['type'] : ''; - - switch ( $field_type ) { - - case 'quantities': - // $total_quantities = 0; - foreach ( $value as $option => $qty ) { - $total_quantities += intval( $qty ); - } - break; - - case 'vm': - // $total_quantities = 0; - foreach ( $value as $option => $qty ) { - $total_quantities += intval( $qty ); - } - break; - - case 'vqmatrix': - // $total_quantities = 0; - foreach ( $value as $option => $qty ) { - $total_quantities += intval( $qty ); - } - break; - - case 'bulkquantity': - $total_quantities = 0; - foreach ( $value as $option => $qty ) { - // ppom_pa($qty); - $total_quantities += intval( $qty ); - } - break; - - case 'eventcalendar': - $total_quantities = 0; - foreach ( $value as $date => $ticket_meta ) { - foreach ( $ticket_meta as $option => $qty ) { - $total_quantities += intval( $qty ); - } - } - break; - - } - } - - return apply_filters( 'ppom_prices_total_quantities', $total_quantities, $ppom_fields_post, $product_id ); +function ppom_price_get_total_quantities( ...$args ) { + return \PPOM\Pricing\Engine::price_get_total_quantities( ...$args ); } -// Get total bulkquantities -function ppom_price_get_total_bulkquantities( $price_array, $product, $ppom_fields_post ) { - - $total_bulkquantities = 0; - $total_bq_baseprice = 0; - if ( $price_array ) { - foreach ( $price_array as $price ) { - - if ( $price['type'] == 'bulkquantity' ) { - $total_bulkquantities += intval( $price['quantity'] ); - } - $total_bq_baseprice += floatval( $price['base_price'] ); - } - } - - // checking for base price - $product_price = 0; - foreach ( $ppom_fields_post as $dataname => $val ) { - $meta = ppom_get_field_meta_by_dataname( $product->get_id(), $dataname ); - if ( isset( $meta['type'] ) && $meta['type'] == 'bulkquantity' ) { - $includeprice = apply_filters( 'ppom_bulkquantity_includeprice', '', $meta, $product ); - if ( $includeprice === 'on' ) { - $product_price = $product->get_price(); - } - } - } - - - $bq_data = array( - 'quantity' => $total_bulkquantities, - 'base_price' => $total_bq_baseprice, - 'includeproductprice' => $product_price, - ); - - return $bq_data; +function ppom_price_get_total_bulkquantities( ...$args ) { + return \PPOM\Pricing\Engine::price_get_total_bulkquantities( ...$args ); } -// Get total fixedprice -function ppom_price_get_total_fixedprice( $price_array ) { - - $total_fixedprice = 0; - $total_fp_qty = 0; - - if ( $price_array ) { - foreach ( $price_array as $price ) { - - if ( $price['type'] == 'fixedprice' ) { - $total_fixedprice += $price['price']; - $total_fp_qty += $price['quantity']; - } - } - } - - $fixedprice_data = array( - 'quantity' => $total_fp_qty, - 'base_price' => $total_fixedprice, - ); - - return $fixedprice_data; +function ppom_price_get_total_fixedprice( ...$args ) { + return \PPOM\Pricing\Engine::price_get_total_fixedprice( ...$args ); } -// Get total fixedprice -function ppom_price_get_total_measure( $price_array ) { - $total_measure = 0; - - if ( $price_array ) { - foreach ( $price_array as $price ) { - if ( $price['type'] !== 'measure' ) { - continue; - } - - $measure = $price['quantity']; - if ( isset( $price['price-multiplier'] ) && is_numeric( $price['price-multiplier'] ) ) { - $measure *= floatval( $price['price-multiplier'] ); - } - - if ( $total_measure == 0 ) { - $total_measure = $measure; - } else { - $total_measure *= $measure; - } - } - } - - return $total_measure; +function ppom_price_get_total_measure( ...$args ) { + return \PPOM\Pricing\Engine::price_get_total_measure( ...$args ); } -// Get product base price -function ppom_price_get_product_base( - $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 = floatval($base_price); - // $base_price = $product->get_price(); - // ppom_pa($product); - // var_dump($product_quantity); - $product_id = ppom_get_product_id( $product ); - // var_dump('varia',$product_id); - - $total_addon_price = ppom_price_get_addon_total( $ppom_field_prices ); - $total_cart_fee_price = ppom_price_get_cart_fee_total( $ppom_field_prices ); - - $source = 'product'; - - $matrix_found = null; - if ( $ppom_pricematrix ) { - $matrix_found = ppom_parse_price_matrix( $ppom_pricematrix, $product, $product_quantity, $base_price, $total_addon_price, $total_cart_fee_price ); - } - - - $quantities_found = ppom_price_get_total_quantities( $ppom_fields_post, $product_id ); - $bulkquantities_found = ppom_price_get_total_bulkquantities( $ppom_field_prices, $product, $ppom_fields_post ); - $fixedprice_found = ppom_price_get_total_fixedprice( $ppom_field_prices ); - $measure_found = ppom_price_get_total_measure( $ppom_field_prices ); - // If price matrix found - // ppom_pa($matrix_found); - if ( $matrix_found ) { - if ( $matrix_found['matrix_price'] > 0 ) { - - $base_price = $matrix_found['matrix_price']; - $source = 'matrix'; - } /* - else if( isset($matrix_found['matrix_discount']) && $matrix_found['matrix_discount'] != '' ) { - var_dump($total_addon_price); - $matrix_discount = $matrix_found['matrix_discount']; - if( $matrix_found['apply'] == 'both' ) { - $base_price = ($base_price + $total_addon_price); - } - $base_price = floatval($base_price) - $matrix_discount; - $source = 'matrix_discount'; - }*/ - - } - - // var_dump($quantities_found); - // If quantities found - if ( $quantities_found > 0 ) { - - if ( ! ppom_is_cart_quantity_updatable( $product_id ) ) { - // if quantities has price then no need to multiply with total quantities - // just use 1 - $qty = 1; - $base_price = $base_price * $qty; - } - } - - // ppom_pa($bulkquantities_found); - // If Bulkquantities found, no base price is effective - if ( $bulkquantities_found['quantity'] > 0 ) { - $base_price = $bulkquantities_found['base_price'] + $bulkquantities_found['includeproductprice']; - $source = 'bulkquantities'; - } - - // If Fixedprice found, Set base price - if ( $fixedprice_found['quantity'] > 0 ) { - $base_price = 0; - $source = 'fixedprice'; - } - - // If Measure found, Set base price - if ( $measure_found > 0 ) { - $base_price *= $measure_found; - $source = 'measure'; - } - - $price_info = array( - 'price' => $base_price, - 'source' => $source, - ); - - return apply_filters( 'ppom_price_info', $price_info, $product, $ppom_fields_post, $ppom_field_prices ); +function ppom_price_get_product_base( ...$args ) { + return \PPOM\Pricing\Engine::price_get_product_base( ...$args ); } -// If price set by pricematrix in cart return matrix -function ppom_price_matrix_chunk( $product, $pricematrix_fields, $product_quantity ) { - - $matrix_found = array(); - $pm_applied = array(); - - - if ( count( $pricematrix_fields ) > 0 ) { - - foreach ( $pricematrix_fields as $pm ) { - - $pm_dataname = isset( $pm['data_name'] ) ? $pm['data_name'] : ''; - // var_dump($pm_dataname, ppom_is_field_hidden_by_condition( $pm_dataname )); - - if ( ppom_is_field_hidden_by_condition( $pm_dataname ) ) { - continue; - } - - $pm_applied = $pm; - break; - } - $matrix_found = ppom_extract_matrix_by_quantity( $pm_applied, $product, $product_quantity ); - // ppom_pa($matrix_found); exit; - } - - return apply_filters( 'ppom_price_matrix_chunk_cart', $matrix_found, $product, $pm_applied ); +function ppom_price_matrix_chunk( ...$args ) { + return \PPOM\Pricing\Engine::price_matrix_chunk( ...$args ); } -// If Bulkquantity add-on is used, get it's chunk -function ppom_price_bulkquantity_chunk( $product, $bulkquantity_options, $product_quantity ) { - - $bq_found = array(); - - if ( count( $bulkquantity_options ) > 0 ) { - - foreach ( $bulkquantity_options as $bq ) { - - // ppom_pa($bq); - $range = $bq['Quantity Range']; - $range_array = explode( '-', $range ); - $range_start = intval( $range_array[0] ); - $range_end = intval( $range_array[1] ); - - // var_dump($bq); - $quantity = intval( $product_quantity ); - if ( $quantity >= $range_start && $quantity <= $range_end ) { - $bq_found = $bq; - break; - } - } - } - - return apply_filters( 'ppom_price_bulkquantity_chunk_cart', $bq_found, $product ); +function ppom_price_bulkquantity_chunk( ...$args ) { + return \PPOM\Pricing\Engine::price_bulkquantity_chunk( ...$args ); } - -// If Bulkquantity add-on is used, get it's chunk -function ppom_price_fixedprice_chunk( $product, $fixedprice_options, $product_quantity ) { - - $fixedprice_found = array(); - - if ( count( $fixedprice_options ) > 0 ) { - - foreach ( $fixedprice_options as $fp ) { - - $fp_dataname = isset( $fp['data_name'] ) ? $fp['data_name'] : ''; - if ( ppom_is_field_hidden_by_condition( $fp_dataname ) ) { - continue; - } - - if ( $fp['raw'] == $product_quantity ) { - $fixedprice_found = $fp; - break; - } - } - } - - return apply_filters( 'ppom_price_fixedprice_chunk_cart', $fixedprice_found, $product ); - +function ppom_price_fixedprice_chunk( ...$args ) { + return \PPOM\Pricing\Engine::price_fixedprice_chunk( ...$args ); } -/** - * Calculating Fixed Fees - * **/ - -function ppom_price_cart_fee( $cart ) { - $fee_no = 1; - $cart_counter = 1; - foreach ( $cart->get_cart() as $item ) { - - if ( ! isset( $item['ppom']['fields'] ) ) { - continue; - } - - $product = $item['data']; - $ppom_fields_post = $item['ppom']['fields']; - $product_id = $item['product_id']; - $variation_id = isset( $item['variation_id'] ) ? $item['variation_id'] : ''; - $quantity = $item['quantity']; - $ppom_field_prices = ppom_get_field_prices( $ppom_fields_post, $product_id, $quantity, $variation_id, $item ); - // ppom_pa($ppom_field_prices); - - // Thing to remember: - // This is not product price, it will be price in cart item - $cart_item_price = floatval( $product->get_price() ); - - if ( $matrix_found = ppom_price_has_discount_matrix( $product, $quantity ) ) { - - // $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' ) { - $total_addon_price = ppom_price_get_addon_total( $ppom_field_prices ); - $total_cart_fee_price = ppom_price_get_cart_fee_total( $ppom_field_prices ); - $price_tobe_discount = ( $cart_item_price * $quantity ) + $total_cart_fee_price; - } - - // var_dump($price_tobe_discount); - - - if ( ! empty( $matrix_found['percent'] ) ) { - $matrix_discount = ppom_get_amount_after_percentage( $price_tobe_discount, $matrix_found['percent'] ); - } else { - $matrix_discount = $matrix_found['raw_price']; - } - $discount_label = $cart_counter . '-' . $matrix_found['label']; - $matrix_discount = floatval( $matrix_discount ); - $discount_taxable = apply_filters( 'ppom_matrix_discount_taxable', false, $item, $cart ); - $cart->add_fee( esc_html( $discount_label ), - $matrix_discount, $discount_taxable ); - // ppom_pa($discount_label); - } - - foreach ( $ppom_field_prices as $fee ) { - - if ( $fee['apply'] != 'cart_fee' ) { - continue; - } - - $label = $fee['label']; - $option_label = isset( $fee['option_label'] ) ? $fee['option_label'] : ''; - $fee_price = apply_filters( 'ppom_option_price', $fee['price'] ); - $taxable = $fee['taxable']; // deprecated soon - - $label = "{$fee_no}-{$label} ({$option_label})"; - $label = apply_filters( 'ppom_fixed_fee_label', $label, $fee, $item ); - - - if ( ! empty( $fee['without_tax'] ) ) { - $fee_price = $fee['without_tax']; - } - - $taxable = ppom_get_option( 'ppom_taxable_fixed_price' ); - $taxable = $taxable == 'yes' ? true : false; - // var_dump($taxable); - - $fee_price = apply_filters( 'ppom_cart_fixed_fee', $fee_price, $fee, $cart ); - $taxable = apply_filters( 'ppom_cart_fixed_fee_taxable', $taxable, $fee, $cart ); - - if ( $fee_price != 0 ) { - $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 ); - - $total_tax = array_sum($tax); - $fee_price = $fee_price - $total_tax; - } - - $cart->add_fee( esc_html( $label ), $fee_price, $taxable, $tax_class ); - $fee_no ++; - } - } - - $cart_counter ++; - } +function ppom_price_cart_fee( ...$args ) { + return \PPOM\Pricing\Engine::price_cart_fee( ...$args ); } -// Check if price is being pulled by matrix -function ppom_price_is_matrix_found( $product, $product_quantity, $base_price, $addon_price, $cart_fee ) { - - $matrix_discount = 0.0; - $matrix_price = 0.0; - // Check if Price Matrix is used - $pricematrix_field = ppom_has_field_by_type( ppom_get_product_id( $product ), 'pricematrix' ); - if ( ! $pricematrix_field ) { - return null; - } - - $matrix_found = ppom_price_matrix_chunk( $product, $pricematrix_field, $product_quantity ); - // ppom_pa($matrix_found); - - if ( isset( $matrix_found['discount'] ) ) { - if ( ! empty( $matrix_found['percent'] ) ) { - - // If discount only on BASE Price - $price_tobe_discount = $base_price; - // If discount only on BASE Price + Options - if ( $matrix_found['discount'] == 'both' ) { - $price_tobe_discount += $addon_price + $cart_fee; - } - - $matrix_discount = ppom_get_amount_after_percentage( $price_tobe_discount, $matrix_found['percent'] ); - $matrix_discount = floatval( $matrix_discount ); - // var_dump($price_tobe_discount); - } else { - $matrix_discount = isset( $matrix_found['raw_price'] ) ? floatval( $matrix_found['raw_price'] ) : 0; - } - } else { - $matrix_price = isset( $matrix_found['raw_price'] ) ? $matrix_found['raw_price'] : $base_price; - } - $matrix = array( - 'matrix_price' => $matrix_price, - 'matrix_discount' => $matrix_discount, - ); - - return apply_filters( 'ppom_price_is_matrix_found', $matrix, $product ); +function ppom_price_is_matrix_found( ...$args ) { + return \PPOM\Pricing\Engine::price_is_matrix_found( ...$args ); } -function ppom_parse_price_matrix( $ppom_pricematrix, $product, $product_quantity, $base_price, $addon_price, $cart_fee ) { - - $matrix_discount = 0.0; - $matrix_price = 0.0; - $matrix_found = ppom_extract_matrix_by_quantity( $ppom_pricematrix, $product, $product_quantity ); - // ppom_pa($matrix_found); - if ( isset( $matrix_found['discount'] ) ) { - if ( ! empty( $matrix_found['percent'] ) ) { - - // If discount only on BASE Price - $price_tobe_discount = $base_price; - // If discount only on BASE Price + Options - if ( $matrix_found['discount'] == 'both' ) { - $price_tobe_discount += $addon_price + $cart_fee; - } - - $matrix_discount = ppom_get_amount_after_percentage( $price_tobe_discount, $matrix_found['percent'] ); - $matrix_discount = floatval( $matrix_discount ); - // var_dump($price_tobe_discount); - } else { - $matrix_discount = isset( $matrix_found['raw_price'] ) ? floatval( $matrix_found['raw_price'] ) : 0; - } - } else { - if ( 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; - } - } - - $matrix = array( - 'matrix_price' => $matrix_price, - 'matrix_discount' => $matrix_discount, - ); - - return apply_filters( 'ppom_price_is_matrix_found', $matrix, $product ); +function ppom_parse_price_matrix( ...$args ) { + return \PPOM\Pricing\Engine::parse_price_matrix( ...$args ); } -function ppom_is_field_has_price( $meta ) { - - $type = isset( $meta['type'] ) ? $meta['type'] : ''; - $has_price = false; - switch ( $type ) { - - case 'selectqty': - case 'vqmatrix': - case 'quantities': - case 'qtypack': - if ( isset( $meta['default_price'] ) && $meta['default_price'] != '' ) { - $has_price = true; - break; - } - - if ( isset( $meta['options'] ) ) { - foreach ( $meta['options'] as $option ) { - if ( isset( $option['price'] ) && $option['price'] != '' ) { - $has_price = true; - break; - } - } - } - break; - - case 'eventcalendar': - if ( isset( $meta['options'] ) ) { - foreach ( $meta['options'] as $option ) { - if ( isset( $option['price'] ) && $option['price'] != '' ) { - $has_price = true; - break; - } - } - } - break; - - case 'file': - if ( isset( $meta['file_cost'] ) && $meta['file_cost'] != '' ) { - $has_price = true; - } - break; - - default: - if ( isset( $meta['options'] ) ) { - foreach ( $meta['options'] as $option ) { - if ( isset( $option['price'] ) && $option['price'] != '' ) { - $has_price = true; - break; - } - } - } - break; - - } - - return apply_filters( 'ppom_field_has_price', $has_price, $meta ); +function ppom_is_field_has_price( ...$args ) { + return \PPOM\Pricing\Engine::is_field_has_price( ...$args ); } -// Check if product has discount price matrix -// Return percent -function ppom_price_has_discount_matrix( $product, $quantity ) { - - $pricematrix_field = ppom_has_field_by_type( ppom_get_product_id( $product ), 'pricematrix' ); - if ( ! $pricematrix_field ) { - return false; - } - - $matrix_found = ppom_price_matrix_chunk( $product, $pricematrix_field, $quantity ); - // ppom_pa($matrix_found); - - if ( ! isset( $matrix_found['discount'] ) ) { - return false; - } - - return $matrix_found; +function ppom_price_has_discount_matrix( ...$args ) { + return \PPOM\Pricing\Engine::price_has_discount_matrix( ...$args ); } -// Return ammount after apply percent -function ppom_get_amount_after_percentage( $base_amount, $percent ) { - - $base_amount = floatval( $base_amount ); - $percent_amount = 0; - $percent = substr( $percent, 0, - 1 ); - $percent_amount = wc_format_decimal( ( floatval( $percent ) / 100 ) * $base_amount, wc_get_price_decimals() ); - - return $percent_amount; +function ppom_get_amount_after_percentage( ...$args ) { + return \PPOM\Pricing\Engine::get_amount_after_percentage( ...$args ); } -// Checking the price matrix -function ppom_price_check_price_matrix( $cart_items, $values ) { - - if ( ! isset( $cart_items['ppom'] ) ) { - return $cart_items; - } - // ppom_pa($cart_items); - - $wc_product = $cart_items['data']; - $product_id = ppom_get_product_id( $wc_product ); - - $pricematrix_field = ppom_has_field_by_type( $product_id, 'pricematrix' ); - if ( ! $pricematrix_field ) { - return $cart_items; - } - - $matrix_found = []; - foreach ( $pricematrix_field as $pm ) { - - $pm_dataname = isset( $pm['data_name'] ) ? $pm['data_name'] : ''; - // var_dump($pm_dataname, ppom_is_field_hidden_by_condition( $pm_dataname )); - $conditionally_hidden = $cart_items['ppom']['conditionally_hidden']; - if ( ppom_is_field_hidden_by_condition( $pm_dataname, $conditionally_hidden ) ) { - continue; - } - - $matrix_found = $pm; - break; - } - - // ppom_pa($pm_applied); - // $matrix_found = ppom_extract_matrix_by_quantity($pm_applied, $wc_product, $product_quantity); - $cart_items['ppom']['price_matrix_found'] = apply_filters( 'ppom_price_marix_found', $matrix_found, $cart_items ); - - return $cart_items; +function ppom_price_check_price_matrix( ...$args ) { + return \PPOM\Pricing\Engine::price_check_price_matrix( ...$args ); } -add_filter( 'ppom_option_price_vat', 'ppom_option_price_handle_vat', 9, 2 ); -function ppom_option_price_handle_vat( $option_price, $product ) { - - if ( ! function_exists( 'wc_get_price_excluding_tax' ) || $option_price == '' ) { - return $option_price; - } - - if ( 'yes' != ppom_get_option( 'ppom_taxable_option_price' ) ) { - return $option_price; - } - - 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 = [ - 'price' => $option_price, - 'quantity' => 1, - ]; - if ( $vat_type == 'excl' ) { - $option_price = wc_get_price_excluding_tax( $product, $args ); - } else { - $option_price = wc_get_price_including_tax( $product, $args ); - } - } - - 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 = [ - 'price' => $option_price, - 'quantity' => 1, - ]; - if ( $vat_type == 'excl' ) { - $option_price = wc_get_price_excluding_tax( $product, $args ); - } else { - $option_price = wc_get_price_including_tax( $product, $args ); - } - } +function ppom_option_price_handle_vat( ...$args ) { + return \PPOM\Pricing\Engine::option_price_handle_vat( ...$args ); +} - return $option_price; +function ppom_wwp_product_cart_price( ...$args ) { + return \PPOM\Pricing\Engine::wwp_product_cart_price( ...$args ); } -// Wholesale (WWP) plugin: Cart price handling +add_filter( 'ppom_option_price_vat', 'ppom_option_price_handle_vat', 9, 2 ); add_filter( 'ppom_product_price_on_cart', 'ppom_wwp_product_cart_price', 11, 2 ); -function ppom_wwp_product_cart_price( $product_price, $cart_content ) { - - // Wholesale price - if ( isset( $cart_content['data']->wwp_data['wholesale_priced'] ) && $cart_content['data']->wwp_data['wholesale_priced'] == 'yes' ) { - $product_price = $cart_content['data']->get_price(); - } - - return $product_price; -} diff --git a/inc/rest.class.php b/inc/rest.class.php deleted file mode 100644 index 324cba40..00000000 --- a/inc/rest.class.php +++ /dev/null @@ -1,864 +0,0 @@ - 'GET', - 'callback' => array( $this, 'get_ppom_meta_info_product' ), - 'permission_callback' => '__return_true', - ) - ); - - // getting ppom fields by id - register_rest_route( - 'ppom/v1', - '/get/id/(?P\d+)', - array( - 'methods' => 'GET', - 'callback' => array( $this, 'get_ppom_meta_by_id' ), - 'permission_callback' => '__return_true', - ) - ); - - // setting meta fields about meta against product - register_rest_route( - 'ppom/v1', - '/set/product/', - array( - 'methods' => 'POST', - 'callback' => array( $this, 'ppom_save_meta_product' ), - 'permission_callback' => '__return_true', - ) - ); - - // delete meta fields about meta against product - register_rest_route( - 'ppom/v1', - '/delete/product/', - array( - 'methods' => 'POST', - 'callback' => array( $this, 'delete_ppom_fields_product' ), - 'permission_callback' => '__return_true', - ) - ); - - - // Orders - // getting ppom fields against product - register_rest_route( - 'ppom/v1', - '/get/order/', - array( - 'methods' => 'GET', - 'callback' => array( $this, 'get_ppom_meta_info_order' ), - 'permission_callback' => '__return_true', - ) - ); - - // setting meta fields about meta against product - register_rest_route( - 'ppom/v1', - '/set/order/', - array( - 'methods' => 'POST', - 'callback' => array( $this, 'ppom_update_meta_order' ), - 'permission_callback' => '__return_true', - ) - ); - - // delete meta fields about meta against product - register_rest_route( - 'ppom/v1', - '/delete/order/', - array( - 'methods' => 'POST', - 'callback' => array( $this, 'delete_ppom_fields_order' ), - 'permission_callback' => '__return_true', - ) - ); - } - - - // Getting ppom meta info - function get_ppom_meta_info_product( WP_REST_Request $request ) { - - $this->set_headers(); - - // getting request params: - $product_id = $request->get_param( 'product_id' ); - - $response_info = array(); - if ( $product_id == '' ) { - $response_info = array( - 'status' => 'no_product', - 'message' => __( 'No Product Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - $product_id = intval( $product_id ); - $ppom = new PPOM_Meta( $product_id ); - if ( ! $ppom->is_exists ) { - - $response_info = array( - 'status' => 'no_meta', - 'message' => __( 'No Meta Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - $meta_id = $ppom->single_meta_id; - $ppom_fields = $ppom->fields; - - - $ppom_fields = $this->filter_required_keys_only( $ppom_fields ); - - $response_info = array( - 'status' => 'success', - /* translators: %s: Field Meta ID */ - 'message' => sprintf( __( 'Meta found %s', 'woocommerce-product-addon' ), $meta_id ), - 'meta_id' => intval( $meta_id ), - 'product_id' => $product_id, - 'ppom_fields' => $ppom_fields, - ); - - - // Create the response object - $response = new WP_REST_Response( $response_info ); - - return $response; - } - - function get_ppom_meta_by_id( WP_REST_Request $request ) { - - $this->set_headers(); - - // getting request params: - $ppom_id = $request->get_param( 'id' ); - $ppom_id = intval( $ppom_id ); - - $ppom = new PPOM_Meta(); - $ppom_fields = $ppom->get_fields_by_id( $ppom_id ); - if ( ! $ppom_fields ) { - - $response_info = array( - 'status' => false, - 'message' => __( 'No Meta Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - $ppom_fields = $this->filter_required_keys_only( $ppom_fields ); - - $response_info = array( - 'status' => true, - /* translators: %s: Field Meta ID */ - 'message' => sprintf( __( 'Meta found %s', 'woocommerce-product-addon' ), $ppom_id ), - 'meta_id' => intval( $ppom_id ), - 'ppom_fields' => $ppom_fields, - ); - - - // Create the response object - $response = new WP_REST_Response( $response_info ); - - return $response; - } - - // Save meta against product - // Getting ppom meta info - function ppom_save_meta_product( WP_REST_Request $request ) { - - $this->set_headers(); - - // getting request params: - $product_id = $request->get_param( 'product_id' ); - $secretkey = $request->get_param( 'secret_key' ); - - $all_data = $request->get_params(); - - if ( empty( $all_data['fields'] ) ) { - $response_info = array( - 'status' => 'no_fields', - 'message' => __( 'No fields to save', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - if ( empty( $secretkey ) || ! $this->is_secret_key_valid( $secretkey ) ) { - $response_info = array( - 'status' => 'key_not_valid', - 'message' => __( 'Secret key is not valid', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - $response_info = array(); - if ( $product_id == '' ) { - $response_info = array( - 'status' => 'no_product', - 'message' => __( 'No Product Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - - $product_id = intval( $product_id ); - $ppom = new PPOM_Meta( $product_id ); - $ppom_settings = $ppom->ppom_settings; - - $ppom_fields = json_decode( stripslashes( $all_data['fields'] ), true ); - - $meta_response = array(); - if ( empty( $ppom_settings ) ) { - $meta_response = $this->save_new_meta_data( $product_id, $ppom_fields ); - } else { - $meta_response = $this->update_meta_data( $ppom_settings, $ppom_fields, $product_id ); - } - - - // 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 ) { - - $this->set_headers(); - - // getting request params: - $product_id = $request->get_param( 'product_id' ); - $secretkey = $request->get_param( 'secret_key' ); - - $all_data = $request->get_params(); - - if ( empty( $all_data['fields'] ) ) { - $response_info = array( - 'status' => 'no_fields', - 'message' => __( 'No fields to save', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - if ( empty( $secretkey ) || ! $this->is_secret_key_valid( $secretkey ) ) { - $response_info = array( - 'status' => 'key_not_valid', - 'message' => __( 'Secret key is not valid', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - $response_info = array(); - if ( $product_id == '' ) { - $response_info = array( - 'status' => 'no_product', - 'message' => __( 'No Product Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - $product_id = intval( $product_id ); - $ppom = new PPOM_Meta( $product_id ); - $ppom_settings = $ppom->ppom_settings; - - $delete_fields = json_decode( stripslashes( $all_data['fields'] ) ); - - $meta_response = array(); - $meta_response = $this->delete_meta_data( $ppom_settings, $delete_fields, $product_id ); - - // 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 ) { - - $api_key = ppom_get_option( 'ppom_rest_secret_key', true ); - - $key_valide = false; - - if ( trim( $api_key == $secretkey ) ) { - $key_valide = true; - } - - return $key_valide; - } - - // build new meta entry - function save_new_meta_data( $product_id, $ppom_fields ) { - - $product = new WC_Product( $product_id ); - - $productmeta_name = $product->get_title(); - $productmeta_validation = 'no'; - $dynamic_price_hide = 'no'; - $send_file_attachment = ''; - $show_cart_thumb = 'no'; - $aviary_api_key = ''; - $productmeta_style = ''; - $productmeta_categories = ''; - - $dt = array( - 'productmeta_name' => $productmeta_name, - 'productmeta_validation' => $productmeta_validation, - '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 ), - 'productmeta_style' => $productmeta_style, - 'productmeta_categories' => $productmeta_categories, - 'the_meta' => json_encode( $ppom_fields ), - 'productmeta_created' => current_time( 'mysql' ), - ); - - - $format = array( - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - '%s', - ); - - global $wpdb; - $ppom_table = $wpdb->prefix . PPOM_TABLE_META; - $wpdb->insert( $ppom_table, $dt, $format ); - $res_id = $wpdb->insert_id; - - $ppom_fields = apply_filters( 'ppom_meta_data_saving', $ppom_fields, $res_id ); - // Updating PPOM Meta with ppom_id in each meta array - ppom_admin_update_ppom_meta_only( $res_id, $ppom_fields ); - - $resp = array(); - if ( $res_id ) { - - $resp = array( - 'status' => 'success', - 'meta_id' => $res_id, - 'product_id' => $product_id, - 'fields' => $ppom_fields, - ); - - // Also setting ppom meta to porduct - update_post_meta( $product_id, PPOM_PRODUCT_META_KEY, $res_id ); - } else { - - $resp = array( - 'message' => __( 'No changes found.', 'woocommerce-product-addon' ), - 'status' => 'error', - 'meta_id' => '', - 'product_id' => $product_id, - ); - } - - return $resp; - } - - function update_meta_data( $ppom_meta, $ppom_fields, $product_id ) { - - $existing_fields = json_decode( $ppom_meta->the_meta, true ); - // var_dump($ppom_meta); exit; - - $saved_fields = array(); - $merger_array = array(); - - // First saving new fields - foreach ( $ppom_fields as $new_field ) { - - $merger_array[] = $new_field; - $saved_fields[] = $new_field['data_name']; - } - - // Now checking old fields - foreach ( $existing_fields as $old_field ) { - - if ( ! in_array( $old_field['data_name'], $saved_fields ) ) { - - $merger_array[] = $old_field; - } - } - - $merger_array = apply_filters( 'ppom_meta_data_saving', $merger_array, $ppom_meta->productmeta_id ); - - $data = array( 'the_meta' => json_encode( $merger_array ) ); - $where = array( - 'productmeta_id' => $ppom_meta->productmeta_id, - ); - - $format = array( '%s' ); - $where_format = array( '%d' ); - - global $wpdb; - $ppom_table = $wpdb->prefix . PPOM_TABLE_META; - $rows_effected = $wpdb->update( $ppom_table, $data, $where, $format, $where_format ); - - $resp = array( - 'status' => 'success', - 'meta_id' => $ppom_meta->productmeta_id, - 'product_id' => $product_id, - 'fields' => $merger_array, - ); - - return $resp; - } - - function delete_meta_data( $ppom_meta, $delete_fields, $product_id ) { - - $existing_fields = json_decode( $ppom_meta->the_meta ); - - global $wpdb; - $merger_array = array(); - - // Check if all feilds request exist - if ( in_array( '__all_keys', $delete_fields ) ) { - - // unset product meta key - delete_post_meta( $product_id, PPOM_PRODUCT_META_KEY ); - - // Deleting all fields - $ppom_table = $wpdb->prefix . PPOM_TABLE_META; - $res = $wpdb->query( $wpdb->prepare( "DELETE FROM $ppom_table WHERE productmeta_id = %d", $ppom_meta->productmeta_id ) ); - $delete_fields_resp = array( 'ppom_id' => $ppom_meta->productmeta_id ); - - $resp = array( - 'status' => 'success', - 'meta_id' => $ppom_meta->productmeta_id, - 'product_id' => $product_id, - 'fields' => '', - ); - - return $resp; - } - - - // Only adding those fields which are not deleted - foreach ( $existing_fields as $field ) { - - if ( ! isset( $field->data_name ) ) { - continue; - } - - if ( ! in_array( $field->data_name, $delete_fields ) ) { - - $merger_array[] = $field; - } - } - - - $data = array( 'the_meta' => json_encode( $merger_array ) ); - $where = array( - 'productmeta_id' => $ppom_meta->productmeta_id, - ); - - $format = array( '%s' ); - $where_format = array( '%d' ); - - $ppom_table = $wpdb->prefix . PPOM_TABLE_META; - $rows_effected = $wpdb->update( $ppom_table, $data, $where, $format, $where_format ); - - $resp = array( - 'status' => 'success', - 'meta_id' => $ppom_meta->productmeta_id, - 'product_id' => $product_id, - 'fields' => $merger_array, - ); - - return $resp; - } - - /** - * ==================================================================== - * ========================== ORDERS ================================== - * ==================================================================== - * */ - - // Getting ppom meta info - function get_ppom_meta_info_order( WP_REST_Request $request ) { - - $this->set_headers(); - - // getting request params: - $order_id = $request->get_param( 'order_id' ); - - $order = wc_get_order( $order_id ); - // return new WP_REST_Response( ['items'=>$order] ); - - $response_info = array(); - if ( ! $order ) { - $response_info = array( - 'status' => 'no_order', - 'message' => __( 'No Order Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - - $item_product_meta = $this->get_order_item_meta( $order_id ); - - $response_info = array( - 'status' => 'success', - /* translators: %s: WooCommerce Order ID */ - 'message' => sprintf( __( 'Order found %s', 'woocommerce-product-addon' ), $order_id ), - 'order_items_meta' => $item_product_meta, - ); - - - // Create the response object - $response = new WP_REST_Response( $response_info ); - - return $response; - } - - // update meta against order - function ppom_update_meta_order( WP_REST_Request $request ) { - - $this->set_headers(); - - // getting request params: - $order_id = $request->get_param( 'order_id' ); - $secretkey = $request->get_param( 'secret_key' ); - - $all_data = $request->get_params(); - - - $order = wc_get_order( $order_id ); - - $response_info = array(); - if ( ! $order ) { - $response_info = array( - 'status' => 'no_order', - 'message' => __( 'No Order Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - if ( empty( $all_data['fields'] ) ) { - $response_info = array( - 'status' => 'no_fields', - 'message' => __( 'No meta found to save', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - if ( empty( $secretkey ) || ! $this->is_secret_key_valid( $secretkey ) ) { - $response_info = array( - 'status' => 'key_not_valid', - 'message' => __( 'Secret key is not valid', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - - $item_product_meta = array(); - $order_item_meta = json_decode( stripslashes( $all_data['fields'] ) ); - - // return new WP_REST_Response( $order_item_meta ); - - if ( empty( $order_item_meta ) ) { - $response_info = array( - 'status' => 'fields_not_valid', - 'message' => __( 'Submitted fields are in valid format.', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - - foreach ( $order->get_items() as $item_id => $item_product ) { - - // Get the special meta data in an array: - $product_id = $item_product->get_product_id(); - - foreach ( $item_product->get_meta_data() as $item_meta_data ) { - - foreach ( $order_item_meta as $order_product_id => $item_meta ) { - - // check if product id exists in requested fields - $order_product_id = intval( $order_product_id ); - - if ( $order_product_id == $product_id ) { - - foreach ( $item_meta as $meta_key => $meta_val ) { - - - $scalar_value = $meta_val; - - if ( is_array( $meta_val ) ) { - $scalar_value = json_encode( $meta_val ); - } - - $meta_update_res = wc_update_order_item_meta( $item_id, $meta_key, $scalar_value ); - } - } - } - } - } - - - $item_product_meta = $this->get_order_item_meta( $order_id ); - - $response_info = array( - 'status' => 'success', - /* translators: %s: WooCommerce Order ID */ - 'message' => sprintf( __( 'Order updated %s', 'woocommerce-product-addon' ), $order_id ), - 'order_items_meta' => $item_product_meta, - ); - - return new WP_REST_Response( $response_info ); - } - - /** - * Delete fields against order - * params: - * order_id: integer - * secret_key: string - * fields : array() - **/ - function delete_ppom_fields_order( WP_REST_Request $request ) { - - $this->set_headers(); - - // getting request params: - $order_id = $request->get_param( 'order_id' ); - $secretkey = $request->get_param( 'secret_key' ); - - $all_data = $request->get_params(); - - $order = wc_get_order( $order_id ); - - $response_info = array(); - if ( ! $order ) { - $response_info = array( - 'status' => 'no_order', - 'message' => __( 'No Order Found', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - if ( empty( $all_data['fields'] ) ) { - $response_info = array( - 'status' => 'no_fields', - 'message' => __( 'No fields to delete', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - if ( empty( $secretkey ) || ! $this->is_secret_key_valid( $secretkey ) ) { - $response_info = array( - 'status' => 'key_not_valid', - 'message' => __( 'Secret key is not valid', 'woocommerce-product-addon' ), - ); - - return new WP_REST_Response( $response_info ); - } - - $item_product_meta = array(); - $order_item_meta = json_decode( stripslashes( $all_data['fields'] ) ); - - foreach ( $order->get_items() as $item_id => $item_product ) { - - // Get the special meta data in an array: - $product_id = $item_product->get_product_id(); - - foreach ( $item_product->get_meta_data() as $item_meta_data ) { - - - foreach ( $order_item_meta as $order_product_id => $delete_meta ) { - - // check if product id exists in requested fields - if ( $order_product_id == $product_id ) { - - foreach ( $delete_meta as $meta_key ) { - - wc_delete_order_item_meta( $item_id, $meta_key ); - } - } - } - } - } - - - $item_product_meta = $this->get_order_item_meta( $order_id ); - - $response_info = array( - 'status' => 'success', - /* translators: %s: WooCommerce Order ID */ - 'message' => sprintf( __( 'Order updated %s', 'woocommerce-product-addon' ), $order_id ), - 'order_items_meta' => $item_product_meta, - ); - - return new WP_REST_Response( $response_info ); - - } - - - // Return all order items' meta - function get_order_item_meta( $order_id ) { - - $order = wc_get_order( $order_id ); - - $order_item_meta_data = array(); - - foreach ( $order->get_items() as $item_id => $item_product ) { - - // Get the special meta data in an array: - $product_id = $item_product->get_product_id(); - $ppom_meta_data = $item_product->get_meta( '_ppom_fields' ); - $context = 'api'; - $ppom_meta_ids = null; - $ppom_meta = ppom_generate_cart_meta( $ppom_meta_data, $product_id, $ppom_meta_ids, $context ); - - // getting checkbox/radio/select price detail - $meta_info = array(); - foreach ( $item_product->get_meta_data() as $meta_data ) { - - $formatted_data = array(); - $fields_info = ppom_get_field_meta_by_dataname( $product_id, $meta_data->key ); - if ( ! $fields_info ) { - continue; - } - - $formatted_data['id'] = $meta_data->id; - $formatted_data['key'] = $meta_data->key; - $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']; - } - - $meta_info[] = $formatted_data; - } - - - $order_item_meta_data[] = array( - 'product_id' => $product_id, - 'product_meta_data' => $meta_info, - ); - } - - return $order_item_meta_data; - } - - - function filter_required_keys_only( $ppom_fields ) { - - $new_ppom_fields = array(); - if ( $ppom_fields ) { - - foreach ( $ppom_fields as $field ) { - - $title = isset( $field['title'] ) ? $field['title'] : ''; - $type = isset( $field['type'] ) ? $field['type'] : ''; - $data_name = isset( $field['data_name'] ) ? $field['data_name'] : ''; - $description = isset( $field['description'] ) ? $field['description'] : ''; - $required = isset( $field['required'] ) ? $field['required'] : ''; - $placeholder = isset( $field['placeholder'] ) ? $field['placeholder'] : ''; - - if ( $type == 'imageselect' || $type == 'image' ) { - $options = isset( $field['images'] ) ? $field['images'] : ''; - } else { - $options = isset( $field['options'] ) ? $field['options'] : ''; - } - - $new_ppom_fields[] = apply_filters( - "ppom_rest_field_$type", - array( - 'title' => $title, - 'type' => $type, - 'data_name' => $data_name, - 'description' => $description, - 'required' => $required, - 'placeholder' => $placeholder, - 'options' => $options, - ), - $field, - $ppom_fields - ); - } - } - - return apply_filters( 'ppom_rest_fields', $new_ppom_fields, $ppom_fields ); - } - - // settings headers - public function set_headers() { - - if ( isset( $_SERVER['HTTP_ORIGIN'] ) ) { - header( "Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}" ); - header( 'Access-Control-Allow-Credentials: true' ); - header( 'Access-Control-Max-Age: 86400' ); // cache for 1 day - } - - // Access-Control headers are received during OPTIONS requests - if ( $_SERVER['REQUEST_METHOD'] == 'OPTIONS' ) { - - if ( isset( $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] ) ) { - header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' ); - } - - if ( isset( $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] ) ) { - header( "Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}" ); - } - - exit( 0 ); - } - - } -} - -new PPOM_Rest(); diff --git a/inc/validation.php b/inc/validation.php index 272473cf..0ba47163 100644 --- a/inc/validation.php +++ b/inc/validation.php @@ -1,376 +1,83 @@ array(), - 'class' => array(), - 'type' => array(), - 'id' => array(), - 'dir' => array(), - 'lang' => array(), - 'style' => array(), - 'xml:lang' => array(), - 'src' => array(), - 'alt' => array(), - 'href' => array(), - 'rel' => array(), - 'rev' => array(), - 'target' => array(), - 'novalidate' => array(), - 'type' => array(), - 'value' => array(), - 'name' => array(), - 'tabindex' => array(), - 'action' => array(), - 'method' => array(), - 'for' => array(), - 'width' => array(), - 'height' => array(), - 'data' => array(), - 'title' => array(), - 'onclick' => array(), - 'onchange' => array(), - 'onkeyup' => array(), - 'data-*' => true, // allows all data-* attributes - 'style' => array(), - ); - $allowedposttags['form'] = $allowed_atts; - $allowedposttags['label'] = $allowed_atts; - $allowedposttags['input'] = $allowed_atts; - $allowedposttags['textarea'] = $allowed_atts; - $allowedposttags['iframe'] = $allowed_atts; - $allowedposttags['script'] = $allowed_atts; - $allowedposttags['style'] = $allowed_atts; - $allowedposttags['strong'] = $allowed_atts; - $allowedposttags['small'] = $allowed_atts; - $allowedposttags['table'] = $allowed_atts; - $allowedposttags['span'] = $allowed_atts; - $allowedposttags['abbr'] = $allowed_atts; - $allowedposttags['code'] = $allowed_atts; - $allowedposttags['pre'] = $allowed_atts; - $allowedposttags['div'] = $allowed_atts; - $allowedposttags['img'] = $allowed_atts; - $allowedposttags['h1'] = $allowed_atts; - $allowedposttags['h2'] = $allowed_atts; - $allowedposttags['h3'] = $allowed_atts; - $allowedposttags['h4'] = $allowed_atts; - $allowedposttags['h5'] = $allowed_atts; - $allowedposttags['h6'] = $allowed_atts; - $allowedposttags['ol'] = $allowed_atts; - $allowedposttags['ul'] = $allowed_atts; - $allowedposttags['li'] = $allowed_atts; - $allowedposttags['em'] = $allowed_atts; - $allowedposttags['hr'] = $allowed_atts; - $allowedposttags['br'] = $allowed_atts; - $allowedposttags['tr'] = $allowed_atts; - $allowedposttags['td'] = $allowed_atts; - $allowedposttags['p'] = $allowed_atts; - $allowedposttags['a'] = $allowed_atts; - $allowedposttags['b'] = $allowed_atts; - $allowedposttags['i'] = $allowed_atts; - $allowedposttags['br'] = $allowed_atts; - $allowed_tags = wp_kses_allowed_html( 'post' ); - - return wp_kses( stripslashes_deep( $content ), $allowed_tags ); + return \PPOM\Validation\Validator::esc_html( $content ); } -// sanitization array data before saving data -function ppom_sanitize_array_data( $array ) { - foreach ( $array as $key => &$value ) { - if ( is_array( $value ) ) { - $value = ppom_sanitize_array_data( $value ); - } else { - if ( in_array( $key, ppom_fields_with_html(), true ) ) { - $value = ppom_esc_html( $value ); - } else { - $value = sanitize_text_field( $value ); - } - } - } - - return $array; +/** + * Recursively sanitizes PPOM field definitions before they are stored. + * + * @param array $data Untrusted field-definition array. + * @return array + */ +function ppom_sanitize_array_data( $data ) { + return \PPOM\Validation\Validator::sanitize_array_data( $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' ]; - - return apply_filters( 'ppom_fields_with_html', $have_html ); + return \PPOM\Validation\Validator::fields_with_html(); } /** - * Updates the quantity arguments. + * Applies PPOM quantity limits to WooCommerce quantity input arguments. * * @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() ) { - return $data; - } - - $product_id = $product->get_id(); - $variation_id = 0; - - if ( $product->is_type( 'variation' ) ) { - $product_id = $product->get_parent_id(); - $variation_id = $product->get_id(); - } - - $limits = ppom_get_product_limits( $product_id, $variation_id ); - - // Min qty - if ( $limits['min_qty'] > 0 ) { - - if ( $product->managing_stock() && ! $product->backorders_allowed() && absint( $limits['min_qty'] ) > $product->get_stock_quantity() ) { - $data['min_value'] = $product->get_stock_quantity(); - - } else { - $data['min_value'] = $limits['min_qty']; - } - } - - // Max qty - if ( $limits['max_qty'] > 0 ) { - - if ( $product->managing_stock() && $product->backorders_allowed() ) { - $data['max_value'] = $limits['max_qty']; - - } elseif ( $product->managing_stock() && absint( $limits['max_qty'] ) > $product->get_stock_quantity() ) { - $data['max_value'] = $product->get_stock_quantity(); - - } else { - $data['max_value'] = $limits['max_qty']; - } - } - - // Step - if ( $limits['step'] > 0 ) { - $data['step'] = $limits['step']; - // If both minimum and maximum quantity are set, make sure both are equally divisible by group of quantity. - if ( ( empty( $limits['max_qty'] ) || absint( $limits['max_qty'] ) % absint( $limits['step'] ) === 0 ) && ( empty( $limits['min_qty'] ) || absint( $limits['min_qty'] ) % absint( $limits['step'] ) === 0 ) ) { - $data['step'] = $limits['step']; - } - } - - if ( empty( $limits['min_qty'] ) && ! $product->is_type( 'group' ) && $limits['step'] > 0 && $data['min_value'] <= 1 ) { - $data['min_value'] = $limits['step']; - } - - - return $data; + return \PPOM\Validation\Validator::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. * @param \WC_Product_Variable $variation Variation object. - * - * @return array $data + * @return array */ function ppom_validation_variation_limits( $data, $product, $variation ) { - - if ( ppom_is_client_validation_enabled() ) { - return $data; - } - - $product_id = $product->get_id(); - $variation_id = 0; - - if ( $product->is_type( 'variation' ) ) { - $product_id = $product->get_parent_id(); - $variation_id = $product->get_id(); - } - - $limits = ppom_get_product_limits( $product_id, $variation_id ); - if ( $limits['min_qty'] > 0 ) { - - if ( $product->managing_stock() && ! $product->backorders_allowed() && absint( $limits['min_qty'] ) > $product->get_stock_quantity() ) { - $data['min_qty'] = $product->get_stock_quantity(); - - } else { - $data['min_qty'] = $limits['min_qty']; - } - } - - if ( $limits['max_qty'] > 0 ) { - - if ( $product->managing_stock() && $product->backorders_allowed() ) { - $data['max_qty'] = $limits['max_qty']; - - } elseif ( $product->managing_stock() && absint( $limits['max_qty'] ) > $product->get_stock_quantity() ) { - $data['max_qty'] = $product->get_stock_quantity(); - - } else { - $data['max_qty'] = $limits['max_qty']; - } - } - - if ( $limits['step'] > 0 ) { - $data['step'] = 1; - // If both minimum and maximum quantity are set, make sure both are equally divisible by group of quantity. - if ( ( empty( $limits['max_qty'] ) || absint( $limits['max_qty'] ) % absint( $limits['step'] ) === 0 ) && ( empty( $limits['min_qty'] ) || absint( $limits['min_qty'] ) % absint( $limits['step'] ) === 0 ) ) { - $data['step'] = $limits['step']; - } - } - - if ( empty( $limits['min_qty'] ) && ! $product->is_type( 'group' ) && $limits['step'] > 0 && $data['min_qty'] <= 1 ) { - $data['min_qty'] = $limits['step']; - } - - if ( $limits['input_value'] > 0 ) { - $data['input_value'] = $limits['input_value']; - } - - - return $data; + return \PPOM\Validation\Validator::validation_variation_limits( $data, $product, $variation ); } +/** + * Derives quantity limits from PPOM quantity and price-matrix fields. + * + * @param int $product_id Parent product ID. + * @param int $variation_id Variation ID when resolving variation-specific data. + * @return array + */ function ppom_get_product_limits( $product_id, $variation_id ) { - - $product = wc_get_product( $product_id ); - $ppom = new PPOM_Meta( $product_id ); - - - $min_quantity = 0; - $max_quantity = 0; - $qty_step = 1; - $input_val = - 1; - - $limits['min_qty'] = intval( $min_quantity ); - $limits['max_qty'] = intval( $max_quantity ); - $limits['step'] = intval( $qty_step ); - $limits['input_value'] = $input_val; - - if ( ! $ppom->is_exists ) { - return $limits; - } - - $ppom_matrix_found = ppom_has_field_by_type( $product_id, 'pricematrix' ); - if ( $ppom_matrix_found ) { - foreach ( $ppom_matrix_found as $meta ) { - - // If it is Discount Matrix, do not set min quantity - // if( isset($meta['discount']) && $meta['discount'] == 'on' ) continue; - $options = $meta['options']; - $ranges = ppom_convert_options_to_key_val( $options, $meta, $product ); - - if ( empty( $ranges ) ) { - continue; - } - - $first_range = reset( $ranges ); - $qty_ranges = explode( '-', $first_range['raw'] ); - $min_quantity = $qty_ranges[0]; - } - } - - if ( $ppom_matrix_found ) { - foreach ( $ppom_matrix_found as $meta ) { - - // If it is Discount Matrix, do not set max quantity - if ( isset( $meta['discount'] ) && $meta['discount'] == 'on' ) { - continue; - } - - $options = $meta['options']; - // ppom_pa($options); - $ranges = ppom_convert_options_to_key_val( $options, $meta, $product ); - - if ( empty( $ranges ) ) { - continue; - } - - $last_range = end( $ranges ); - $qty_ranges = explode( '-', $last_range['raw'] ); - $max_quantity = $qty_ranges[1]; - } - } - - // Check min quantity for variations - $ppom_quantities_found = ppom_has_field_by_type( $product_id, 'quantities' ); - if ( $ppom_quantities_found ) { - foreach ( $ppom_quantities_found as $qty ) { - if ( ! $qty['min_qty'] ) { - continue; - } - - if ( $min_quantity < floatval( $qty['min_qty'] ) ) { - $min_quantity = $qty['min_qty']; - } - } - } - - // Step - $last_range = array(); - if ( $ppom_matrix_found ) { - foreach ( $ppom_matrix_found as $meta ) { - - $qty_step = empty( $meta['qty_step'] ) ? 1 : $meta['qty_step']; - } - } - - // Input value - if ( $ppom_matrix_found ) { - - $price_matrix = reset( $ppom_matrix_found ); - // If it is Discount Matrix, do not set min quantity - // if( isset($meta['discount']) && $meta['discount'] == 'on' ) continue; - $options = $price_matrix['options']; - $ranges = ppom_convert_options_to_key_val( $options, $price_matrix, $product ); - if ( ! empty( $ranges ) ) { - $first_range = reset( $ranges ); - $qty_ranges = explode( '-', $first_range['raw'] ); - $input_val = $qty_ranges[0]; - } - } - - $limits['min_qty'] = intval( $min_quantity ); - $limits['max_qty'] = intval( $max_quantity ); - $limits['step'] = intval( $qty_step ); - $limits['input_value'] = $input_val; - - // ppom_pa($limits); - return $limits; + return \PPOM\Validation\Validator::get_product_limits( $product_id, $variation_id ); } /** - * 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; - } - - return $allow_css; + return \PPOM\Validation\Validator::safecss_filter_attr( $allow_css, $css_string ); } + add_filter( 'safecss_filter_attr_allow_css', 'ppom_safecss_filter_attr', 10, 2 ); diff --git a/inc/woocommerce.php b/inc/woocommerce.php index 0734bee1..184a4256 100644 --- a/inc/woocommerce.php +++ b/inc/woocommerce.php @@ -1,1490 +1,21 @@ fields ) { - return ''; - } - - if ( ! $ppom->has_unique_datanames() ) { - - printf( '
' . __( "Some of your fields has duplicated datanames, please fix it", 'woocommerce-product-addon' ) . '
', 'ppom' ); - - return; - } - - - $ppom_box_id = is_array( $ppom->meta_id ) ? implode( '-', $ppom->meta_id ) : $ppom->meta_id; - $ppom_html = '
'; - - if ( ppom_get_price_table_location() === 'before' ) { - $ppom_html .= '
'; - } - - $template_vars = array( - 'ppom_settings' => $ppom->ppom_settings, - 'product' => $product, - 'ppom_fields_meta' => $ppom->fields, - 'ppom_id' => $ppom->meta_id, - 'args' => $args, - ); - ob_start(); - ppom_load_template( 'render-fields.php', $template_vars ); - $ppom_html .= ob_get_clean(); - - if ( ppom_get_price_table_location() === 'after' ) { - $ppom_html .= '
'; - } - - // Clear fix - $ppom_html .= '
'; // Clear fix - $ppom_html .= '
'; // Ends ppom-wrappper - - echo apply_filters( 'ppom_fields_html', $ppom_html, $product ); -} - -// Template Base Callback function -function ppom_woocommerce_inputs_template_base() { - - global $product; - - $product_id = ppom_get_product_id( $product ); - - $args = apply_filters( 'ppom_rendering_template_args', [ 'enable_add_to_cart_id' => false ], $product ); - - ppom_woocommerce_template_base_inputs_rendering( $product_id, $args ); -} - -function ppom_woocommerce_template_base_inputs_rendering( $product_id, $args = null ) { - - $product = wc_get_product( $product_id ); - - // @TODO: have to re-check abou args param for the Form class - $form_obj = new PPOM_Form( $product, $args ); - - // Check if PPOM fields is empty - if ( ! $form_obj->has_ppom_fields() ) { - return ''; - } - - $ppom_html = ''; - $template_vars = [ 'form_obj' => $form_obj ]; - - ob_start(); - ppom_load_input_templates( 'frontend/ppom-fields.php', $template_vars ); - $ppom_html .= ob_get_clean(); - - echo apply_filters( 'ppom_fields_html', $ppom_html, $product ); -} - -function ppom_woocommerce_load_scripts() { - - if ( ! is_product() ) { - return ''; - } - - global $post; - $product = wc_get_product( $post->ID ); - - $ppom = new PPOM_Meta( $product->get_id() ); - - - if ( ! $ppom->fields ) { - return ''; - } - - // Loading all required scripts/css for inputs like datepicker, fileupload etc - ppom_hooks_load_input_scripts( $product ); - - do_action( 'ppom_after_scripts_loaded', $ppom, $product ); -} - - -function ppom_woocommerce_validate_product( $passed, $product_id, $qty ) { - - $ppom = new PPOM_Meta( $product_id ); - if ( ! $ppom->ajax_validation_enabled ) { - $passed = ppom_check_validation( $product_id, $_POST ); - } - - 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'] ) - ) { - $error_message = __( 'Sorry, an error has occurred. Please enable JavaScript or contact site owner.', 'woocommerce-product-addon' ); - ppom_wc_add_notice( $error_message ); - $passed = false; - - return $passed; - } - } - - return $passed; -} - -function ppom_woocommerce_ajax_validate() { - - // ppom_pa($_POST); exit; - $ppom_nonce = $_REQUEST['ppom_nonce']; - $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' ) ); - $response = array( - 'status' => 'error', - 'message' => $message, - ); - wp_send_json( $response ); - } - - $errors_found = array(); - - $product_id = intval( $_POST['ppom_product_id'] ); - $passed = ppom_check_validation( $product_id, $_POST ); - - $all_notices = wc_get_notices(); - wc_clear_notices(); - - $response = array(); - if ( ! $passed ) { - ob_start(); - foreach ( $all_notices as $type => $message ) { - - if ( $type != 'error' ) { - continue; - } - wc_get_template( - "notices/{$type}.php", - array( - 'messages' => $message, - ) - ); - } - - $all_notices = wc_kses_notice( ob_get_clean() ); - $response = array( - 'status' => 'error', - 'message' => $all_notices, - ); - } else { - $response = array( 'status' => 'success' ); - } - // $all_notices = '
'.$all_notices.'
'; - // ppom_pa($all_notices); - - wp_send_json( $response ); -} - -function ppom_check_validation( $product_id, $post_data, $passed = true ) { - - $ppom = new PPOM_Meta( $product_id ); - - if ( ! $ppom->fields ) { - return $passed; - } - - $ppom_posted_fields = isset( $post_data['ppom']['fields'] ) ? $post_data['ppom']['fields'] : null; - if ( ! $ppom_posted_fields ) { - return $passed; - } - - foreach ( $ppom->fields as $field ) { - - // ppom_pa($field); - - // Check field Visibility settings - if ( ! ppom_is_field_visible( $field ) ) { - continue; - } - - if ( - empty( $field['data_name'] ) || - ( - ( ! isset( $field['required'] ) || 'on' !== $field['required'] ) && - empty( $field['min_checked'] ) && - empty( $field['max_checked'] ) - ) - ) { - continue; - } - - $passed = apply_filters( 'ppom_before_fields_validation', $passed, $field, $post_data, $product_id ); - - $data_name = sanitize_key( $field['data_name'] ); - - $title = isset( $field['title'] ) ? $field['title'] : ''; - - // var_dump($data_name, ppom_is_field_hidden_by_condition($data_name)); - // Check if field is required by hidden by condition - if ( ppom_is_field_hidden_by_condition( $data_name ) ) { - continue; - } - - if ( ! ppom_has_posted_field_value( $ppom_posted_fields, $field ) ) { - - // Note: Checkbox is being validate by hook: ppom_has_posted_field_value - // $error_message = isset($field['error_message']) ? $field['error_message'] : ''; - // $error_message = (isset($field['error_message']) && $field['error_message'] != '') ? $title.": ".$field['error_message'] : "{$title} is a required field"; - - $error_message = ( isset( $field['error_message'] ) && $field['error_message'] != '' ) - ? sprintf( '%1$s: %2$s', $title, $field['error_message'] ) - : sprintf( - /* translators: %s: the name of the field. */ - __( '%s is a required field', 'woocommerce-product-addon' ), - $title - ); - $error_message = $error_message; - $error_message = stripslashes( $error_message ); - ppom_wc_add_notice( $error_message ); - $passed = false; - } - } - - // ppom_pa($post_data); exit; - - return apply_filters( 'ppom_add_to_cart_validation', $passed, $ppom, $product_id ); -} - - -function ppom_woocommerce_add_cart_item_data( $cart, $product_id ) { - - if ( ! isset( $_POST['ppom'] ) ) { - return $cart; - } - - $ppom = new PPOM_Meta( $product_id ); - if ( ! $ppom->ppom_settings ) { - return $cart; - } - - WC()->cart->remove_cart_item( $_POST['ppom_cart_key'] ); - - // ADDED WC BUNDLES COMPATIBILITY - if ( function_exists( 'wc_pb_is_bundled_cart_item' ) && wc_pb_is_bundled_cart_item( $cart ) ) { - return $cart; - } - - // PPOM also saving cropped images under this filter. - $ppom_posted_fields = apply_filters( 'ppom_add_cart_item_data', $_POST['ppom'], $_POST ); - $cart['ppom'] = $ppom_posted_fields; - - return $cart; -} - -function ppom_woocommerce_update_cart_fees( $cart_items, $values ) { - - if ( empty( $cart_items ) ) { - return $cart_items; - } - - if ( ! isset( $values['ppom']['ppom_option_price'] ) ) { - return $cart_items; - } - - $wc_product = $cart_items['data']; - $product_id = ppom_get_product_id( $wc_product ); - - $ppom_meta_ids = ''; - // removing id field - if ( ! empty( $values ['ppom'] ['fields']['id'] ) ) { - $ppom_meta_ids = $values ['ppom'] ['fields']['id']; - unset( $values ['ppom'] ['fields']['id'] ); - } - - // 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_order_qty = floatval( $cart_items['quantity'] ); - - // Getting option price - $option_prices = json_decode( stripslashes( $values['ppom']['ppom_option_price'] ), true ); - // ppom_pa($option_prices); - $total_option_price = 0; - $ppom_matrix_price = 0; - - $ppom_quantities_price = 0; - $ppom_quantities_usebaseprice = false; - $ppom_quantities_include_base = false; - - $ppom_total_quantities = 0; - $ppom_total_discount = 0; - $ppon_onetime_cost = 0; - $ppomm_measures = 1; // meassure need to be multiple with each so it will be 1 - - - // If quantities field found then we need to get total quantity to get correct matrix price - // if matrix is also used - - if ( $option_prices ) { - foreach ( $option_prices as $option ) { - if ( $option['apply'] == 'quantities' ) { - $ppom_total_quantities += $option['quantity']; - $ppom_item_order_qty = $ppom_total_quantities; - } - } - } - - - // Check if price is set by matrix - $matrix_found = ppom_get_price_matrix_chunk( $wc_product, $option_prices, $ppom_item_order_qty ); - // ppom_pa($matrix_found); - - // Calculating option prices - if ( $option_prices ) { - foreach ( $option_prices as $option ) { - - // Do not add if option is fixed/onetime - // if( $option['apply'] != 'variable' ) continue; - - // ppom_get_field_option_price - - switch ( $option['apply'] ) { - - case 'variable': - $option_price = $option['price']; - // verify prices from server due to security - if ( isset( $option['data_name'] ) && isset( $option['option_id'] ) ) { - - $option_price = ppom_get_field_option_price_by_id( $option, $wc_product, $ppom_meta_ids ); - } - - $total_option_price += wc_format_decimal( $option_price, wc_get_price_decimals() ); - break; - - case 'onetime': - $option_price = $option['price']; - // verify prices from server due to security - if ( isset( $option['data_name'] ) && isset( $option['option_id'] ) ) { - - $option_price = ppom_get_field_option_price_by_id( $option, $wc_product, $ppom_meta_ids ); - } - $ppon_onetime_cost += wc_format_decimal( $option_price, wc_get_price_decimals() ); - break; - - case 'quantities': - $ppom_quantities_use_option_price = apply_filters( 'ppom_quantities_use_option_price', true, $option_prices ); - if ( $ppom_quantities_use_option_price ) { - - $quantity_price = $option['price']; - - // If matrix found now product org price will be set to matrix - if ( ! empty( $matrix_found ) && ! isset( $matrix_found['discount'] ) ) { - - $quantity_price = $matrix_found['price']; - - } - - $ppom_quantities_price += wc_format_decimal( ( $quantity_price * $option['quantity'] ), wc_get_price_decimals() ); - // $ppom_total_quantities += $option['quantity']; - } - - if ( ! empty( $option['include'] ) && $option['include'] == 'on' ) { - $ppom_quantities_include_base = true; - } - break; - - case 'bulkquantity': - // Note: May need to add matrix price like in quantites (above) - - $ppom_quantities_price += wc_format_decimal( ( $option['price'] * $option['quantity'] ), wc_get_price_decimals() ); - $ppom_quantities_price += isset( $option['base'] ) ? $option['base'] : 0; - - if ( isset( $option['usebase_price'] ) && $option['usebase_price'] == 'yes' ) { - $ppom_quantities_usebaseprice = true; - } - break; - - // Fixed price addon - case 'fixedprice': - $ppom_item_org_price = $option['unitprice']; - - // Well, it should NOT be like this but have to do this. will see later. - $ppom_item_order_qty = 1; - break; - - 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']; - - $ppomm_measures *= $measer_qty * $price_multiplier; - - - break; - - } - - - /** - * @since 15.4: Updating options weight - */ - if ( ppom_pro_is_installed() ) { - $option_weight = ppom_get_field_option_weight_by_id( $option, $ppom_meta_ids ); - if ( $option_weight > 0 ) { - $new_weight = $wc_product->get_weight() + $option_weight; - $wc_product->set_weight( $new_weight ); - } - } - } - } - - - // ppom_pa($matrix_found); - if ( ! empty( $matrix_found ) ) { - - // Check that it's not a discount matrix - if ( ! isset( $matrix_found['discount'] ) ) { - $ppom_item_org_price = $matrix_found['price']; - } else { - - // Discount matrix found - if ( ! empty( $matrix_found['percent'] ) ) { - - $total_with_options = $ppom_item_org_price + $total_option_price + $ppon_onetime_cost; - - // Check wheather to apply on Both (Base+Options) or only Base - if ( $matrix_found['discount'] == 'both' ) { - - // Also adding quantities price if used - $total_price_to_be_discount = $total_with_options + $ppom_quantities_price; - - $price_after_precent = ppom_get_amount_after_percentage( $total_price_to_be_discount, $matrix_found['percent'] ); - } elseif ( $matrix_found['discount'] == 'base' ) { - - $total_price_to_be_discount = $ppom_item_org_price + $ppom_quantities_price; - $price_after_precent = ppom_get_amount_after_percentage( $total_price_to_be_discount, $matrix_found['percent'] ); - } - - $ppom_total_discount += $price_after_precent; - } else { - /** - * when discount is in PRICE not Percent then applied to whole price Base+Option) - * so need to get per unit discount - */ - - /* - ** @since 16.8 - ** When each variation has own quantity, then cart quantity is disabled only one price is set - ** not indivisual - **/ - if ( ! $ppom_quantities_usebaseprice ) { - - $ppom_total_discount += $matrix_found['price']; - } else { - $discount_per_unit = $matrix_found['price'] / $ppom_item_order_qty; - $ppom_total_discount += $discount_per_unit; - } - } - } - } - - - if ( $ppom_quantities_price > 0 ) { - - if ( ! $ppom_quantities_include_base ) { - // $ppom_item_org_price = ($ppom_item_org_price * $ppom_total_quantities); - $ppom_item_org_price = 0; - - // when base price is NOT included the quantity is updated so it must be multiplied by options - $total_option_price = ( $total_option_price * $ppom_total_quantities ); - } - } - - // If measures found, Multiply it with options - if ( $ppomm_measures > 0 ) { - // $total_option_price = $total_option_price * $ppomm_measures; - $ppom_item_org_price = $ppom_item_org_price * $ppomm_measures; - } - - - // var_dump($ppom_total_discount); - // var_dump($ppom_item_org_price); - // var_dump($total_option_price); - // var_dump($ppom_quantities_price); - - - $cart_line_total = ( $ppom_item_org_price + $total_option_price + $ppom_quantities_price - $ppom_total_discount ); - - $cart_line_total = apply_filters( 'ppom_cart_line_total', $cart_line_total, $cart_items, $values ); - - $wc_product->set_price( $cart_line_total ); - - return $cart_items; -} - -function ppom_calculate_totals_from_session( $cart ) { - $cart->calculate_totals(); -} - - -function ppom_woocommerce_add_fixed_fee( $cart ) { - - $fee_no = 1; - foreach ( $cart->get_cart() as $item ) { - - if ( empty( $item['ppom']['ppom_option_price'] ) ) { - continue; - } - - // Getting option price - $option_prices = json_decode( stripslashes( $item['ppom']['ppom_option_price'] ), true ); - - if ( $option_prices ) { - foreach ( $option_prices as $fee ) { - - if ( $fee['apply'] != 'onetime' ) { - continue; - } - - - $label = $fee_no . '-' . $fee['product_title'] . ': ' . $fee['label']; - $label = apply_filters( 'ppom_fixed_fee_label', $label, $fee, $item ); - - $taxable = ( isset( $fee['taxable'] ) && $fee['taxable'] == 'on' ) ? true : false; - $fee_price = $fee['price']; - - if ( ! empty( $fee['without_tax'] ) ) { - $fee_price = $fee['without_tax']; - } - - // if( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) { - // $taxable = false; - // } - - $fee_price = apply_filters( 'ppom_cart_fixed_fee', $fee_price, $fee, $cart ); - - if ( $fee_price != 0 ) { - $cart->add_fee( esc_html( $label ), $fee_price, $taxable ); - $fee_no ++; - } - } - } - } -} - -// Show fixed fee in mini cart -function ppom_woocommerce_mini_cart_fixed_fee() { - - if ( ! WC()->cart->get_fees() ) { - return ''; - } - - $fixed_fee_html = ''; - foreach ( WC()->cart->get_fees() as $fee ) { - - $item_fee = $fee->amount; - if ( WC()->cart->display_prices_including_tax() && $fee->taxable ) { - - $item_fee = $fee->total + $fee->tax; - } - // var_dump($fee); - $fixed_fee_html .= ''; - $fixed_fee_html .= ''; - $fixed_fee_html .= ''; - $fixed_fee_html .= ''; - } - - $fixed_fee_html .= ''; - $fixed_fee_html .= '
' . esc_html( $fee->name ); - '' . wc_price( $item_fee ) . '
' . __( 'Total will be calculated in the cart', 'woocommerce-product-addon' ) . '
'; - - echo apply_filters( 'ppom_mini_cart_fixed_fee', $fixed_fee_html ); -} - -function ppom_woocommerce_add_item_meta( $item_meta, $cart_item ) { - - if ( ! isset( $cart_item['ppom']['fields'] ) ) { - return $item_meta; - } - - - // ADDED WC BUNDLES COMPATIBILITY - if ( function_exists( 'wc_pb_is_bundled_cart_item' ) && wc_pb_is_bundled_cart_item( $cart_item ) ) { - return $item_meta; - } - - $ppom_meta = ppom_make_meta_data( $cart_item ); - // ppom_pa($ppom_meta); - - foreach ( $ppom_meta as $key => $meta ) { - - $hidden = isset( $meta['hidden'] ) ? $meta['hidden'] : false; - $meta_name = isset( $meta['name'] ) ? $meta['name'] : ''; - $meta_value = isset( $meta['value'] ) ? $meta['value'] : ''; - $display = isset( $meta['display'] ) ? $meta['display'] : $meta_value; - if ( $key == 'ppom_has_quantities' ) { - $hidden = true; - } - - - // If no value - if ( ! $display ) { - continue; - } - - if ( ! empty( $meta_name ) ) { - - if ( apply_filters( 'ppom_show_option_price_cart', false ) && isset( $meta['price'] ) ) { - $meta_value .= ' (' . wc_price( $meta['price'] ) . ')'; - } - - $meta_key = stripslashes( $meta_name ); - - // WPML - $meta_key = ppom_wpml_translate( $meta_key, 'PPOM' ); - - $item_meta[] = array( - 'name' => wp_strip_all_tags( $meta_key ), - 'value' => $meta_value, - 'hidden' => $hidden, - 'display' => $display, - ); - } else { - $item_meta[] = array( - 'name' => ( $key ), - 'value' => $meta, - 'hidden' => $hidden, - 'display' => $display, - ); - } - } - - return $item_meta; -} - -// alter price on shop page if price matrix found -function ppom_woocommerce_alter_price( $price, $product ) { - - $product_id = ppom_get_product_id( $product ); - - if ( class_exists( 'sitepress' ) ) { - $default_lang = apply_filters( 'wpml_default_language', null ); - $product = wc_get_product( apply_filters( 'wpml_object_id', $product->get_id(), 'product', true, $default_lang ) ); - } - - $price_matrix_found = ppom_has_field_by_type( $product_id, 'pricematrix' ); - if ( empty( $price_matrix_found ) && apply_filters( 'ppom_hide_product_price_if_zero', true, $product ) ) { - if ( $product->get_price() <= 0 ) { - return ''; - } - } - - if ( empty( $price_matrix_found ) ) { - return $price; - } - - $from_pice = ''; - $to_price = ''; - - if ( ! in_array( $product->get_type(), array( 'variable', 'grouped', 'external' ) ) ) { - - $price_range = array(); - - foreach ( $price_matrix_found as $meta ) { - - // ppom_pa($meta); - - if ( ! ppom_is_field_visible( $meta ) ) { - continue; - } - - if ( $meta['type'] == 'pricematrix' ) { - - $options = $meta['options']; - $ranges = ppom_convert_options_to_key_val( $options, $meta, $product ); - // ppom_pa($ranges); - - if ( isset( $meta['discount'] ) && $meta['discount'] == 'on' ) { - - $last_discount = end( $ranges ); - $least_price = $last_discount['price']; - - if ( ! empty( $last_discount['percent'] ) ) { - $max_discount = $last_discount['percent']; - $least_price = ppom_get_amount_after_percentage( $product->get_price(), $max_discount ); - } - - $least_price = floatval( $product->get_price() ) - $least_price; - $least_price = wc_format_decimal( $least_price, wc_get_price_decimals() ); - // var_dump($least_price); - $price = wc_price( $least_price ) . '-' . $price; - } else { - - foreach ( $ranges as $range ) { - $price_range[] = $range['price']; - } - - if ( ! empty( $price_range ) ) { - - $from_pice = min( $price_range ); - $to_price = max( $price_range ); - $price = wc_format_price_range( $from_pice, $to_price ); - } - } - } - } - } - - return apply_filters( 'ppom_loop_matrix_price', $price, $from_pice, $to_price ); -} - -/* -function ppom_hide_variation_price_html($show, $parent, $variation) { - - $product_id = $parent->get_id(); - $ppom = new PPOM_Meta( $product_id ); - - if( $ppom->is_exists && $ppom->price_display != 'hide' ) { - $show = false; - } - - return $show; - -}*/ - -// Set default quantity for price matrix -function ppom_woocommerce_product_default_quantity( $args, $product ) { - - if ( ! is_product() ) { - return $args; - } - - $product_id = ppom_get_product_id( $product ); - $ppom = new PPOM_Meta( $product_id ); - if ( ! $ppom->is_exists ) { - return $args; - } - - $ppom_matrix_found = ppom_has_field_by_type( $product_id, 'pricematrix' ); - - if ( $ppom_matrix_found ) { - - $price_matrix = reset( $ppom_matrix_found ); - // If it is Discount Matrix, do not set min quantity - // if( isset($meta['discount']) && $meta['discount'] == 'on' ) continue; - $options = $price_matrix['options']; - $ranges = ppom_convert_options_to_key_val( $options, $price_matrix, $product ); - if ( ! empty( $ranges ) ) { - $first_range = reset( $ranges ); - $qty_ranges = explode( '-', $first_range['raw'] ); - $args['input_value'] = $qty_ranges[0]; - } - } - - return $args; -} - -// Set min quantity for price matrix -function ppom_woocommerce_set_min_quantity( $min_quantity, $product ) { - - $product_id = ppom_get_product_id( $product ); - $ppom = new PPOM_Meta( $product_id ); - if ( ! $ppom->is_exists ) { - return $min_quantity; - } - - $ppom_matrix_found = ppom_has_field_by_type( $product_id, 'pricematrix' ); - if ( $ppom_matrix_found ) { - foreach ( $ppom_matrix_found as $meta ) { - - // If it is Discount Matrix, do not set min quantity - // if( isset($meta['discount']) && $meta['discount'] == 'on' ) continue; - $options = $meta['options']; - $ranges = ppom_convert_options_to_key_val( $options, $meta, $product ); - - if ( empty( $ranges ) ) { - continue; - } - - $first_range = reset( $ranges ); - $qty_ranges = explode( '-', $first_range['raw'] ); - $min_quantity = $qty_ranges[0]; - } - } - - // Check min quantity for variations - $ppom_quantities_found = ppom_has_field_by_type( $product_id, 'quantities' ); - if ( $ppom_quantities_found ) { - foreach ( $ppom_quantities_found as $qty ) { - if ( ! $qty['min_qty'] ) { - continue; - } - - if ( $min_quantity < floatval( $qty['min_qty'] ) ) { - $min_quantity = $qty['min_qty']; - } - } - } - - return $min_quantity; -} - -// Set max quantity for price matrix -function ppom_woocommerce_set_max_quantity( $max_quantity, $product ) { - - $product_id = ppom_get_product_id( $product ); - $ppom = new PPOM_Meta( $product_id ); - if ( ! $ppom->is_exists ) { - return $max_quantity; - } - - $last_range = array(); - - $ppom_matrix_found = ppom_has_field_by_type( $product_id, 'pricematrix' ); - - if ( $ppom_matrix_found ) { - foreach ( $ppom_matrix_found as $meta ) { - - // If it is Discount Matrix, do not set max quantity - if ( isset( $meta['discount'] ) && $meta['discount'] == 'on' ) { - continue; - } - - $options = $meta['options']; - // ppom_pa($options); - $ranges = ppom_convert_options_to_key_val( $options, $meta, $product ); - - if ( empty( $ranges ) ) { - continue; - } - - $last_range = end( $ranges ); - $qty_ranges = explode( '-', $last_range['raw'] ); - $max_quantity = $qty_ranges[1]; - } - } - - // Check min quantity for variations - $ppom_quantities_found = ppom_has_field_by_type( $product_id, 'quantities' ); - if ( $ppom_quantities_found ) { - foreach ( $ppom_quantities_found as $qty ) { - if ( ! $qty['max_qty'] ) { - continue; - } - - if ( $max_quantity < floatval( $qty['max_qty'] ) ) { - $max_quantity = $qty['max_qty']; - } - } - } - - return $max_quantity; -} - -// Set quantity step for price matrix -function ppom_woocommerce_set_quantity_step( $quantity_step, $product ) { - - $product_id = ppom_get_product_id( $product ); - $ppom = new PPOM_Meta( $product_id ); - if ( ! $ppom->is_exists ) { - return $quantity_step; - } - - $last_range = array(); - - $ppom_matrix_found = ppom_has_field_by_type( $product_id, 'pricematrix' ); - if ( $ppom_matrix_found ) { - foreach ( $ppom_matrix_found as $meta ) { - - $quantity_step = empty( $meta['qty_step'] ) ? 1 : $meta['qty_step']; - } - } - - return $quantity_step; -} - -// When quantities is used then reset quantity to 1 -function ppom_woocommerce_add_to_cart_quantity( $quantity, $product_id ) { - - if ( ppom_reset_cart_quantity_to_one( $product_id ) ) { - $quantity = 1; - } - - return $quantity; -} - -// It is change cart quantity label -function ppom_woocommerce_control_cart_quantity_legacy( $quantity, $cart_item_key ) { - - $cart_item = WC()->cart->get_cart_item( $cart_item_key ); - - // ppom_pa($cart_item) - if ( ! isset( $cart_item['ppom']['ppom_option_price'] ) && - ! isset( $cart_item['ppom']['ppom_pricematrix'] ) ) { - return $quantity; - } - - // Getting option price - $option_prices = json_decode( stripslashes( $cart_item['ppom']['ppom_option_price'] ), true ); - $ppom_has_quantities = 0; - // ppom_pa($option_prices); - - if ( empty( $option_prices ) ) { - return $quantity; - } - - foreach ( $option_prices as $option ) { - - if ( isset( $option['include'] ) && $option['include'] == '' ) { - if ( isset( $option['quantity'] ) ) { - $ppom_has_quantities += intval( $option['quantity'] ); - } - } elseif ( isset( $option['include'] ) && $option['include'] == 'on' ) { - $ppom_has_quantities = 1; - } - } - - // var_dump($ppom_has_quantities); - // If no quantity updated then return default - $ppom_quantitiles_allow_update_cart = apply_filters( 'ppom_quantities_allow_cart_update', false, $option_prices ); - if ( $ppom_has_quantities != 0 && ! $ppom_quantitiles_allow_update_cart ) { - $quantity = '' . $ppom_has_quantities . ''; - } - - return $quantity; -} - -function ppom_woocommerce_control_cart_quantity( $quantity, $cart_item_key ) { - - $cart_item = WC()->cart->get_cart_item( $cart_item_key ); - - if ( ! isset( $cart_item['ppom']['fields'] ) ) { - return $quantity; - } - - $ppom_fields_post = $cart_item['ppom']['fields']; - $product_id = $cart_item['product_id']; - - if ( ppom_is_cart_quantity_updatable( $product_id ) ) { - return $quantity; - } - - $ppom_has_quantities = ppom_price_get_total_quantities( $ppom_fields_post, $product_id ); - - // var_dump(!$ppom_quantitiles_allow_update_cart); - // If no quantity updated then return default - $ppom_quantitiles_allow_update_cart = apply_filters( 'ppom_quantities_allow_cart_update', false, $ppom_fields_post ); - if ( $ppom_has_quantities != 0 && ! $ppom_quantitiles_allow_update_cart ) { - $quantity = '' . $ppom_has_quantities . ''; - } - - return $quantity; -} - -// Control subtotal when quantities input used. -function ppom_woocommerce_item_subtotal( $item_subtotal, $cart_item, $cart_item_key ) { - - if ( ! isset( $cart_item['ppom']['ppom_option_price'] ) ) { - return $item_subtotal; - } - - // Getting option price. - $option_prices = json_decode( stripslashes( $cart_item['ppom']['ppom_option_price'] ), true ); - - if ( empty( $option_prices ) ) { - return $item_subtotal; - } - - $price = 0; - foreach ( $option_prices as $option ) { - $option = ppom_translation_options( $option ); - $option_price = isset( $option['price'] ) ? $option['price'] : 0; - if ( 0 === $option_price || ( ! isset( $option['apply'] ) || 'onetime' !== $option['apply'] ) ) { - continue; - } - $price = isset( $option['discount'] ) && $option['discount'] > 0 ? $option['discount'] : $option_price; - if ( ! empty( $price ) ) { - $price = apply_filters( 'ppom_option_price', $price ); - $price = floatval( wp_strip_all_tags( $price ) ); - } - } - - if ( 0 === $price ) { - return $item_subtotal; - } - $product_id = $cart_item['product_id']; - $quantity = $cart_item['quantity']; - $product_data = new WC_Product( $product_id ); - $product_price = floatval( $product_data->get_price() ) * $quantity; - $item_subtotal = $product_price + $price; - return ppom_price( $item_subtotal ); -} - -function ppom_woocommerce_control_checkout_quantity( $quantity, $cart_item, $cart_item_key ) { - - // ppom_pa($cart_item); - if ( ! isset( $cart_item['ppom']['fields'] ) ) { - return $quantity; - } - - $ppom_fields_post = $cart_item['ppom']['fields']; - $product_id = $cart_item['product_id']; - - if ( ppom_is_cart_quantity_updatable( $product_id ) ) { - return $quantity; - } - - $ppom_has_quantities = ppom_price_get_total_quantities( $ppom_fields_post, $product_id ); - - // If no quantity updated then return default - if ( $ppom_has_quantities > 0 ) { - $quantity = '' . sprintf( '× %s', $ppom_has_quantities ) . ''; - } - - return $quantity; -} - -function ppom_woocommerce_control_oder_item_quantity( $quantity, $item ) { - - $ppom_has_quantities = 0; - - $product_id = $item->get_product_id(); - - $ppom_fields_post = wc_get_order_item_meta( $item->get_id(), '_ppom_fields' ); - if ( ! isset( $ppom_fields_post['fields'] ) ) { - return $quantity; - } - - $ppom_fields_post = $ppom_fields_post['fields']; - - if ( ppom_is_cart_quantity_updatable( $product_id ) ) { - return $quantity; - } - - $ppom_has_quantities = ppom_price_get_total_quantities( $ppom_fields_post, $product_id ); - - if ( $ppom_has_quantities > 0 ) { - $quantity = '' . sprintf( '× %s', $ppom_has_quantities ) . ''; - } - - return $quantity; -} - -function ppom_woocommerce_control_email_item_quantity( $quantity, $item ) { - - $ppom_has_quantities = 0; - - $product_id = $item->get_product_id(); - - $ppom_fields_post = wc_get_order_item_meta( $item->get_id(), '_ppom_fields' ); - if ( ! isset( $ppom_fields_post['fields'] ) ) { - return $quantity; - } - - $ppom_fields_post = $ppom_fields_post['fields']; - - if ( ppom_is_cart_quantity_updatable( $product_id ) ) { - return $quantity; - } - - $ppom_has_quantities = ppom_price_get_total_quantities( $ppom_fields_post, $product_id ); - - if ( $ppom_has_quantities > 0 ) { - $quantity = '' . esc_html( $ppom_has_quantities ) . ''; - } - - return $quantity; -} - -function ppom_woocommerce_control_order_item_quantity( $quantity, $item ) { - - $ppom_has_quantities = 0; - - $product_id = $item->get_product_id(); - - $ppom_fields_post = wc_get_order_item_meta( $item->get_id(), '_ppom_fields' ); - if ( ! isset( $ppom_fields_post['fields'] ) ) { - return $quantity; - } - - $ppom_fields_post = $ppom_fields_post['fields']; - - if ( ppom_is_cart_quantity_updatable( $product_id ) ) { - return $quantity; - } - - $ppom_has_quantities = ppom_price_get_total_quantities( $ppom_fields_post, $product_id ); - - if ( $ppom_has_quantities > 0 ) { - $quantity = $ppom_has_quantities; - } - - return $quantity; -} - -function ppom_woocommerce_cart_update_validate( $cart_validated, $cart_item_key, $values, $quantity ) { - - $max_quantity = ppom_get_cart_item_max_quantity( $values ); - - if ( ! is_null( $max_quantity ) && $quantity > intval( $max_quantity ) ) { - - $cart_validated = false; - wc_add_notice( - sprintf( - // translators: %d: the number of maximum quantity. - __( 'Sorry, maximum quantity is %d.', 'woocommerce-product-addon' ), - $max_quantity - ), - 'error' - ); - } - - return $cart_validated; -} - - -function ppom_woocommerce_order_item_meta( $item, $cart_item_key, $values, $order ) { - - if ( ! isset( $values ['ppom']['fields'] ) ) { - return; - } - // ADDED WC BUNDLES COMPATIBILITY - if ( function_exists( 'wc_pb_is_bundled_cart_item' ) && wc_pb_is_bundled_cart_item( $values ) ) { - return; - } - - $ppom_meta = ppom_make_meta_data( $values, 'order' ); - // ppom_pa($item->get_product_id()); exit; - - $cropper_fields = []; - foreach ( $ppom_meta as $key => $meta ) { - - if ( ! isset( $meta['value'] ) ) { - continue; - } - - // WPML - $meta_key = ppom_wpml_translate( $key, 'PPOM' ); - - $meta_value = isset( $meta['display'] ) ? $meta['display'] : $meta['value']; - $item->update_meta_data( $key, $meta_value ); - - // Since 24.5: Removing the image cropper base64 data - // Reason: https://clients.najeebmedia.com/forums/topic/order-search-in-woocommerce-not-working/ - $meta = ppom_get_field_meta_by_dataname( $item->get_product_id(), $key ); - if ( isset( $meta['type'] ) && $meta['type'] == 'cropper' ) { - $cropper_fields[] = $key; - } - } - - $no_base64 = array_diff_key( $values ['ppom']['fields'], array_flip( $cropper_fields ) ); - $values['ppom']['fields'] = $no_base64; - - // Since 15.2, saving all fields as another meta - $item->update_meta_data( '_ppom_fields', $values ['ppom'] ); -} - -// Changing order item meta key to label -function ppom_woocommerce_order_key( $display_key, $meta, $item ) { - - if ( $item->get_type() != 'line_item' ) { - return $display_key; - } - - $field_meta = ppom_get_field_meta_by_dataname( $item->get_product_id(), $display_key ); - if ( isset( $field_meta['title'] ) && $field_meta['title'] != '' ) { - $display_key = stripslashes( $field_meta['title'] ); - } - - return $display_key; -} - -function ppom_woocommerce_order_value( $display_value, $meta = null, $item = null ) { - - if ( is_null( $item ) ) { - return $display_value; - } - - if ( $item->get_type() != 'line_item' ) { - return $display_value; - } - - $field_meta = ppom_get_field_meta_by_dataname( $item->get_product_id(), $meta->key ); - - // if( ! isset($field_meta['type']) ) return $display_value; - - $input_type = isset( $field_meta['type'] ) ? $field_meta['type'] : ''; - - switch ( $input_type ) { - - case 'file': - case 'cropper': - /** - * File upload and croppers now save only filename in meta - * seperated by commas, now here we will build it's html to show thumbs in item orde - * - * @since: 10.10 - */ - $display_value = ppom_generate_html_for_files( $meta->value, $input_type, $item ); - break; - - case 'image': - $display_value = $meta->value; - break; - - default: - // Important hook: changing order value format using local hooks - // Also being used for export order lite - $display_value = apply_filters( 'ppom_order_display_value', $display_value, $meta, $item ); - break; - - } - - return $display_value; -} - - -// Hiding some ppom meta like ppom_has_quantities -function ppom_woocommerce_hide_order_meta( $formatted_meta, $order_item ) { - - if ( empty( $formatted_meta ) ) { - return $formatted_meta; - } - - $ppom_meta_searching = $formatted_meta; - // ppom_has_quantities - foreach ( $ppom_meta_searching as $meta_id => $meta_data ) { - - if ( $meta_data->key == 'ppom_has_quantities' ) { - unset( $formatted_meta[ $meta_id ] ); - } - } - - return $formatted_meta; -} - -// When order paid update filename with order number -function ppom_woocommerce_rename_files( $order_id, $posted_data, $order ) { - - global $woocommerce; - - // getting product id in cart - $cart = WC()->cart->get_cart(); - - // ppom_pa($cart); exit; - - - // since 8.1, files will be send to email as attachment - - // ppom_pa($cart); exit; - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - - // ppom_pa($cart_item); exit; - if ( ! isset( $cart_item['ppom']['fields'] ) ) { - continue; - } - - $product_id = $cart_item['product_id']; - $all_moved_files = array(); - - foreach ( $cart_item['ppom']['fields'] as $key => $values ) { - - if ( $key == 'id' ) { - continue; - } - - $field_meta = ppom_get_field_meta_by_dataname( $product_id, $key ); - if ( ! $field_meta ) { - continue; - } - - $field_type = $field_meta['type']; - $field_label = isset( $field_meta['title'] ) ? $field_meta['title'] : $field_meta['data_name']; - $moved_files = array(); - - if ( $field_type == 'file' || $field_type == 'cropper' ) { - - $base_dir_path = ppom_get_dir_path(); - $confirm_dir = 'confirmed/' . $order_id; - $confirmed_dir_path = ppom_get_dir_path( $confirm_dir ); - $edits_dir_path = ppom_get_dir_path( 'edits' ); - - foreach ( $values as $file_id => $file_data ) { - if ( ! isset( $file_data['org'] ) ) { - continue; - } - $file_name = $file_data['org']; - $file_cropped = isset( $file_data['cropped'] ) ? true : false; - - $new_filename = ppom_file_get_name( $file_name, $product_id, $cart_item ); - $source_file = $base_dir_path . $file_name; - $destination_path = $confirmed_dir_path . $new_filename; - - - if ( file_exists( $destination_path ) ) { - break; - } - - /* - $moved_files[] = array('path' => $destination_path, - 'file_name' => $file_name, - 'product_id' => $product_id);*/ - - if ( file_exists( $source_file ) ) { - - if ( ! rename( $source_file, $destination_path ) ) { - die( 'Error while re-naming order image ' . $source_file ); - } - } - - // renaming edited files - $source_file_edit = $edits_dir_path . $file_name; - $destination_path_edit = ''; - - $file_edited = false; - if ( file_exists( $source_file_edit ) ) { - - $destination_path_edit = $edits_dir_path . $new_filename; - if ( ! rename( $source_file_edit, $destination_path_edit ) ) { - die( 'Error while re-naming order image ' . $source_file_edit ); - } else { - $file_edited = true; - } - } - - $moved_files[] = array( - 'path' => $destination_path, - 'file_name' => $file_name, - 'file_label' => $field_label, - 'file_cropped' => $file_cropped, - 'file_edited' => $file_edited, - 'file_edit_path' => $destination_path_edit, - 'product_id' => $product_id, - 'field_name' => $key, - ); - - // $moved_files['file_edited'] = $file_edited; - } - - $all_moved_files[ $key ] = $moved_files; - } - } - - do_action( 'ppom_after_files_moved', $all_moved_files, $order_id, $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 \WC_Order_Item_Product $item Order Item Product - * @param \WC_Order $order - * @return void - */ -function ppom_wc_order_again_compatibility( $cart_item_data, $item, $order ) { - $ppom_data = $item->get_meta('_ppom_fields'); - - if( is_array($ppom_data) && array_key_exists( 'fields', $ppom_data ) ) { - $cart_item_data['ppom'] = $ppom_data; - } - - return $cart_item_data; -} - -/** - * Outputs the formatted meta data for WooCommerce order items. - * - * @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(); - $meta_item_html = ''; - $output_args = apply_filters( 'ppom_woocommerce_item_meta_args', - array( - 'before' => '
  • ', - 'after' => '
', - 'separator' => '
  • ', - 'label_before' => '', - 'label_after' => ': ', - ) - ); - foreach ( $formatted_meta as $meta ) { - $strings[] = $output_args['label_before'] . wp_kses_post( $meta->display_key ) . $output_args['label_after'] . ppom_woocommerce_order_value( $meta->display_value, $meta, $item ); - } - - if ( $strings ) { - $meta_item_html = $output_args['before'] . implode( $output_args['separator'], $strings ) . $output_args['after']; - } - echo wp_kses_post( $meta_item_html ); -} - -/** - * Check if the email improvements feature is enabled. - * - * @return bool - */ -function ppom_wc_email_improvements_enabled() { - return 'yes' === get_option( 'woocommerce_feature_email_improvements_enabled', 'no' ); -} - -/** - * Outputs the formatted meta data for invoice or packing slips. - * - * @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. - */ -function ppom_invoice_packing_slips_html( $html, $item, $args = array() ) { - $strings = array(); - $args = wp_parse_args( - $args, - array( - 'before' => '
    • ', - 'after' => '
    ', - 'separator' => '
  • ', - 'echo' => true, - 'autop' => false, - 'label_before' => '', - 'label_after' => ': ', - ) - ); - - foreach ( $item->get_all_formatted_meta_data() as $meta_id => $meta ) { - $meta_value = ppom_woocommerce_order_value( $meta->display_value, $meta, $item ); - $value = $args['autop'] ? wp_kses_post( $meta_value ) : wp_kses_post( make_clickable( trim( $meta_value ) ) ); - $strings[] = $args['label_before'] . wp_kses_post( $meta->display_key ) . $args['label_after'] . $value; - } - - if ( $strings ) { - $html = $args['before'] . implode( $args['separator'], $strings ) . $args['after']; - } - - return $html; -} \ No newline at end of file +require_once __DIR__ . '/woocommerce/product.php'; +require_once __DIR__ . '/woocommerce/catalog.php'; +require_once __DIR__ . '/woocommerce/cart.php'; +require_once __DIR__ . '/woocommerce/order.php'; diff --git a/inc/woocommerce/cart.php b/inc/woocommerce/cart.php new file mode 100644 index 00000000..f7e0d8ba --- /dev/null +++ b/inc/woocommerce/cart.php @@ -0,0 +1,71 @@ +} + * 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"]').removeClass('ppom_handle_fields_tab').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 += '' + data_name + ''; - html += '' + type + ''; - html += '' + title + ''; - html += '' + placeholder + ''; - html += '' + _ok + ''; - 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 add_cond_selector = clone_new_field.find('.ppom-conditional-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(); - 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) { - - 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); - 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) { - - 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) { - 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, - 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 += '' + data_name + ''; + html += '' + type + ''; + html += '' + title + ''; + html += '' + placeholder + ''; + html += '' + _ok + ''; + 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'; + } + } ); + } ); + +/** + * PPOM_Template_Wizard + * + * Behavior for the Quick Setup template-library modal rendered by + * templates/admin/template-wizard.php. Handles three click flows on + * #ppom-template-wizard-modal: + * - "Start from scratch" card β†’ navigate to data-href on the wrapper. + * - Free / unlocked Pro template tile β†’ POST ppom_import_template, + * redirect to the new field-group editor on success. + * - Locked Pro template tile β†’ no-op (the inline upgrade link handles + * the upsell on its own). + * + * Styles for the modal live in css/ppom-admin.css under the + * "Quick Setup template-library wizard" section. + */ +( function ( $ ) { + 'use strict'; + + var $modal = $( '#ppom-template-wizard-modal' ); + + if ( ! $modal.length ) { + return; + } + + var defaultErrorMsg = ( window.ppom_vars && window.ppom_vars.i18n && window.ppom_vars.i18n.errorOccurred ) + ? window.ppom_vars.i18n.errorOccurred + : 'An error occurred. Please try again.'; + + $modal.on( 'click', '.ppom-template-card--scratch', function () { + var href = $( this ).data( 'href' ); + if ( href ) { + window.location.href = href; + } + } ); + + $modal.on( 'click', '.ppom-template-tile:not(.ppom-template-locked)', function ( e ) { + e.preventDefault(); + + var $btn = $( this ); + var $card = $btn.closest( '.ppom-template-card' ).length ? $btn.closest( '.ppom-template-card' ) : $btn; + var slug = $btn.data( 'template' ); + var nonce = $modal.find( '[name="ppom_import_template_nonce"]' ).val(); + + if ( ! slug || $card.hasClass( 'is-busy' ) ) { + return; + } + + $card.addClass( 'is-busy' ); + + $.post( ajaxurl, { + action: 'ppom_import_template', + template: slug, + ppom_import_template_nonce: nonce, + } ).done( function ( response ) { + if ( response && 'success' === response.status && response.redirect_to ) { + window.location.href = response.redirect_to; + return; + } + $card.removeClass( 'is-busy' ); + window.alert( response && response.message ? response.message : defaultErrorMsg ); + } ).fail( function () { + $card.removeClass( 'is-busy' ); + window.alert( defaultErrorMsg ); + } ); + } ); +} )( jQuery ); 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'); - - $('', { - '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..e598b0fd 100644 --- a/js/admin/ppom-meta-table.js +++ b/js/admin/ppom-meta-table.js @@ -1,199 +1,249 @@ -"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({ - title: window?.ppom_vars?.i18n.popup.confirmTitle, + } ); + 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 + * @param {string} checkedProducts_names + * @return {void} + */ + function deleteSelectedProducts( checkedProducts_ids, checkedProducts_names = '' ) { + let title = window?.ppom_vars?.i18n.popup.deleteGroup; + title = checkedProducts_names ? title.replace( '%s', checkedProducts_names ) : window?.ppom_vars?.i18n.popup.confirmTitle; + window?.ppomPopup?.open( { + title: title, 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' ); + let title = window?.ppom_vars?.i18n.popup.deleteGroup; + const productName = $( this ).data( 'name' ); + title = productName ? title.replace( '%s', productName ) : window?.ppom_vars?.i18n.popup.confirmTitle; - window?.ppomPopup?.open({ - title: window?.ppom_vars?.i18n.popup.confirmTitle, + window?.ppomPopup?.open( { + title: title, 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(); + const checkedProductsNames = $( '.ppom_product_checkbox:checked' ) + .map( function () { + return this.dataset.name && this.dataset.name.trim() !== '' + ? this.dataset.name + : this.value; + } ) + .get() + .join( ', ' ); + // 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, checkedProductsNames ); + } 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 0524ffc4..5da7e2bf 100644 --- a/js/file-upload.js +++ b/js/file-upload.js @@ -1,703 +1,980 @@ /** - * 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; - - 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); - // 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'); - $(croppie_dom).croppie('destroy'); - const viewport = {'width': v_width, 'height': v_height}; - 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) { - - 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', '#') - .html('Change image') - .appendTo(cropp_preview_container) - .click(function(e){ - e.preventDefault(); - location.reload(); - }); - - - // 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 container_width = jQuery( + '#ppom-file-container-' + file_name + ).width(); + 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; + + file_list_preview_containers[ file_name ].container_width = + container_width; + 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; + } + + const preview = file_list_preview_containers[ file_name ]; + let applied = option; + if ( + preview.container_width !== undefined && + preview.container_width !== null + ) { + applied = get_responsive_croppie_options( + option, + preview.container_width + ); + } + // console.log($filelist_DIV[file_name]['croppie'][image_id]); + preview.croppie[ image_id ].croppie( applied ); + } + } ); } // 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_data_name = file_input.data_name; - if ( plupload_instances[file_data_name] !== undefined ) { - return; - } - - 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-' + file_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 ((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 { - - 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)) { - plupload_instances[file_data_name].stop(); - plupload_instances[file_data_name].removeFile(file); - alert(img_dim_errormsg); - } - else if (img_height >= parseFloat(file_input.max_img_h) || img_height <= parseFloat(file_input.min_img_h)) { - plupload_instances[file_data_name].stop(); - plupload_instances[file_data_name].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); - setTimeout('plupload_instances[\'' + file_data_name + '\'].start()', 100); - } - }; - 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); - setTimeout('plupload_instances[\'' + file_data_name + '\'].start()', 100); - } - - - // 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() - }); - - - // 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 ) { + if ( -600 === err.code && file_input.file_size ) { + alert( ppom_file_vars.max_file_size.replace( '%s', file_input.file_size.toUpperCase() ) ); + } else if ( -601 === err.code && file_input.file_types ) { + alert( ppom_file_vars.invalid_file_type); + } else if ( -602 === err.code ) { + alert( ppom_file_vars.duplicate_file ); + } else { + alert( 'Error #' + 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 ]; +} + +// 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 ] ); + } ); + } ); } -// 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]); - - }); - }); +/** + * Scale Croppie boundary/viewport when the field container is narrower than + * configured dimensions (responsive cropper). + * + * @param {Object} baseOptions Croppie options object for the field. + * @param {number} max_width Container width in pixels. + * @return {Object} Options safe to pass to .croppie() without mutating globals. + */ +function get_responsive_croppie_options( baseOptions, max_width ) { + const boundary = baseOptions.boundary || {}; + + const boundaryWidthOriginal = Number( boundary.width ); + const boundaryHeightOriginal = Number( boundary.height ); + if ( + ! Number.isFinite( boundaryWidthOriginal ) || + ! Number.isFinite( boundaryHeightOriginal ) || + boundaryWidthOriginal <= 0 || + boundaryHeightOriginal <= 0 + ) { + return baseOptions; + } + if ( max_width >= boundaryWidthOriginal ) { + return baseOptions; + } + if ( ! Number.isFinite( max_width ) ) { + return baseOptions; + } + const aspectRatio = boundaryHeightOriginal / boundaryWidthOriginal || 1; + const boundaryWidth = Math.floor( max_width ); + const boundaryHeight = Math.floor( boundaryWidth * aspectRatio ); + + const result = { + ...baseOptions, + boundary: { + ...baseOptions.boundary, + width: boundaryWidth, + height: boundaryHeight, + }, + }; + + if ( + baseOptions.viewport && + baseOptions.viewport.width && + baseOptions.viewport.height + ) { + const scale = boundaryWidth / boundaryWidthOriginal; + const viewportWidth = Math.floor( baseOptions.viewport.width * scale ); + const viewportHeight = Math.floor( baseOptions.viewport.height * scale ); + + result.viewport = { + ...baseOptions.viewport, + width: Math.min( viewportWidth, boundaryWidth - 2 ), + height: Math.min( viewportHeight, boundaryHeight - 2 ), + }; + } + + return result; } 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 = $('