diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..8c7b6ef3 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,72 @@ +/** + * ESLint for OnePress theme. + * @wordpress/scripts `lint:js` only picks up `.eslintrc.js` (not `.eslintrc.cjs`). + * Webpack production build does not run ESLint. + */ +module.exports = { + root: true, + extends: [ 'plugin:@wordpress/eslint-plugin/recommended' ], + env: { + browser: true, + es2021: true, + jquery: true, + }, + globals: { + C_Icon_Picker: 'readonly', + Color: 'readonly', + QTags: 'readonly', + _: 'readonly', + switchEditors: 'readonly', + tinymce: 'readonly', + tinyMCEPreInit: 'readonly', + wp: 'readonly', + }, + ignorePatterns: [ + '**/node_modules/**', + '**/assets/**/*.js', + '**/vendor/**', + ], + overrides: [ + { + files: [ 'src/admin/**/*.js', 'src/admin/**/*.jsx' ], + globals: { + ONEPRESS_CUSTOMIZER_DATA: 'readonly', + _wpCustomizeSettings: 'readonly', + _wpEditor: 'readonly', + onepress_customizer_settings: 'readonly', + quicktags: 'readonly', + tinyMCE: 'readonly', + }, + rules: { + '@wordpress/no-unused-vars-before-return': 'off', + 'array-callback-return': 'off', + 'dot-notation': 'off', + eqeqeq: 'off', + 'jsdoc/check-tag-names': 'off', + 'jsdoc/no-undefined-types': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-type': 'off', + 'no-alert': 'off', + 'no-cond-assign': 'off', + 'no-else-return': 'off', + 'no-implicit-globals': 'off', + 'no-lonely-if': 'off', + 'no-nested-ternary': 'off', + 'no-shadow': 'off', + 'no-undef': 'off', + 'no-unreachable': 'off', + 'no-unused-vars': 'off', + 'no-var': 'off', + 'object-shorthand': 'off', + 'prefer-const': 'off', + camelcase: 'off', + 'prettier/prettier': 'off', + 'vars-on-top': 'off', + 'jsx-a11y/anchor-has-content': 'off', + 'jsx-a11y/anchor-is-valid': 'off', + 'jsx-a11y/label-has-associated-control': 'off', + 'react-hooks/exhaustive-deps': 'off', + }, + }, + ], +}; diff --git a/.gitignore b/.gitignore index b9ace8ed..0cd29027 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,21 @@ -# Sass cache -.sass-cache - -# Grunt -/node_modules/ -/releases/ -npm-debug.log - -# PhpStorm -.idea - -# macOS -.DS_Store - -# SASS Source Map -*.css.map - -# Dist file -*.zip \ No newline at end of file +# Build output — regenerated by `npm run build` from src/ via webpack. +# See webpack.config.js: output.path = path.resolve(__dirname, "assets") +assets/ + +# Node +/node_modules/ +npm-debug.log + +# Legacy Sass cache +.sass-cache + +# Legacy SASS source maps (now inside assets/, but defensive) +*.css.map + +# IDE / OS +.idea +.DS_Store + +# Release artifacts +/releases/ +*.zip diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..7772721f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# OnePress — Agent Guide + +OnePress is a Bootstrap-4–based, one-page WordPress theme by FameThemes, derived from Underscores. It targets PHP ≥ 7.4 and WP ≤ 6.9, text domain `onepress`. The front page is composed of independently toggleable **sections** wired through the Customizer; the rest of the site uses a conventional `_s`-style template hierarchy. WooCommerce is first-class. + +## ⚠️ Production theme — 60,000+ active installs + +OnePress is published on WordPress.org with **60,000+ active installations**. **Every code change must be evaluated against the installed base.** A "small cleanup" that renames a theme mod, drops a default value, changes default CSS, or removes a hook can silently break tens of thousands of live customer sites — there is no staged rollout and no recall mechanism. + +**Default operating mode is additive-only and conservative:** + +- **Additive-only.** Do not delete, rename, or repurpose any existing PHP function, class, method, template file, section, hook, theme mod, option, post meta, image size, or CSS class shipped in any prior release. Add new code alongside old code. The old code path must keep working with its original behavior — forever, or at minimum until an explicit major-version removal that ships a migration. See [spec-conventions.md → Additive-only mandate](docs/spec-conventions.md#additive-only-mandate). +- **New supersedes old via delegation, not replacement.** When you introduce an improved version of a helper, the old helper stays and either (a) calls the new one with the legacy arguments, or (b) is left untouched and the new helper is what new callers use. Either way: **the old symbol still resolves and still does what it always did.** +- **Do not change defaults** that affect visual output without a back-compat shim that preserves the old behavior on existing sites (use a version-gated migration if you must). +- **Every PR/commit message must state the BC impact** in one line: `BC: none — additive helper`, `BC: none — internal refactor, all old symbols preserved`, `BC: deprecation — old key still read indefinitely`, etc. + +The full BC contract and deprecation patterns are in [spec-conventions.md → Backward Compatibility](docs/spec-conventions.md#backward-compatibility). Read it before any non-trivial change. + +--- + +This file is an **index**. Detailed specs live under [docs/](docs/) — open the one matching your task. + +> Site-level rules (WP Studio CLI, SQLite, do-not-edit-core, etc.) live in [/CLAUDE.md](../../../CLAUDE.md) and [/STUDIO.md](../../../STUDIO.md). The specs below only cover the theme. + +## Spec index + +| Spec | Read when you need to … | +|---|---| +| [spec-architecture.md](docs/spec-architecture.md) | Find where any concern lives — file map with deep links | +| [spec-build.md](docs/spec-build.md) | Understand `npm` scripts, webpack entries, RTL, asset enqueuing, line-ending normalizer plugin | +| [spec-sections.md](docs/spec-sections.md) | Add/modify a front-page section, understand activation state, render flow, dots-nav | +| [spec-customizer.md](docs/spec-customizer.md) | Add a Customizer setting, pick the right custom control or sanitizer, wire selective refresh, register sidebars, theme supports, image sizes | +| [spec-hooks.md](docs/spec-hooks.md) | Look up an action/filter, use loop props, copy a hook recipe | +| [spec-admin.md](docs/spec-admin.md) | Touch the page meta box, the theme dashboard, recommended actions, or block-editor styles | +| [spec-block-editor.md](docs/spec-block-editor.md) | Editor ↔ frontend parity architecture, color palette + font size filters, block-specific styling, theme supports | +| [spec-naming.md](docs/spec-naming.md) | Pick the right name for a function, class, theme mod, hook, CSS class, image size, etc. — also lists known frozen inconsistencies | +| [spec-conventions.md](docs/spec-conventions.md) | Check sanitize/escape rules, i18n, RTL, WC gating, Plus detection, public API stability, additive-only mandate | +| [spec-line-endings.md](docs/spec-line-endings.md) | Audit / fix CRLF — LF-only policy and playbook | +| [spec-commits.md](docs/spec-commits.md) | Commit rules — anatomy, scopes, BC footer, release checklist | + +## Active plans + +| Plan | Status | +|---|---| +| [plan-block-editor-parity.md](docs/plan-block-editor-parity.md) | ✅ Phases 0–4, 6 complete in working tree; pending review for `2.4.0` tag | +| [plan-css-var-integration.md](docs/plan-css-var-integration.md) | 📋 Drafted — one-shot refactor: layout + colors (Customizer→theme.json bridge) + font sizes + font families + spacing; awaits review | + +## First-time orientation (60 seconds) + +1. Read this file + [spec-architecture.md](docs/spec-architecture.md). +2. Before naming **anything new** (function, class, hook, theme mod, CSS class, …): [spec-naming.md](docs/spec-naming.md). +3. If working on the front page: [spec-sections.md](docs/spec-sections.md). +4. If adding settings: [spec-customizer.md](docs/spec-customizer.md). +5. If editing JS/CSS: [spec-build.md](docs/spec-build.md) — never edit `assets/` directly. +6. Before committing: [spec-line-endings.md](docs/spec-line-endings.md) + [spec-commits.md](docs/spec-commits.md). + +## Hard rules (must-know, always) + +- **Treat every change as touching 60,000+ live sites.** See banner above and [spec-conventions.md → Backward Compatibility](docs/spec-conventions.md#backward-compatibility). +- **Additive-only: never delete or remove** an existing public PHP function, class, method, template, hook, theme mod, option, post meta, image size, or CSS class. Add new code; leave old code in place as a working fallback. See [spec-conventions.md → Additive-only mandate](docs/spec-conventions.md#additive-only-mandate). +- **Never rename** any of the above — rename = remove + add, which violates the additive rule. +- **Never change a default value** that alters rendered output without a back-compat shim or version-gated upgrade. +- **Never edit `wp-includes/` or `wp-admin/`** — see [/STUDIO.md](../../../STUDIO.md). +- **Never edit `assets/`** — it's build output. Edit `src/` and rebuild. See [spec-build.md](docs/spec-build.md). +- **Never edit `node_modules/`** — CRLF is normalized at bundle time. See [spec-line-endings.md](docs/spec-line-endings.md). +- **Never use `sed -i`** for normalization — use `perl -i -pe`. See [spec-line-endings.md](docs/spec-line-endings.md). +- **Never `git add -A`** — stage files by name. See [spec-commits.md](docs/spec-commits.md). +- **Never bypass nonces** on admin POST handlers. See [spec-admin.md](docs/spec-admin.md). diff --git a/Gruntfile.js b/Gruntfile.js index c5a55484..cfeebf0e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -73,18 +73,28 @@ module.exports = function (grunt) { "!phpunit.xml.dist", "!*.sh", "!*.map", + "!**/*.map", "!Gruntfile.js", "!package.json", "!.gitignore", "!phpunit.xml", "!README.md", "!sass/**", + "!src/**", "!codesniffer.ruleset.xml", "!vendor/**", "!composer.json", "!composer.lock", "!package-lock.json", "!phpcs.xml.dist", + "!webpack.config.js", + "!.babelrc", + // Since 2.3.18: dev-only documentation should not ship in the + // distributable zip. `docs/` and `plans/` hold spec / plan / + // agent guides; `plan-*.md` anywhere is a development artifact. + "!docs/**", + "!plans/**", + "!**/plan-*.md", ], dest: "onepress/", }, @@ -122,8 +132,6 @@ module.exports = function (grunt) { theme_main: { src: [ "style.css", - "assets/**/*.css", - "src/**/*.scss", ], overwrite: true, replacements: [ @@ -199,12 +207,176 @@ module.exports = function (grunt) { "compress:main", "clean:main", ]); + + /** + * Create or update the `v` GitHub release for this theme. + * + * Behaviour: + * - If `v` already exists on GitHub, the task refreshes the + * release notes (re-extracted from changelog.md) and replaces the + * attached zip via `--clobber`. + * - If `v` doesn't exist, the task creates the release and + * tags HEAD of the current branch. + * + * Notes: + * - Release notes come from the matching `# ` block in + * changelog.md; falls back to "Release " if not found. + * - Requires `gh` CLI authenticated (`gh auth login`). The task + * fails fast with a clear message if `gh` is missing or unauth'd. + * - Push local commits first — the tag will reference HEAD; an + * unpushed HEAD makes the release point at a commit origin + * hasn't seen. + */ + grunt.registerTask( + "gh_release", + "Create/update GitHub release for current version", + function () { + const done = this.async(); + const os = require("os"); + const { execSync } = require("child_process"); + + const version = pkgInfo.version; + const tag = "v" + version; + // Theme zip drops the `v` prefix that the plugin uses + // (see compress:main config above — `onepress-.zip`). + const zip = "onepress-" + version + ".zip"; + + if (!fs.existsSync(zip)) { + grunt.fail.fatal( + "Release zip not found: " + zip + " — run `zipfile` first." + ); + return done(false); + } + + try { + execSync("gh --version", { stdio: ["ignore", "ignore", "ignore"] }); + } catch (e) { + grunt.fail.fatal( + "`gh` CLI not found. Install from https://cli.github.com and run `gh auth login`." + ); + return done(false); + } + + try { + execSync("gh auth status", { stdio: ["ignore", "ignore", "ignore"] }); + } catch (e) { + grunt.fail.fatal( + "`gh` CLI is not authenticated — run `gh auth login`." + ); + return done(false); + } + + // Pull the matching changelog block from changelog.md. + // Match `# 2.3.18` heading up to next `# ` or EOF. + // No /m flag — `$` must mean end-of-string here so the lookahead + // only terminates at the next version header or true EOF. + // `(?:^|\n)` handles the heading anywhere in the file. + let notes = ""; + try { + const changelog = fs.readFileSync("changelog.md", "utf8"); + const versionEsc = version.replace(/\./g, "\\."); + const re = new RegExp( + "(?:^|\\n)#\\s*" + + versionEsc + + "\\s*\\n([\\s\\S]*?)(?=\\n#\\s+[\\w\\.]|$)" + ); + const m = changelog.match(re); + if (m) { + notes = m[1].replace(/\r/g, "").trim(); + } + } catch (e) { + // best-effort; fall through to default notes below + } + if (!notes) { + notes = "Release " + version; + } + + const notesPath = path.join( + os.tmpdir(), + "onepress-gh-release-notes-" + Date.now() + ".md" + ); + fs.writeFileSync(notesPath, notes); + + function shellEscape(s) { + return "'" + String(s).replace(/'/g, "'\\''") + "'"; + } + + function runVisible(cmd) { + grunt.log.writeln("$ " + cmd); + execSync(cmd, { stdio: ["inherit", "inherit", "inherit"] }); + } + + // Probe for existing release. Non-zero exit = doesn't exist. + let exists = false; + try { + execSync( + "gh release view " + shellEscape(tag) + " --json tagName -q .tagName", + { stdio: ["ignore", "pipe", "ignore"] } + ); + exists = true; + } catch (e) { + exists = false; + } + + try { + if (exists) { + grunt.log.writeln( + "Release " + tag + " exists — updating notes + replacing zip." + ); + runVisible( + "gh release edit " + + shellEscape(tag) + + " --notes-file " + + shellEscape(notesPath) + ); + runVisible( + "gh release upload " + + shellEscape(tag) + + " " + + shellEscape(zip) + + " --clobber" + ); + } else { + grunt.log.writeln( + "Release " + tag + " does not exist — creating." + ); + runVisible( + "gh release create " + + shellEscape(tag) + + " " + + shellEscape(zip) + + " --title " + + shellEscape(tag) + + " --notes-file " + + shellEscape(notesPath) + ); + } + grunt.log.ok( + "GitHub release " + tag + " synced (asset: " + zip + ")." + ); + done(); + } catch (e) { + grunt.log.error(String((e && e.message) || e)); + grunt.fail.fatal("`gh` command failed — see output above."); + done(false); + } finally { + try { + fs.unlinkSync(notesPath); + } catch (_) { + /* ignore */ + } + } + } + ); + grunt.registerTask("release", function (ver) { let newVersion = pkgInfo.version; grunt.task.run("shell:build"); grunt.task.run("bumpup:" + newVersion); grunt.task.run("replace"); - + grunt.task.run("zipfile"); + grunt.task.run("gh_release"); + // i18n // grunt.task.run(['addtextdomain', 'makepot']); // re create css file and min diff --git a/archive.php b/archive.php index fa4a798e..b90e3d5d 100644 --- a/archive.php +++ b/archive.php @@ -9,7 +9,8 @@ get_header(); -$layout = onepress_get_layout(); +$layout = onepress_get_layout(); +$blog_loop = onepress_get_blog_posts_loop_layout_config(); ?>
@@ -25,24 +26,45 @@
-
+
- - - - - /* - * Include the Post-Format-specific template for the content. - * If you want to override this in a child theme, then include a file - * called content-___.php (where ___ is the Post Format name) and that will be used instead. - */ - get_template_part( 'template-parts/content', 'list' ); - ?> + +
+ - + + '; + } + /* + * Include the Post-Format-specific template for the content. + * If you want to override this in a child theme, then include a file + * called content-___.php (where ___ is the Post Format name) and that will be used instead. + */ + get_template_part( 'template-parts/content', 'list' ); + if ( $blog_loop['is_grid'] ) { + echo '
'; + } + endwhile; + ?> + + +
+ diff --git a/assets/build/admin/editor-rtl.css b/assets/build/admin/editor-rtl.css deleted file mode 100644 index d700db6a..00000000 --- a/assets/build/admin/editor-rtl.css +++ /dev/null @@ -1,1061 +0,0 @@ -/*!**********************************************************************************************************************************************************************************************************************************************************************************************************************!*\ - !*** css ./node_modules/@wordpress/scripts/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[4].use[1]!./node_modules/@wordpress/scripts/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[4].use[2]!./node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[4].use[3]!./src/frontend/sass/editor.scss ***! - \**********************************************************************************************************************************************************************************************************************************************************************************************************************/ -/*------------------------------ - 2.1 Typography -------------------------------*/ -html, body { - margin: 0; - padding: 0; -} - -html { - box-sizing: border-box; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - font-size: 16px; -} -@media (max-width: 991px) { - html { - font-size: 15px; - } -} -@media (max-width: 767px) { - html { - font-size: 14px; - } -} - -body { - background: #ffffff; -} - -.site { - background: #FFFFFF; - position: relative; -} - -body, button, input, select, textarea { - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: 14px; - font-size: 0.875rem; - line-height: 1.7; - color: #777777; -} - -pre, -code, -input, -textarea { - font: inherit; -} - -::-moz-selection { - background: #000000; - color: #FFFFFF; -} - -::selection { - background: #000000; - color: #FFFFFF; -} - -/*------------------------------ - 2.2 Links -------------------------------*/ -a { - color: #03c4eb; - text-decoration: none; - outline: none; -} -a:hover { - text-decoration: none; - color: #777777; - text-decoration: underline; -} -a:active, a:focus, a:hover { - outline: none; -} - -/*------------------------------ - 2.3 Heading -------------------------------*/ -h1, -h2, -h3, -h4, -h5, -h6 { - clear: both; - font-family: "Raleway", Helvetica, Arial, sans-serif; - font-weight: 600; - margin-bottom: 15px; - margin-bottom: 15px; - margin-bottom: 0.9375rem; - margin-top: 0; - color: #333333; -} -h1 a, -h2 a, -h3 a, -h4 a, -h5 a, -h6 a { - color: #333333; - text-decoration: none; -} - -h1 { - line-height: 1.3; - font-size: 33px; - font-size: 2.0625rem; -} -@media (min-width: 768px) { - h1 { - font-size: 40px; - font-size: 2.5rem; - } -} -h1 span { - font-weight: bold; -} - -h2 { - line-height: 1.2; - font-size: 25px; - font-size: 1.5625rem; -} -@media (min-width: 768px) { - h2 { - font-size: 32px; - font-size: 2rem; - } -} - -h3 { - font-size: 20px; - font-size: 1.25rem; - font-weight: 600; -} - -h4 { - font-size: 17px; - font-size: 1.0625rem; - margin-bottom: 12px; -} - -h5 { - text-transform: uppercase; - font-size: 15px; - font-size: 0.9375rem; - font-weight: 700; -} - -h6 { - font-weight: 700; - text-transform: uppercase; - font-size: 12px; - font-size: 0.75rem; - letter-spacing: 1px; -} - -/*------------------------------ - 2.4 Base -------------------------------*/ -ul, ol, dl, p, details, address, .vcard, figure, pre, fieldset, table, dt, dd, hr { - margin-bottom: 15px; - margin-bottom: 0.9375rem; - margin-top: 0; -} - -/*------------------------------ - 2.5 Content -------------------------------*/ -img { - height: auto; - max-width: 100%; - vertical-align: middle; -} - -b, -strong { - font-weight: bold; -} - -blockquote { - clear: both; - margin: 20px 0; -} -blockquote p { - font-style: italic; -} -blockquote cite { - font-style: normal; - margin-bottom: 20px; - font-size: 13px; -} - -dfn, -cite, -em, -i { - font-style: italic; -} - -figure { - margin: 0; -} - -address { - margin: 20px 0; -} - -hr { - border: 0; - border-top: 1px solid #e9e9e9; - height: 1px; - margin-bottom: 20px; -} - -tt, -kbd, -pre, -code, -samp, -var { - font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; - background-color: #e9e9e9; - padding: 5px 7px; - border-radius: 2px; -} - -pre { - overflow: auto; - white-space: pre-wrap; - max-width: 100%; - line-height: 1.7; - margin: 20px 0; - padding: 20px; -} - -details summary { - font-weight: bold; - margin-bottom: 20px; -} -details :focus { - outline: none; -} - -abbr, -acronym, -dfn { - cursor: help; - font-size: 0.95em; - text-transform: uppercase; - border-bottom: 1px dotted #e9e9e9; - letter-spacing: 1px; -} - -mark { - background-color: #fff9c0; - text-decoration: none; -} - -small { - font-size: 82%; -} - -big { - font-size: 125%; -} - -ul, ol { - padding-right: 20px; -} - -ul { - list-style: disc; -} - -ol { - list-style: decimal; -} - -ul li, ol li { - margin: 8px 0; -} - -dt { - font-weight: bold; -} - -dd { - margin: 0 20px 20px; -} - -/*------------------------------ - 2.6 Table -------------------------------*/ -table { - width: 100%; - margin-bottom: 20px; - border: 1px solid #e9e9e9; - border-collapse: collapse; - border-spacing: 0; -} - -table > thead > tr > th, -table > tbody > tr > th, -table > tfoot > tr > th, -table > thead > tr > td, -table > tbody > tr > td, -table > tfoot > tr > td { - border: 1px solid #e9e9e9; - line-height: 1.42857; - padding: 5px; - vertical-align: middle; -} - -table > thead > tr > th, table > thead > tr > td { - border-bottom-width: 2px; -} - -table th { - font-size: 14px; - letter-spacing: 2px; - text-transform: uppercase; -} - -/*------------------------------ - 2.7 Form -------------------------------*/ -fieldset { - padding: 20px; - border: 1px solid #e9e9e9; -} - -input[type=reset], input[type=submit], input[type=submit], -.pirate-forms-submit-button, .contact-form div.wpforms-container-full .wpforms-form .wpforms-submit { - cursor: pointer; - background: #03c4eb; - border: none; - display: inline-block; - color: #FFFFFF; - letter-spacing: 1px; - text-transform: uppercase; - line-height: 1; - text-align: center; - padding: 15px 23px 15px 23px; - border-radius: 2px; - box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.1) inset; - margin: 3px 0; - text-decoration: none; - font-weight: 600; - font-size: 13px; -} -input[type=reset]:hover, input[type=submit]:hover, input[type=submit]:hover, -.pirate-forms-submit-button:hover, .contact-form div.wpforms-container-full .wpforms-form .wpforms-submit:hover { - opacity: 0.8; - background: #03c4eb; - border: none; -} - -input[type=button]:hover, input[type=button]:focus, input[type=reset]:hover, -input[type=reset]:focus, input[type=submit]:hover, input[type=submit]:focus, -button:hover, button:focus { - cursor: pointer; -} - -textarea { - resize: vertical; -} - -select { - max-width: 100%; - overflow: auto; - vertical-align: top; - outline: none; - border: 1px solid #e9e9e9; - padding: 10px; -} - -textarea:not(.editor-post-title__input), -input[type=date], -input[type=datetime], -input[type=datetime-local], -input[type=email], -input[type=month], -input[type=number], -input[type=password], -input[type=search], -input[type=tel], -input[type=text], -input[type=time], -input[type=url], -input[type=week] { - padding: 10px; - max-width: 100%; - border: 0px; - font-size: 15px; - font-weight: normal; - line-height: 22px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -o-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - transition: all 0.2s linear; - -moz-transition: all 0.2s linear; - -webkit-transition: all 0.2s linear; - -o-transition: all 0.2s linear; - background-color: #f2f2f2; - border-bottom: 1px solid #fff; - box-sizing: border-box; - color: #000000; -} -textarea:not(.editor-post-title__input):focus, -input[type=date]:focus, -input[type=datetime]:focus, -input[type=datetime-local]:focus, -input[type=email]:focus, -input[type=month]:focus, -input[type=number]:focus, -input[type=password]:focus, -input[type=search]:focus, -input[type=tel]:focus, -input[type=text]:focus, -input[type=time]:focus, -input[type=url]:focus, -input[type=week]:focus { - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -o-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - transition: all 0.2s linear; - -moz-transition: all 0.2s linear; - -webkit-transition: all 0.2s linear; - -o-transition: all 0.2s linear; - border-color: #EBEBEB; - border-bottom: 1px solid #fff !important; - background: #e9e9e9; -} - -button::-moz-focus-inner { - border: 0; - padding: 0; -} - -input[type=radio], input[type=checkbox] { - margin: 0 10px; -} - -/*------------------------------ - 2.8 Accessibility -------------------------------*/ -/* Text meant only for screen readers */ -.screen-reader-text { - clip: rect(1px, 1px, 1px, 1px); - position: absolute !important; -} - -.screen-reader-text:hover, -.screen-reader-text:active, -.screen-reader-text:focus { - background-color: #f8f9f9; - border-radius: 3px; - clip: auto !important; - color: #03c4eb; - display: block; - height: auto; - right: 5px; - line-height: normal; - padding: 17px; - text-decoration: none; - top: 5px; - width: auto; - z-index: 100000; /* Above WP toolbar */ -} - -/*------------------------------ - 2.8 Accessibility -------------------------------*/ -/*------------------------------ - 2.9 Alignments -------------------------------*/ -.alignleft { - display: inline; - float: right; - margin-left: 3.5em; -} - -.alignright { - display: inline; - float: left; - margin-right: 3.5em; -} - -.aligncenter { - clear: both; - display: block; - margin-right: auto; - margin-left: auto; -} - -/*------------------------------ - 3.0 Clearings -------------------------------*/ -.clear:before, -.clear:after, -.entry-content:before, -.entry-content:after, -.comment-content:before, -.comment-content:after, -.site-header:before, -.site-header:after, -.site-content:before, -.site-content:after, -.site-footer:before, -.site-footer:after { - content: ""; - display: table; - clear: both; -} - -.clear:after, -.entry-content:after, -.comment-content:after, -.site-header:after, -.site-content:after, -.site-footer:after { - clear: both; -} - -/*------------------------------ - 3.1 Infinite Scroll -------------------------------*/ -/* Globally hidden elements when Infinite Scroll is supported and in use. */ -.infinite-scroll .posts-navigation, -.infinite-scroll.neverending .site-footer { /* Theme Footer (when set to scrolling) */ - display: none; -} - -/* When Infinite Scroll has reached its end we need to re-display elements that were hidden (via .neverending) before. */ -.infinity-end.neverending .site-footer { - display: block; -} - -/*------------------------------ - 3.1 Helper. -------------------------------*/ -.hide { - display: none; -} - -.clearleft { - clear: right; -} - -.break, h1, -h2, -h3, -h4, -h5, -h6, p, ul, ol, dl, blockquote, pre { - word-break: break-word; - word-wrap: break-word; -} - -body.mce-content-body { - margin: 20px 40px; - font-size: 13px; -} - -/* Page: 404 -------------------------------*/ -.error-404 .search-form, -.error-404 .widget { - margin-bottom: 40px; -} -.error-404 .widgettitle, -.error-404 .widget-title { - font-size: 15px; - text-transform: uppercase; - letter-spacing: 2px; - margin-bottom: 13px; - font-weight: 700; -} -.error-404 ul { - padding-right: 0px; -} -.error-404 ul li { - list-style: none; -} - -/* Page: Search -------------------------------*/ -.search-results .hentry { - border-bottom: 1px solid #e9e9e9; - padding-bottom: 25px; - margin-bottom: 25px; -} -.search-results .entry-summary p { - margin-bottom: 0px; -} -.search-results .entry-header .entry-title { - font-size: 22px; - line-height: 1.5; - font-weight: 500; -} -.search-results .entry-header .entry-title a:hover { - text-decoration: none; -} - -/* Entry Header -------------------------------*/ -.entry-header .entry-title { - font-weight: 500; - text-transform: none; - letter-spacing: -0.6px; - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: 25px; - line-height: 1.3; -} -@media screen and (min-width: 940px) { - .entry-header .entry-title { - font-size: 32px; - line-height: 1.5; - } -} - -.entry-thumbnail { - margin-bottom: 30px; -} - -.single .entry-header .entry-title { - margin-bottom: 10px; -} - -.highlight { - color: #03c4eb; -} - -/* Entry Content -------------------------------*/ -.entry-content { - margin-bottom: 30px; -} -.entry-content blockquote { - padding: 30px; - position: relative; - background: #f8f9f9; - border-right: 3px solid #03c4eb; - font-style: italic; -} -.entry-content blockquote p { - margin: 0px; -} - -/* Entry Stuff -------------------------------*/ -.entry-meta { - margin-bottom: 30px; - text-transform: uppercase; - letter-spacing: 1.5px; - font-size: 12px; - font-weight: 600; - padding-bottom: 30px; - border-bottom: 1px solid #e9e9e9; -} - -.entry-footer { - margin-bottom: 30px; - padding-top: 30px; - border-top: 1px solid #e9e9e9; -} -.entry-footer .cat-links, -.entry-footer .tags-links { - display: block; - text-transform: uppercase; - letter-spacing: 1.5px; - font-size: 12px; - font-weight: 600; - margin-top: 5px; -} - -.nav-links { - padding: 30px 0px; - border-right: none; - border-left: none; - margin-bottom: 50px; - flex-basis: 100%; - text-align: center; -} -.nav-links .nav-previous { - float: right; -} -.nav-links .nav-next { - float: left; -} -.nav-links a, -.nav-links .page-numbers { - background: #cccccc; - color: #FFFFFF; - padding: 12px 20px; - font-weight: 600; - font-size: 12px; - letter-spacing: 1px; - text-transform: uppercase; - border-radius: 2px; -} -@media screen and (max-width: 940px) { - .nav-links a, - .nav-links .page-numbers { - padding: 6px 10px; - } -} -.nav-links a:hover, .nav-links a.current, -.nav-links .page-numbers:hover, -.nav-links .page-numbers.current { - background: #03c4eb; - text-decoration: none; -} - -.bypostauthor { - margin: 0; -} - -/* Sticky Post -------------------------------*/ -.sticky .entry-title { - padding-right: 20px; - position: relative; -} -.sticky .entry-title:after { - content: "\f276"; - display: inline-block; - font-family: "FontAwesome"; - font-style: normal; - font-weight: normal; - width: 12px; - height: 12px; - position: absolute; - right: 0px; - top: 2px; - font-size: 22px; - color: #aaaaaa; -} - -/* WordPress caption style -------------------------------*/ -.wp-caption { - max-width: 100%; - font-style: italic; - line-height: 1.35; - margin-bottom: 15px; - margin-top: 5px; -} -.wp-caption img[class*=wp-image-] { - display: block; - max-width: 100%; -} -.wp-caption .wp-caption-text { - margin: 10px 0px; -} - -.wp-caption-text, -.entry-thumbnail-caption, -.cycle-caption { - font-style: italic; - line-height: 1.35; - font-size: 13px; -} - -/* WordPress Gallery -------------------------------*/ -.gallery { - margin-bottom: 1.5em; -} - -.gallery-item { - display: inline-block; - text-align: center; - vertical-align: top; - width: 100%; -} -.gallery-columns-2 .gallery-item { - max-width: 50%; -} -.gallery-columns-3 .gallery-item { - max-width: 33.33%; -} -.gallery-columns-4 .gallery-item { - max-width: 25%; -} -.gallery-columns-5 .gallery-item { - max-width: 20%; -} -.gallery-columns-6 .gallery-item { - max-width: 16.66%; -} -.gallery-columns-7 .gallery-item { - max-width: 14.28%; -} -.gallery-columns-8 .gallery-item { - max-width: 12.5%; -} -.gallery-columns-9 .gallery-item { - max-width: 11.11%; -} - -.gallery-caption { - display: block; -} - -/* Comments -------------------------------*/ -#comments { - padding-top: 30px; - border-top: 1px solid #e9e9e9; -} -#comments .comments-title { - margin-bottom: 20px; - font-size: 18px; - line-height: 26px; - letter-spacing: 1.5px; - text-transform: uppercase; -} -#comments .comment-list { - list-style: none; - padding-right: 0px; -} -#comments .comment-list .pingback { - border-bottom: 1px solid #e9e9e9; - padding: 20px 0; - margin: 0; -} -#comments .comment-list .pingback p { - margin: 0px; -} -#comments .comment-list .pingback:last-child { - margin-bottom: 40px; -} -#comments .comment-content.entry-content { - margin-bottom: 0px; -} -#comments .comment { - list-style: none; - margin: 30px 0; -} -#comments .comment .avatar { - width: 60px; - float: right; - border-radius: 3px; -} -#comments .comment .comment-wrapper { - margin-right: 90px; - padding: 25px 30px 15px 30px; - background: #f8f9f9; - position: relative; -} -#comments .comment .comment-wrapper:before { - border-color: rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) rgba(0, 0, 0, 0) #f6f7f9; - border-style: solid; - border-width: 0 0 10px 10px; - content: ""; - height: 0; - right: -9px; - position: absolute; - top: 0; - width: 0; -} -#comments .comment .comment-wrapper .comment-meta .comment-time, -#comments .comment .comment-wrapper .comment-meta .comment-reply-link, -#comments .comment .comment-wrapper .comment-meta .comment-edit-link { - color: #aaaaaa; - text-transform: uppercase; - letter-spacing: 0.3px; - font-size: 11px; -} -#comments .comment .comment-wrapper .comment-meta .comment-time:hover, -#comments .comment .comment-wrapper .comment-meta .comment-reply-link:hover, -#comments .comment .comment-wrapper .comment-meta .comment-edit-link:hover { - color: #03c4eb; -} -#comments .comment .comment-wrapper .comment-meta .comment-time:after, -#comments .comment .comment-wrapper .comment-meta .comment-reply-link:after, -#comments .comment .comment-wrapper .comment-meta .comment-edit-link:after { - content: "/"; - padding: 0px 5px; -} -#comments .comment .comment-wrapper .comment-meta a:last-child:after { - content: ""; -} -#comments .comment .comment-wrapper .comment-meta cite .fn { - font-weight: bold; - font-style: normal; - margin-left: 5px; - text-transform: uppercase; - letter-spacing: 1.5px; - font-size: 14px; -} -#comments .comment .comment-wrapper .comment-meta cite span { - padding: 3px 10px; - background: #e9e9e9; - border-radius: 4px; - margin-left: 10px; -} -#comments .comment .comment-wrapper a { - text-decoration: none; -} -#comments .comment .children { - padding-right: 30px; -} -#comments .comment .children .children { - padding-right: 30px; -} -#comments .comment .children .children .children { - padding-right: 0px; -} -@media screen and (min-width: 940px) { - #comments .comment .children { - padding-right: 90px; - } - #comments .comment .children .children { - padding-right: 90px; - } - #comments .comment .children .children .children { - padding-right: 90px; - } -} -#comments .form-allowed-tags { - display: none; -} -#comments a { - text-decoration: none; -} -#comments a:hover { - text-decoration: underline; -} - -.comment-respond textarea, -.comment-respond textarea { - width: 100%; -} - -/* Comment Form -------------------------------*/ -#respond { - padding-top: 20px; -} -#respond .comment-form label { - display: block; - margin-bottom: 4px; -} -#respond .form-allowed-tags { - font-size: 12px; -} -#respond .form-allowed-tags code { - background: none; -} -#respond .comment-reply-title { - font-size: 18px; - letter-spacing: 1.5px; - margin-bottom: 20px; - text-transform: uppercase; -} -#respond .comment-notes { - display: none; -} -#respond label { - font-size: 13px; - text-transform: uppercase; - letter-spacing: 1.5px; -} - -.full-screen .comments-area { - max-width: 1110px; - margin: 0 auto; -} - -/* woocommerce -------------------------------*/ -.woocommerce div.product form.cart .variations td.label { - color: #777; -} - -/* . Gutenberg Editor - Block Editor */ -.wp-block-gallery.is-layout-flex { - display: flex; - flex-wrap: wrap; -} - -.single-post .content-inner { - margin-right: auto; - margin-left: auto; -} - -.single-post .right-sidebar .content-inner { - margin-right: 0px; -} - -.single-post .left-sidebar .content-inner { - margin-left: 0px; -} - -.entry-content ul, -.entry-content ol { - margin: 1.5em auto; - list-style-position: outside; -} - -.entry-content li { - margin-right: 2.5em; - margin-bottom: 6px; -} - -.entry-content ul ul, -.entry-content ol ol, -.entry-content ul ol, -.entry-content ol ul { - margin: 0 auto; -} - -.entry-content ul ul li, -.entry-content ol ol li, -.entry-content ul ol li, -.entry-content ol ul li { - margin-right: 0; -} - -/*-------------------------------------------------------------- - # Block Color Palette Colors - --------------------------------------------------------------*/ -.has-strong-blue-color { - color: #0073aa; -} - -.has-strong-blue-background-color { - background-color: #0073aa; -} - -.has-lighter-blue-color { - color: #229fd8; -} - -.has-lighter-blue-background-color { - background-color: #229fd8; -} - -.has-very-light-gray-color { - color: #eee; -} - -.has-very-light-gray-background-color { - background-color: #eee; -} - -.has-very-dark-gray-color { - color: #444; -} - -.has-very-dark-gray-background-color { - background-color: #444; -} diff --git a/assets/build/admin/editor.asset.php b/assets/build/admin/editor.asset.php deleted file mode 100644 index 41d7d5dd..00000000 --- a/assets/build/admin/editor.asset.php +++ /dev/null @@ -1 +0,0 @@ - array(), 'version' => '30ae77467bd23a889d8f'); diff --git a/assets/build/admin/editor.css b/assets/build/admin/editor.css deleted file mode 100644 index eac23a05..00000000 --- a/assets/build/admin/editor.css +++ /dev/null @@ -1,1063 +0,0 @@ -/*!**********************************************************************************************************************************************************************************************************************************************************************************************************************!*\ - !*** css ./node_modules/@wordpress/scripts/node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[4].use[1]!./node_modules/@wordpress/scripts/node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[4].use[2]!./node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[4].use[3]!./src/frontend/sass/editor.scss ***! - \**********************************************************************************************************************************************************************************************************************************************************************************************************************/ -/*------------------------------ - 2.1 Typography -------------------------------*/ -html, body { - margin: 0; - padding: 0; -} - -html { - box-sizing: border-box; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - font-size: 16px; -} -@media (max-width: 991px) { - html { - font-size: 15px; - } -} -@media (max-width: 767px) { - html { - font-size: 14px; - } -} - -body { - background: #ffffff; -} - -.site { - background: #FFFFFF; - position: relative; -} - -body, button, input, select, textarea { - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: 14px; - font-size: 0.875rem; - line-height: 1.7; - color: #777777; -} - -pre, -code, -input, -textarea { - font: inherit; -} - -::-moz-selection { - background: #000000; - color: #FFFFFF; -} - -::selection { - background: #000000; - color: #FFFFFF; -} - -/*------------------------------ - 2.2 Links -------------------------------*/ -a { - color: #03c4eb; - text-decoration: none; - outline: none; -} -a:hover { - text-decoration: none; - color: #777777; - text-decoration: underline; -} -a:active, a:focus, a:hover { - outline: none; -} - -/*------------------------------ - 2.3 Heading -------------------------------*/ -h1, -h2, -h3, -h4, -h5, -h6 { - clear: both; - font-family: "Raleway", Helvetica, Arial, sans-serif; - font-weight: 600; - margin-bottom: 15px; - margin-bottom: 15px; - margin-bottom: 0.9375rem; - margin-top: 0; - color: #333333; -} -h1 a, -h2 a, -h3 a, -h4 a, -h5 a, -h6 a { - color: #333333; - text-decoration: none; -} - -h1 { - line-height: 1.3; - font-size: 33px; - font-size: 2.0625rem; -} -@media (min-width: 768px) { - h1 { - font-size: 40px; - font-size: 2.5rem; - } -} -h1 span { - font-weight: bold; -} - -h2 { - line-height: 1.2; - font-size: 25px; - font-size: 1.5625rem; -} -@media (min-width: 768px) { - h2 { - font-size: 32px; - font-size: 2rem; - } -} - -h3 { - font-size: 20px; - font-size: 1.25rem; - font-weight: 600; -} - -h4 { - font-size: 17px; - font-size: 1.0625rem; - margin-bottom: 12px; -} - -h5 { - text-transform: uppercase; - font-size: 15px; - font-size: 0.9375rem; - font-weight: 700; -} - -h6 { - font-weight: 700; - text-transform: uppercase; - font-size: 12px; - font-size: 0.75rem; - letter-spacing: 1px; -} - -/*------------------------------ - 2.4 Base -------------------------------*/ -ul, ol, dl, p, details, address, .vcard, figure, pre, fieldset, table, dt, dd, hr { - margin-bottom: 15px; - margin-bottom: 0.9375rem; - margin-top: 0; -} - -/*------------------------------ - 2.5 Content -------------------------------*/ -img { - height: auto; - max-width: 100%; - vertical-align: middle; -} - -b, -strong { - font-weight: bold; -} - -blockquote { - clear: both; - margin: 20px 0; -} -blockquote p { - font-style: italic; -} -blockquote cite { - font-style: normal; - margin-bottom: 20px; - font-size: 13px; -} - -dfn, -cite, -em, -i { - font-style: italic; -} - -figure { - margin: 0; -} - -address { - margin: 20px 0; -} - -hr { - border: 0; - border-top: 1px solid #e9e9e9; - height: 1px; - margin-bottom: 20px; -} - -tt, -kbd, -pre, -code, -samp, -var { - font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace; - background-color: #e9e9e9; - padding: 5px 7px; - border-radius: 2px; -} - -pre { - overflow: auto; - white-space: pre-wrap; - max-width: 100%; - line-height: 1.7; - margin: 20px 0; - padding: 20px; -} - -details summary { - font-weight: bold; - margin-bottom: 20px; -} -details :focus { - outline: none; -} - -abbr, -acronym, -dfn { - cursor: help; - font-size: 0.95em; - text-transform: uppercase; - border-bottom: 1px dotted #e9e9e9; - letter-spacing: 1px; -} - -mark { - background-color: #fff9c0; - text-decoration: none; -} - -small { - font-size: 82%; -} - -big { - font-size: 125%; -} - -ul, ol { - padding-left: 20px; -} - -ul { - list-style: disc; -} - -ol { - list-style: decimal; -} - -ul li, ol li { - margin: 8px 0; -} - -dt { - font-weight: bold; -} - -dd { - margin: 0 20px 20px; -} - -/*------------------------------ - 2.6 Table -------------------------------*/ -table { - width: 100%; - margin-bottom: 20px; - border: 1px solid #e9e9e9; - border-collapse: collapse; - border-spacing: 0; -} - -table > thead > tr > th, -table > tbody > tr > th, -table > tfoot > tr > th, -table > thead > tr > td, -table > tbody > tr > td, -table > tfoot > tr > td { - border: 1px solid #e9e9e9; - line-height: 1.42857; - padding: 5px; - vertical-align: middle; -} - -table > thead > tr > th, table > thead > tr > td { - border-bottom-width: 2px; -} - -table th { - font-size: 14px; - letter-spacing: 2px; - text-transform: uppercase; -} - -/*------------------------------ - 2.7 Form -------------------------------*/ -fieldset { - padding: 20px; - border: 1px solid #e9e9e9; -} - -input[type=reset], input[type=submit], input[type=submit], -.pirate-forms-submit-button, .contact-form div.wpforms-container-full .wpforms-form .wpforms-submit { - cursor: pointer; - background: #03c4eb; - border: none; - display: inline-block; - color: #FFFFFF; - letter-spacing: 1px; - text-transform: uppercase; - line-height: 1; - text-align: center; - padding: 15px 23px 15px 23px; - border-radius: 2px; - box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.1) inset; - margin: 3px 0; - text-decoration: none; - font-weight: 600; - font-size: 13px; -} -input[type=reset]:hover, input[type=submit]:hover, input[type=submit]:hover, -.pirate-forms-submit-button:hover, .contact-form div.wpforms-container-full .wpforms-form .wpforms-submit:hover { - opacity: 0.8; - background: #03c4eb; - border: none; -} - -input[type=button]:hover, input[type=button]:focus, input[type=reset]:hover, -input[type=reset]:focus, input[type=submit]:hover, input[type=submit]:focus, -button:hover, button:focus { - cursor: pointer; -} - -textarea { - resize: vertical; -} - -select { - max-width: 100%; - overflow: auto; - vertical-align: top; - outline: none; - border: 1px solid #e9e9e9; - padding: 10px; -} - -textarea:not(.editor-post-title__input), -input[type=date], -input[type=datetime], -input[type=datetime-local], -input[type=email], -input[type=month], -input[type=number], -input[type=password], -input[type=search], -input[type=tel], -input[type=text], -input[type=time], -input[type=url], -input[type=week] { - padding: 10px; - max-width: 100%; - border: 0px; - font-size: 15px; - font-weight: normal; - line-height: 22px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -o-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - transition: all 0.2s linear; - -moz-transition: all 0.2s linear; - -webkit-transition: all 0.2s linear; - -o-transition: all 0.2s linear; - background-color: #f2f2f2; - border-bottom: 1px solid #fff; - box-sizing: border-box; - color: #000000; -} -textarea:not(.editor-post-title__input):focus, -input[type=date]:focus, -input[type=datetime]:focus, -input[type=datetime-local]:focus, -input[type=email]:focus, -input[type=month]:focus, -input[type=number]:focus, -input[type=password]:focus, -input[type=search]:focus, -input[type=tel]:focus, -input[type=text]:focus, -input[type=time]:focus, -input[type=url]:focus, -input[type=week]:focus { - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - -o-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; - transition: all 0.2s linear; - -moz-transition: all 0.2s linear; - -webkit-transition: all 0.2s linear; - -o-transition: all 0.2s linear; - border-color: #EBEBEB; - border-bottom: 1px solid #fff !important; - background: #e9e9e9; -} - -button::-moz-focus-inner { - border: 0; - padding: 0; -} - -input[type=radio], input[type=checkbox] { - margin: 0 10px; -} - -/*------------------------------ - 2.8 Accessibility -------------------------------*/ -/* Text meant only for screen readers */ -.screen-reader-text { - clip: rect(1px, 1px, 1px, 1px); - position: absolute !important; -} - -.screen-reader-text:hover, -.screen-reader-text:active, -.screen-reader-text:focus { - background-color: #f8f9f9; - border-radius: 3px; - clip: auto !important; - color: #03c4eb; - display: block; - height: auto; - left: 5px; - line-height: normal; - padding: 17px; - text-decoration: none; - top: 5px; - width: auto; - z-index: 100000; /* Above WP toolbar */ -} - -/*------------------------------ - 2.8 Accessibility -------------------------------*/ -/*------------------------------ - 2.9 Alignments -------------------------------*/ -.alignleft { - display: inline; - float: left; - margin-right: 3.5em; -} - -.alignright { - display: inline; - float: right; - margin-left: 3.5em; -} - -.aligncenter { - clear: both; - display: block; - margin-left: auto; - margin-right: auto; -} - -/*------------------------------ - 3.0 Clearings -------------------------------*/ -.clear:before, -.clear:after, -.entry-content:before, -.entry-content:after, -.comment-content:before, -.comment-content:after, -.site-header:before, -.site-header:after, -.site-content:before, -.site-content:after, -.site-footer:before, -.site-footer:after { - content: ""; - display: table; - clear: both; -} - -.clear:after, -.entry-content:after, -.comment-content:after, -.site-header:after, -.site-content:after, -.site-footer:after { - clear: both; -} - -/*------------------------------ - 3.1 Infinite Scroll -------------------------------*/ -/* Globally hidden elements when Infinite Scroll is supported and in use. */ -.infinite-scroll .posts-navigation, -.infinite-scroll.neverending .site-footer { /* Theme Footer (when set to scrolling) */ - display: none; -} - -/* When Infinite Scroll has reached its end we need to re-display elements that were hidden (via .neverending) before. */ -.infinity-end.neverending .site-footer { - display: block; -} - -/*------------------------------ - 3.1 Helper. -------------------------------*/ -.hide { - display: none; -} - -.clearleft { - clear: left; -} - -.break, h1, -h2, -h3, -h4, -h5, -h6, p, ul, ol, dl, blockquote, pre { - word-break: break-word; - word-wrap: break-word; -} - -body.mce-content-body { - margin: 20px 40px; - font-size: 13px; -} - -/* Page: 404 -------------------------------*/ -.error-404 .search-form, -.error-404 .widget { - margin-bottom: 40px; -} -.error-404 .widgettitle, -.error-404 .widget-title { - font-size: 15px; - text-transform: uppercase; - letter-spacing: 2px; - margin-bottom: 13px; - font-weight: 700; -} -.error-404 ul { - padding-left: 0px; -} -.error-404 ul li { - list-style: none; -} - -/* Page: Search -------------------------------*/ -.search-results .hentry { - border-bottom: 1px solid #e9e9e9; - padding-bottom: 25px; - margin-bottom: 25px; -} -.search-results .entry-summary p { - margin-bottom: 0px; -} -.search-results .entry-header .entry-title { - font-size: 22px; - line-height: 1.5; - font-weight: 500; -} -.search-results .entry-header .entry-title a:hover { - text-decoration: none; -} - -/* Entry Header -------------------------------*/ -.entry-header .entry-title { - font-weight: 500; - text-transform: none; - letter-spacing: -0.6px; - font-family: "Open Sans", Helvetica, Arial, sans-serif; - font-size: 25px; - line-height: 1.3; -} -@media screen and (min-width: 940px) { - .entry-header .entry-title { - font-size: 32px; - line-height: 1.5; - } -} - -.entry-thumbnail { - margin-bottom: 30px; -} - -.single .entry-header .entry-title { - margin-bottom: 10px; -} - -.highlight { - color: #03c4eb; -} - -/* Entry Content -------------------------------*/ -.entry-content { - margin-bottom: 30px; -} -.entry-content blockquote { - padding: 30px; - position: relative; - background: #f8f9f9; - border-left: 3px solid #03c4eb; - font-style: italic; -} -.entry-content blockquote p { - margin: 0px; -} - -/* Entry Stuff -------------------------------*/ -.entry-meta { - margin-bottom: 30px; - text-transform: uppercase; - letter-spacing: 1.5px; - font-size: 12px; - font-weight: 600; - padding-bottom: 30px; - border-bottom: 1px solid #e9e9e9; -} - -.entry-footer { - margin-bottom: 30px; - padding-top: 30px; - border-top: 1px solid #e9e9e9; -} -.entry-footer .cat-links, -.entry-footer .tags-links { - display: block; - text-transform: uppercase; - letter-spacing: 1.5px; - font-size: 12px; - font-weight: 600; - margin-top: 5px; -} - -.nav-links { - padding: 30px 0px; - border-left: none; - border-right: none; - margin-bottom: 50px; - flex-basis: 100%; - text-align: center; -} -.nav-links .nav-previous { - float: left; -} -.nav-links .nav-next { - float: right; -} -.nav-links a, -.nav-links .page-numbers { - background: #cccccc; - color: #FFFFFF; - padding: 12px 20px; - font-weight: 600; - font-size: 12px; - letter-spacing: 1px; - text-transform: uppercase; - border-radius: 2px; -} -@media screen and (max-width: 940px) { - .nav-links a, - .nav-links .page-numbers { - padding: 6px 10px; - } -} -.nav-links a:hover, .nav-links a.current, -.nav-links .page-numbers:hover, -.nav-links .page-numbers.current { - background: #03c4eb; - text-decoration: none; -} - -.bypostauthor { - margin: 0; -} - -/* Sticky Post -------------------------------*/ -.sticky .entry-title { - padding-left: 20px; - position: relative; -} -.sticky .entry-title:after { - content: "\f276"; - display: inline-block; - font-family: "FontAwesome"; - font-style: normal; - font-weight: normal; - width: 12px; - height: 12px; - position: absolute; - left: 0px; - top: 2px; - font-size: 22px; - color: #aaaaaa; -} - -/* WordPress caption style -------------------------------*/ -.wp-caption { - max-width: 100%; - font-style: italic; - line-height: 1.35; - margin-bottom: 15px; - margin-top: 5px; -} -.wp-caption img[class*=wp-image-] { - display: block; - max-width: 100%; -} -.wp-caption .wp-caption-text { - margin: 10px 0px; -} - -.wp-caption-text, -.entry-thumbnail-caption, -.cycle-caption { - font-style: italic; - line-height: 1.35; - font-size: 13px; -} - -/* WordPress Gallery -------------------------------*/ -.gallery { - margin-bottom: 1.5em; -} - -.gallery-item { - display: inline-block; - text-align: center; - vertical-align: top; - width: 100%; -} -.gallery-columns-2 .gallery-item { - max-width: 50%; -} -.gallery-columns-3 .gallery-item { - max-width: 33.33%; -} -.gallery-columns-4 .gallery-item { - max-width: 25%; -} -.gallery-columns-5 .gallery-item { - max-width: 20%; -} -.gallery-columns-6 .gallery-item { - max-width: 16.66%; -} -.gallery-columns-7 .gallery-item { - max-width: 14.28%; -} -.gallery-columns-8 .gallery-item { - max-width: 12.5%; -} -.gallery-columns-9 .gallery-item { - max-width: 11.11%; -} - -.gallery-caption { - display: block; -} - -/* Comments -------------------------------*/ -#comments { - padding-top: 30px; - border-top: 1px solid #e9e9e9; -} -#comments .comments-title { - margin-bottom: 20px; - font-size: 18px; - line-height: 26px; - letter-spacing: 1.5px; - text-transform: uppercase; -} -#comments .comment-list { - list-style: none; - padding-left: 0px; -} -#comments .comment-list .pingback { - border-bottom: 1px solid #e9e9e9; - padding: 20px 0; - margin: 0; -} -#comments .comment-list .pingback p { - margin: 0px; -} -#comments .comment-list .pingback:last-child { - margin-bottom: 40px; -} -#comments .comment-content.entry-content { - margin-bottom: 0px; -} -#comments .comment { - list-style: none; - margin: 30px 0; -} -#comments .comment .avatar { - width: 60px; - float: left; - border-radius: 3px; -} -#comments .comment .comment-wrapper { - margin-left: 90px; - padding: 25px 30px 15px 30px; - background: #f8f9f9; - position: relative; -} -#comments .comment .comment-wrapper:before { - border-color: rgba(0, 0, 0, 0) #f6f7f9 rgba(0, 0, 0, 0) rgba(0, 0, 0, 0); - border-style: solid; - border-width: 0 10px 10px 0; - content: ""; - height: 0; - left: -9px; - position: absolute; - top: 0; - width: 0; -} -#comments .comment .comment-wrapper .comment-meta .comment-time, -#comments .comment .comment-wrapper .comment-meta .comment-reply-link, -#comments .comment .comment-wrapper .comment-meta .comment-edit-link { - color: #aaaaaa; - text-transform: uppercase; - letter-spacing: 0.3px; - font-size: 11px; -} -#comments .comment .comment-wrapper .comment-meta .comment-time:hover, -#comments .comment .comment-wrapper .comment-meta .comment-reply-link:hover, -#comments .comment .comment-wrapper .comment-meta .comment-edit-link:hover { - color: #03c4eb; -} -#comments .comment .comment-wrapper .comment-meta .comment-time:after, -#comments .comment .comment-wrapper .comment-meta .comment-reply-link:after, -#comments .comment .comment-wrapper .comment-meta .comment-edit-link:after { - content: "/"; - padding: 0px 5px; -} -#comments .comment .comment-wrapper .comment-meta a:last-child:after { - content: ""; -} -#comments .comment .comment-wrapper .comment-meta cite .fn { - font-weight: bold; - font-style: normal; - margin-right: 5px; - text-transform: uppercase; - letter-spacing: 1.5px; - font-size: 14px; -} -#comments .comment .comment-wrapper .comment-meta cite span { - padding: 3px 10px; - background: #e9e9e9; - border-radius: 4px; - margin-right: 10px; -} -#comments .comment .comment-wrapper a { - text-decoration: none; -} -#comments .comment .children { - padding-left: 30px; -} -#comments .comment .children .children { - padding-left: 30px; -} -#comments .comment .children .children .children { - padding-left: 0px; -} -@media screen and (min-width: 940px) { - #comments .comment .children { - padding-left: 90px; - } - #comments .comment .children .children { - padding-left: 90px; - } - #comments .comment .children .children .children { - padding-left: 90px; - } -} -#comments .form-allowed-tags { - display: none; -} -#comments a { - text-decoration: none; -} -#comments a:hover { - text-decoration: underline; -} - -.comment-respond textarea, -.comment-respond textarea { - width: 100%; -} - -/* Comment Form -------------------------------*/ -#respond { - padding-top: 20px; -} -#respond .comment-form label { - display: block; - margin-bottom: 4px; -} -#respond .form-allowed-tags { - font-size: 12px; -} -#respond .form-allowed-tags code { - background: none; -} -#respond .comment-reply-title { - font-size: 18px; - letter-spacing: 1.5px; - margin-bottom: 20px; - text-transform: uppercase; -} -#respond .comment-notes { - display: none; -} -#respond label { - font-size: 13px; - text-transform: uppercase; - letter-spacing: 1.5px; -} - -.full-screen .comments-area { - max-width: 1110px; - margin: 0 auto; -} - -/* woocommerce -------------------------------*/ -.woocommerce div.product form.cart .variations td.label { - color: #777; -} - -/* . Gutenberg Editor - Block Editor */ -.wp-block-gallery.is-layout-flex { - display: flex; - flex-wrap: wrap; -} - -.single-post .content-inner { - margin-left: auto; - margin-right: auto; -} - -.single-post .right-sidebar .content-inner { - margin-left: 0px; -} - -.single-post .left-sidebar .content-inner { - margin-right: 0px; -} - -.entry-content ul, -.entry-content ol { - margin: 1.5em auto; - list-style-position: outside; -} - -.entry-content li { - margin-left: 2.5em; - margin-bottom: 6px; -} - -.entry-content ul ul, -.entry-content ol ol, -.entry-content ul ol, -.entry-content ol ul { - margin: 0 auto; -} - -.entry-content ul ul li, -.entry-content ol ol li, -.entry-content ul ol li, -.entry-content ol ul li { - margin-left: 0; -} - -/*-------------------------------------------------------------- - # Block Color Palette Colors - --------------------------------------------------------------*/ -.has-strong-blue-color { - color: #0073aa; -} - -.has-strong-blue-background-color { - background-color: #0073aa; -} - -.has-lighter-blue-color { - color: #229fd8; -} - -.has-lighter-blue-background-color { - background-color: #229fd8; -} - -.has-very-light-gray-color { - color: #eee; -} - -.has-very-light-gray-background-color { - background-color: #eee; -} - -.has-very-dark-gray-color { - color: #444; -} - -.has-very-dark-gray-background-color { - background-color: #444; -} - -/*# sourceMappingURL=editor.css.map*/ \ No newline at end of file diff --git a/assets/build/admin/editor.js b/assets/build/admin/editor.js deleted file mode 100644 index 6660e615..00000000 --- a/assets/build/admin/editor.js +++ /dev/null @@ -1,28 +0,0 @@ -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ // The require scope -/******/ var __webpack_require__ = {}; -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/************************************************************************/ -var __webpack_exports__ = {}; -/*!***************************************!*\ - !*** ./src/frontend/sass/editor.scss ***! - \***************************************/ -__webpack_require__.r(__webpack_exports__); -// extracted by mini-css-extract-plugin - -/******/ })() -; -//# sourceMappingURL=editor.js.map \ No newline at end of file diff --git a/assets/build/admin/editor.js.map b/assets/build/admin/editor.js.map deleted file mode 100644 index dfa408da..00000000 --- a/assets/build/admin/editor.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"admin/editor.js","mappings":";;UAAA;UACA;;;;;WCDA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D,E;;;;;;;;;ACNA","sources":["webpack://onepress/webpack/bootstrap","webpack://onepress/webpack/runtime/make namespace object","webpack://onepress/./src/frontend/sass/editor.scss?d98b"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","// extracted by mini-css-extract-plugin\nexport {};"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/assets/build/frontend/gallery-carousel.asset.php b/assets/build/frontend/gallery-carousel.asset.php deleted file mode 100644 index 0af44acd..00000000 --- a/assets/build/frontend/gallery-carousel.asset.php +++ /dev/null @@ -1 +0,0 @@ - array(), 'version' => '130f7c048697db16894e'); diff --git a/assets/build/frontend/gallery-carousel.js b/assets/build/frontend/gallery-carousel.js deleted file mode 100644 index 8598d1f7..00000000 --- a/assets/build/frontend/gallery-carousel.js +++ /dev/null @@ -1,3191 +0,0 @@ -/******/ (() => { // webpackBootstrap -/*!***************************************************!*\ - !*** ./src/frontend/libs/gallery/owl.carousel.js ***! - \***************************************************/ -/** - * Owl Carousel v2.3.4 - * Copyright 2013-2018 David Deutsch - * Licensed under: SEE LICENSE IN https://github.com/OwlCarousel2/OwlCarousel2/blob/master/LICENSE - */ -/** - * Owl carousel - * @version 2.3.4 - * @author Bartosz Wojciechowski - * @author David Deutsch - * @license The MIT License (MIT) - * @todo Lazy Load Icon - * @todo prevent animationend bubling - * @todo itemsScaleUp - * @todo Test Zepto - * @todo stagePadding calculate wrong active classes - */ -; -(function ($, window, document, undefined) { - /** - * Creates a carousel. - * @class The Owl Carousel. - * @public - * @param {HTMLElement|jQuery} element - The element to create the carousel for. - * @param {Object} [options] - The options - */ - function Owl(element, options) { - /** - * Current settings for the carousel. - * @public - */ - this.settings = null; - - /** - * Current options set by the caller including defaults. - * @public - */ - this.options = $.extend({}, Owl.Defaults, options); - - /** - * Plugin element. - * @public - */ - this.$element = $(element); - - /** - * Proxied event handlers. - * @protected - */ - this._handlers = {}; - - /** - * References to the running plugins of this carousel. - * @protected - */ - this._plugins = {}; - - /** - * Currently suppressed events to prevent them from being retriggered. - * @protected - */ - this._supress = {}; - - /** - * Absolute current position. - * @protected - */ - this._current = null; - - /** - * Animation speed in milliseconds. - * @protected - */ - this._speed = null; - - /** - * Coordinates of all items in pixel. - * @todo The name of this member is missleading. - * @protected - */ - this._coordinates = []; - - /** - * Current breakpoint. - * @todo Real media queries would be nice. - * @protected - */ - this._breakpoint = null; - - /** - * Current width of the plugin element. - */ - this._width = null; - - /** - * All real items. - * @protected - */ - this._items = []; - - /** - * All cloned items. - * @protected - */ - this._clones = []; - - /** - * Merge values of all items. - * @todo Maybe this could be part of a plugin. - * @protected - */ - this._mergers = []; - - /** - * Widths of all items. - */ - this._widths = []; - - /** - * Invalidated parts within the update process. - * @protected - */ - this._invalidated = {}; - - /** - * Ordered list of workers for the update process. - * @protected - */ - this._pipe = []; - - /** - * Current state information for the drag operation. - * @todo #261 - * @protected - */ - this._drag = { - time: null, - target: null, - pointer: null, - stage: { - start: null, - current: null - }, - direction: null - }; - - /** - * Current state information and their tags. - * @type {Object} - * @protected - */ - this._states = { - current: {}, - tags: { - 'initializing': ['busy'], - 'animating': ['busy'], - 'dragging': ['interacting'] - } - }; - $.each(['onResize', 'onThrottledResize'], $.proxy(function (i, handler) { - this._handlers[handler] = $.proxy(this[handler], this); - }, this)); - $.each(Owl.Plugins, $.proxy(function (key, plugin) { - this._plugins[key.charAt(0).toLowerCase() + key.slice(1)] = new plugin(this); - }, this)); - $.each(Owl.Workers, $.proxy(function (priority, worker) { - this._pipe.push({ - 'filter': worker.filter, - 'run': $.proxy(worker.run, this) - }); - }, this)); - this.setup(); - this.initialize(); - } - - /** - * Default options for the carousel. - * @public - */ - Owl.Defaults = { - items: 3, - loop: false, - center: false, - rewind: false, - checkVisibility: true, - mouseDrag: true, - touchDrag: true, - pullDrag: true, - freeDrag: false, - margin: 0, - stagePadding: 0, - merge: false, - mergeFit: true, - autoWidth: false, - startPosition: 0, - rtl: false, - smartSpeed: 250, - fluidSpeed: false, - dragEndSpeed: false, - responsive: {}, - responsiveRefreshRate: 200, - responsiveBaseElement: window, - fallbackEasing: 'swing', - slideTransition: '', - info: false, - nestedItemSelector: false, - itemElement: 'div', - stageElement: 'div', - refreshClass: 'owl-refresh', - loadedClass: 'owl-loaded', - loadingClass: 'owl-loading', - rtlClass: 'owl-rtl', - responsiveClass: 'owl-responsive', - dragClass: 'owl-drag', - itemClass: 'owl-item', - stageClass: 'owl-stage', - stageOuterClass: 'owl-stage-outer', - grabClass: 'owl-grab' - }; - - /** - * Enumeration for width. - * @public - * @readonly - * @enum {String} - */ - Owl.Width = { - Default: 'default', - Inner: 'inner', - Outer: 'outer' - }; - - /** - * Enumeration for types. - * @public - * @readonly - * @enum {String} - */ - Owl.Type = { - Event: 'event', - State: 'state' - }; - - /** - * Contains all registered plugins. - * @public - */ - Owl.Plugins = {}; - - /** - * List of workers involved in the update process. - */ - Owl.Workers = [{ - filter: ['width', 'settings'], - run: function () { - this._width = this.$element.width(); - } - }, { - filter: ['width', 'items', 'settings'], - run: function (cache) { - cache.current = this._items && this._items[this.relative(this._current)]; - } - }, { - filter: ['items', 'settings'], - run: function () { - this.$stage.children('.cloned').remove(); - } - }, { - filter: ['width', 'items', 'settings'], - run: function (cache) { - var margin = this.settings.margin || '', - grid = !this.settings.autoWidth, - rtl = this.settings.rtl, - css = { - 'width': 'auto', - 'margin-left': rtl ? margin : '', - 'margin-right': rtl ? '' : margin - }; - !grid && this.$stage.children().css(css); - cache.css = css; - } - }, { - filter: ['width', 'items', 'settings'], - run: function (cache) { - var width = (this.width() / this.settings.items).toFixed(3) - this.settings.margin, - merge = null, - iterator = this._items.length, - grid = !this.settings.autoWidth, - widths = []; - cache.items = { - merge: false, - width: width - }; - while (iterator--) { - merge = this._mergers[iterator]; - merge = this.settings.mergeFit && Math.min(merge, this.settings.items) || merge; - cache.items.merge = merge > 1 || cache.items.merge; - widths[iterator] = !grid ? this._items[iterator].width() : width * merge; - } - this._widths = widths; - } - }, { - filter: ['items', 'settings'], - run: function () { - var clones = [], - items = this._items, - settings = this.settings, - // TODO: Should be computed from number of min width items in stage - view = Math.max(settings.items * 2, 4), - size = Math.ceil(items.length / 2) * 2, - repeat = settings.loop && items.length ? settings.rewind ? view : Math.max(view, size) : 0, - append = '', - prepend = ''; - repeat /= 2; - while (repeat > 0) { - // Switch to only using appended clones - clones.push(this.normalize(clones.length / 2, true)); - append = append + items[clones[clones.length - 1]][0].outerHTML; - clones.push(this.normalize(items.length - 1 - (clones.length - 1) / 2, true)); - prepend = items[clones[clones.length - 1]][0].outerHTML + prepend; - repeat -= 1; - } - this._clones = clones; - $(append).addClass('cloned').appendTo(this.$stage); - $(prepend).addClass('cloned').prependTo(this.$stage); - } - }, { - filter: ['width', 'items', 'settings'], - run: function () { - var rtl = this.settings.rtl ? 1 : -1, - size = this._clones.length + this._items.length, - iterator = -1, - previous = 0, - current = 0, - coordinates = []; - while (++iterator < size) { - previous = coordinates[iterator - 1] || 0; - current = this._widths[this.relative(iterator)] + this.settings.margin; - coordinates.push(previous + current * rtl); - } - this._coordinates = coordinates; - } - }, { - filter: ['width', 'items', 'settings'], - run: function () { - var padding = this.settings.stagePadding, - coordinates = this._coordinates, - css = { - 'width': Math.ceil(Math.abs(coordinates[coordinates.length - 1])) + padding * 2, - 'padding-left': padding || '', - 'padding-right': padding || '' - }; - this.$stage.css(css); - } - }, { - filter: ['width', 'items', 'settings'], - run: function (cache) { - var iterator = this._coordinates.length, - grid = !this.settings.autoWidth, - items = this.$stage.children(); - if (grid && cache.items.merge) { - while (iterator--) { - cache.css.width = this._widths[this.relative(iterator)]; - items.eq(iterator).css(cache.css); - } - } else if (grid) { - cache.css.width = cache.items.width; - items.css(cache.css); - } - } - }, { - filter: ['items'], - run: function () { - this._coordinates.length < 1 && this.$stage.removeAttr('style'); - } - }, { - filter: ['width', 'items', 'settings'], - run: function (cache) { - cache.current = cache.current ? this.$stage.children().index(cache.current) : 0; - cache.current = Math.max(this.minimum(), Math.min(this.maximum(), cache.current)); - this.reset(cache.current); - } - }, { - filter: ['position'], - run: function () { - this.animate(this.coordinates(this._current)); - } - }, { - filter: ['width', 'position', 'items', 'settings'], - run: function () { - var rtl = this.settings.rtl ? 1 : -1, - padding = this.settings.stagePadding * 2, - begin = this.coordinates(this.current()) + padding, - end = begin + this.width() * rtl, - inner, - outer, - matches = [], - i, - n; - for (i = 0, n = this._coordinates.length; i < n; i++) { - inner = this._coordinates[i - 1] || 0; - outer = Math.abs(this._coordinates[i]) + padding * rtl; - if (this.op(inner, '<=', begin) && this.op(inner, '>', end) || this.op(outer, '<', begin) && this.op(outer, '>', end)) { - matches.push(i); - } - } - this.$stage.children('.active').removeClass('active'); - this.$stage.children(':eq(' + matches.join('), :eq(') + ')').addClass('active'); - this.$stage.children('.center').removeClass('center'); - if (this.settings.center) { - this.$stage.children().eq(this.current()).addClass('center'); - } - } - }]; - - /** - * Create the stage DOM element - */ - Owl.prototype.initializeStage = function () { - this.$stage = this.$element.find('.' + this.settings.stageClass); - - // if the stage is already in the DOM, grab it and skip stage initialization - if (this.$stage.length) { - return; - } - this.$element.addClass(this.options.loadingClass); - - // create stage - this.$stage = $('<' + this.settings.stageElement + '>', { - "class": this.settings.stageClass - }).wrap($('
', { - "class": this.settings.stageOuterClass - })); - - // append stage - this.$element.append(this.$stage.parent()); - }; - - /** - * Create item DOM elements - */ - Owl.prototype.initializeItems = function () { - var $items = this.$element.find('.owl-item'); - - // if the items are already in the DOM, grab them and skip item initialization - if ($items.length) { - this._items = $items.get().map(function (item) { - return $(item); - }); - this._mergers = this._items.map(function () { - return 1; - }); - this.refresh(); - return; - } - - // append content - this.replace(this.$element.children().not(this.$stage.parent())); - - // check visibility - if (this.isVisible()) { - // update view - this.refresh(); - } else { - // invalidate width - this.invalidate('width'); - } - this.$element.removeClass(this.options.loadingClass).addClass(this.options.loadedClass); - }; - - /** - * Initializes the carousel. - * @protected - */ - Owl.prototype.initialize = function () { - this.enter('initializing'); - this.trigger('initialize'); - this.$element.toggleClass(this.settings.rtlClass, this.settings.rtl); - if (this.settings.autoWidth && !this.is('pre-loading')) { - var imgs, nestedSelector, width; - imgs = this.$element.find('img'); - nestedSelector = this.settings.nestedItemSelector ? '.' + this.settings.nestedItemSelector : undefined; - width = this.$element.children(nestedSelector).width(); - if (imgs.length && width <= 0) { - this.preloadAutoWidthImages(imgs); - } - } - this.initializeStage(); - this.initializeItems(); - - // register event handlers - this.registerEventHandlers(); - this.leave('initializing'); - this.trigger('initialized'); - }; - - /** - * @returns {Boolean} visibility of $element - * if you know the carousel will always be visible you can set `checkVisibility` to `false` to - * prevent the expensive browser layout forced reflow the $element.is(':visible') does - */ - Owl.prototype.isVisible = function () { - return this.settings.checkVisibility ? this.$element.is(':visible') : true; - }; - - /** - * Setups the current settings. - * @todo Remove responsive classes. Why should adaptive designs be brought into IE8? - * @todo Support for media queries by using `matchMedia` would be nice. - * @public - */ - Owl.prototype.setup = function () { - var viewport = this.viewport(), - overwrites = this.options.responsive, - match = -1, - settings = null; - if (!overwrites) { - settings = $.extend({}, this.options); - } else { - $.each(overwrites, function (breakpoint) { - if (breakpoint <= viewport && breakpoint > match) { - match = Number(breakpoint); - } - }); - settings = $.extend({}, this.options, overwrites[match]); - if (typeof settings.stagePadding === 'function') { - settings.stagePadding = settings.stagePadding(); - } - delete settings.responsive; - - // responsive class - if (settings.responsiveClass) { - this.$element.attr('class', this.$element.attr('class').replace(new RegExp('(' + this.options.responsiveClass + '-)\\S+\\s', 'g'), '$1' + match)); - } - } - this.trigger('change', { - property: { - name: 'settings', - value: settings - } - }); - this._breakpoint = match; - this.settings = settings; - this.invalidate('settings'); - this.trigger('changed', { - property: { - name: 'settings', - value: this.settings - } - }); - }; - - /** - * Updates option logic if necessery. - * @protected - */ - Owl.prototype.optionsLogic = function () { - if (this.settings.autoWidth) { - this.settings.stagePadding = false; - this.settings.merge = false; - } - }; - - /** - * Prepares an item before add. - * @todo Rename event parameter `content` to `item`. - * @protected - * @returns {jQuery|HTMLElement} - The item container. - */ - Owl.prototype.prepare = function (item) { - var event = this.trigger('prepare', { - content: item - }); - if (!event.data) { - event.data = $('<' + this.settings.itemElement + '/>').addClass(this.options.itemClass).append(item); - } - this.trigger('prepared', { - content: event.data - }); - return event.data; - }; - - /** - * Updates the view. - * @public - */ - Owl.prototype.update = function () { - var i = 0, - n = this._pipe.length, - filter = $.proxy(function (p) { - return this[p]; - }, this._invalidated), - cache = {}; - while (i < n) { - if (this._invalidated.all || $.grep(this._pipe[i].filter, filter).length > 0) { - this._pipe[i].run(cache); - } - i++; - } - this._invalidated = {}; - !this.is('valid') && this.enter('valid'); - }; - - /** - * Gets the width of the view. - * @public - * @param {Owl.Width} [dimension=Owl.Width.Default] - The dimension to return. - * @returns {Number} - The width of the view in pixel. - */ - Owl.prototype.width = function (dimension) { - dimension = dimension || Owl.Width.Default; - switch (dimension) { - case Owl.Width.Inner: - case Owl.Width.Outer: - return this._width; - default: - return this._width - this.settings.stagePadding * 2 + this.settings.margin; - } - }; - - /** - * Refreshes the carousel primarily for adaptive purposes. - * @public - */ - Owl.prototype.refresh = function () { - this.enter('refreshing'); - this.trigger('refresh'); - this.setup(); - this.optionsLogic(); - this.$element.addClass(this.options.refreshClass); - this.update(); - this.$element.removeClass(this.options.refreshClass); - this.leave('refreshing'); - this.trigger('refreshed'); - }; - - /** - * Checks window `resize` event. - * @protected - */ - Owl.prototype.onThrottledResize = function () { - window.clearTimeout(this.resizeTimer); - this.resizeTimer = window.setTimeout(this._handlers.onResize, this.settings.responsiveRefreshRate); - }; - - /** - * Checks window `resize` event. - * @protected - */ - Owl.prototype.onResize = function () { - if (!this._items.length) { - return false; - } - if (this._width === this.$element.width()) { - return false; - } - if (!this.isVisible()) { - return false; - } - this.enter('resizing'); - if (this.trigger('resize').isDefaultPrevented()) { - this.leave('resizing'); - return false; - } - this.invalidate('width'); - this.refresh(); - this.leave('resizing'); - this.trigger('resized'); - }; - - /** - * Registers event handlers. - * @todo Check `msPointerEnabled` - * @todo #261 - * @protected - */ - Owl.prototype.registerEventHandlers = function () { - if ($.support.transition) { - this.$stage.on($.support.transition.end + '.owl.core', $.proxy(this.onTransitionEnd, this)); - } - if (this.settings.responsive !== false) { - this.on(window, 'resize', this._handlers.onThrottledResize); - } - if (this.settings.mouseDrag) { - this.$element.addClass(this.options.dragClass); - this.$stage.on('mousedown.owl.core', $.proxy(this.onDragStart, this)); - this.$stage.on('dragstart.owl.core selectstart.owl.core', function () { - return false; - }); - } - if (this.settings.touchDrag) { - this.$stage.on('touchstart.owl.core', $.proxy(this.onDragStart, this)); - this.$stage.on('touchcancel.owl.core', $.proxy(this.onDragEnd, this)); - } - }; - - /** - * Handles `touchstart` and `mousedown` events. - * @todo Horizontal swipe threshold as option - * @todo #261 - * @protected - * @param {Event} event - The event arguments. - */ - Owl.prototype.onDragStart = function (event) { - var stage = null; - if (event.which === 3) { - return; - } - if ($.support.transform) { - stage = this.$stage.css('transform').replace(/.*\(|\)| /g, '').split(','); - stage = { - x: stage[stage.length === 16 ? 12 : 4], - y: stage[stage.length === 16 ? 13 : 5] - }; - } else { - stage = this.$stage.position(); - stage = { - x: this.settings.rtl ? stage.left + this.$stage.width() - this.width() + this.settings.margin : stage.left, - y: stage.top - }; - } - if (this.is('animating')) { - $.support.transform ? this.animate(stage.x) : this.$stage.stop(); - this.invalidate('position'); - } - this.$element.toggleClass(this.options.grabClass, event.type === 'mousedown'); - this.speed(0); - this._drag.time = new Date().getTime(); - this._drag.target = $(event.target); - this._drag.stage.start = stage; - this._drag.stage.current = stage; - this._drag.pointer = this.pointer(event); - $(document).on('mouseup.owl.core touchend.owl.core', $.proxy(this.onDragEnd, this)); - $(document).one('mousemove.owl.core touchmove.owl.core', $.proxy(function (event) { - var delta = this.difference(this._drag.pointer, this.pointer(event)); - $(document).on('mousemove.owl.core touchmove.owl.core', $.proxy(this.onDragMove, this)); - if (Math.abs(delta.x) < Math.abs(delta.y) && this.is('valid')) { - return; - } - event.preventDefault(); - this.enter('dragging'); - this.trigger('drag'); - }, this)); - }; - - /** - * Handles the `touchmove` and `mousemove` events. - * @todo #261 - * @protected - * @param {Event} event - The event arguments. - */ - Owl.prototype.onDragMove = function (event) { - var minimum = null, - maximum = null, - pull = null, - delta = this.difference(this._drag.pointer, this.pointer(event)), - stage = this.difference(this._drag.stage.start, delta); - if (!this.is('dragging')) { - return; - } - event.preventDefault(); - if (this.settings.loop) { - minimum = this.coordinates(this.minimum()); - maximum = this.coordinates(this.maximum() + 1) - minimum; - stage.x = ((stage.x - minimum) % maximum + maximum) % maximum + minimum; - } else { - minimum = this.settings.rtl ? this.coordinates(this.maximum()) : this.coordinates(this.minimum()); - maximum = this.settings.rtl ? this.coordinates(this.minimum()) : this.coordinates(this.maximum()); - pull = this.settings.pullDrag ? -1 * delta.x / 5 : 0; - stage.x = Math.max(Math.min(stage.x, minimum + pull), maximum + pull); - } - this._drag.stage.current = stage; - this.animate(stage.x); - }; - - /** - * Handles the `touchend` and `mouseup` events. - * @todo #261 - * @todo Threshold for click event - * @protected - * @param {Event} event - The event arguments. - */ - Owl.prototype.onDragEnd = function (event) { - var delta = this.difference(this._drag.pointer, this.pointer(event)), - stage = this._drag.stage.current, - direction = delta.x > 0 ^ this.settings.rtl ? 'left' : 'right'; - $(document).off('.owl.core'); - this.$element.removeClass(this.options.grabClass); - if (delta.x !== 0 && this.is('dragging') || !this.is('valid')) { - this.speed(this.settings.dragEndSpeed || this.settings.smartSpeed); - this.current(this.closest(stage.x, delta.x !== 0 ? direction : this._drag.direction)); - this.invalidate('position'); - this.update(); - this._drag.direction = direction; - if (Math.abs(delta.x) > 3 || new Date().getTime() - this._drag.time > 300) { - this._drag.target.one('click.owl.core', function () { - return false; - }); - } - } - if (!this.is('dragging')) { - return; - } - this.leave('dragging'); - this.trigger('dragged'); - }; - - /** - * Gets absolute position of the closest item for a coordinate. - * @todo Setting `freeDrag` makes `closest` not reusable. See #165. - * @protected - * @param {Number} coordinate - The coordinate in pixel. - * @param {String} direction - The direction to check for the closest item. Ether `left` or `right`. - * @return {Number} - The absolute position of the closest item. - */ - Owl.prototype.closest = function (coordinate, direction) { - var position = -1, - pull = 30, - width = this.width(), - coordinates = this.coordinates(); - if (!this.settings.freeDrag) { - // check closest item - $.each(coordinates, $.proxy(function (index, value) { - // on a left pull, check on current index - if (direction === 'left' && coordinate > value - pull && coordinate < value + pull) { - position = index; - // on a right pull, check on previous index - // to do so, subtract width from value and set position = index + 1 - } else if (direction === 'right' && coordinate > value - width - pull && coordinate < value - width + pull) { - position = index + 1; - } else if (this.op(coordinate, '<', value) && this.op(coordinate, '>', coordinates[index + 1] !== undefined ? coordinates[index + 1] : value - width)) { - position = direction === 'left' ? index + 1 : index; - } - return position === -1; - }, this)); - } - if (!this.settings.loop) { - // non loop boundries - if (this.op(coordinate, '>', coordinates[this.minimum()])) { - position = coordinate = this.minimum(); - } else if (this.op(coordinate, '<', coordinates[this.maximum()])) { - position = coordinate = this.maximum(); - } - } - return position; - }; - - /** - * Animates the stage. - * @todo #270 - * @public - * @param {Number} coordinate - The coordinate in pixels. - */ - Owl.prototype.animate = function (coordinate) { - var animate = this.speed() > 0; - this.is('animating') && this.onTransitionEnd(); - if (animate) { - this.enter('animating'); - this.trigger('translate'); - } - if ($.support.transform3d && $.support.transition) { - this.$stage.css({ - transform: 'translate3d(' + coordinate + 'px,0px,0px)', - transition: this.speed() / 1000 + 's' + (this.settings.slideTransition ? ' ' + this.settings.slideTransition : '') - }); - } else if (animate) { - this.$stage.animate({ - left: coordinate + 'px' - }, this.speed(), this.settings.fallbackEasing, $.proxy(this.onTransitionEnd, this)); - } else { - this.$stage.css({ - left: coordinate + 'px' - }); - } - }; - - /** - * Checks whether the carousel is in a specific state or not. - * @param {String} state - The state to check. - * @returns {Boolean} - The flag which indicates if the carousel is busy. - */ - Owl.prototype.is = function (state) { - return this._states.current[state] && this._states.current[state] > 0; - }; - - /** - * Sets the absolute position of the current item. - * @public - * @param {Number} [position] - The new absolute position or nothing to leave it unchanged. - * @returns {Number} - The absolute position of the current item. - */ - Owl.prototype.current = function (position) { - if (position === undefined) { - return this._current; - } - if (this._items.length === 0) { - return undefined; - } - position = this.normalize(position); - if (this._current !== position) { - var event = this.trigger('change', { - property: { - name: 'position', - value: position - } - }); - if (event.data !== undefined) { - position = this.normalize(event.data); - } - this._current = position; - this.invalidate('position'); - this.trigger('changed', { - property: { - name: 'position', - value: this._current - } - }); - } - return this._current; - }; - - /** - * Invalidates the given part of the update routine. - * @param {String} [part] - The part to invalidate. - * @returns {Array.} - The invalidated parts. - */ - Owl.prototype.invalidate = function (part) { - if ($.type(part) === 'string') { - this._invalidated[part] = true; - this.is('valid') && this.leave('valid'); - } - return $.map(this._invalidated, function (v, i) { - return i; - }); - }; - - /** - * Resets the absolute position of the current item. - * @public - * @param {Number} position - The absolute position of the new item. - */ - Owl.prototype.reset = function (position) { - position = this.normalize(position); - if (position === undefined) { - return; - } - this._speed = 0; - this._current = position; - this.suppress(['translate', 'translated']); - this.animate(this.coordinates(position)); - this.release(['translate', 'translated']); - }; - - /** - * Normalizes an absolute or a relative position of an item. - * @public - * @param {Number} position - The absolute or relative position to normalize. - * @param {Boolean} [relative=false] - Whether the given position is relative or not. - * @returns {Number} - The normalized position. - */ - Owl.prototype.normalize = function (position, relative) { - var n = this._items.length, - m = relative ? 0 : this._clones.length; - if (!this.isNumeric(position) || n < 1) { - position = undefined; - } else if (position < 0 || position >= n + m) { - position = ((position - m / 2) % n + n) % n + m / 2; - } - return position; - }; - - /** - * Converts an absolute position of an item into a relative one. - * @public - * @param {Number} position - The absolute position to convert. - * @returns {Number} - The converted position. - */ - Owl.prototype.relative = function (position) { - position -= this._clones.length / 2; - return this.normalize(position, true); - }; - - /** - * Gets the maximum position for the current item. - * @public - * @param {Boolean} [relative=false] - Whether to return an absolute position or a relative position. - * @returns {Number} - */ - Owl.prototype.maximum = function (relative) { - var settings = this.settings, - maximum = this._coordinates.length, - iterator, - reciprocalItemsWidth, - elementWidth; - if (settings.loop) { - maximum = this._clones.length / 2 + this._items.length - 1; - } else if (settings.autoWidth || settings.merge) { - iterator = this._items.length; - if (iterator) { - reciprocalItemsWidth = this._items[--iterator].width(); - elementWidth = this.$element.width(); - while (iterator--) { - reciprocalItemsWidth += this._items[iterator].width() + this.settings.margin; - if (reciprocalItemsWidth > elementWidth) { - break; - } - } - } - maximum = iterator + 1; - } else if (settings.center) { - maximum = this._items.length - 1; - } else { - maximum = this._items.length - settings.items; - } - if (relative) { - maximum -= this._clones.length / 2; - } - return Math.max(maximum, 0); - }; - - /** - * Gets the minimum position for the current item. - * @public - * @param {Boolean} [relative=false] - Whether to return an absolute position or a relative position. - * @returns {Number} - */ - Owl.prototype.minimum = function (relative) { - return relative ? 0 : this._clones.length / 2; - }; - - /** - * Gets an item at the specified relative position. - * @public - * @param {Number} [position] - The relative position of the item. - * @return {jQuery|Array.} - The item at the given position or all items if no position was given. - */ - Owl.prototype.items = function (position) { - if (position === undefined) { - return this._items.slice(); - } - position = this.normalize(position, true); - return this._items[position]; - }; - - /** - * Gets an item at the specified relative position. - * @public - * @param {Number} [position] - The relative position of the item. - * @return {jQuery|Array.} - The item at the given position or all items if no position was given. - */ - Owl.prototype.mergers = function (position) { - if (position === undefined) { - return this._mergers.slice(); - } - position = this.normalize(position, true); - return this._mergers[position]; - }; - - /** - * Gets the absolute positions of clones for an item. - * @public - * @param {Number} [position] - The relative position of the item. - * @returns {Array.} - The absolute positions of clones for the item or all if no position was given. - */ - Owl.prototype.clones = function (position) { - var odd = this._clones.length / 2, - even = odd + this._items.length, - map = function (index) { - return index % 2 === 0 ? even + index / 2 : odd - (index + 1) / 2; - }; - if (position === undefined) { - return $.map(this._clones, function (v, i) { - return map(i); - }); - } - return $.map(this._clones, function (v, i) { - return v === position ? map(i) : null; - }); - }; - - /** - * Sets the current animation speed. - * @public - * @param {Number} [speed] - The animation speed in milliseconds or nothing to leave it unchanged. - * @returns {Number} - The current animation speed in milliseconds. - */ - Owl.prototype.speed = function (speed) { - if (speed !== undefined) { - this._speed = speed; - } - return this._speed; - }; - - /** - * Gets the coordinate of an item. - * @todo The name of this method is missleanding. - * @public - * @param {Number} position - The absolute position of the item within `minimum()` and `maximum()`. - * @returns {Number|Array.} - The coordinate of the item in pixel or all coordinates. - */ - Owl.prototype.coordinates = function (position) { - var multiplier = 1, - newPosition = position - 1, - coordinate; - if (position === undefined) { - return $.map(this._coordinates, $.proxy(function (coordinate, index) { - return this.coordinates(index); - }, this)); - } - if (this.settings.center) { - if (this.settings.rtl) { - multiplier = -1; - newPosition = position + 1; - } - coordinate = this._coordinates[position]; - coordinate += (this.width() - coordinate + (this._coordinates[newPosition] || 0)) / 2 * multiplier; - } else { - coordinate = this._coordinates[newPosition] || 0; - } - coordinate = Math.ceil(coordinate); - return coordinate; - }; - - /** - * Calculates the speed for a translation. - * @protected - * @param {Number} from - The absolute position of the start item. - * @param {Number} to - The absolute position of the target item. - * @param {Number} [factor=undefined] - The time factor in milliseconds. - * @returns {Number} - The time in milliseconds for the translation. - */ - Owl.prototype.duration = function (from, to, factor) { - if (factor === 0) { - return 0; - } - return Math.min(Math.max(Math.abs(to - from), 1), 6) * Math.abs(factor || this.settings.smartSpeed); - }; - - /** - * Slides to the specified item. - * @public - * @param {Number} position - The position of the item. - * @param {Number} [speed] - The time in milliseconds for the transition. - */ - Owl.prototype.to = function (position, speed) { - var current = this.current(), - revert = null, - distance = position - this.relative(current), - direction = (distance > 0) - (distance < 0), - items = this._items.length, - minimum = this.minimum(), - maximum = this.maximum(); - if (this.settings.loop) { - if (!this.settings.rewind && Math.abs(distance) > items / 2) { - distance += direction * -1 * items; - } - position = current + distance; - revert = ((position - minimum) % items + items) % items + minimum; - if (revert !== position && revert - distance <= maximum && revert - distance > 0) { - current = revert - distance; - position = revert; - this.reset(current); - } - } else if (this.settings.rewind) { - maximum += 1; - position = (position % maximum + maximum) % maximum; - } else { - position = Math.max(minimum, Math.min(maximum, position)); - } - this.speed(this.duration(current, position, speed)); - this.current(position); - if (this.isVisible()) { - this.update(); - } - }; - - /** - * Slides to the next item. - * @public - * @param {Number} [speed] - The time in milliseconds for the transition. - */ - Owl.prototype.next = function (speed) { - speed = speed || false; - this.to(this.relative(this.current()) + 1, speed); - }; - - /** - * Slides to the previous item. - * @public - * @param {Number} [speed] - The time in milliseconds for the transition. - */ - Owl.prototype.prev = function (speed) { - speed = speed || false; - this.to(this.relative(this.current()) - 1, speed); - }; - - /** - * Handles the end of an animation. - * @protected - * @param {Event} event - The event arguments. - */ - Owl.prototype.onTransitionEnd = function (event) { - // if css2 animation then event object is undefined - if (event !== undefined) { - event.stopPropagation(); - - // Catch only owl-stage transitionEnd event - if ((event.target || event.srcElement || event.originalTarget) !== this.$stage.get(0)) { - return false; - } - } - this.leave('animating'); - this.trigger('translated'); - }; - - /** - * Gets viewport width. - * @protected - * @return {Number} - The width in pixel. - */ - Owl.prototype.viewport = function () { - var width; - if (this.options.responsiveBaseElement !== window) { - width = $(this.options.responsiveBaseElement).width(); - } else if (window.innerWidth) { - width = window.innerWidth; - } else if (document.documentElement && document.documentElement.clientWidth) { - width = document.documentElement.clientWidth; - } else { - console.warn('Can not detect viewport width.'); - } - return width; - }; - - /** - * Replaces the current content. - * @public - * @param {HTMLElement|jQuery|String} content - The new content. - */ - Owl.prototype.replace = function (content) { - this.$stage.empty(); - this._items = []; - if (content) { - content = content instanceof jQuery ? content : $(content); - } - if (this.settings.nestedItemSelector) { - content = content.find('.' + this.settings.nestedItemSelector); - } - content.filter(function () { - return this.nodeType === 1; - }).each($.proxy(function (index, item) { - item = this.prepare(item); - this.$stage.append(item); - this._items.push(item); - this._mergers.push(item.find('[data-merge]').addBack('[data-merge]').attr('data-merge') * 1 || 1); - }, this)); - this.reset(this.isNumeric(this.settings.startPosition) ? this.settings.startPosition : 0); - this.invalidate('items'); - }; - - /** - * Adds an item. - * @todo Use `item` instead of `content` for the event arguments. - * @public - * @param {HTMLElement|jQuery|String} content - The item content to add. - * @param {Number} [position] - The relative position at which to insert the item otherwise the item will be added to the end. - */ - Owl.prototype.add = function (content, position) { - var current = this.relative(this._current); - position = position === undefined ? this._items.length : this.normalize(position, true); - content = content instanceof jQuery ? content : $(content); - this.trigger('add', { - content: content, - position: position - }); - content = this.prepare(content); - if (this._items.length === 0 || position === this._items.length) { - this._items.length === 0 && this.$stage.append(content); - this._items.length !== 0 && this._items[position - 1].after(content); - this._items.push(content); - this._mergers.push(content.find('[data-merge]').addBack('[data-merge]').attr('data-merge') * 1 || 1); - } else { - this._items[position].before(content); - this._items.splice(position, 0, content); - this._mergers.splice(position, 0, content.find('[data-merge]').addBack('[data-merge]').attr('data-merge') * 1 || 1); - } - this._items[current] && this.reset(this._items[current].index()); - this.invalidate('items'); - this.trigger('added', { - content: content, - position: position - }); - }; - - /** - * Removes an item by its position. - * @todo Use `item` instead of `content` for the event arguments. - * @public - * @param {Number} position - The relative position of the item to remove. - */ - Owl.prototype.remove = function (position) { - position = this.normalize(position, true); - if (position === undefined) { - return; - } - this.trigger('remove', { - content: this._items[position], - position: position - }); - this._items[position].remove(); - this._items.splice(position, 1); - this._mergers.splice(position, 1); - this.invalidate('items'); - this.trigger('removed', { - content: null, - position: position - }); - }; - - /** - * Preloads images with auto width. - * @todo Replace by a more generic approach - * @protected - */ - Owl.prototype.preloadAutoWidthImages = function (images) { - images.each($.proxy(function (i, element) { - this.enter('pre-loading'); - element = $(element); - $(new Image()).one('load', $.proxy(function (e) { - element.attr('src', e.target.src); - element.css('opacity', 1); - this.leave('pre-loading'); - !this.is('pre-loading') && !this.is('initializing') && this.refresh(); - }, this)).attr('src', element.attr('src') || element.attr('data-src') || element.attr('data-src-retina')); - }, this)); - }; - - /** - * Destroys the carousel. - * @public - */ - Owl.prototype.destroy = function () { - this.$element.off('.owl.core'); - this.$stage.off('.owl.core'); - $(document).off('.owl.core'); - if (this.settings.responsive !== false) { - window.clearTimeout(this.resizeTimer); - this.off(window, 'resize', this._handlers.onThrottledResize); - } - for (var i in this._plugins) { - this._plugins[i].destroy(); - } - this.$stage.children('.cloned').remove(); - this.$stage.unwrap(); - this.$stage.children().contents().unwrap(); - this.$stage.children().unwrap(); - this.$stage.remove(); - this.$element.removeClass(this.options.refreshClass).removeClass(this.options.loadingClass).removeClass(this.options.loadedClass).removeClass(this.options.rtlClass).removeClass(this.options.dragClass).removeClass(this.options.grabClass).attr('class', this.$element.attr('class').replace(new RegExp(this.options.responsiveClass + '-\\S+\\s', 'g'), '')).removeData('owl.carousel'); - }; - - /** - * Operators to calculate right-to-left and left-to-right. - * @protected - * @param {Number} [a] - The left side operand. - * @param {String} [o] - The operator. - * @param {Number} [b] - The right side operand. - */ - Owl.prototype.op = function (a, o, b) { - var rtl = this.settings.rtl; - switch (o) { - case '<': - return rtl ? a > b : a < b; - case '>': - return rtl ? a < b : a > b; - case '>=': - return rtl ? a <= b : a >= b; - case '<=': - return rtl ? a >= b : a <= b; - default: - break; - } - }; - - /** - * Attaches to an internal event. - * @protected - * @param {HTMLElement} element - The event source. - * @param {String} event - The event name. - * @param {Function} listener - The event handler to attach. - * @param {Boolean} capture - Wether the event should be handled at the capturing phase or not. - */ - Owl.prototype.on = function (element, event, listener, capture) { - if (element.addEventListener) { - element.addEventListener(event, listener, capture); - } else if (element.attachEvent) { - element.attachEvent('on' + event, listener); - } - }; - - /** - * Detaches from an internal event. - * @protected - * @param {HTMLElement} element - The event source. - * @param {String} event - The event name. - * @param {Function} listener - The attached event handler to detach. - * @param {Boolean} capture - Wether the attached event handler was registered as a capturing listener or not. - */ - Owl.prototype.off = function (element, event, listener, capture) { - if (element.removeEventListener) { - element.removeEventListener(event, listener, capture); - } else if (element.detachEvent) { - element.detachEvent('on' + event, listener); - } - }; - - /** - * Triggers a public event. - * @todo Remove `status`, `relatedTarget` should be used instead. - * @protected - * @param {String} name - The event name. - * @param {*} [data=null] - The event data. - * @param {String} [namespace=carousel] - The event namespace. - * @param {String} [state] - The state which is associated with the event. - * @param {Boolean} [enter=false] - Indicates if the call enters the specified state or not. - * @returns {Event} - The event arguments. - */ - Owl.prototype.trigger = function (name, data, namespace, state, enter) { - var status = { - item: { - count: this._items.length, - index: this.current() - } - }, - handler = $.camelCase($.grep(['on', name, namespace], function (v) { - return v; - }).join('-').toLowerCase()), - event = $.Event([name, 'owl', namespace || 'carousel'].join('.').toLowerCase(), $.extend({ - relatedTarget: this - }, status, data)); - if (!this._supress[name]) { - $.each(this._plugins, function (name, plugin) { - if (plugin.onTrigger) { - plugin.onTrigger(event); - } - }); - this.register({ - type: Owl.Type.Event, - name: name - }); - this.$element.trigger(event); - if (this.settings && typeof this.settings[handler] === 'function') { - this.settings[handler].call(this, event); - } - } - return event; - }; - - /** - * Enters a state. - * @param name - The state name. - */ - Owl.prototype.enter = function (name) { - $.each([name].concat(this._states.tags[name] || []), $.proxy(function (i, name) { - if (this._states.current[name] === undefined) { - this._states.current[name] = 0; - } - this._states.current[name]++; - }, this)); - }; - - /** - * Leaves a state. - * @param name - The state name. - */ - Owl.prototype.leave = function (name) { - $.each([name].concat(this._states.tags[name] || []), $.proxy(function (i, name) { - this._states.current[name]--; - }, this)); - }; - - /** - * Registers an event or state. - * @public - * @param {Object} object - The event or state to register. - */ - Owl.prototype.register = function (object) { - if (object.type === Owl.Type.Event) { - if (!$.event.special[object.name]) { - $.event.special[object.name] = {}; - } - if (!$.event.special[object.name].owl) { - var _default = $.event.special[object.name]._default; - $.event.special[object.name]._default = function (e) { - if (_default && _default.apply && (!e.namespace || e.namespace.indexOf('owl') === -1)) { - return _default.apply(this, arguments); - } - return e.namespace && e.namespace.indexOf('owl') > -1; - }; - $.event.special[object.name].owl = true; - } - } else if (object.type === Owl.Type.State) { - if (!this._states.tags[object.name]) { - this._states.tags[object.name] = object.tags; - } else { - this._states.tags[object.name] = this._states.tags[object.name].concat(object.tags); - } - this._states.tags[object.name] = $.grep(this._states.tags[object.name], $.proxy(function (tag, i) { - return $.inArray(tag, this._states.tags[object.name]) === i; - }, this)); - } - }; - - /** - * Suppresses events. - * @protected - * @param {Array.} events - The events to suppress. - */ - Owl.prototype.suppress = function (events) { - $.each(events, $.proxy(function (index, event) { - this._supress[event] = true; - }, this)); - }; - - /** - * Releases suppressed events. - * @protected - * @param {Array.} events - The events to release. - */ - Owl.prototype.release = function (events) { - $.each(events, $.proxy(function (index, event) { - delete this._supress[event]; - }, this)); - }; - - /** - * Gets unified pointer coordinates from event. - * @todo #261 - * @protected - * @param {Event} - The `mousedown` or `touchstart` event. - * @returns {Object} - Contains `x` and `y` coordinates of current pointer position. - */ - Owl.prototype.pointer = function (event) { - var result = { - x: null, - y: null - }; - event = event.originalEvent || event || window.event; - event = event.touches && event.touches.length ? event.touches[0] : event.changedTouches && event.changedTouches.length ? event.changedTouches[0] : event; - if (event.pageX) { - result.x = event.pageX; - result.y = event.pageY; - } else { - result.x = event.clientX; - result.y = event.clientY; - } - return result; - }; - - /** - * Determines if the input is a Number or something that can be coerced to a Number - * @protected - * @param {Number|String|Object|Array|Boolean|RegExp|Function|Symbol} - The input to be tested - * @returns {Boolean} - An indication if the input is a Number or can be coerced to a Number - */ - Owl.prototype.isNumeric = function (number) { - return !isNaN(parseFloat(number)); - }; - - /** - * Gets the difference of two vectors. - * @todo #261 - * @protected - * @param {Object} - The first vector. - * @param {Object} - The second vector. - * @returns {Object} - The difference. - */ - Owl.prototype.difference = function (first, second) { - return { - x: first.x - second.x, - y: first.y - second.y - }; - }; - - /** - * The jQuery Plugin for the Owl Carousel - * @todo Navigation plugin `next` and `prev` - * @public - */ - $.fn.owlCarousel = function (option) { - var args = Array.prototype.slice.call(arguments, 1); - return this.each(function () { - var $this = $(this), - data = $this.data('owl.carousel'); - if (!data) { - data = new Owl(this, typeof option == 'object' && option); - $this.data('owl.carousel', data); - $.each(['next', 'prev', 'to', 'destroy', 'refresh', 'replace', 'add', 'remove'], function (i, event) { - data.register({ - type: Owl.Type.Event, - name: event - }); - data.$element.on(event + '.owl.carousel.core', $.proxy(function (e) { - if (e.namespace && e.relatedTarget !== this) { - this.suppress([event]); - data[event].apply(this, [].slice.call(arguments, 1)); - this.release([event]); - } - }, data)); - }); - } - if (typeof option == 'string' && option.charAt(0) !== '_') { - data[option].apply(data, args); - } - }); - }; - - /** - * The constructor for the jQuery Plugin - * @public - */ - $.fn.owlCarousel.Constructor = Owl; -})(window.Zepto || window.jQuery, window, document); - -/** - * AutoRefresh Plugin - * @version 2.3.4 - * @author Artus Kolanowski - * @author David Deutsch - * @license The MIT License (MIT) - */ -; -(function ($, window, document, undefined) { - /** - * Creates the auto refresh plugin. - * @class The Auto Refresh Plugin - * @param {Owl} carousel - The Owl Carousel - */ - var AutoRefresh = function (carousel) { - /** - * Reference to the core. - * @protected - * @type {Owl} - */ - this._core = carousel; - - /** - * Refresh interval. - * @protected - * @type {number} - */ - this._interval = null; - - /** - * Whether the element is currently visible or not. - * @protected - * @type {Boolean} - */ - this._visible = null; - - /** - * All event handlers. - * @protected - * @type {Object} - */ - this._handlers = { - 'initialized.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.autoRefresh) { - this.watch(); - } - }, this) - }; - - // set default options - this._core.options = $.extend({}, AutoRefresh.Defaults, this._core.options); - - // register event handlers - this._core.$element.on(this._handlers); - }; - - /** - * Default options. - * @public - */ - AutoRefresh.Defaults = { - autoRefresh: true, - autoRefreshInterval: 500 - }; - - /** - * Watches the element. - */ - AutoRefresh.prototype.watch = function () { - if (this._interval) { - return; - } - this._visible = this._core.isVisible(); - this._interval = window.setInterval($.proxy(this.refresh, this), this._core.settings.autoRefreshInterval); - }; - - /** - * Refreshes the element. - */ - AutoRefresh.prototype.refresh = function () { - if (this._core.isVisible() === this._visible) { - return; - } - this._visible = !this._visible; - this._core.$element.toggleClass('owl-hidden', !this._visible); - this._visible && this._core.invalidate('width') && this._core.refresh(); - }; - - /** - * Destroys the plugin. - */ - AutoRefresh.prototype.destroy = function () { - var handler, property; - window.clearInterval(this._interval); - for (handler in this._handlers) { - this._core.$element.off(handler, this._handlers[handler]); - } - for (property in Object.getOwnPropertyNames(this)) { - typeof this[property] != 'function' && (this[property] = null); - } - }; - $.fn.owlCarousel.Constructor.Plugins.AutoRefresh = AutoRefresh; -})(window.Zepto || window.jQuery, window, document); - -/** - * Lazy Plugin - * @version 2.3.4 - * @author Bartosz Wojciechowski - * @author David Deutsch - * @license The MIT License (MIT) - */ -; -(function ($, window, document, undefined) { - /** - * Creates the lazy plugin. - * @class The Lazy Plugin - * @param {Owl} carousel - The Owl Carousel - */ - var Lazy = function (carousel) { - /** - * Reference to the core. - * @protected - * @type {Owl} - */ - this._core = carousel; - - /** - * Already loaded items. - * @protected - * @type {Array.} - */ - this._loaded = []; - - /** - * Event handlers. - * @protected - * @type {Object} - */ - this._handlers = { - 'initialized.owl.carousel change.owl.carousel resized.owl.carousel': $.proxy(function (e) { - if (!e.namespace) { - return; - } - if (!this._core.settings || !this._core.settings.lazyLoad) { - return; - } - if (e.property && e.property.name == 'position' || e.type == 'initialized') { - var settings = this._core.settings, - n = settings.center && Math.ceil(settings.items / 2) || settings.items, - i = settings.center && n * -1 || 0, - position = (e.property && e.property.value !== undefined ? e.property.value : this._core.current()) + i, - clones = this._core.clones().length, - load = $.proxy(function (i, v) { - this.load(v); - }, this); - //TODO: Need documentation for this new option - if (settings.lazyLoadEager > 0) { - n += settings.lazyLoadEager; - // If the carousel is looping also preload images that are to the "left" - if (settings.loop) { - position -= settings.lazyLoadEager; - n++; - } - } - while (i++ < n) { - this.load(clones / 2 + this._core.relative(position)); - clones && $.each(this._core.clones(this._core.relative(position)), load); - position++; - } - } - }, this) - }; - - // set the default options - this._core.options = $.extend({}, Lazy.Defaults, this._core.options); - - // register event handler - this._core.$element.on(this._handlers); - }; - - /** - * Default options. - * @public - */ - Lazy.Defaults = { - lazyLoad: false, - lazyLoadEager: 0 - }; - - /** - * Loads all resources of an item at the specified position. - * @param {Number} position - The absolute position of the item. - * @protected - */ - Lazy.prototype.load = function (position) { - var $item = this._core.$stage.children().eq(position), - $elements = $item && $item.find('.owl-lazy'); - if (!$elements || $.inArray($item.get(0), this._loaded) > -1) { - return; - } - $elements.each($.proxy(function (index, element) { - var $element = $(element), - image, - url = window.devicePixelRatio > 1 && $element.attr('data-src-retina') || $element.attr('data-src') || $element.attr('data-srcset'); - this._core.trigger('load', { - element: $element, - url: url - }, 'lazy'); - if ($element.is('img')) { - $element.one('load.owl.lazy', $.proxy(function () { - $element.css('opacity', 1); - this._core.trigger('loaded', { - element: $element, - url: url - }, 'lazy'); - }, this)).attr('src', url); - } else if ($element.is('source')) { - $element.one('load.owl.lazy', $.proxy(function () { - this._core.trigger('loaded', { - element: $element, - url: url - }, 'lazy'); - }, this)).attr('srcset', url); - } else { - image = new Image(); - image.onload = $.proxy(function () { - $element.css({ - 'background-image': 'url("' + url + '")', - 'opacity': '1' - }); - this._core.trigger('loaded', { - element: $element, - url: url - }, 'lazy'); - }, this); - image.src = url; - } - }, this)); - this._loaded.push($item.get(0)); - }; - - /** - * Destroys the plugin. - * @public - */ - Lazy.prototype.destroy = function () { - var handler, property; - for (handler in this.handlers) { - this._core.$element.off(handler, this.handlers[handler]); - } - for (property in Object.getOwnPropertyNames(this)) { - typeof this[property] != 'function' && (this[property] = null); - } - }; - $.fn.owlCarousel.Constructor.Plugins.Lazy = Lazy; -})(window.Zepto || window.jQuery, window, document); - -/** - * AutoHeight Plugin - * @version 2.3.4 - * @author Bartosz Wojciechowski - * @author David Deutsch - * @license The MIT License (MIT) - */ -; -(function ($, window, document, undefined) { - /** - * Creates the auto height plugin. - * @class The Auto Height Plugin - * @param {Owl} carousel - The Owl Carousel - */ - var AutoHeight = function (carousel) { - /** - * Reference to the core. - * @protected - * @type {Owl} - */ - this._core = carousel; - this._previousHeight = null; - - /** - * All event handlers. - * @protected - * @type {Object} - */ - this._handlers = { - 'initialized.owl.carousel refreshed.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.autoHeight) { - this.update(); - } - }, this), - 'changed.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.autoHeight && e.property.name === 'position') { - this.update(); - } - }, this), - 'loaded.owl.lazy': $.proxy(function (e) { - if (e.namespace && this._core.settings.autoHeight && e.element.closest('.' + this._core.settings.itemClass).index() === this._core.current()) { - this.update(); - } - }, this) - }; - - // set default options - this._core.options = $.extend({}, AutoHeight.Defaults, this._core.options); - - // register event handlers - this._core.$element.on(this._handlers); - this._intervalId = null; - var refThis = this; - - // These changes have been taken from a PR by gavrochelegnou proposed in #1575 - // and have been made compatible with the latest jQuery version - $(window).on('load', function () { - if (refThis._core.settings.autoHeight) { - refThis.update(); - } - }); - - // Autoresize the height of the carousel when window is resized - // When carousel has images, the height is dependent on the width - // and should also change on resize - $(window).resize(function () { - if (refThis._core.settings.autoHeight) { - if (refThis._intervalId != null) { - clearTimeout(refThis._intervalId); - } - refThis._intervalId = setTimeout(function () { - refThis.update(); - }, 250); - } - }); - }; - - /** - * Default options. - * @public - */ - AutoHeight.Defaults = { - autoHeight: false, - autoHeightClass: 'owl-height' - }; - - /** - * Updates the view. - */ - AutoHeight.prototype.update = function () { - var start = this._core._current, - end = start + this._core.settings.items, - lazyLoadEnabled = this._core.settings.lazyLoad, - visible = this._core.$stage.children().toArray().slice(start, end), - heights = [], - maxheight = 0; - $.each(visible, function (index, item) { - heights.push($(item).height()); - }); - maxheight = Math.max.apply(null, heights); - if (maxheight <= 1 && lazyLoadEnabled && this._previousHeight) { - maxheight = this._previousHeight; - } - this._previousHeight = maxheight; - this._core.$stage.parent().height(maxheight).addClass(this._core.settings.autoHeightClass); - }; - AutoHeight.prototype.destroy = function () { - var handler, property; - for (handler in this._handlers) { - this._core.$element.off(handler, this._handlers[handler]); - } - for (property in Object.getOwnPropertyNames(this)) { - typeof this[property] !== 'function' && (this[property] = null); - } - }; - $.fn.owlCarousel.Constructor.Plugins.AutoHeight = AutoHeight; -})(window.Zepto || window.jQuery, window, document); - -/** - * Video Plugin - * @version 2.3.4 - * @author Bartosz Wojciechowski - * @author David Deutsch - * @license The MIT License (MIT) - */ -; -(function ($, window, document, undefined) { - /** - * Creates the video plugin. - * @class The Video Plugin - * @param {Owl} carousel - The Owl Carousel - */ - var Video = function (carousel) { - /** - * Reference to the core. - * @protected - * @type {Owl} - */ - this._core = carousel; - - /** - * Cache all video URLs. - * @protected - * @type {Object} - */ - this._videos = {}; - - /** - * Current playing item. - * @protected - * @type {jQuery} - */ - this._playing = null; - - /** - * All event handlers. - * @todo The cloned content removale is too late - * @protected - * @type {Object} - */ - this._handlers = { - 'initialized.owl.carousel': $.proxy(function (e) { - if (e.namespace) { - this._core.register({ - type: 'state', - name: 'playing', - tags: ['interacting'] - }); - } - }, this), - 'resize.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.video && this.isInFullScreen()) { - e.preventDefault(); - } - }, this), - 'refreshed.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.is('resizing')) { - this._core.$stage.find('.cloned .owl-video-frame').remove(); - } - }, this), - 'changed.owl.carousel': $.proxy(function (e) { - if (e.namespace && e.property.name === 'position' && this._playing) { - this.stop(); - } - }, this), - 'prepared.owl.carousel': $.proxy(function (e) { - if (!e.namespace) { - return; - } - var $element = $(e.content).find('.owl-video'); - if ($element.length) { - $element.css('display', 'none'); - this.fetch($element, $(e.content)); - } - }, this) - }; - - // set default options - this._core.options = $.extend({}, Video.Defaults, this._core.options); - - // register event handlers - this._core.$element.on(this._handlers); - this._core.$element.on('click.owl.video', '.owl-video-play-icon', $.proxy(function (e) { - this.play(e); - }, this)); - }; - - /** - * Default options. - * @public - */ - Video.Defaults = { - video: false, - videoHeight: false, - videoWidth: false - }; - - /** - * Gets the video ID and the type (YouTube/Vimeo/vzaar only). - * @protected - * @param {jQuery} target - The target containing the video data. - * @param {jQuery} item - The item containing the video. - */ - Video.prototype.fetch = function (target, item) { - var type = function () { - if (target.attr('data-vimeo-id')) { - return 'vimeo'; - } else if (target.attr('data-vzaar-id')) { - return 'vzaar'; - } else { - return 'youtube'; - } - }(), - id = target.attr('data-vimeo-id') || target.attr('data-youtube-id') || target.attr('data-vzaar-id'), - width = target.attr('data-width') || this._core.settings.videoWidth, - height = target.attr('data-height') || this._core.settings.videoHeight, - url = target.attr('href'); - if (url) { - /* - Parses the id's out of the following urls (and probably more): - https://www.youtube.com/watch?v=:id - https://youtu.be/:id - https://vimeo.com/:id - https://vimeo.com/channels/:channel/:id - https://vimeo.com/groups/:group/videos/:id - https://app.vzaar.com/videos/:id - Visual example: https://regexper.com/#(http%3A%7Chttps%3A%7C)%5C%2F%5C%2F(player.%7Cwww.%7Capp.)%3F(vimeo%5C.com%7Cyoutu(be%5C.com%7C%5C.be%7Cbe%5C.googleapis%5C.com)%7Cvzaar%5C.com)%5C%2F(video%5C%2F%7Cvideos%5C%2F%7Cembed%5C%2F%7Cchannels%5C%2F.%2B%5C%2F%7Cgroups%5C%2F.%2B%5C%2F%7Cwatch%5C%3Fv%3D%7Cv%5C%2F)%3F(%5BA-Za-z0-9._%25-%5D*)(%5C%26%5CS%2B)%3F - */ - - id = url.match(/(http:|https:|)\/\/(player.|www.|app.)?(vimeo\.com|youtu(be\.com|\.be|be\.googleapis\.com|be\-nocookie\.com)|vzaar\.com)\/(video\/|videos\/|embed\/|channels\/.+\/|groups\/.+\/|watch\?v=|v\/)?([A-Za-z0-9._%-]*)(\&\S+)?/); - if (id[3].indexOf('youtu') > -1) { - type = 'youtube'; - } else if (id[3].indexOf('vimeo') > -1) { - type = 'vimeo'; - } else if (id[3].indexOf('vzaar') > -1) { - type = 'vzaar'; - } else { - throw new Error('Video URL not supported.'); - } - id = id[6]; - } else { - throw new Error('Missing video URL.'); - } - this._videos[url] = { - type: type, - id: id, - width: width, - height: height - }; - item.attr('data-video', url); - this.thumbnail(target, this._videos[url]); - }; - - /** - * Creates video thumbnail. - * @protected - * @param {jQuery} target - The target containing the video data. - * @param {Object} info - The video info object. - * @see `fetch` - */ - Video.prototype.thumbnail = function (target, video) { - var tnLink, - icon, - path, - dimensions = video.width && video.height ? 'width:' + video.width + 'px;height:' + video.height + 'px;' : '', - customTn = target.find('img'), - srcType = 'src', - lazyClass = '', - settings = this._core.settings, - create = function (path) { - icon = '
'; - if (settings.lazyLoad) { - tnLink = $('
', { - "class": 'owl-video-tn ' + lazyClass, - "srcType": path - }); - } else { - tnLink = $('
', { - "class": "owl-video-tn", - "style": 'opacity:1;background-image:url(' + path + ')' - }); - } - target.after(tnLink); - target.after(icon); - }; - - // wrap video content into owl-video-wrapper div - target.wrap($('
', { - "class": "owl-video-wrapper", - "style": dimensions - })); - if (this._core.settings.lazyLoad) { - srcType = 'data-src'; - lazyClass = 'owl-lazy'; - } - - // custom thumbnail - if (customTn.length) { - create(customTn.attr(srcType)); - customTn.remove(); - return false; - } - if (video.type === 'youtube') { - path = "//img.youtube.com/vi/" + video.id + "/hqdefault.jpg"; - create(path); - } else if (video.type === 'vimeo') { - $.ajax({ - type: 'GET', - url: '//vimeo.com/api/v2/video/' + video.id + '.json', - jsonp: 'callback', - dataType: 'jsonp', - success: function (data) { - path = data[0].thumbnail_large; - create(path); - } - }); - } else if (video.type === 'vzaar') { - $.ajax({ - type: 'GET', - url: '//vzaar.com/api/videos/' + video.id + '.json', - jsonp: 'callback', - dataType: 'jsonp', - success: function (data) { - path = data.framegrab_url; - create(path); - } - }); - } - }; - - /** - * Stops the current video. - * @public - */ - Video.prototype.stop = function () { - this._core.trigger('stop', null, 'video'); - this._playing.find('.owl-video-frame').remove(); - this._playing.removeClass('owl-video-playing'); - this._playing = null; - this._core.leave('playing'); - this._core.trigger('stopped', null, 'video'); - }; - - /** - * Starts the current video. - * @public - * @param {Event} event - The event arguments. - */ - Video.prototype.play = function (event) { - var target = $(event.target), - item = target.closest('.' + this._core.settings.itemClass), - video = this._videos[item.attr('data-video')], - width = video.width || '100%', - height = video.height || this._core.$stage.height(), - html, - iframe; - if (this._playing) { - return; - } - this._core.enter('playing'); - this._core.trigger('play', null, 'video'); - item = this._core.items(this._core.relative(item.index())); - this._core.reset(item.index()); - html = $(''); - html.attr('height', height); - html.attr('width', width); - if (video.type === 'youtube') { - html.attr('src', '//www.youtube.com/embed/' + video.id + '?autoplay=1&rel=0&v=' + video.id); - } else if (video.type === 'vimeo') { - html.attr('src', '//player.vimeo.com/video/' + video.id + '?autoplay=1'); - } else if (video.type === 'vzaar') { - html.attr('src', '//view.vzaar.com/' + video.id + '/player?autoplay=true'); - } - iframe = $(html).wrap('
').insertAfter(item.find('.owl-video')); - this._playing = item.addClass('owl-video-playing'); - }; - - /** - * Checks whether an video is currently in full screen mode or not. - * @todo Bad style because looks like a readonly method but changes members. - * @protected - * @returns {Boolean} - */ - Video.prototype.isInFullScreen = function () { - var element = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement; - return element && $(element).parent().hasClass('owl-video-frame'); - }; - - /** - * Destroys the plugin. - */ - Video.prototype.destroy = function () { - var handler, property; - this._core.$element.off('click.owl.video'); - for (handler in this._handlers) { - this._core.$element.off(handler, this._handlers[handler]); - } - for (property in Object.getOwnPropertyNames(this)) { - typeof this[property] != 'function' && (this[property] = null); - } - }; - $.fn.owlCarousel.Constructor.Plugins.Video = Video; -})(window.Zepto || window.jQuery, window, document); - -/** - * Animate Plugin - * @version 2.3.4 - * @author Bartosz Wojciechowski - * @author David Deutsch - * @license The MIT License (MIT) - */ -; -(function ($, window, document, undefined) { - /** - * Creates the animate plugin. - * @class The Navigation Plugin - * @param {Owl} scope - The Owl Carousel - */ - var Animate = function (scope) { - this.core = scope; - this.core.options = $.extend({}, Animate.Defaults, this.core.options); - this.swapping = true; - this.previous = undefined; - this.next = undefined; - this.handlers = { - 'change.owl.carousel': $.proxy(function (e) { - if (e.namespace && e.property.name == 'position') { - this.previous = this.core.current(); - this.next = e.property.value; - } - }, this), - 'drag.owl.carousel dragged.owl.carousel translated.owl.carousel': $.proxy(function (e) { - if (e.namespace) { - this.swapping = e.type == 'translated'; - } - }, this), - 'translate.owl.carousel': $.proxy(function (e) { - if (e.namespace && this.swapping && (this.core.options.animateOut || this.core.options.animateIn)) { - this.swap(); - } - }, this) - }; - this.core.$element.on(this.handlers); - }; - - /** - * Default options. - * @public - */ - Animate.Defaults = { - animateOut: false, - animateIn: false - }; - - /** - * Toggles the animation classes whenever an translations starts. - * @protected - * @returns {Boolean|undefined} - */ - Animate.prototype.swap = function () { - if (this.core.settings.items !== 1) { - return; - } - if (!$.support.animation || !$.support.transition) { - return; - } - this.core.speed(0); - var left, - clear = $.proxy(this.clear, this), - previous = this.core.$stage.children().eq(this.previous), - next = this.core.$stage.children().eq(this.next), - incoming = this.core.settings.animateIn, - outgoing = this.core.settings.animateOut; - if (this.core.current() === this.previous) { - return; - } - if (outgoing) { - left = this.core.coordinates(this.previous) - this.core.coordinates(this.next); - previous.one($.support.animation.end, clear).css({ - 'left': left + 'px' - }).addClass('animated owl-animated-out').addClass(outgoing); - } - if (incoming) { - next.one($.support.animation.end, clear).addClass('animated owl-animated-in').addClass(incoming); - } - }; - Animate.prototype.clear = function (e) { - $(e.target).css({ - 'left': '' - }).removeClass('animated owl-animated-out owl-animated-in').removeClass(this.core.settings.animateIn).removeClass(this.core.settings.animateOut); - this.core.onTransitionEnd(); - }; - - /** - * Destroys the plugin. - * @public - */ - Animate.prototype.destroy = function () { - var handler, property; - for (handler in this.handlers) { - this.core.$element.off(handler, this.handlers[handler]); - } - for (property in Object.getOwnPropertyNames(this)) { - typeof this[property] != 'function' && (this[property] = null); - } - }; - $.fn.owlCarousel.Constructor.Plugins.Animate = Animate; -})(window.Zepto || window.jQuery, window, document); - -/** - * Autoplay Plugin - * @version 2.3.4 - * @author Bartosz Wojciechowski - * @author Artus Kolanowski - * @author David Deutsch - * @author Tom De Caluwé - * @license The MIT License (MIT) - */ -; -(function ($, window, document, undefined) { - /** - * Creates the autoplay plugin. - * @class The Autoplay Plugin - * @param {Owl} scope - The Owl Carousel - */ - var Autoplay = function (carousel) { - /** - * Reference to the core. - * @protected - * @type {Owl} - */ - this._core = carousel; - - /** - * The autoplay timeout id. - * @type {Number} - */ - this._call = null; - - /** - * Depending on the state of the plugin, this variable contains either - * the start time of the timer or the current timer value if it's - * paused. Since we start in a paused state we initialize the timer - * value. - * @type {Number} - */ - this._time = 0; - - /** - * Stores the timeout currently used. - * @type {Number} - */ - this._timeout = 0; - - /** - * Indicates whenever the autoplay is paused. - * @type {Boolean} - */ - this._paused = true; - - /** - * All event handlers. - * @protected - * @type {Object} - */ - this._handlers = { - 'changed.owl.carousel': $.proxy(function (e) { - if (e.namespace && e.property.name === 'settings') { - if (this._core.settings.autoplay) { - this.play(); - } else { - this.stop(); - } - } else if (e.namespace && e.property.name === 'position' && this._paused) { - // Reset the timer. This code is triggered when the position - // of the carousel was changed through user interaction. - this._time = 0; - } - }, this), - 'initialized.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.autoplay) { - this.play(); - } - }, this), - 'play.owl.autoplay': $.proxy(function (e, t, s) { - if (e.namespace) { - this.play(t, s); - } - }, this), - 'stop.owl.autoplay': $.proxy(function (e) { - if (e.namespace) { - this.stop(); - } - }, this), - 'mouseover.owl.autoplay': $.proxy(function () { - if (this._core.settings.autoplayHoverPause && this._core.is('rotating')) { - this.pause(); - } - }, this), - 'mouseleave.owl.autoplay': $.proxy(function () { - if (this._core.settings.autoplayHoverPause && this._core.is('rotating')) { - this.play(); - } - }, this), - 'touchstart.owl.core': $.proxy(function () { - if (this._core.settings.autoplayHoverPause && this._core.is('rotating')) { - this.pause(); - } - }, this), - 'touchend.owl.core': $.proxy(function () { - if (this._core.settings.autoplayHoverPause) { - this.play(); - } - }, this) - }; - - // register event handlers - this._core.$element.on(this._handlers); - - // set default options - this._core.options = $.extend({}, Autoplay.Defaults, this._core.options); - }; - - /** - * Default options. - * @public - */ - Autoplay.Defaults = { - autoplay: false, - autoplayTimeout: 5000, - autoplayHoverPause: false, - autoplaySpeed: false - }; - - /** - * Transition to the next slide and set a timeout for the next transition. - * @private - * @param {Number} [speed] - The animation speed for the animations. - */ - Autoplay.prototype._next = function (speed) { - this._call = window.setTimeout($.proxy(this._next, this, speed), this._timeout * (Math.round(this.read() / this._timeout) + 1) - this.read()); - if (this._core.is('interacting') || document.hidden) { - return; - } - this._core.next(speed || this._core.settings.autoplaySpeed); - }; - - /** - * Reads the current timer value when the timer is playing. - * @public - */ - Autoplay.prototype.read = function () { - return new Date().getTime() - this._time; - }; - - /** - * Starts the autoplay. - * @public - * @param {Number} [timeout] - The interval before the next animation starts. - * @param {Number} [speed] - The animation speed for the animations. - */ - Autoplay.prototype.play = function (timeout, speed) { - var elapsed; - if (!this._core.is('rotating')) { - this._core.enter('rotating'); - } - timeout = timeout || this._core.settings.autoplayTimeout; - - // Calculate the elapsed time since the last transition. If the carousel - // wasn't playing this calculation will yield zero. - elapsed = Math.min(this._time % (this._timeout || timeout), timeout); - if (this._paused) { - // Start the clock. - this._time = this.read(); - this._paused = false; - } else { - // Clear the active timeout to allow replacement. - window.clearTimeout(this._call); - } - - // Adjust the origin of the timer to match the new timeout value. - this._time += this.read() % timeout - elapsed; - this._timeout = timeout; - this._call = window.setTimeout($.proxy(this._next, this, speed), timeout - elapsed); - }; - - /** - * Stops the autoplay. - * @public - */ - Autoplay.prototype.stop = function () { - if (this._core.is('rotating')) { - // Reset the clock. - this._time = 0; - this._paused = true; - window.clearTimeout(this._call); - this._core.leave('rotating'); - } - }; - - /** - * Pauses the autoplay. - * @public - */ - Autoplay.prototype.pause = function () { - if (this._core.is('rotating') && !this._paused) { - // Pause the clock. - this._time = this.read(); - this._paused = true; - window.clearTimeout(this._call); - } - }; - - /** - * Destroys the plugin. - */ - Autoplay.prototype.destroy = function () { - var handler, property; - this.stop(); - for (handler in this._handlers) { - this._core.$element.off(handler, this._handlers[handler]); - } - for (property in Object.getOwnPropertyNames(this)) { - typeof this[property] != 'function' && (this[property] = null); - } - }; - $.fn.owlCarousel.Constructor.Plugins.autoplay = Autoplay; -})(window.Zepto || window.jQuery, window, document); - -/** - * Navigation Plugin - * @version 2.3.4 - * @author Artus Kolanowski - * @author David Deutsch - * @license The MIT License (MIT) - */ -; -(function ($, window, document, undefined) { - 'use strict'; - - /** - * Creates the navigation plugin. - * @class The Navigation Plugin - * @param {Owl} carousel - The Owl Carousel. - */ - var Navigation = function (carousel) { - /** - * Reference to the core. - * @protected - * @type {Owl} - */ - this._core = carousel; - - /** - * Indicates whether the plugin is initialized or not. - * @protected - * @type {Boolean} - */ - this._initialized = false; - - /** - * The current paging indexes. - * @protected - * @type {Array} - */ - this._pages = []; - - /** - * All DOM elements of the user interface. - * @protected - * @type {Object} - */ - this._controls = {}; - - /** - * Markup for an indicator. - * @protected - * @type {Array.} - */ - this._templates = []; - - /** - * The carousel element. - * @type {jQuery} - */ - this.$element = this._core.$element; - - /** - * Overridden methods of the carousel. - * @protected - * @type {Object} - */ - this._overrides = { - next: this._core.next, - prev: this._core.prev, - to: this._core.to - }; - - /** - * All event handlers. - * @protected - * @type {Object} - */ - this._handlers = { - 'prepared.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.dotsData) { - this._templates.push('
' + $(e.content).find('[data-dot]').addBack('[data-dot]').attr('data-dot') + '
'); - } - }, this), - 'added.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.dotsData) { - this._templates.splice(e.position, 0, this._templates.pop()); - } - }, this), - 'remove.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._core.settings.dotsData) { - this._templates.splice(e.position, 1); - } - }, this), - 'changed.owl.carousel': $.proxy(function (e) { - if (e.namespace && e.property.name == 'position') { - this.draw(); - } - }, this), - 'initialized.owl.carousel': $.proxy(function (e) { - if (e.namespace && !this._initialized) { - this._core.trigger('initialize', null, 'navigation'); - this.initialize(); - this.update(); - this.draw(); - this._initialized = true; - this._core.trigger('initialized', null, 'navigation'); - } - }, this), - 'refreshed.owl.carousel': $.proxy(function (e) { - if (e.namespace && this._initialized) { - this._core.trigger('refresh', null, 'navigation'); - this.update(); - this.draw(); - this._core.trigger('refreshed', null, 'navigation'); - } - }, this) - }; - - // set default options - this._core.options = $.extend({}, Navigation.Defaults, this._core.options); - - // register event handlers - this.$element.on(this._handlers); - }; - - /** - * Default options. - * @public - * @todo Rename `slideBy` to `navBy` - */ - Navigation.Defaults = { - nav: false, - navText: ['', ''], - navSpeed: false, - navElement: 'button type="button" role="presentation"', - navContainer: false, - navContainerClass: 'owl-nav', - navClass: ['owl-prev', 'owl-next'], - slideBy: 1, - dotClass: 'owl-dot', - dotsClass: 'owl-dots', - dots: true, - dotsEach: false, - dotsData: false, - dotsSpeed: false, - dotsContainer: false - }; - - /** - * Initializes the layout of the plugin and extends the carousel. - * @protected - */ - Navigation.prototype.initialize = function () { - var override, - settings = this._core.settings; - - // create DOM structure for relative navigation - this._controls.$relative = (settings.navContainer ? $(settings.navContainer) : $('
').addClass(settings.navContainerClass).appendTo(this.$element)).addClass('disabled'); - this._controls.$previous = $('<' + settings.navElement + '>').addClass(settings.navClass[0]).html(settings.navText[0]).prependTo(this._controls.$relative).on('click', $.proxy(function (e) { - this.prev(settings.navSpeed); - }, this)); - this._controls.$next = $('<' + settings.navElement + '>').addClass(settings.navClass[1]).html(settings.navText[1]).appendTo(this._controls.$relative).on('click', $.proxy(function (e) { - this.next(settings.navSpeed); - }, this)); - - // create DOM structure for absolute navigation - if (!settings.dotsData) { - this._templates = [$(''); - $('.wp-editor-tabs', w).append(''); - $('.wp-editor-tabs', w).append(''); - w.on('click', '.close-wp-editor', function (e) { - e.preventDefault(); - control.editing_editor.removeClass('wpe-active'); - $('.wp-js-editor-preview').removeClass('wpe-focus'); - }); - $('.preview-wp-editor', w).hover(function () { - w.closest('.modal-wp-js-editor').css({opacity: 0}); - }, function () { - w.closest('.modal-wp-js-editor').css({opacity: 1}); - }); - w.on('click', '.fullscreen-wp-editor', function (e) { - e.preventDefault(); - w.closest('.modal-wp-js-editor').toggleClass('fullscreen'); - setTimeout(function () { - $(window).resize(); - }, 600); - }); - } - }); - - - } - }, - - _init: function () { - - var control = this; - - control.editing_area.on('change', function () { - control.preview.html(window.switchEditors._wp_Autop($(this).val())); - }); - - control.preview.on('click', function (e) { - control._add_editor(); - $('.modal-wp-js-editor').removeClass('wpe-active'); - control.editing_editor.toggleClass('wpe-active'); - tinyMCE.get(control.editor_id).focus(); - control.preview.addClass('wpe-focus'); - control._resize(); - return false; - }); - - - control.container.on('click', '.wp-js-editor-preview', function (e) { - e.preventDefault(); - }); - - }, - - _resize: function () { - var control = this; - var w = $('#wp-' + control.editor_id + '-wrap'); - var height = w.innerHeight(); - var tb_h = w.find('.mce-toolbar-grp').eq(0).height(); - tb_h += w.find('.wp-editor-tools').eq(0).height(); - tb_h += 50; - //var width = $( window ).width(); - var editor = tinymce.get(control.editor_id); - if (editor) { - control.editing_editor.width(''); - editor.theme.resizeTo('100%', height - tb_h); - w.find('textarea.wp-editor-area').height(height - tb_h); - } - - } - - }; - - _editor.ready(container); - - } - - function _remove_editor($context) { - $('textarea', $context).each(function () { - var id = $(this).attr('id') || ''; - var editor_id = 'wpe-for-' + id; - try { - var editor = tinymce.get(editor_id); - if (editor) { - editor.remove(); - } - $('#wrap-' + editor_id).remove(); - $('#wrap-' + id).remove(); - - if (typeof tinyMCEPreInit.mceInit[editor_id] !== "undefined") { - delete tinyMCEPreInit.mceInit[editor_id]; - } - - if (typeof tinyMCEPreInit.qtInit[editor_id] !== "undefined") { - delete tinyMCEPreInit.qtInit[editor_id]; - } - - } catch (e) { - - } - - }); - } - - var _is_init_editors = {}; - - // jQuery( document ).ready( function( $ ){ - - api.bind('ready', function (e, b) { - - $('#customize-theme-controls .accordion-section').each(function () { - var section = $(this); - var id = section.attr('id') || ''; - if (id) { - if (typeof _is_init_editors[id] === "undefined") { - _is_init_editors[id] = true; - - setTimeout(function () { - if ($('.wp-js-editor', section).length > 0) { - $('.wp-js-editor', section).each(function () { - _the_editor($(this)); - }); - } - - if ($('.repeatable-customize-control:not(.no-changeable) .item-editor', section).length > 0) { - $('.repeatable-customize-control:not(.no-changeable) .item-editor', section).each(function () { - _the_editor($(this)); - }); - } - }, 10); - - } - } - }); - - // Check section when focus - if (_wpCustomizeSettings.autofocus) { - if (_wpCustomizeSettings.autofocus.section) { - var id = "sub-accordion-section-" + _wpCustomizeSettings.autofocus.section; - _is_init_editors[id] = true; - var section = $('#' + id); - setTimeout(function () { - if ($('.wp-js-editor', section).length > 0) { - $('.wp-js-editor', section).each(function () { - _the_editor($(this)); - }); - } - - if ($('.repeatable-customize-control:not(.no-changeable) .item-editor', section).length > 0) { - $('.repeatable-customize-control:not(.no-changeable) .item-editor', section).each(function () { - _the_editor($(this)); - }); - } - }, 1000); - - } else if (_wpCustomizeSettings.autofocus.panel) { - - } - } - - - $('body').on('repeater-control-init-item', function (e, container) { - $('.item-editor', container).each(function () { - _the_editor($(this)); - }); - }); - - $('body').on('repeat-control-remove-item', function (e, container) { - _remove_editor(container); - }); - }); - - -})(wp.customize, jQuery); - - -jQuery(window).ready(function ($) { - - if (typeof onepress_customizer_settings !== "undefined") { - if (onepress_customizer_settings.number_action > 0) { - $('.control-section-themes h3.accordion-section-title').append('' + onepress_customizer_settings.number_action + ''); - } - } - - /** - * For Hero layout content settings - */ - $('select[data-customize-setting-link="onepress_hero_layout"]').on('change on_custom_load', function () { - var v = $(this).val() || ''; - - $("li[id^='customize-control-onepress_hcl']").hide(); - $("li[id^='customize-control-onepress_hcl" + v + "']").show(); - - }); - $('select[data-customize-setting-link="onepress_hero_layout"]').trigger('on_custom_load'); - - - /** - * For Gallery content settings - */ - $('select[data-customize-setting-link="onepress_gallery_source"]').on('change on_custom_load', function () { - var v = $(this).val() || ''; - - $("li[id^='customize-control-onepress_gallery_source_']").hide(); - $("li[id^='customize-control-onepress_gallery_api_']").hide(); - $("li[id^='customize-control-onepress_gallery_settings_']").hide(); - $("li[id^='customize-control-onepress_gallery_source_" + v + "']").show(); - $("li[id^='customize-control-onepress_gallery_api_" + v + "']").show(); - $("li[id^='customize-control-onepress_gallery_settings_" + v + "']").show(); - - }); - - $('select[data-customize-setting-link="onepress_gallery_source"]').trigger('on_custom_load'); - - /** - * For Gallery display settings - */ - $('select[data-customize-setting-link="onepress_gallery_display"]').on('change on_custom_load', function () { - var v = $(this).val() || ''; - switch (v) { - case 'slider': - $("#customize-control-onepress_g_row_height, #customize-control-onepress_g_col, #customize-control-onepress_g_spacing").hide(); - break; - case 'justified': - $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").hide(); - $("#customize-control-onepress_g_row_height").show(); - break; - case 'carousel': - $("#customize-control-onepress_g_row_height, #customize-control-onepress_g_col").hide(); - $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").show(); - break; - case 'masonry': - $("#customize-control-onepress_g_row_height").hide(); - $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").show(); - break; - default: - $("#customize-control-onepress_g_row_height").hide(); - $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").show(); - - } - - }); - $('select[data-customize-setting-link="onepress_gallery_display"]').trigger('on_custom_load'); - -}); - - -/** - * Icon picker - */ -jQuery(document).ready(function ($) { - - window.editing_icon = false; - var icon_picker = $('
'); - var options_font_type = '', icon_group = ''; - - $.each(C_Icon_Picker.fonts, function (key, font) { - - font = $.extend({}, { - url: '', - name: '', - prefix: '', - icons: '' - }, font); - - if ( Array.isArray(font.url) ) { - font.url.map(el => { - $('') - .appendTo('head') - .attr({type: 'text/css', rel: 'stylesheet'}) - .attr('id', 'customizer-icon-' + el?.key) - .attr('href', el?.url); - }) - - } else { - $('') - .appendTo('head') - .attr({type: 'text/css', rel: 'stylesheet'}) - .attr('id', 'customizer-icon-' + key) - .attr('href', font.url); - } - - - options_font_type += ''; - - var icons_array = font.icons.split('|'); - - icon_group += ''; - - }); - icon_picker.find('.c-icon-search input').attr('placeholder', C_Icon_Picker.search); - icon_picker.find('.c-icon-type').html(options_font_type); - icon_picker.find('.c-icon-list').append(icon_group); - $('.wp-full-overlay').append(icon_picker); - - // Change icon type - $('body').on('change', 'select.c-icon-type', function () { - var t = $(this).val(); - icon_picker.find('.ic-icons-group').hide(); - icon_picker.find('.ic-icons-group[data-group-name="' + t + '"]').show(); - - }); - icon_picker.find('select.c-icon-type').trigger('change'); - - // When type to search - $('body').on('keyup', '.c-icon-search input', function () { - var v = $(this).val(); - if (v == '') { - $('.c-icon-list span').show(); - } else { - $('.c-icon-list span').hide(); - try { - $('.c-icon-list span[data-name*="' + v + '"]').show(); - } catch (e) { - - } - } - }); - - // Edit icon - $('body').on('click', '.icon-wrapper', function (e) { - e.preventDefault(); - var icon = $(this); - window.editing_icon = icon; - icon_picker.addClass('ic-active'); - $('body').find('.icon-wrapper').removeClass('icon-editing'); - icon.addClass('icon-editing'); - }); - // Remove icon - $('body').on('click', '.item-icon .remove-icon', function (e) { - e.preventDefault(); - var item = $(this).closest('.item-icon'); - item.find('.icon-wrapper input').val(''); - item.find('.icon-wrapper input').trigger('change'); - item.find('.icon-wrapper i').attr('class', ''); - $('body').find('.icon-wrapper').removeClass('icon-editing'); - }); - - // Selected icon - $('body').on('click', '.c-icon-list span', function (e) { - e.preventDefault(); - var icon_name = $(this).attr('data-name') || ''; - if (window.editing_icon) { - window.editing_icon.find('i').attr('class', '').addClass($(this).find('i').attr('class')); - window.editing_icon.find('input').val(icon_name).trigger('change'); - } - icon_picker.removeClass('ic-active'); - window.editing_icon = false; - $('body').find('.icon-wrapper').removeClass('icon-editing'); - }); - - $(document).mouseup(function (e) { - if (window.editing_icon) { - if (!window.editing_icon.is(e.target) // if the target of the click isn't the container... - && window.editing_icon.has(e.target).length === 0 // ... nor a descendant of the container - && ( - !icon_picker.is(e.target) - && icon_picker.has(e.target).length === 0 - ) - ) { - icon_picker.removeClass('ic-active'); - // window.editing_icon = false; - } - } - }); - - - var display_footer_layout = function (l) { - $('li[id^="customize-control-footer_custom_"]').hide(); - $('li[id^="customize-control-footer_custom_' + l + '_columns"]').show(); - }; - - display_footer_layout($('#customize-control-footer_layout select').val()); - $('#customize-control-footer_layout select').on('change', function () { - display_footer_layout($(this).val()); - }); - - -}); \ No newline at end of file diff --git a/changelog.md b/changelog.md index a82d79cd..47051747 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,39 @@ # CHANGELOG +# 2.3.19 +- FIXED: Custom text colors set on blocks (paragraph, heading, etc.) were being overridden by the theme's default gray. Renamed the "Text" palette slug to avoid clashing with WordPress core's `.has-text-color` class. + +# 2.3.18 +- NEW: Block editor canvas matches the rendered frontend for in-scope blocks. +- NEW: Theme color palette and font size scale exposed to the block editor via theme.json. +- NEW: Block-width cap follows the sidebar layout (790px with sidebar, 1110px no-sidebar, 100vw stretched template). +- NEW: Switching the Page Template in the editor sidebar updates the block-width cap live. +- NEW: Customizer Site Colors (Primary / Secondary) update the editor and frontend preview live, no reload. +- IMPROVED: `alignleft` / `alignright` blocks float inside the editor canvas and bleed to viewport edge on frontend. +- IMPROVED: `alignwide` widens to 1230px; `alignfull` reaches viewport edges. +- IMPROVED: Primary / Secondary colors propagate to block-library blocks using those palette slugs. +- IMPROVED: Semantic font-family aliases `--wp--preset--font-family--body` and `--…--heading` exposed for child themes and OnePress Plus to override. +- IMPROVED: `template-fullwidth.php` content cap stays at theme default; `template-fullwidth-stretched.php` stretches edge-to-edge. +- FIXED: HTML tags inside the hero rotating phrase block now render instead of being stripped to plain text. +- FIXED: WP `[gallery]` shortcode now uses CSS grid layout, matching the gallery block. +- FIXED: Section Order & Styling list shows every row regardless of section active state. + +# 2.3.17 +- NEW: Support grid blog layout for blog page, section news. +- IMPROVED: Add altt text to hero image slider. +- IMPROVED: Support self-hosted video lightbox & media control. +- IMPROVED: Improve customizer controls, support svg icon. +- IMPROVED: Reorder sanitization and adjust escaping. + +# 2.3.16 +- FIXED: Fix security issues. +- FIXED: Alert copy text. + +# 2.3.15 + +- FIXED: Fix security issues. +- UPDATED: Update style files. + # 2.3.12 - FIXED: fix security issues. diff --git a/docs/plan-block-editor-parity.md b/docs/plan-block-editor-parity.md new file mode 100644 index 00000000..4595bd3a --- /dev/null +++ b/docs/plan-block-editor-parity.md @@ -0,0 +1,316 @@ +# Plan: Block Editor ↔ Frontend Parity + +**Status:** ✅ Phases 0–4, 6 complete in working tree. Phase 5 (patterns) deferred. Awaiting human review before commit/tag. +**Target release:** OnePress `2.4.0` (minor, additive-only). +**Owner:** TBD. +**Last updated:** 2026-05-26. + +Tracks the work to make posts edited in the WP Block Editor (`wp-admin/post.php?action=edit&post=N`) render visually identical to the published frontend (`/?p=N`). + +--- + +## Approved decisions + +Locked in before work starts. Reopen only with a documented reason. + +| Decision | Value | Notes | +|---|---|---| +| **Guiding priority** | **Editor parity > frontend preservation** | When there is a trade-off, change frontend to match the desired editor look — but keep frontend deltas small and only in the direction of "more correct" (better spacing, fixed alignment, etc.). | +| **Frontend regression tolerance** | Small visual changes acceptable | No structural / breaking changes. Each delta logged in changelog under "Visual". | +| **Color palette source** | Dynamic from theme mods | Resolved per-request via `block_editor_settings_all` filter; reflects user's Customizer settings live. Not `add_theme_support( 'editor-color-palette' )` (which is static). | +| **Block patterns (Phase 5)** | **Deferred** | Not in 2.4.0. Re-evaluate after 2.4 ships. | +| **Release vehicle** | Minor `2.4.0` | Additive-only, no rename/removal of public API. Existing 60k installs upgrade safely. | +| **Existing dead code in `OnePress_Editor`** | Keep, fix path only | Per [additive-only mandate](spec-conventions.md#additive-only-mandate): don't remove `load_style()` / `css_file()` / `editor_style_url()`. Fix `$editor_file` path so it stops returning empty. | + +--- + +## Architecture findings (do not re-investigate) + +1. **`editor.scss` already shares partials with `style.scss`:** + + ```scss + // src/frontend/styles/editor.scss + @use "variables"; + @use "document"; + @use "contents"; + @use "gutenberg"; + ``` + + → any rule added to `_contents.scss` or `_gutenberg.scss` automatically applies to **both** editor and frontend. This is the leverage point. + +2. **`_gutenberg.scss` is mostly comments** (~57 active lines, mostly `.single-post .content-inner` and `.entry-content ul/ol/li`). The bulk of `.wp-block-*` rules are commented-out scaffolding from the Underscores starter. **Un-commenting + tuning is the bulk of Phase 1–2 work.** + +3. **Color theme mods available** for palette source: + - `onepress_primary_color` (default `#03c4eb`) + - `onepress_secondary_color` (default `#00aeef`) + - Plus other section-specific color mods in `inc/customize-configs/options-colors.php` + +4. **`OnePress_Editor` latent bug:** `$editor_file = 'assets/css/admin/editor.css'` points to a non-existent path. Actual file is at `assets/admin/editor.css`. The `load_style()` → `editor_settings()` chain currently returns empty. The actual editor CSS today is loaded by `add_editor_style('assets/admin/editor.css', …)` in `functions.php:184`. + +5. **`add_editor_style()` in `functions.php:184`** is the real working channel — it loads `assets/admin/editor.css` + Google Fonts into the editor canvas. Keep working alongside it. + +6. **Existing theme supports:** `editor-styles`, `align-wide`, `wp-block-styles`. Wide/full alignment already declared; styling needs work. + +--- + +## Phases + +### Phase 0 — Discovery, baseline, latent bug fix +**Effort:** ~30 min. **BC:** none. **Status:** ✅ completed. + +**Goals:** +- Boot site via WP Studio and confirm editor renders without error today. +- Identify a **fixture set** containing in-scope blocks. (Updated: the site already ships the standard Gutenberg theme-test posts — reuse them instead of authoring our own.) +- Fix `OnePress_Editor::$editor_file` typo: `'assets/css/admin/editor.css'` → `'assets/admin/editor.css'`. + +**Files touched:** +- `inc/admin/class-editor.php` — path fix + inline comment + +**Reuse, do not create — existing fixture posts on `onepress.wp.local`:** + +| ID | Title | Blocks covered | +|---|---|---| +| `94` | Gutenberg: Common Blocks | paragraph, heading, list, image, quote, gallery | +| `116` | Gutenberg: Embed Blocks | embeds (responsive) | +| `122` | Gutenberg: Formatting Blocks | code, preformatted, pullquote, table, verse | +| `125` | Gutenberg: Layout Element Blocks | columns, group, cover, separator, spacer, button | +| `79`, `1177` | Image Alignment | image left/right/center/wide/full | +| `90` | Widget Blocks | latest posts, search, archives, calendar | + +**Per-phase verification opens both URLs (replace ``):** + +``` +editor: https://onepress.wp.local/wp-admin/post.php?post=&action=edit +frontend: https://onepress.wp.local/?p= +``` + +**Effect of the path fix:** `OnePress_Editor::load_style()` now returns the actual `editor.css` content instead of an empty string. The file is now injected into the editor via two channels (existing `add_editor_style()` in `functions.php` + the newly-working `block_editor_settings_all` filter). Functionally identical CSS in both channels — benign duplication, ~few kb. **Do not** "fix" the duplication by removing either channel without a Phase 6 review (would violate additive-only for `add_editor_style` call sites that integrators may rely on). + +**Done.** + +--- + +### Phase 1 — Editor wrapper parity +**Effort:** ~2–3h. **BC:** visual on both editor + frontend (small). + +**Goals:** +- Match typography exactly: font family, size, line-height, headings h1–h6. +- Constrain `.editor-styles-wrapper .wp-block` max-width to `single_layout_content_width` (default 800px) — already partially done by `OnePress_Editor::css()`; verify and refine. +- Style paragraph, links, headings, lists, code (inline + pre), blockquote in the wrapper context. + +**Files touched:** +- `src/frontend/styles/_gutenberg.scss` (un-comment + tune typography section) +- `src/frontend/styles/_variables.scss` (only if extracting shared tokens; avoid renaming existing vars) + +**Selector discipline:** +All new rules MUST scope to either: +- `.editor-styles-wrapper` (editor-only), or +- `.entry-content` / `.wp-block-*` (apply to both editor + frontend via shared partials). + +**Never** add rules at higher-level selectors (e.g. `body`, `p`) that would leak into header/footer/widgets. + +**Verification:** +- Rebuild: `npm run build` +- Re-screenshot editor + frontend; diff against baseline. Frontend deltas must be small (typography refinements OK; no layout shifts). + +**Done when:** typography in editor matches frontend; frontend diff reviewed and acceptable. + +--- + +### Phase 2 — Block-specific rules +**Effort:** ~4–6h. **BC:** visual on both (small, scoped to `.wp-block-*`). + +**Goals:** Style each common block to match frontend appearance. + +#### Block scope for Phases 1–2 + +| Block | Selector | Notes | +|---|---|---| +| Paragraph | `.wp-block-paragraph` | Inherits from `.entry-content p` | +| Headings | `.wp-block-heading` (h1–h6) | Use `$font_heading`, match `_contents.scss` h1–h6 | +| List | `.wp-block-list`, `.wp-block-list-item` | Honor existing `.entry-content ul/ol/li` rules in `_gutenberg.scss` | +| Image | `.wp-block-image` | Caption styling, alignment (left/right/center/wide/full) | +| Gallery | `.wp-block-gallery` | Cross-reference existing rules in `_contents.scss:482` | +| Button | `.wp-block-button`, `.wp-block-button__link` | Primary variant uses `onepress_primary_color`; add `.is-style-secondary` if useful | +| Quote | `.wp-block-quote` | Left border + italic; match frontend blockquote | +| Pullquote | `.wp-block-pullquote` | Larger emphasis, centered | +| Cover | `.wp-block-cover` | Full-bleed at `alignfull` | +| Columns | `.wp-block-columns`, `.wp-block-column` | Bootstrap-compatible gutters | +| Group | `.wp-block-group` | Container behavior | +| Table | `.wp-block-table` | Borders, alternating rows | +| Separator | `.wp-block-separator` | Subtle / wide / dots variants | +| Embed | `.wp-block-embed` | Responsive (`responsive-embeds` support in Phase 3) | +| Code | `.wp-block-code` | Use `$monaco` font, background | +| Preformatted | `.wp-block-preformatted` | Same monospace + background | +| Spacer | `.wp-block-spacer` | No-op visually; just respect height | + +#### Alignment + +`.alignwide` and `.alignfull` must: +- In editor: extend beyond `.editor-styles-wrapper` max-width (use negative margins or `:not()` exclusion as already done by `OnePress_Editor::css()`). +- In frontend: match the page template's container behavior. Test on `single.php` and `page.php` (right-sidebar layout) — alignfull should bleed to viewport edges. + +**Files touched:** +- `src/frontend/styles/_gutenberg.scss` (main work, additive only) + +**Verification:** rebuild, screenshot diff per block, log frontend deltas. + +**Done when:** every block in scope renders ≈ identically in editor vs frontend. + +--- + +### Phase 3 — Editor configuration (theme supports + filters) +**Effort:** ~1–2h. **BC:** additive (no CSS changed, only editor flags). + +**Goals:** Register editor-side configuration so the UI surfaces the right tools. + +**Add via filter (NOT `add_theme_support`)** in `OnePress_Editor`: + +| Setting | Source | Why filter (not theme support) | +|---|---|---| +| `colors` (color palette) | Theme mods: `onepress_primary_color`, `onepress_secondary_color`, `$text` (#777), `$heading` (#333) | Dynamic per site — `add_theme_support()` requires static array | +| `fontSizes` | Match `$base` (20px) scale: small (14), medium (18), large (22), x-large (28) | Static OK, but keep filter for consistency | +| Responsive embeds | `add_theme_support( 'responsive-embeds' )` | Static — OK as theme support | +| `custom-line-height` | `add_theme_support( 'custom-line-height' )` | Static | +| `custom-spacing` | `add_theme_support( 'custom-spacing' )` | Static | +| `custom-units` | `add_theme_support( 'custom-units' )` | Static | + +**Files touched:** +- `inc/admin/class-editor.php` — new method `register_block_editor_supports()` called from constructor; new method `filter_editor_settings()` returning palette/fontSizes +- `functions.php` — add the 4 static theme supports in `onepress_setup()` + +**Do NOT add:** +- `disable-custom-colors` / `disable-custom-font-sizes` — too restrictive for users. + +**Don't break Plus:** `onepress_typography_render_style()` already injects custom typography. Audit specificity to ensure Plus still wins. + +**Verification:** open editor sidebar → Color → confirm palette swatches match Customizer; same for Font Sizes. + +**Done when:** palette is dynamic, font-size picker has theme-relevant options, embed/spacing/line-height controls present. + +--- + +### Phase 4 — Body class mirror +**Effort:** ~1h. **BC:** additive. + +**Goals:** Editor canvas `` element gets a class that cascades the same context as the frontend `.entry-content` wrapper, so any rule using `.entry-content > .wp-block-*` works without dual-selector hacks. + +**Approach:** +- Filter `block_editor_settings_all` → add `body_class` (or use `admin_body_class` for the surrounding admin shell where appropriate). +- The editor iframe wrapper is `.editor-styles-wrapper`; add a class like `.entry-content` to it via filter, OR adjust SCSS selectors to handle both `.entry-content` and `.editor-styles-wrapper`. + +**Decision point during implementation:** which approach is less invasive — adding a class vs duplicating selectors. Default to the SCSS approach (`.entry-content, .editor-styles-wrapper { … }`) since it doesn't depend on iframe internals that WP may change. + +**Files touched:** +- `src/frontend/styles/_gutenberg.scss` (group selectors) +- Possibly `inc/admin/class-editor.php` (if class-injection approach chosen) + +**Done when:** removing any `.entry-content` rule from frontend doesn't leave editor unstyled (or vice versa). + +--- + +### Phase 5 — Block patterns +**Status:** **DEFERRED**. Not in 2.4.0. + +Re-evaluate after 2.4 ships. When picked up, scope: hero, feature-grid, team-card, CTA, testimonial. Category: "OnePress". + +--- + +### Phase 6 — Documentation update +**Effort:** ~1h. **BC:** docs only. + +**Goals:** +- Update [spec-admin.md → Block editor integration](spec-admin.md#block-editor-integration) to reflect the new architecture: palette filter, font sizes, parity rule, fixture post location. +- Add a new spec if the editor surface grows beyond what fits in `spec-admin.md`: candidate file `docs/spec-block-editor.md`. +- Update [changelog.md](../changelog.md) with 2.4.0 entry listing: + - "Added: block editor color palette and font sizes mirroring theme settings." + - "Added: responsive embeds, custom line-height/spacing/units in block editor." + - "Improved: block editor canvas now matches frontend typography and block styling." + - "Visual: minor adjustments to `.entry-content` blockquote/list/code styling for consistency between editor and frontend." + - "Fixed: dead editor stylesheet path in `OnePress_Editor`." + +**Files touched:** +- `docs/spec-admin.md` +- `docs/spec-block-editor.md` (new, if needed) +- `changelog.md` +- `style.css` (version bump) +- `package.json` (version bump) + +**Done when:** docs reflect shipped reality; version bumped; changelog entries staged in same release commit. + +--- + +## Verification protocol + +### Per-phase visual diff + +```bash +# 1. Confirm site is up +studio site status + +# 2. (Phase 0 only) Create fixture +studio wp post create \ + --post_title="Block parity fixture" \ + --post_content="$(cat tests/fixtures/block-parity-post.html)" \ + --post_status=publish --post_type=post + +# 3. Rebuild after every SCSS change +npm run build + +# 4. Open editor + frontend in two browser windows; screenshot at 1280px +# editor: https://onepress.wp.local/wp-admin/post.php?post=&action=edit +# frontend: https://onepress.wp.local/?p= + +# 5. Diff against baseline; log any frontend deltas in the phase's PR/commit body +``` + +### Pre-release smoke + +Before tagging `2.4.0`: +- [ ] Deactivate OnePress Plus → editor still functional. +- [ ] Activate OnePress Plus → typography from Plus still wins. +- [ ] Deactivate WooCommerce → editor still functional. +- [ ] Activate WooCommerce → product editor unaffected. +- [ ] Toggle every section on/off in Customizer → no editor side-effects. +- [ ] Run the [line-endings audit](spec-line-endings.md#audit--normalize-playbook). +- [ ] Confirm `assets/` is in same commit as `src/` (see [build-artifact rule](spec-commits.md#build-artifact-rule)). + +--- + +## Risks (logged) + +| Risk | Likelihood | Mitigation | +|---|---|---| +| Un-commenting `_gutenberg.scss` rules leaks into header/footer/widgets | Medium | Strict selector scoping (`.wp-block-*`, `.entry-content`, `.editor-styles-wrapper` only); review every commit's selector list | +| Plus typography conflict | Low | Plus injects last via `enqueue_block_editor_assets` priority; theme styles loaded first; Plus wins by load order | +| Static color palette declared via `add_theme_support` cached before theme mod resolves | High if done wrong | Use filter `block_editor_settings_all` instead | +| Frontend `.entry-content` rules break sites with heavy custom CSS | Medium | Frontend changes scoped to selectors not present before; audit by grepping current rules in `_contents.scss` before adding | +| WP Studio PHP WASM behaves differently from prod | Low | All changes are PHP + CSS, no native deps; smoke-test on a real WP install before 2.4 tag if available | +| User's Customizer "Additional CSS" overrides break in editor (CSS not loaded in editor by default) | Medium | Out of scope for 2.4. Document the limitation in spec-admin.md. | + +--- + +## BC contract for this work + +Every commit in this plan must satisfy [additive-only mandate](spec-conventions.md#additive-only-mandate): + +- ✅ Add new `OnePress_Editor` methods (`register_block_editor_supports`, `filter_editor_settings`, etc.). +- ✅ Add new `add_theme_support()` calls in `onepress_setup()`. +- ✅ Add new SCSS rules in `_gutenberg.scss`. +- ✅ Add new theme mods (`onepress_editor_*` if any introduced) with defaults that preserve existing behavior. +- ❌ Don't remove `OnePress_Editor::load_style()`, `css_file()`, `editor_style_url()` even though path fix may make them redundant. +- ❌ Don't rename existing CSS classes in `_contents.scss`. +- ❌ Don't change defaults of existing theme mods (`single_layout_content_width`, color mods, etc.). +- ❌ Don't drop any `add_theme_support()` already declared. + +--- + +## Out of scope for 2.4.0 + +- Converting to a block theme (FSE / `theme.json` / `templates/` / `parts/`). +- Custom OnePress blocks (e.g. "hero slider as a block"). +- Block patterns (deferred — Phase 5). +- `disable-custom-colors` / `disable-custom-font-sizes` lockdowns. +- Editor JS module with custom inspector controls / sidebar panels. +- Loading user's Customizer "Additional CSS" inside the editor. +- Migration of existing posts to new block markup. diff --git a/docs/plan-css-var-integration.md b/docs/plan-css-var-integration.md new file mode 100644 index 00000000..b20253d1 --- /dev/null +++ b/docs/plan-css-var-integration.md @@ -0,0 +1,380 @@ +# Plan: theme.json Global Attributes Integration (one-shot) + +**Status:** Drafted, awaiting review. Not started. +**Target release:** OnePress `2.4.0` (single bundle with the block-editor parity work, or a separate `2.4.x` tag — TBD). +**Owner:** TBD. +**Last updated:** 2026-05-26. +**Scope decision:** **One-shot all applicable global attributes** (layout + colors + font sizes + font families + spacing). Confirmed by user 2026-05-26. Other categories with no current OnePress integration (gradients, duotones, shadows, aspect ratios) are intentionally untouched — they aren't declared in theme.json and present no divergence. +**Supersedes:** the original layout-only draft of this same file. + +--- + +## The bigger problem in one paragraph + +OnePress ships [`theme.json`](../theme.json) (since pre-2.4) declaring layout sizes, colors, font sizes, font families, and spacing scale. WP transforms these into CSS custom properties (`--wp--style--global--*`, `--wp--preset--*`) and utility classes (`.has-{slug}-color`, `.has-{slug}-font-size`, …). Block content authored in the editor relies on these vars and classes to render. **But the SCSS bundle ignores almost all of them** — it hardcodes literal values from `_variables.scss` (`$primary`, `$grid`, `$font_text`, etc.), and the Customizer color mods (`onepress_primary_color`, `onepress_secondary_color`) are wired through ad-hoc inline CSS that doesn't touch theme.json's preset slugs at all. The result: theme.json is decorative; block content can visually diverge from the rest of the theme; and Customizer color changes don't propagate into block content. + +This plan refactors OnePress so theme.json is the **single source of truth** for all applicable global presets, with Customizer mods feeding theme.json values via the `wp_theme_json_data_theme` filter and SCSS consuming `var(...)` with SCSS literals as fallbacks. + +## Approved decisions + +| Decision | Value | +|---|---| +| Priority | Editor parity > frontend preservation; small frontend changes acceptable | +| Scope | One-shot: layout + colors + font sizes + font families + spacing | +| Release vehicle | Minor `2.4.0` (additive only) — may slip to `2.4.1` if scope balloons | +| BC contract | [Additive-only](spec-conventions.md#additive-only-mandate) — no rename/remove of public symbols | +| Layout architecture | Stay classic (not converting to block theme); use manual `.entry-content > X` rules consuming WP CSS vars; do **not** inject `.is-layout-constrained` | +| Existing 2.3.x sites | Must not see visual regression except where the change is "more correct" per theme.json's declared intent | +| `theme.json` `secondary = #333333` | **Frozen**, do not touch — see [Known inconsistencies](spec-block-editor.md#known-inconsistencies) | + +## Global attributes coverage (one-shot) + +| Category | theme.json key | CSS vars emitted | Status in this plan | +|---|---|---|---| +| Layout — content/wide | `settings.layout.contentSize/wideSize` | `--wp--style--global--{content,wide}-size` | **IN — Phase A** | +| Color palette | `settings.color.palette` (8 slugs) | `--wp--preset--color--{slug}` | **IN — Phase B** (primary syncs Customizer; secondary frozen) | +| Font sizes | `settings.typography.fontSizes` (6 slugs) | `--wp--preset--font-size--{slug}` | **IN — Phase C** (light refactor) | +| Font families | `settings.typography.fontFamilies` (4 slugs) | `--wp--preset--font-family--{slug}` | **IN — Phase D** (light refactor) | +| Spacing scale | `settings.spacing.spacingScale.steps:7` | `--wp--preset--spacing--{20..70}` | **IN — Phase E** (verification only) | +| Layout — root padding | `useRootPaddingAwareAlignments: true` | `--wp--style--root--padding-*` | **OUT** — inert on classic theme; documented | +| Gradients / Duotone / Shadow / Aspect ratio | (not declared in OnePress theme.json) | (none emitted) | **OUT** — no work needed | +| Element styles (`styles.elements.*`) | declared in theme.json | (WP auto-applies CSS rules) | **OUT** — already works | +| Block layout types (`.is-layout-{constrained,flex,…}`) | (WP core) | (WP CSS) | **OUT** — explicitly rejected (regression risk) | +| Text/vertical alignment utility classes | (WP block-library) | (WP CSS) | **OUT** — works automatically | + +--- + +## Reference: WP canonical pattern + +Verified for this site via `wp_get_global_stylesheet()` and authoritative docs: + +### CSS variables emitted by theme.json (on `:root`) + +```css +:root { + --wp--style--global--content-size: 1110px; /* layout.contentSize */ + --wp--style--global--wide-size: 1230px; /* layout.wideSize */ + --wp--style--root--padding-{...}: …; /* useRootPaddingAware */ + + --wp--preset--color--primary: #03c4eb; /* color.palette[slug=primary] */ + --wp--preset--color--secondary: #333333; /* color.palette[slug=secondary] */ + --wp--preset--color--heading: #333333; + --wp--preset--color--text: #777777; + --wp--preset--color--border: #e9e9e9; + --wp--preset--color--light: #f8f9f9; + --wp--preset--color--white: #ffffff; + --wp--preset--color--black: #000000; + + --wp--preset--font-size--small: 12px; + --wp--preset--font-size--normal: 14px; + --wp--preset--font-size--medium: 18px; + --wp--preset--font-size--large: 25px; + --wp--preset--font-size--x-large: 33px; + --wp--preset--font-size--xx-large: 40px; + + --wp--preset--font-family--open-sans: "Open Sans", Helvetica, Arial, sans-serif; + --wp--preset--font-family--raleway: Raleway, Helvetica, Arial, sans-serif; + --wp--preset--font-family--system: system-ui, -apple-system, …; + --wp--preset--font-family--monospace: Monaco, Consolas, "Andale Mono", …; + + --wp--preset--spacing--20: …; /* via spacingScale.steps:7 */ + --wp--preset--spacing--30: …; + /* … through --70 */ +} +``` + +### Utility classes WP auto-emits + +- `.has-{slug}-color`, `.has-{slug}-background-color` — applied on blocks with explicit color +- `.has-{slug}-font-size`, `.has-{slug}-font-family` +- `.has-{slug}-padding-{top,bottom,left,right}`, `.has-{slug}-margin-{...}` +- `.has-text-align-{left,center,right}`, `.is-vertically-aligned-{top,center,bottom}` + +### Idiomatic consumption pattern (classic theme) + +```scss +// SCSS rules in .entry-content / .editor-styles-wrapper scope use CSS vars +// with SCSS literal as fallback. This way: +// - theme.json updates propagate automatically +// - Customizer-driven theme.json overrides (Phase B) propagate +// - Sites that strip theme.json fall back to SCSS literal (no breakage) + +.entry-content > .alignwide { + max-width: var(--wp--style--global--wide-size, #{variables.$grid + 120px}); +} + +.entry-content blockquote { + border-left: 3px solid var(--wp--preset--color--primary, #{variables.$primary}); +} + +.entry-content p { + font-family: var(--wp--preset--font-family--open-sans, #{variables.$font_text}); +} +``` + +### Customizer ↔ theme.json bridge + +```php +add_filter( 'wp_theme_json_data_theme', function ( WP_Theme_JSON_Data $theme_json ) { + $primary_mod = sanitize_hex_color( '#' . ltrim( get_theme_mod( 'onepress_primary_color', '' ), '#' ) ); + if ( ! $primary_mod ) { + return $theme_json; + } + $data = $theme_json->get_data(); + foreach ( $data['settings']['color']['palette'] as &$color ) { + if ( $color['slug'] === 'primary' ) { + $color['color'] = $primary_mod; + } + } + return $theme_json->update_with( $data ); +} ); +``` + +References: +- [Layout — Theme Handbook (WordPress.org)](https://developer.wordpress.org/themes/global-settings-and-styles/settings/layout/) +- [Global Settings & Styles (theme.json) — Block Editor Handbook](https://developer.wordpress.org/block-editor/how-to-guides/themes/global-settings-and-styles/) +- [Using The New Constrained Layout — CSS-Tricks](https://css-tricks.com/using-the-new-constrained-layout-in-wordpress-block-themes/) +- [WordPress Global Styles Reference Tables — CSS-Tricks](https://css-tricks.com/wordpress-global-styles-reference-tables/) +- `wp_get_global_stylesheet()` output captured live for this site + +--- + +## Audit: current state + +### Layout (Phase A) + +| File | Line | Selector | Current value | Replace with | +|---|---|---|---|---| +| `src/frontend/styles/_gutenberg.scss` | ~684 | `.entry-content > .alignwide` | `1110px` (hard-coded) | `var(--wp--style--global--wide-size, 1230px)` — **note: theme.json says wide=1230, not 1110** | +| same | (editor block) | `.editor-styles-wrapper .wp-block.alignwide` | `variables.$grid` (=1110) | `var(--wp--style--global--wide-size, 1230px)` | +| same | (no rule today) | `.entry-content > *:not(.alignwide):not(.alignfull)` | n/a | `max-width: var(--wp--style--global--content-size, 800px)` | +| `inc/template-tags.php` | 1015 | inline content-width | theme mod value | also emit `:root { --wp--style--global--content-size: px; }` so Phase A rule honors it | +| `inc/admin/class-editor.php` | `css()` | inline content-width | theme mod value | same pattern | + +### Colors (Phase B) + +**11 `$primary`/`$secondary` SCSS usages within content/block scope** (out of 42 total in the bundle). Audit: + +| File | Line | Selector | Use | +|---|---|---|---| +| `_contents.scss` | 82 | `.highlight` | `color: $primary` | +| `_contents.scss` | 94 | `.entry-content blockquote` | `border-left: 3px solid $primary` | +| `_contents.scss` | 167 | `.nav-links a:hover` | `background: $primary` | +| `_contents.scss` | 351 | (comment-related) | `color: $primary` | +| `_gutenberg.scss` | 329 | `.editor-styles-wrapper blockquote` | `border-left: 3px solid $primary` (Phase 1 mirror) | +| `_gutenberg.scss` | 387 | `.editor-styles-wrapper a` | `color: $primary` | +| `_gutenberg.scss` | 460 | `.wp-block-button__link` | `background-color: $primary` | +| `_gutenberg.scss` | 474 | `.wp-block-button__link:hover` | `background-color: $secondary` | +| `_gutenberg.scss` | 482 | `.wp-block-button.is-style-outline` | `color: $primary` | +| `_gutenberg.scss` | 483 | same | `border: 2px solid $primary` | +| `_gutenberg.scss` | 487 | outline `:hover` | `background-color: $primary` | + +The remaining 31 usages (out of 42) are in headers/footers/navigation/sections — not content-scoped and not affected by this plan. + +**Inline CSS in `inc/template-tags.php`** also applies Customizer mods to ad-hoc selectors (~10 places). These are not currently bridged to theme.json — Customizer changes never reach block content. Phase B fixes this via the `wp_theme_json_data_theme` filter approach. + +### Font sizes (Phase C) + +`_document.scss` defines h1–h6 sizes inline (`33px`, `25px`, `20px`, etc.). theme.json has matching `fontSizes` entries (`x-large: 33px`, `large: 25px`, etc.). When a block author picks "X-Large" in the editor, `.has-x-large-font-size` is emitted on the element with `font-size: var(--wp--preset--font-size--x-large)` — works automatically via WP block-library CSS, but the values are duplicated. + +Phase C scope: keep `_document.scss` h1–h6 declarations (additive-only — they're already shipping), but verify the utility classes (`.has-*-font-size`) flow through correctly and don't conflict. + +### Font families (Phase D) + +Same shape as Phase C — `_document.scss` `body { font-family: $font_text; }`, theme.json declares same font families with preset slugs. Block utility classes (`.has-open-sans-font-family`) work automatically. Phase D scope: verification + light refactor where the SCSS rule could consume the var. + +### Spacing (Phase E) + +theme.json declares `spacingScale.steps: 7`. WP auto-generates 7 spacing preset vars (`--wp--preset--spacing--20` through `--80`). Block authors can apply `.has-30-padding-top` etc. Currently OnePress SCSS doesn't reference these vars — block content gets the user-selected spacing via WP's own block-library CSS rules. Phase E is verification only. + +--- + +## Phased refactor + +### Phase A — Layout CSS vars + content-size constraint (~1.5h) + +**Files:** +- `src/frontend/styles/_gutenberg.scss` — alignwide rules (frontend + editor), new `.entry-content > *:not(.alignwide):not(.alignfull)` content-size rule +- `inc/template-tags.php` — `onepress_custom_inline_style()` rewrite: emit `:root { --wp--style--global--content-size: …px; }` override + keep `.single-post .site-main` as legacy +- `inc/admin/class-editor.php` — `css()` method: same CSS-var override pattern + +**Visual deltas:** +- alignwide: 1110 → 1230 (matches theme.json declaration; intentional) +- Regular `.entry-content > *`: now capped at content-size (1110 default). On default sites with no-sidebar single posts container is ~1110 → invisible. On fullwidth template pages → content visibly narrower. + +**BC impact:** small, intentional, both deltas move toward theme.json's declared intent. + +--- + +### Phase B — Color palette: Customizer → theme.json bridge + SCSS var consumption (~3h) + +**B1. PHP: Customizer → theme.json filter** + +New function `onepress_filter_theme_json_palette()` hooked on `wp_theme_json_data_theme`. Override only the `primary` slug from `onepress_primary_color` Customizer mod when set. **Do not** touch `secondary` (frozen at `#333333` per [Known inconsistencies](spec-block-editor.md#known-inconsistencies)). + +Why only primary: +- theme.json `primary` default `#03c4eb` matches `onepress_primary_color` default → override is safe (same default). +- theme.json `secondary = #333333` ≠ `onepress_secondary_color` default `#00aeef` → override would visibly change `.has-secondary-background-color` on existing 60k sites (regression). + +**B2. SCSS: refactor 11 in-scope `$primary` / `$secondary` references** + +Replace pattern (one example): + +```diff +- background-color: variables.$primary; ++ background-color: var(--wp--preset--color--primary, #{variables.$primary}); +``` + +Apply to all 11 usages listed in the audit. SCSS literal remains as fallback for sites that strip theme.json. + +The remaining 31 `$primary`/`$secondary` usages outside content scope are **not changed** — they continue to use the SCSS variable. Header/footer/nav are not block-content; their Customizer color story is handled by existing inline CSS in `template-tags.php`. + +**B3. PHP: keep existing inline CSS color injection intact** + +`inc/template-tags.php` continues emitting `onepress_primary_color` / `onepress_secondary_color` as ad-hoc inline CSS for non-content selectors (e.g. `.feature-item:hover`). No change there — additive-only. + +**Files:** +- new file `inc/admin/class-theme-json-bridge.php` OR a new function inside `inc/admin/class-editor.php` +- `src/frontend/styles/_contents.scss` (4 lines) +- `src/frontend/styles/_gutenberg.scss` (7 lines) +- `functions.php` — require the new bridge file if separated out + +**Visual deltas:** +- Sites that customized `onepress_primary_color`: block content (`.has-primary-color`, `.has-primary-background-color`, theme.json `styles.elements.button.color.background`) **now follows the Customizer value**. Most users will see this as "finally consistent". A minority may see a color shift on block content they hadn't intended. +- Sites that never customized: zero visual delta. + +**BC impact:** the primary-color visual shift is intentional convergence. Documented loudly in changelog and `spec-block-editor.md`. + +--- + +### Phase C — Font sizes verification + utility class audit (~1h) + +**Goals:** confirm `.has-{slug}-font-size` utility classes work in both editor and frontend (they should — WP block-library CSS handles them). No SCSS refactor unless a conflict shows up. + +**Test:** +- Add a paragraph with explicit font size "Large" (25px) → verify both surfaces show 25px +- Verify `var(--wp--preset--font-size--large)` resolves to `25px` (or its fluid value) +- Check for any specificity conflicts where `_document.scss` h1–h6 rule wins against `.has-large-font-size` + +**Optional refactor (if no conflict surfaces):** make `.entry-content { font-size: var(--wp--preset--font-size--normal, 14px); }` to lock body text to theme.json normal size. Skip if it risks 60k-site regression. + +**Files (probably):** none changed. Documentation update only. + +--- + +### Phase D — Font families verification + utility class audit (~1h) + +Same shape as Phase C, for fontFamily presets. Verify `.has-open-sans-font-family`, `.has-raleway-font-family` etc. apply correctly in content. Optional: refactor `_document.scss` body font-family to consume `var(--wp--preset--font-family--open-sans, …)`. + +**Files:** likely none changed. + +--- + +### Phase E — Spacing verification (~30 min) + +`var(--wp--preset--spacing--N)` and `.has-{slug}-padding-X` utility classes are pure WP — verify they apply correctly inside `.entry-content` and `.editor-styles-wrapper`. No theme rules should override them in normal cases. + +**Files:** none changed. Verification only. + +--- + +### Phase F — Editor mirrors (~30 min) + +For every Phase A–B rule added under `.entry-content`, add the editor mirror under `.editor-styles-wrapper`. Follow the pattern established by Phase 1 of the prior plan. + +**Files:** `src/frontend/styles/_gutenberg.scss` + +--- + +### Phase G — Documentation + changelog (~1h) + +**Files:** +- `docs/spec-block-editor.md` — add "CSS Variable Integration" section with the consumption pattern + Customizer bridge mechanism +- `docs/plan-block-editor-parity.md` — link forward to this plan +- `changelog.md` — describe each phase +- `style.css` + `package.json` — version stays `2.4.0` (if shipping together) or bumps to `2.4.1` + +--- + +### Phase H — Verification matrix (~1h) + +| # | Scenario | Template | Customizer | theme.json | Expected | +|---|---|---|---|---|---| +| 1 | Default site | no-sidebar single | unset | shipping | regular ≤ 1110, alignwide = 1230, alignfull = 100vw, button bg = `#03c4eb` | +| 2 | With sidebar | right-sidebar single | unset | shipping | regular ≤ column (~680), alignwide = 1230 (overflows column — known limitation), alignfull = 100vw | +| 3 | Custom content width | no-sidebar single | `single_layout_content_width=700` | shipping | regular ≤ 700, alignwide = 1230, alignfull = 100vw | +| 4 | Custom theme.json widths | no-sidebar single | unset | `contentSize:900, wideSize:1400` | regular ≤ 900, alignwide = 1400, alignfull = 100vw | +| 5 | Custom primary color | no-sidebar single | `onepress_primary_color=#ff5500` | shipping | block button bg = `#ff5500`, `.has-primary-background-color` = `#ff5500`, frontend `.entry-content` button bg = `#ff5500` | +| 6 | Custom secondary color | no-sidebar single | `onepress_secondary_color=#00ff00` | shipping | `.has-secondary-*` still `#333333` (frozen); header secondary uses Customizer value; documented inconsistency | +| 7 | Font size picker | no-sidebar single | unset | shipping | `.has-large-font-size` para renders at 25px both surfaces | +| 8 | Font family picker | no-sidebar single | unset | shipping | `.has-raleway-font-family` para renders in Raleway both surfaces | +| 9 | Editor canvas | post.php | (any) | shipping | matches frontend visually within iframe constraints | +| 10 | Plus active | (any) | (any) | shipping | typography from Plus still wins; no regression from Phase A–B | + +--- + +## Total effort: ~9–11 hours + +| Phase | Estimate | +|---|---| +| A — Layout | 1.5h | +| B — Colors | 3h | +| C — Font sizes | 1h | +| D — Font families | 1h | +| E — Spacing | 0.5h | +| F — Editor mirrors | 0.5h | +| G — Docs + changelog | 1h | +| H — Verification | 1h | +| **Total** | **~9.5h** | + +--- + +## BC analysis (combined) + +| Change | Sites affected | Visual delta | Risk | Mitigation | +|---|---|---|---|---| +| alignwide widens 1110 → 1230 | sites using alignwide blocks | +120px width | low — matches theme.json intent | document in changelog | +| Regular `.entry-content > *` capped at 1110 | fullwidth-template sites with very wide hand-coded content | content narrows visibly | low — matches theme.json intent | document | +| `single_layout_content_width` mod → CSS var override | sites with mod set | none (same effective constraint) | none | n/a | +| Customizer primary color now reaches block content | sites that customized primary | block content rebrands to primary | low — most users expect this | document loudly + offer filter to opt out | +| Secondary color **NOT** touched | sites using `.has-secondary-*` | none | n/a | document the frozen inconsistency | +| 11 SCSS `$primary` usages replaced with `var()` + fallback | all sites | none | none — fallback covers no-theme.json case | n/a | +| Font sizes / families / spacing | all sites | none expected (verification only) | none | n/a | + +**Verdict:** safe under additive-only. The two intentional visual deltas (alignwide widening, content-size capping on fullwidth templates) plus the primary-color convergence are the entire surface of change. Document all three. + +--- + +## Out of scope (will not be touched) + +- Converting to block theme (FSE). +- Changing theme.json's `secondary = #333333` (frozen). +- Changing theme.json `contentSize` (1110), `wideSize` (1230), or any palette/fontSize/fontFamily VALUE. +- Adding `.is-layout-constrained` class to `.entry-content` (would inherit WP `margin-block-start: 24px` rule — rejected). +- Refactoring the 31 out-of-content-scope `$primary`/`$secondary` SCSS usages (header/footer/nav). +- Adding new palette / fontSize / fontFamily / spacing entries to theme.json (no need for 2.4.0). +- `useRootPaddingAwareAlignments` work (inert on classic theme; leave declared). +- Block patterns (deferred from prior plan). + +--- + +## Resolved decisions (locked 2026-05-26) + +1. **Phase B Customizer bridge: override `primary` only.** `secondary` is frozen (`#333333` mismatch with `onepress_secondary_color`'s `#00aeef` default would regress `.has-secondary-*` on existing 60k sites). +2. **Phase C/D body text refactor: skip** unless a conflict is discovered during verification. Keep SCSS literals; `_document.scss` body/h1–h6 declarations stay. +3. **Release vehicle: `2.4.1` separate tag.** Block-editor parity work (Phase 0–6 of prior plan) ships first as `2.4.0`; this plan ships on top as `2.4.1` so the primary-color convergence has its own rollback boundary and changelog focus. +4. **Phase 3 defensive PHP palette: keep.** `OnePress_Editor::get_editor_color_palette()` / `get_editor_font_sizes()` remain as fallbacks for sites that strip theme.json. They are inert in normal operation. +5. **Customizer mod scope for bridge: `onepress_primary_color` only.** Footer/menu color mods have different intent (chrome-specific) and would clutter the block palette. + +--- + +## References + +- [Layout — Theme Handbook](https://developer.wordpress.org/themes/global-settings-and-styles/settings/layout/) +- [Color — Theme Handbook](https://developer.wordpress.org/themes/global-settings-and-styles/settings/color/) +- [Typography — Theme Handbook](https://developer.wordpress.org/themes/global-settings-and-styles/settings/typography/) +- [Spacing — Theme Handbook](https://developer.wordpress.org/themes/global-settings-and-styles/settings/spacing/) +- [Global Settings & Styles (theme.json) — Block Editor Handbook](https://developer.wordpress.org/block-editor/how-to-guides/themes/global-settings-and-styles/) +- [WordPress Global Styles Reference Tables — CSS-Tricks](https://css-tricks.com/wordpress-global-styles-reference-tables/) +- [`wp_theme_json_data_theme` filter — Block Editor Handbook](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_theme/) +- Live capture: `wp_get_global_stylesheet()` output for this site diff --git a/docs/spec-admin.md b/docs/spec-admin.md new file mode 100644 index 00000000..39c740d9 --- /dev/null +++ b/docs/spec-admin.md @@ -0,0 +1,67 @@ +# spec-admin — Page Meta Box & Theme Dashboard + +Admin-side surfaces shipped by the theme: a per-page meta box and the theme info page. + +## Page meta box + +[`OnePress_MetaBox`](../inc/metabox.php) adds a **Page Settings** sidebar metabox to the `page` post type with three flags. Values are stored as post meta. Nonce action: `onepress_page_settings`. + +| Meta key | Effect | +|---|---| +| `_hide_page_title` | Adds body class `hiding-page-title`; templates respect it when rendering the page title | +| `_hide_header` | Suppresses the site header (read by [`onepress_header()`](../inc/template-tags.php) ~line 255) | +| `_hide_footer` | Suppresses the site footer (read by [../footer.php](../footer.php), also honored for the WC shop page) | +| `_hide_breadcrumb` | Suppresses the WC breadcrumb on the shop page (read by [../woocommerce.php](../woocommerce.php)) | + +When adding template behavior that should respect "hide" toggles, read these meta keys directly. + +## Theme dashboard + +[`Onepress_Dashboard`](../inc/admin/dashboard.php) (singleton) registers an info page under **Appearance → About OnePress** at `themes.php?page=ft_onepress`. + +### Responsibilities + +- Renders tabs incl. **Recommended Actions** (dismissable; persisted in option `onepress_actions_dismiss`). +- Handles the **section toggle form** — nonce field `onepress_settings_nonce`, action `onepres_save_settings`. On submit it calls [`Onepress_Config::save_settings()`](../inc/class-config.php), which writes the option `onepress_sections_settings`. +- Shows the **"switch theme" admin notice** (dismissal stored in option `onepress_dismiss_switch_theme_notice`). +- Resets recommended-action dismissals when the theme is switched (`switch_theme` hook). +- Exposes recommended-action counts to the Customizer via the localized `onepress_customizer_settings` object: + + ```js + { + number_action: , // remaining actions + is_plus_activated: 'y' | 'n', // class_exists('OnePress_Plus') + action_url: '/themes.php?page=ft_onepress&tab=recommended_actions' + } + ``` + +### Saving options safely + +- Nonces are checked on every POST handler. **Never POST to dashboard endpoints without `wp_nonce_field( 'onepres_save_settings', 'onepress_settings_nonce' )`.** +- Inputs are sanitized through [../inc/sanitize.php](../inc/sanitize.php) helpers — see [spec-customizer.md](spec-customizer.md#sanitizers). + +## Block editor integration + +[`OnePress_Editor`](../inc/admin/class-editor.php) is the entry point. Since 2.4.0, the editor canvas matches the rendered frontend for in-scope blocks (typography, blockquote, lists, code, image, gallery, button, columns, group, cover, table, separator, embed, etc.) and exposes a dynamic color palette + font sizes derived from theme mods. + +Full details: [spec-block-editor.md](spec-block-editor.md). + +Quick reference: + +- Editor stylesheet injected via `block_editor_settings_all` (WP ≥ 5.8) / `block_editor_settings` (older). Source: `src/frontend/styles/editor.scss` → `assets/admin/editor.css`. +- AJAX endpoint `onepress_load_editor_style` returns the CSS dynamically. +- Plus typography (when `onepress_typography_render_style()` exists) injected via `wp_add_inline_style( 'wp-edit-post', … )`. +- Block-editor assets registered on `enqueue_block_editor_assets`. +- Theme supports added in [functions.php](../functions.php) `onepress_setup()`: `editor-styles`, `align-wide`, `wp-block-styles`, `responsive-embeds`, `custom-line-height`, `custom-spacing`, `custom-units`. +- Filters: `onepress_editor_color_palette`, `onepress_editor_font_sizes`. + +## Hooks/options reference (admin side) + +| Option / hook | Purpose | +|---|---| +| Option `onepress_sections_settings` | Active section flags (managed by `Onepress_Config`) | +| Option `onepress_actions_dismiss` | Dismissed recommended actions | +| Option `onepress_dismiss_switch_theme_notice` | "Switch theme" notice dismissed | +| Nonce `onepres_save_settings` | Dashboard section-toggle form | +| Nonce `onepress_page_settings` | Page meta box save | +| Admin URL | `themes.php?page=ft_onepress` | diff --git a/docs/spec-architecture.md b/docs/spec-architecture.md new file mode 100644 index 00000000..0f874ace --- /dev/null +++ b/docs/spec-architecture.md @@ -0,0 +1,35 @@ +# spec-architecture — File Map + +Where each concern lives in the theme. Use this as a routing table before opening files. + +| Concern | Location | +|---|---| +| Theme bootstrap, supports, enqueues | [../functions.php](../functions.php) | +| Template hierarchy roots | `../index.php`, `../home.php`, `../archive.php`, `../single.php`, `../page.php`, `../search.php`, `../404.php`, `../comments.php`, `../sidebar.php`, `../header.php`, `../footer.php` | +| Page templates | `../template-frontpage.php`, `../template-fullwidth.php`, `../template-fullwidth-stretched.php`, `../template-left-sidebar.php` | +| Loop partials | [../template-parts/](../template-parts/) — `content.php`, `content-single.php`, `content-page.php`, `content-list.php`, `content-search.php`, `content-none.php` | +| Front-page sections (markup) | [../section-parts/section-*.php](../section-parts/) | +| Customizer registration | [../inc/customizer.php](../inc/customizer.php) | +| Customizer panels/sections/settings | [../inc/customize-configs/](../inc/customize-configs/) — one file per section + global option files | +| Custom controls (alpha color, repeater, editor, …) | [../inc/customize-controls/](../inc/customize-controls/), bootstrapped by [../inc/customizer-controls.php](../inc/customizer-controls.php) | +| Customizer selective refresh | [../inc/customizer-selective-refresh.php](../inc/customizer-selective-refresh.php) | +| Template tags / rendering helpers | [../inc/template-tags.php](../inc/template-tags.php) | +| Section toggle registry | [../inc/class-config.php](../inc/class-config.php) (`Onepress_Config`) | +| Sections navigation (dots) | [../inc/class-sections-navigation.php](../inc/class-sections-navigation.php) | +| Sanitizers used by Customizer | [../inc/sanitize.php](../inc/sanitize.php) | +| Page-side meta box | [../inc/metabox.php](../inc/metabox.php) (`OnePress_MetaBox`) | +| Admin info page + recommended actions | [../inc/admin/dashboard.php](../inc/admin/dashboard.php) (`Onepress_Dashboard`) | +| Block editor integration | [../inc/admin/class-editor.php](../inc/admin/class-editor.php) (`OnePress_Editor`) | +| FontAwesome 6 icon set for picker | [../inc/list-icon-v6.php](../inc/list-icon-v6.php), assets at [../assets/fontawesome-v6/](../assets/fontawesome-v6/) | +| Misc body classes, excerpts, WC detection | [../inc/extras.php](../inc/extras.php) | +| WooCommerce template override | [../woocommerce.php](../woocommerce.php) | +| Translations | [../languages/](../languages/) (POT + locale files) | +| WPML config | [../wpml-config.xml](../wpml-config.xml) | +| Source (edited) | [../src/](../src/) — JS, SCSS, images | +| Build output (generated, do not edit) | [../assets/](../assets/) | + +## Top-level scripts + +- [../functions.php](../functions.php) — entrypoint; loads everything in `inc/` in this order: `class-config.php` → `sanitize.php` → `metabox.php` → `template-tags.php` → `extras.php` → `class-sections-navigation.php` → `customizer.php` → `admin/dashboard.php` → `admin/class-editor.php`. +- [../webpack.config.js](../webpack.config.js) — build pipeline; see [spec-build.md](spec-build.md). +- [../Gruntfile.js](../Gruntfile.js) — kept for ZIP packaging only; not used for code builds. diff --git a/docs/spec-block-editor.md b/docs/spec-block-editor.md new file mode 100644 index 00000000..5fb31fd2 --- /dev/null +++ b/docs/spec-block-editor.md @@ -0,0 +1,323 @@ +# spec-block-editor — Block Editor Integration + +OnePress is a **classic theme** with a "Gutenberg-friendly" editor integration: posts edited in the block editor render visually identical (or very close) to the published frontend. + +OnePress is **not** a block theme — there is no `theme.json`, no `templates/`, no `parts/`. Front-end templating remains in classic PHP partials. See [spec-architecture.md](spec-architecture.md). + +--- + +## Overall architecture + +| Layer | Where | Role | +|---|---|---| +| Theme supports | [functions.php](../functions.php) `onepress_setup()` | `editor-styles`, `align-wide`, `wp-block-styles`, `responsive-embeds`, `custom-line-height`, `custom-spacing`, `custom-units` | +| Editor stylesheet (classic channel) | [functions.php](../functions.php) `add_editor_style( 'assets/admin/editor.css', onepress_fonts_url() )` | Loads `editor.css` into the iframe head | +| Editor stylesheet (modern channel) | [`OnePress_Editor::editor_settings()`](../inc/admin/class-editor.php) | Injects same CSS via `block_editor_settings_all` filter — both channels load identical CSS (intentional duplication for resilience across WP versions) | +| Dynamic color palette | `OnePress_Editor::get_editor_color_palette()` | Read from Customizer theme mods (`onepress_primary_color`, `onepress_secondary_color`) each request | +| Dynamic font sizes | `OnePress_Editor::get_editor_font_sizes()` | Theme-aligned scale (13/14/18/24/32 px) | +| Content-width constraint | `OnePress_Editor::css()` | Inline CSS: `.editor-styles-wrapper .wp-block:not([data-align="full"]):not([data-align="wide"]) { max-width: px; }` | +| Plus typography | `OnePress_Editor::assets()` | Pulled from `onepress_typography_render_style()` when OnePress Plus is active | +| SCSS source | `src/frontend/styles/editor.scss` → imports `_variables`, `_document`, `_contents`, `_gutenberg` | Same partials as `style.scss`; the editor inherits all frontend typography | +| Parity rules | `src/frontend/styles/_gutenberg.scss` | The single source of truth for editor ↔ frontend visual parity | + +--- + +## How parity works + +The editor iframe `` carries the class `.editor-styles-wrapper`. The frontend wraps post content in `.entry-content`. Two strategies share rules between them: + +1. **Bare `.wp-block-*` selectors.** Block classes are specific enough to apply identically in both contexts without ancestor prefixes. Used for visual styling (button colors, table borders, etc.). +2. **Companion mirrors `.editor-styles-wrapper X`.** When a rule lives under `.entry-content X` in `_contents.scss` (and the rule must also apply in the editor), `_gutenberg.scss` mirrors it under `.editor-styles-wrapper X`. Used for typography baselines (blockquote, lists, code). + +**Body class injection** (e.g. setting `editor_settings['body_class'] = 'entry-content'`) is intentionally **not used** — the API has shifted across WP 5.x/6.x and is brittle. Selector grouping is the stable approach. + +See the header comment in [_gutenberg.scss](../src/frontend/styles/_gutenberg.scss) for the canonical pattern. + +--- + +## CSS Variable Integration (since 2.4.1) + +theme.json values flow into theme stylesheets via WP's auto-emitted CSS custom properties. + +### Variables consumed by theme SCSS + +| theme.json declaration | Emitted CSS variable | Consumed in | +|---|---|---| +| `settings.layout.contentSize` | `--wp--style--global--content-size` | `.entry-content > *:not(.alignwide):not(.alignfull):not(.alignleft):not(.alignright)` max-width | +| `settings.layout.wideSize` | `--wp--style--global--wide-size` | `.entry-content > .alignwide` / `.editor-styles-wrapper .wp-block.alignwide` max-width | +| `settings.color.palette[slug=primary]` | `--wp--preset--color--primary` | `.entry-content blockquote` border-left, `.editor-styles-wrapper a` color, `.wp-block-button__link` bg, `.wp-block-button.is-style-outline` color/border, `.highlight` color, `.nav-links a:hover` bg, comment `.cat-links a:hover` color | +| `settings.color.palette[slug=secondary]` | `--wp--preset--color--secondary` | **Not consumed** — frozen mismatch with SCSS `$secondary` value (see [Known inconsistencies](#known-inconsistencies)) | + +### Consumption pattern + +Every var-consuming rule uses the WP variable with the SCSS literal as fallback: + +```scss +background-color: var(--wp--preset--color--primary, #{variables.$primary}); +``` + +This way: +- theme.json updates propagate automatically +- Customizer-driven theme.json overrides (see bridge below) propagate +- Sites that strip theme.json fall back to SCSS literal (no breakage) + +### Customizer → theme.json bridge + +[`inc/theme-json-bridge.php`](../inc/theme-json-bridge.php) registers a `wp_theme_json_data_theme` filter that overrides the **`primary` palette entry** at runtime with the value of the `onepress_primary_color` Customizer mod. The result: + +- `--wp--preset--color--primary` follows Customizer +- `.has-primary-color` / `.has-primary-background-color` (utility classes emitted by WP block-library CSS) automatically use the Customizer color +- theme.json's static `#03c4eb` becomes the default (when mod is unset) + +**Scope:** only `primary` is bridged. `secondary` is **not** overridden because theme.json `secondary = #333333` does not match `onepress_secondary_color`'s `#00aeef` default — overriding would visibly rebrand existing sites' `.has-secondary-*` content. + +**Filter:** `onepress_filter_theme_json_palette` — hooked to `wp_theme_json_data_theme`. Extend by hooking the same filter at a later priority and returning a modified `WP_Theme_JSON_Data` object. + +### Content-size override path + +When a user sets the `single_layout_content_width` Customizer mod, [`onepress_custom_inline_style()`](../inc/template-tags.php) (and `OnePress_Editor::css()` for the editor canvas) emit both: + +1. **`:root { --wp--style--global--content-size: px; }`** — overrides the WP global var, propagating to every theme.json-aware rule. +2. **`.single-post .site-main, .single-post .entry-content > *:not(.alignwide):not(.alignfull) { max-width: px; }`** — retained for back-compat with sites that depend on the exact selector chain. + +The two paths emit the same value; the legacy explicit rule serves as defense-in-depth when CSS vars are unavailable. + +--- + +## Architecture: theme.json is the source of truth + +OnePress ships [`theme.json`](../theme.json) (since pre-2.4.0). At runtime, WP merges it into `editor_settings`: + +- `editor_settings['colors']` ← theme.json `settings.color.palette` (8 colors, slugs `primary`, `secondary`, `heading`, `text`, `border`, `light`, `white`, `black`) +- `editor_settings['fontSizes']` ← theme.json `settings.typography.fontSizes` (6 sizes, slugs `small` … `xx-large`) +- `editor_settings['__experimentalFeatures']` ← all of theme.json `settings.*` + +The PHP-side methods (`OnePress_Editor::get_editor_color_palette()` / `get_editor_font_sizes()`) are **defensive fallbacks** — they only fire if the `colors` / `fontSizes` keys are empty (e.g. a child theme has stripped theme.json or `appearanceTools`). In normal operation theme.json wins and the PHP palette is inert. + +**Practical implication:** when editing theme palette / sizes, edit [`theme.json`](../theme.json) primarily. PHP methods exist for integrators who programmatically need the data or to handle the no-theme.json edge case. + +## theme.json reference + +theme.json `settings`: + +| Key | Value | Notes | +|---|---|---| +| `appearanceTools` | `true` | Enables border/spacing/typography UI; subsumes `custom-line-height`, `custom-spacing`, parts of `custom-units` | +| `useRootPaddingAwareAlignments` | `true` | Modern WP layout setting (mostly inert on classic themes) | +| `layout.contentSize` | `1110px` | Matches SCSS `$grid` | +| `layout.wideSize` | `1230px` | Matches SCSS `$width` | +| `color.defaultPalette` | `false` | Drop WP defaults | +| `color.defaultGradients` | `false` | Drop WP gradient defaults | +| `color.palette` | 8 colors (see below) | Frozen public slugs | +| `spacing.units` | `px, em, rem, %, vw, vh` | | +| `spacing.spacingScale.steps` | `7` | | +| `typography.fluid` | `true` | Fluid type scaling | +| `typography.customFontSize` | `true` | Allow custom px | +| `typography.defaultFontSizes` | `false` | Drop WP defaults | +| `typography.fontFamilies` | 4 families | Slugs `open-sans`, `raleway`, `system`, `monospace` | +| `typography.fontSizes` | 6 sizes | Slugs `small` … `xx-large` | + +theme.json `styles` defines body color/font, link/button/heading typography and per-h1–h6 sizes. These map to WP's CSS variables (`var(--wp--preset--color--primary)` etc.) so user content can reference them consistently. + +## Color palette + +Eight slugs, dynamically resolved per request: + +The **theme.json palette** (what users actually see in the editor): + +| Slug | Color | CSS classes | +|---|---|---| +| `primary` | `#03c4eb` | `.has-primary-color`, `.has-primary-background-color` | +| `secondary` | `#333333` | `.has-secondary-*` — ⚠ see [Known inconsistency](#known-inconsistencies) | +| `heading` | `#333333` | `.has-heading-*` | +| `text` | `#777777` | `.has-text-*` | +| `border` | `#e9e9e9` | `.has-border-*` | +| `light` | `#f8f9f9` | `.has-light-*` (slug `light`, SCSS variable `$meta`) | +| `white` | `#ffffff` | `.has-white-*` | +| `black` | `#000000` | `.has-black-*` | + +The **PHP fallback palette** (`OnePress_Editor::get_editor_color_palette()`, only active if theme.json is bypassed): + +| Slug | Source | Default | +|---|---|---| +| `onepress-primary` | `get_theme_mod( 'onepress_primary_color' )` | `#03c4eb` | +| `onepress-secondary` | `get_theme_mod( 'onepress_secondary_color' )` | `#00aeef` | +| `onepress-heading` | constant | `#333333` | +| `onepress-text` | constant | `#777777` | +| `onepress-border` | constant | `#e9e9e9` | +| `onepress-meta` | constant | `#f8f9f9` | +| `onepress-white` | constant | `#ffffff` | + +**Filter:** `onepress_editor_color_palette` — receives the array, returns modified array. + +```php +add_filter( 'onepress_editor_color_palette', function ( $palette ) { + $palette[] = array( + 'name' => __( 'Accent', 'mychild' ), + 'slug' => 'mychild-accent', + 'color' => '#ff5500', + ); + return $palette; +} ); +``` + +**Override rule:** if a child theme has already set `colors` (e.g. via `add_theme_support( 'editor-color-palette', … )` or another `block_editor_settings_all` filter), OnePress does **not** overwrite — `editor_settings()` only injects when `empty( $editor_settings['colors'] )`. Child themes that customize win automatically. + +**API stability:** the slugs above are part of the additive-only contract — user posts may already reference `.has-onepress-primary-color` in saved markup. Never rename a shipped slug. See [spec-conventions.md → Additive-only mandate](spec-conventions.md#additive-only-mandate). + +--- + +## Font sizes + +Five steps: + +| Slug | px | CSS class | +|---|---|---| +| `small` | 13 | `.has-small-font-size` | +| `normal` | 14 | `.has-normal-font-size` | +| `medium` | 18 | `.has-medium-font-size` | +| `large` | 24 | `.has-large-font-size` | +| `huge` | 32 | `.has-huge-font-size` | + +**Filter:** `onepress_editor_font_sizes` — same shape and override semantics as the color palette filter. + +```php +add_filter( 'onepress_editor_font_sizes', function ( $sizes ) { + $sizes[] = array( + 'name' => __( 'Display', 'mychild' ), + 'slug' => 'display', + 'size' => 48, + ); + return $sizes; +} ); +``` + +--- + +## Theme supports (since 2.4.0) + +| Support | Effect | +|---|---| +| `responsive-embeds` | WP wraps embeds in `
` with aspect-ratio CSS; OnePress also styles `.wp-block-embed.is-type-video` with a 16:9 ratio fallback | +| `custom-line-height` | Editor sidebar exposes per-block line-height control | +| `custom-spacing` | Editor sidebar exposes margin/padding controls | +| `custom-units` | Editor accepts non-px units (em, rem, vh, vw, %) | + +These do not change how saved posts render unless the user opts in via the editor UI. + +**NOT enabled** (intentional): + +- `disable-custom-colors` — too restrictive; users would lose the color picker. +- `disable-custom-font-sizes` — same. +- `theme.json` / FSE / `templates/` / `parts/` — out of scope; OnePress is a classic theme. + +--- + +## Block-specific styling + +Block visual rules live in `src/frontend/styles/_gutenberg.scss` under the Phase 2 section. Coverage: + +| Block | Section | +|---|---| +| Paragraph, Heading | Inherited from global `_document.scss` h1–h6, p, body | +| Quote | Mirrored via `.editor-styles-wrapper blockquote` (Phase 1) — same padding / italic / left border / meta background as `.entry-content blockquote` | +| List | Mirrored via `.editor-styles-wrapper ul/ol/li` (Phase 1) | +| Image | `.wp-block-image` — display block, max-width 100%, italic captions, alignleft/right/center | +| Gallery | `.wp-block-gallery` — extends existing `is-layout-flex` rule in `_contents.scss` | +| Button | `.wp-block-button` — primary color background, `is-style-outline` variant | +| Pullquote | `.wp-block-pullquote` — top/bottom borders, centered, italic, `is-style-solid-color` variant | +| Cover | `.wp-block-cover` — white text overlay, 1.5em inner padding | +| Columns / Column | `.wp-block-columns` — `$gutter2` gap, wraps below md breakpoint | +| Group | `.wp-block-group` — 1.5em padding when `.has-background` | +| Table | `.wp-block-table` — `$border` cell border, `$meta` header background, `is-style-stripes` variant | +| Separator | `.wp-block-separator` — short centered default, `is-style-wide` full, `is-style-dots` | +| Embed | `.wp-block-embed.is-type-video` — 16:9 responsive iframe | +| Code / Preformatted | `.wp-block-code`, `.wp-block-preformatted` — `$monaco` font, `$meta` background | +| Spacer | `.wp-block-spacer` — display block, clear both | + +## Alignwide / Alignfull + +Both classes work on the rendered frontend and the editor canvas as of 2.4.0. + +| Class | Frontend behavior | Editor behavior | +|---|---|---| +| `.alignwide` | Breaks past the parent's content-size constraint to reach `var(--wp--style--global--wide-size)` (default 1230px) using `max(calc(50% - 50vw), calc(50% - wideSize/2))` symmetric negative margins. Widens up to wideSize without overflowing the viewport. Since 2.4.1. | Expands to wideSize inside `.editor-styles-wrapper` via `max-width: var(--wp--style--global--wide-size)` | +| `.alignfull` | Edge-to-edge (`100vw`) via negative-viewport margins (`calc(50% - 50vw)`) | `max-width: none`, fills the editor canvas (which is iframe-constrained, so it won't truly hit 100vw — visually expands past the content-width constraint) | + +Frontend rules are scoped to `.entry-content > .alignwide` / `.entry-content > .alignfull` (direct descendant only) so the classes don't leak into widgets or section parts that may reuse them. Editor rules use `.editor-styles-wrapper .wp-block[data-align="wide"|"full"]` + `.wp-block.alignwide|alignfull` selectors with enough specificity to win against Gutenberg's stock alignment CSS without `!important`. + +**Note on sidebars:** alignfull on a single-post page with a sidebar still expands to viewport width, which can look unusual since the post column is narrower. For pages where wide/full blocks are the primary visual element, use the `template-fullwidth.php` or `template-fullwidth-stretched.php` page templates. + +--- + +## Build pipeline + +`editor.css` is generated by webpack from `src/frontend/styles/editor.scss`: + +```bash +npm run dev # generates assets/admin/editor.css (non-minified) +npm run build # generates assets/admin/editor.minified.css +``` + +**Important:** the non-minified file (`editor.css`) is what gets loaded in development. Both `add_editor_style()` in `functions.php` and `OnePress_Editor::$editor_file` reference the non-minified path. Running `npm run build` alone leaves `editor.css` stale; run `npm run dev` (and stop it after the initial compile) to refresh the non-minified output. See [spec-build.md](spec-build.md) for the broader build pipeline. + +RTL CSS is auto-emitted: `editor-rtl.css`, `editor.minified-rtl.css`. + +--- + +## Adding rules — decision tree + +``` +Need a new style for content in the editor and/or frontend? +│ +├── Is it block-specific (class starts with .wp-block-*)? +│ └── YES → put bare rule in _gutenberg.scss Phase 2 section +│ +├── Is it a typography baseline (blockquote, li, p, code, etc.)? +│ ├── Already covered in _document.scss (global h1-h6, body, a)? → done, no work +│ └── Scoped to .entry-content X in _contents.scss? +│ └── add mirror .editor-styles-wrapper X in _gutenberg.scss Phase 1 section +│ +└── Is it new shared content (e.g. .has-*-class emitted by a new feature)? + └── prefer grouped selectors: + .entry-content, + .editor-styles-wrapper { + X { ... } + } + place in _gutenberg.scss +``` + +--- + +## API surface (additive-only, frozen) + +These names are public API as of 2.4.0; per [spec-conventions.md → Additive-only mandate](spec-conventions.md#additive-only-mandate) they cannot be renamed or removed: + +- Class: `OnePress_Editor` +- Methods: `editor_settings()`, `assets()`, `css()`, `editor_style_url()`, `css_file()`, `load_style()`, `get_editor_color_palette()`, `get_editor_font_sizes()` +- Filters: `onepress_editor_color_palette`, `onepress_editor_font_sizes` +- Color palette slugs: `onepress-primary`, `onepress-secondary`, `onepress-heading`, `onepress-text`, `onepress-border`, `onepress-meta`, `onepress-white` +- Font size slugs: `small`, `normal`, `medium`, `large`, `huge` +- AJAX action: `onepress_load_editor_style` (nonce key) +- File path constant: `OnePress_Editor::$editor_file` = `'assets/admin/editor.css'` + +--- + +## Known inconsistencies + +Frozen by [additive-only mandate](spec-conventions.md#additive-only-mandate) — do not "fix" without a major-version migration: + +| Inconsistency | Shipped form | Don't do | +|---|---|---| +| `theme.json` palette has `secondary = #333333` (the heading color), but the SCSS `$secondary` variable and the `onepress_secondary_color` theme mod default to `#00aeef` | `#333333` in theme.json; `#00aeef` in SCSS / theme mod | Don't change theme.json's secondary value — user posts may already render `.has-secondary-background-color` as `#333333`. If parity is needed, introduce a **new** slug rather than modifying the existing one. | +| Light background slug name | theme.json: `light` (#f8f9f9); SCSS variable: `$meta` | Don't rename either side; treat `light` (theme.json) and `meta` (SCSS) as canonical aliases for the same color. | +| `useRootPaddingAwareAlignments: true` is declared but OnePress is a classic theme | inert in practice | Don't remove (could affect a future block-theme migration) | +| `responsive-embeds`, `custom-line-height`, `custom-spacing`, `custom-units` added via `add_theme_support()` in `functions.php` are partially redundant with theme.json's `appearanceTools: true` | both declared | Don't remove either — `add_theme_support()` calls still serve themes that strip theme.json | + +## Future work (deferred) + +- **Block patterns** — register OnePress-themed patterns (hero, feature grid, team card, CTA, testimonial). See [plan-block-editor-parity.md → Phase 5](plan-block-editor-parity.md#phase-5--block-patterns). +- **Custom blocks** — e.g. a "Hero Slider" block that wraps the OnePress hero section markup. +- **Block style variations** — `register_block_style()` for additional button / quote / separator variants tied to OnePress aesthetics. +- **Customizer "Additional CSS" in editor** — currently not loaded in the editor canvas; documented limitation. +- **Migration to block theme (FSE)** — not planned. diff --git a/docs/spec-build.md b/docs/spec-build.md new file mode 100644 index 00000000..0bf9dce4 --- /dev/null +++ b/docs/spec-build.md @@ -0,0 +1,81 @@ +# spec-build — Build Pipeline & Asset Loading + +Covers how source under `src/` becomes the shipped `assets/`, and how PHP enqueues those bundles. + +## Tooling + +`@wordpress/scripts` (webpack 5) + Sass. + +The legacy [../Gruntfile.js](../Gruntfile.js) is **kept for ZIP packaging only** — do **not** use it for code builds. + +```bash +npm install +npm run dev # watch mode (unminified, source maps) +npm run build # production (.minified.* + RTL) +npm run lint:js # ESLint +npm run lint:css # Stylelint +``` + +## Entry points + +Declared in [../webpack.config.js](../webpack.config.js): + +| Source | Output | +|---|---| +| `src/frontend/index.js` | `assets/frontend/theme.js` | +| `src/frontend/libs/gallery/isotope.js` | `assets/frontend/gallery-isotope.js` | +| `src/frontend/libs/gallery/jquery.justified.js` | `assets/frontend/gallery-justified.js` | +| `src/frontend/libs/gallery/owl.carousel.js` | `assets/frontend/gallery-carousel.js` | +| `src/frontend/lightgallery.js` | `assets/frontend/lightgallery.js` | +| `src/admin/admin.js` | `assets/admin/admin.js` | +| `src/admin/customizer.js` | `assets/admin/customizer.js` | +| `src/admin/customizer-liveview.js` | `assets/admin/customizer-liveview.js` | +| `src/frontend/styles/editor.scss` | `assets/admin/editor.css` | + +Production builds emit `*.minified.js` / `*.minified.css`. PHP picks the variant via `WP_DEBUG` (see [`onepress_load_build_script()`](../functions.php) at ~line 271). + +## RTL + +RTL CSS is auto-emitted by `rtlcss-webpack-plugin` (`*-rtl.css`, `*.minified-rtl.css`). **Never hand-author `*-rtl.css`** — it will be overwritten. + +## Images + +`src/images/` is copied to `assets/images/` by `CopyWebpackPlugin`. Place new theme images under `src/images/` and rebuild. + +## Line-ending normalization + +[../webpack.config.js](../webpack.config.js) installs `normalizeLineEndingsPlugin` — every emitted `.js` is rewritten to LF before disk write. This is the only defense against CRLF-shipping npm dependencies. **Do not remove it.** See [spec-line-endings.md](spec-line-endings.md). + +## Enqueuing convention + +Use [`onepress_load_build_script($key, $deps, $is_admin)`](../functions.php) to enqueue anything emitted by webpack. It: + +1. Reads `assets/{frontend|admin}/{key}.asset.php` for the dependency array and version hash. +2. Auto-resolves `.minified` in non-debug environments. +3. Registers handle `onepress-{key}` (the `theme` key uses style handle `onepress-style`). + +Inline CSS goes through [`onepress_custom_inline_style()`](../inc/template-tags.php) → attached to `onepress-style`. + +Front-end JS settings are exposed as the global `onepress_js_settings` via `wp_localize_script` and filtered through `onepress_js_settings` ([functions.php](../functions.php) ~line 417). + +## Conditional gallery loading + +Gallery scripts are loaded conditionally based on the `onepress_gallery_display` theme mod: + +- `grid` (default) — no extra script +- `isotope` / `masonry` — `gallery-isotope` +- `justified` — `gallery-justified` +- `slider` / `carousel` — `gallery-carousel` + +Gallery scripts are **skipped on WooCommerce pages**. Lightgallery is always enqueued on non-shop pages. + +## Google Fonts + +Raleway + Open Sans loaded from [`onepress_fonts_url()`](../functions.php). Setting the theme mod `onepress_disable_g_font`: + +- Removes the stylesheet. +- Blocks runtime-injected font requests via `onepress_block_all_js_google_fonts` (intercepts `head.insertBefore` for `fonts.googleapis.com` / `fonts.gstatic.com`). + +## Never edit `assets/` + +[../assets/](../assets/) is build output. Edit `src/`, run `npm run build`, commit both together. See [spec-commits.md](spec-commits.md). diff --git a/docs/spec-commits.md b/docs/spec-commits.md new file mode 100644 index 00000000..70aaca38 --- /dev/null +++ b/docs/spec-commits.md @@ -0,0 +1,236 @@ +# spec-commits — Commit Conventions + +How to commit to this theme. + +## Hard rules + +- **English only** in commit messages and any new file content. +- **Stage files by name** (`git add `); never `git add -A` / `git add .`. This avoids sweeping in unrelated work-in-progress and accidentally created files (`.env`, large binaries, OS metadata). +- **No `Co-Authored-By:` trailer** on commits made for this project. +- **Never `--amend`** a published commit. Add a new commit instead. +- **Never `--no-verify`** to bypass hooks. If a hook fails, fix the underlying issue. + +## Commit anatomy + +Follow [Conventional Commits](https://www.conventionalcommits.org/) with an explicit OnePress scope: + +``` +()!: ← line 1, ≤ 72 chars, no trailing period + ← line 2 blank (required) + ← multi-line, wrap at ~72 chars + ← blank line +BC: ← footer (mandatory, see below) +Refs: #123 ← optional: issue/PR refs +``` + +- `!` after the scope is the **breaking-change marker** (Conventional Commits standard) — equivalent to `BC: breaking`. Only allowed in major-version commits that satisfy the 5 conditions in [spec-conventions.md → When you genuinely must remove](spec-conventions.md#when-you-genuinely-must-remove). +- `` is optional in the spec but **required here** — it makes git history greppable and changelog generation trivial. + +### Subject-line rules + +- Active voice, imperative mood (`add`, `fix`, `wire`, not `added`, `fixes`, `wiring`). +- Lowercase after the colon. +- No trailing period. +- ≤ 72 characters **including the prefix**. +- One change per commit. If the subject needs `+` / `and` / `,` to glue unrelated changes, split into separate commits. + +### Body discipline + +Keep the body **short and factual**. The diff already shows the code; the body adds what the diff cannot. + +- **≤ 5 lines** of body (excluding subject, blank line, `BC:` footer, `Refs:`). If you need more, the commit is doing too much — split. +- **Each line ≤ 72 chars.** +- **State what changed**, not the journey to get there. Past tense or imperative — match the subject's mood. +- **Bullet list preferred** for 2+ changes. Single sentence when one change is enough. +- **Omit:** narrative explaining how you arrived at the fix, alternative approaches considered, debugging steps, restatement of the subject. +- **Include only when it adds non-obvious value:** + - Why this approach over an alternative (1 short clause). + - Side effects on related code paths a reviewer would otherwise miss. + - References to issues, prior commits, or specs that constrain the change. + +If the body would just paraphrase the subject, **leave it empty** — go straight to the `BC:` footer. + +### Type + +| Type | Use for | +|---|---| +| `feat` | New user-facing feature or new public API symbol | +| `fix` | Bug fix | +| `refactor` | Internal change, no behavior delta, no public surface change | +| `perf` | Performance improvement, no behavior delta | +| `chore` | Maintenance — line endings, formatting, deps housekeeping | +| `docs` | Docs-only (AGENTS.md, `docs/spec-*.md`, inline `@param` blocks) | +| `style` | Code formatting only (no logic, no behavior) | +| `test` | Test additions or fixes | +| `build` | Build/config changes (`webpack.config.js`, `package.json` deps, Gruntfile) | +| `ci` | CI pipeline changes (`.github/`, workflow files) | +| `revert` | Reverts a previous commit (subject = `revert: `) | + +### Scope + +OnePress-specific scopes — pick the **narrowest** one that fits: + +| Scope | Use for | +|---|---| +| `hero` / `about` / `services` / `gallery` / `counter` / `team` / `news` / `contact` / `videolightbox` / `features` | A specific front-page section (markup, settings, partial) | +| `sections` | Section loop machinery (`Onepress_Config`, frontpage template, `onepress_load_section`) | +| `customizer` | Customizer registration, panels, sections, settings (cross-cutting) | +| `controls` | Custom Customizer controls (`OnePress_*_Control`) | +| `sanitize` | `inc/sanitize.php` — sanitizers/validators | +| `dashboard` | Admin info page (`Onepress_Dashboard`, recommended actions) | +| `metabox` | Page meta box (`OnePress_MetaBox`, `_hide_*` keys) | +| `editor` | Block editor integration (`OnePress_Editor`, editor styles) | +| `wc` | WooCommerce integration (`woocommerce.php`, shop sidebar, WC gating) | +| `header` / `footer` | Site header / footer templates and helpers | +| `nav` | Primary menu, sticky header, sections-navigation dots | +| `blog` | Blog/home/archive/single post templates and loop | +| `palette` / `colors` | Color settings, alpha controls, CSS color variables | +| `typography` / `fonts` | Google Fonts, font loading, font disable | +| `gallery-lib` | `src/frontend/libs/gallery/*` | +| `lightgallery` | Lightgallery integration | +| `parallax` | Jarallax/backstretch parallax effects | +| `i18n` | Translations, `.pot` generation, WPML config | +| `rtl` | RTL-specific changes | +| `a11y` | Accessibility (ARIA, skip links, focus management) | +| `build` | Webpack, npm scripts, Gruntfile | +| `assets` | Built artifacts — rarely commit alone, see [build-artifact rule](#build-artifact-rule) | +| `deps` | Bump npm dependencies | +| `docs` | AGENTS.md, `docs/spec-*.md`, changelog | +| `release` | Version bump + changelog (paired with a release tag) | + +### Examples + +**Additive feature** — body lists what was added, defaults note: + +``` +feat(hero): wire title color to Customizer color var + +- New theme mod `onepress_hero_title_color`, default null (inherits palette). +- Per-slide override via existing slides repeater `title_color` row key. + +BC: none — additive theme mod + additive row key, defaults preserve old look +``` + +**Bug fix** — one-line body is enough: + +``` +fix(hero): autoplay not triggering when slider has only one slide + +Removed the `count(images) === 1` early-return in section-hero.php. + +BC: none — no public surface changed +``` + +**Internal refactor** — body lists the new symbol + the legacy fallback: + +``` +refactor(sections): introduce onepress_get_section_data helper + +- Added `onepress_get_section_data()` with cleaner signature. +- Old `onepress_get_section_args()` now delegates to it; signature unchanged. + +BC: deprecation — old helper still functional, marked @deprecated since 2.4 +``` + +**Chore** — body empty, just the BC footer: + +``` +chore(build): bump @wordpress/scripts to 30.20.1 + +BC: none — patch bump, no API change +``` + +**Docs-only** — body empty: + +``` +docs: split AGENTS.md into spec-* files under docs/ + +BC: none — docs only +``` + +**Breaking change** (extremely rare, major-version only): + +``` +feat(image-sizes)!: drop onepress-blog-small after 2-major deprecation + +- Removed `onepress-blog-small` (300×150) image size, deprecated since 2.4. +- Migration in `inc/migrations/3-0-0.php` regenerates affected thumbnails. + +BC: breaking — removes deprecated image size; migration provided +Refs: #842 +``` + +### Grep recipes + +```bash +git log --grep="^BC: breaking" # All breaking changes ever shipped +git log --grep="^BC: deprecation" # Audit deprecation history +git log --grep="^feat(hero)" # Everything that touched the hero section +git log --grep="^.*!:" # All commits flagged with the ! marker +``` + +## Build-artifact rule + +`assets/` is build output. If `assets/` changes, the matching `src/` change must be in the **same commit** (or land first). Never commit a stale `assets/` against newer `src/`, and never commit `assets/`-only changes without their source origin. + +## Release commits + +For user-visible releases, bundle these in one commit: + +1. Bump `Version` in [../style.css](../style.css). +2. Bump `version` in [../package.json](../package.json). +3. Add an entry to [../changelog.md](../changelog.md). +4. Include the freshly built `assets/` (from `npm run build`). + +## Don't include + +- `.env`, `.env.*` (secrets) +- `node_modules/` +- `.DS_Store`, `Thumbs.db` +- `*.zip` build artifacts (e.g. `onepress-2.3.18.zip` in the theme root — that's a packaging artifact, not source) +- Any file you can't explain in a sentence + +## The `BC:` footer (mandatory) + +OnePress has **60,000+ active installs**. Every commit must end with a `BC:` footer line in the body, stating the backward-compatibility impact in one phrase. + +### Format + +``` +BC: +``` + +### Categories + +| Tag | When to use | Example | +|---|---|---| +| `BC: none` | 100% safe — no user, child theme, or integrator can observe the change. Pure refactor, additive helper, comment fix, build/deps housekeeping, docs. | `BC: none — additive theme mod, defaults preserve old look`
`BC: none — internal refactor, all old symbols preserved`
`BC: none — comment-only change` | +| `BC: visual` | Changes default rendered output for **fresh installs**, with a back-compat shim that keeps existing sites looking the same. | `BC: visual — new fresh-install default; sites with saved settings keep old look via version-gated default` | +| `BC: deprecation` | Adds a deprecation warning, but old code path still works indefinitely (or until a documented future major). | `BC: deprecation — onepress_old_filter still fires via apply_filters_deprecated()`
`BC: deprecation — old theme mod key still read as fallback` | +| `BC: breaking` | Removes or repurposes a public symbol. Allowed **only** in a major-version commit that satisfies the 5 conditions in [spec-conventions.md → When you genuinely must remove](spec-conventions.md#when-you-genuinely-must-remove). Must include `!` in the subject and a migration reference. | `BC: breaking — removes onepress_legacy_helper deprecated since 2.0; migration in inc/migrations/3-0-0.php` | + +### Why this is mandatory + +1. **Forces analysis.** If you can't write the `BC:` line, you haven't thought through the change — go re-read [spec-conventions.md → Backward Compatibility](spec-conventions.md#backward-compatibility). +2. **Reviewer signal.** One line tells a reviewer the risk envelope before they read the diff. +3. **Greppable history.** `git log --grep="^BC: breaking"` produces the full list of every breaking change ever shipped. +4. **Required for `must remove` sign-off.** Condition #5 of the removal contract is a `BC: breaking` line referencing the migration. + +If you can't state BC impact, **do not commit** — go read [spec-conventions.md → Backward Compatibility](spec-conventions.md#backward-compatibility) first. + +## Pre-commit checklist + +Before pressing commit: + +- [ ] **Commit body ≤ 5 lines** (excluding subject, blank line, `BC:` footer, `Refs:`); each line ≤ 72 chars; bullets preferred for 2+ changes; empty body OK when subject is self-explanatory (see [Body discipline](#body-discipline)) +- [ ] **BC impact analyzed and stated in the commit body** (see above) +- [ ] **No deletion of any existing public PHP function, class, method, template file, hook, theme mod, option, post meta, image size, or CSS class** — old code stays as a fallback (see [spec-conventions.md → Additive-only mandate](spec-conventions.md#additive-only-mandate)); a removal, if truly necessary, requires the 5 conditions in "When you genuinely must remove" +- [ ] No rename of public API names — additive only (rename = remove + add) +- [ ] No silent default-value change that alters rendered output +- [ ] Diff was reviewed with `git diff --stat` and red (`-`) lines on shipped files are justified (formatting / dead local var / etc., never a public symbol) +- [ ] `npm run lint:js` clean (if JS changed) +- [ ] `npm run lint:css` clean (if CSS/SCSS changed) +- [ ] `npm run build` run and `assets/` is in the staged set (if `src/` changed) +- [ ] Line endings audit clean (see [spec-line-endings.md](spec-line-endings.md)) +- [ ] No `wp-includes/` or `wp-admin/` edits in the diff (core is off-limits) +- [ ] Commit subject in English, prefixed, ≤ 72 chars diff --git a/docs/spec-conventions.md b/docs/spec-conventions.md new file mode 100644 index 00000000..2d5b64be --- /dev/null +++ b/docs/spec-conventions.md @@ -0,0 +1,328 @@ +# spec-conventions — Coding Conventions & Gotchas + +Cross-cutting rules. Each one has bitten someone before. + +## Sanitize input, escape output + +- All Customizer settings **must** declare `sanitize_callback` — see helper list in [spec-customizer.md](spec-customizer.md#sanitizers). +- Output uses: + - `esc_html()` for text + - `esc_attr()` for HTML attributes + - `esc_url()` for URLs + - `wp_kses( $val, onepress_allowed_tags() )` for limited inline HTML (`div`, `span`, `p`, `b`, `i`, `em`, `a`) + - `wp_kses_post()` for richer content (post-content allow-list) +- The repeater control hands raw rows to PHP — sanitize through `onepress_sanitize_repeatable_data_field()` (SVG-aware, see [../inc/sanitize.php](../inc/sanitize.php)). + +## i18n + +- Text domain: **`onepress`**. +- Translation files live in [../languages/](../languages/) (POT + locale files). +- WPML config: [../wpml-config.xml](../wpml-config.xml). +- Don't introduce strings without `__()` / `esc_html__()` / `_x()`. +- Plurals: `_n()`, `_nx()`. +- For machine-readable headers (font subsets, etc.) the codebase uses `_x( 'on', 'Open Sans font: on or off', 'onepress' )` so translators can disable a Google Font for unsupported scripts — keep this pattern. + +## RTL + +- Generated automatically by `rtlcss-webpack-plugin` (`*-rtl.css`, `*.minified-rtl.css`). +- **Do not hand-author RTL CSS** — it will be overwritten on next build. +- `body` gets `is_rtl` exposed to JS via `onepress_js_settings`. + +## Customizer preview gating + +Every `section-parts/section-*.php` short-circuits on `onepress_*_disable`. **Always** un-disable in the preview so editors can re-enable hidden sections: + +```php +if ( onepress_is_selective_refresh() ) { + $disable = false; +} +``` + +See [`onepress_is_selective_refresh()`](../inc/template-tags.php) ~line 1686. + +## WooCommerce gating + +- Use [`onepress_is_wc_active()`](../inc/extras.php) (~line 161) for "is WC active?" checks. +- Use `onepress_is_wc_archive()` for "are we on a WC archive?". +- The shop layout is resolved by [`onepress_get_layout()`](../inc/extras.php) (~line 189) — overridable via the `onepress_get_layout` filter. +- The shop page respects the page meta `_hide_breadcrumb` and `_hide_footer` ([../woocommerce.php](../woocommerce.php), [../footer.php](../footer.php)). +- **Never call WC functions unconditionally** — wrap them in `if ( onepress_is_wc_active() ) { … }`. + +## Plus integration + +- Detect Plus via `defined('ONEPRESS_PLUS_PATH')` (path constant) and `class_exists('OnePress_Plus')` (runtime). +- Plus contributes: + - Extra front-page sections (see [spec-sections.md](spec-sections.md#plus-only-sections)). + - Selective-refresh template parts (fallback lookup in [../inc/customizer-selective-refresh.php](../inc/customizer-selective-refresh.php)). + - Typography render (`onepress_typography_render_style()`). + - Extra gallery script load ([../functions.php](../functions.php) ~line 396). +- Use `onepress_add_upsell_for_section()` to add a "Get Plus" message inside a Customizer section. + +## Backward Compatibility + +> **OnePress has 60,000+ active installs on WordPress.org.** Every change ships to live customer sites with no staged rollout. The single most important rule of this codebase: **assume any name, key, default, hook, class, or CSS selector is depended on by someone**, and design every change around that assumption. + +### Additive-only mandate + +**The single hard rule: never delete or remove anything that has shipped.** Add new code alongside the old. The old code path must keep working with its original behavior. + +This rule applies to: + +- PHP functions (including helpers inside `if ( ! function_exists() )` blocks) +- PHP classes and methods +- Template files (`page.php`, `template-*.php`, `section-parts/section-*.php`, `template-parts/*.php`, `woocommerce.php`, `header.php`, `footer.php`, etc.) +- Customizer settings, panels, sections, controls +- Theme mods (`onepress_*`) +- Options (`onepress_*`) +- Post meta keys (`_hide_*`) +- Action and filter hooks +- Image size slugs +- Menu locations, sidebar IDs, widget areas +- CSS class names emitted by shipped templates +- JS globals (`onepress_js_settings` keys), localized objects +- Recommended plugin slugs + +**Why this matters for a 60k-install theme:** removed code = silent fatal errors on customer sites if a child theme called the function, dead Customizer settings that orphan saved data, vanished hooks that child themes had hooked into, broken CSS overrides in user "Additional CSS", and broken JS in third-party integrations. None of these surface in any test we can run before release. + +### The four patterns for "improving" without removing + +When you'd normally reach for delete/rename/refactor, use one of these instead: + +**Pattern 1 — New helper, old delegates to new** (preferred for refactors) + +```php +// New, improved implementation. +if ( ! function_exists( 'onepress_get_logo_v2' ) ) { + function onepress_get_logo_v2( $args = array() ) { + // …new logic… + } +} + +// Old helper stays. Internally calls the new one with legacy arg shape. +if ( ! function_exists( 'onepress_get_logo' ) ) { + function onepress_get_logo() { + return onepress_get_logo_v2( array( 'mode' => 'legacy' ) ); + } +} +``` + +**Pattern 2 — Old helper untouched, new helper for new callers** (preferred when old behavior is awkward to reproduce) + +Leave the old function exactly as-is. Document the new function as the recommended path going forward. Old callers (child themes, the rest of the theme) keep using the old one until they're individually migrated. + +**Pattern 3 — Read both keys** (for theme mods / options / post meta) + +```php +function onepress_get_hero_title() { + // Prefer new canonical key. + $val = get_theme_mod( 'onepress_hero_title', null ); + if ( $val !== null && $val !== '' ) { + return $val; + } + // Fall back to legacy key — kept readable forever. + return get_theme_mod( 'onepress_header_title', '' ); +} +``` + +Never delete the old `get_theme_mod()` read. The legacy key remains in user databases. + +**Pattern 4 — Fire both hooks** (for renamed actions/filters) + +```php +// New hook name for new integrators. +$value = apply_filters( 'onepress_section_data', $value, $section_id ); + +// Old hook still fires so existing child themes keep working. +$value = apply_filters_deprecated( + 'onepress_section_args', + array( $value, $section_id ), + '2.4.0', + 'onepress_section_data' +); +``` + +### What "additive" forbids in practice + +| ❌ Don't | ✅ Do instead | +|---|---| +| `unlink( section-parts/section-foo.php )` | Leave the file; if the section is gone from defaults, just stop registering it (file still loadable by child themes via `onepress_load_section`) | +| Delete `onepress_old_helper()` | Leave it. If unused inside the theme, it's still callable by child themes. | +| Rename `onepress_setup` → `onepress_init` | Add `onepress_init` that calls `onepress_setup`. Keep `onepress_setup` hooked to `after_setup_theme`. | +| Drop the `onepress-blog-small` image size | Keep registering it. Stop using it in new templates if you want, but old galleries/thumbnails depend on the slug. | +| Remove a Customizer control | Re-register it (even if hidden via `active_callback`) so saved data still validates and the setting stays readable. | +| Change a default from `'fadeIn'` to `'slideUp'` | Add a new mod `onepress_*_animation_v2` with the new default; old mod keeps its old default. | +| Delete a CSS class from a template | Keep emitting it alongside the new class (`
`). | +| Remove a key from `onepress_js_settings` | Keep the key; add new ones additively. | + +### When you genuinely must remove + +Removals are a **last resort** and only happen in major-version releases with: + +1. A **deprecation period of at least one prior major release** where the symbol is marked deprecated but still functional (uses `_deprecated_function()`, `apply_filters_deprecated()`, etc.). +2. A **migration shim** that backfills new keys from old values (see "Migrations" below). +3. An explicit **changelog entry** under "Breaking changes" naming every removed symbol. +4. A **major-version bump** of the theme (`X.0.0`, not `X.Y.Z`). +5. Sign-off documented in the commit message (`BC: breaking — removes deprecated …, migration in …`). + +If any of these five conditions cannot be met, **the removal does not ship**. Add new behavior alongside the old instead. + +### Naming is API + +Every name you ship — function, class, hook, theme mod, CSS class, image size slug — becomes part of the public API and falls under the additive-only mandate. Pick the right name on first ship; once shipped, it's frozen. + +Full conventions, prefix matrix, and the list of known frozen inconsistencies: [spec-naming.md](spec-naming.md). + +### What counts as public API + +All of the following are **stable public API** — renaming or removing any of them is a **breaking change** that requires a major-version bump and a migration. Treat them as immutable in normal day-to-day work: + +| Surface | Examples | +|---|---| +| Theme mod keys | `onepress_hero_disable`, `onepress_gallery_display`, `onepress_disable_g_font`, every `onepress_*` setting | +| Section IDs | `hero`, `about`, `services`, `videolightbox`, `gallery`, `counter`, `features`, `team`, `news`, `contact` | +| Option keys | `onepress_sections_settings`, `onepress_actions_dismiss`, `onepress_dismiss_switch_theme_notice` | +| Post meta keys | `_hide_page_title`, `_hide_header`, `_hide_footer`, `_hide_breadcrumb` | +| Action names | `onepress_header_end`, `onepress_before_section_{id}`, `onepress_frontpage_section_parts`, etc. | +| Filter names | `onepress_get_sections`, `onepress_frontpage_sections_order`, `onepress_js_settings`, `onepress_get_layout`, etc. | +| Public function names | `onepress_*` helpers (any function defined inside `if ( ! function_exists( 'onepress_*' ) )` is **explicitly pluggable** — child themes may have redefined it) | +| Class names | `Onepress_Config`, `Onepress_Dashboard`, `OnePress_MetaBox`, `OnePress_Editor`, all `OnePress_*_Control` | +| Image size slugs | `onepress-blog-small`, `onepress-small`, `onepress-medium` — renaming orphans every thumbnail generated on user installs | +| CSS class names in templates | `.site-header`, `.hero-section`, `.section-{id}`, `.footer-social`, etc. — users target these in Additional CSS | +| Customizer panel/section IDs | Used by `wp_customize->get_section('…')` from child themes and Plus | +| Recommended plugin slugs | Surfaced in dashboard; changing the slug breaks "already installed?" detection | + +### Default values are also API + +A default that has shipped is part of the visual contract: + +```php +// Wrong: silently changes hero animation for every existing site +get_theme_mod( 'onepress_hero_option_animation', 'fadeIn' ); +// Was: 'flipInX' + +// Right: keep the historical default; add a new mod if you want a new default for fresh installs +get_theme_mod( 'onepress_hero_option_animation', 'flipInX' ); +``` + +If you genuinely need to change a default, gate it on a version marker so existing sites keep the old value: + +```php +function onepress_hero_animation_default() { + // Sites that existed before 2.4 keep the old default. + if ( get_option( 'onepress_installed_before_2_4' ) ) { + return 'flipInX'; + } + return 'fadeIn'; +} +``` + +### Pluggable functions + +Any function wrapped in `if ( ! function_exists( 'onepress_*' ) )` is a **pluggable** function — child themes are expected to redefine it. Treat its **signature** (name, parameter list, return type, side effects) as a contract. You can refactor the body, but you cannot change the signature without breaking child themes that override it. + +Audit for pluggable surface: + +```bash +grep -rn "if ( ! function_exists( 'onepress_" inc/ functions.php +``` + +### Deprecation pattern + +When you genuinely need to retire something, deprecate first, remove in a **far-future** major (2+ majors away): + +**For a theme mod / option** — read both keys for at least one major cycle: + +```php +function onepress_get_hero_title() { + // New canonical key. + $val = get_theme_mod( 'onepress_hero_title' ); + if ( $val !== false && $val !== '' ) { + return $val; + } + // Legacy key — still honored. + return get_theme_mod( 'onepress_header_title', '' ); +} +``` + +**For a function:** + +```php +function onepress_old_helper( $arg ) { + _deprecated_function( __FUNCTION__, '2.4.0', 'onepress_new_helper()' ); + return onepress_new_helper( $arg ); +} +``` + +**For a filter / action:** + +```php +$value = apply_filters_deprecated( + 'onepress_old_filter', + array( $value ), + '2.4.0', + 'onepress_new_filter' +); +``` + +**For a CSS class:** keep emitting both the old and new class on the same element for at least one major. + +### Migrations + +If a rename is truly unavoidable, ship a one-time migration: + +```php +function onepress_migrate_2_4() { + if ( get_option( 'onepress_migrated_2_4' ) ) { + return; + } + $old = get_theme_mod( 'onepress_old_key' ); + if ( $old !== false ) { + set_theme_mod( 'onepress_new_key', $old ); + remove_theme_mod( 'onepress_old_key' ); + } + update_option( 'onepress_migrated_2_4', 1 ); +} +add_action( 'after_setup_theme', 'onepress_migrate_2_4', 20 ); +``` + +Migrations must be **idempotent** and **safe to run on a site that never had the old value**. + +### Mandatory BC check on every change + +Before opening a PR or pushing a commit, answer these: + +1. **Did I delete any line that defines a public symbol** (function, class, method, hook, theme mod, option, template file, CSS class, image size, JS global key)? → **Stop. Restore it. Use Pattern 1–4 above.** +2. Did I rename anything from the public-API table below? → **Stop. Rename = remove + add. Add the new name, keep the old as a delegating shim.** +3. Did I change a default that affects rendered output? → **Stop. Gate it on a version marker, or add a new setting.** +4. Did I change a pluggable function's signature? → **Stop. Add a new function; leave the old one with its original signature.** +5. Did I change HTML structure or CSS class names in shipped templates? → Keep the old classes as aliases on the same elements. +6. Did I bump the JS interface (`onepress_js_settings` shape, etc.)? → Add fields additively, never remove or repurpose. + +State the BC impact explicitly in the commit message — see [spec-commits.md](spec-commits.md). + +## WordPress Studio environment + +This site runs via WordPress Studio (PHP WASM + SQLite). When acting on the install via CLI: + +- Prefix `wp` commands with `studio wp` (see [../../../../STUDIO.md](../../../../STUDIO.md)). +- DB is SQLite — never reference `DB_NAME` / `DB_HOST` / `DB_USER` / `DB_PASSWORD` constants. +- No `FULLTEXT` indexes (use a search plugin or `LIKE`). +- Don't edit `wp-includes/` or `wp-admin/` — core changes don't persist. + +## Never edit `assets/` + +[../assets/](../assets/) is build output. Edit `src/`, run `npm run build`, commit both together. See [spec-build.md](spec-build.md) and [spec-commits.md](spec-commits.md). + +## Never edit `node_modules/` + +Even if a dep ships CRLF or has a bug — leave it. The webpack normalizer handles line endings. For bugs, patch via the build (alias, swizzle) or fork upstream. + +## Release checklist + +For user-visible releases: + +1. Bump `Version` in [../style.css](../style.css). +2. Bump `version` in [../package.json](../package.json). +3. Add an entry to [../changelog.md](../changelog.md). +4. Run `npm run build`. +5. Commit `src/`, `assets/`, version bumps, and changelog **in one commit** (see [spec-commits.md](spec-commits.md)). diff --git a/docs/spec-customizer.md b/docs/spec-customizer.md new file mode 100644 index 00000000..f332a597 --- /dev/null +++ b/docs/spec-customizer.md @@ -0,0 +1,141 @@ +# spec-customizer — Customizer, Theme Supports, Sidebars + +Settings live in the WordPress Customizer (no separate options page outside the dashboard form). Everything is a `theme_mod` with the `onepress_` prefix. + +## Registration + +Entrypoint: [`onepress_customize_register()`](../inc/customizer.php) — hooked on `customize_register`. It: + +1. Loads custom control classes via [../inc/customizer-controls.php](../inc/customizer-controls.php). +2. Switches `blogname`, `blogdescription`, `header_textcolor` to `postMessage` transport. +3. Fires `onepress_customize_before_register` action. +4. Loads each global option file under [../inc/customize-configs/](../inc/customize-configs/): `site-identity`, `options`, `options-global`, `options-colors`, `options-header`, `options-navigation`, `options-sections-navigation`, `options-page`, `options-blog-posts`, `options-single`, `options-footer`. +5. For each active section ([`Onepress_Config::is_section_active()`](../inc/class-config.php)), loads `inc/customize-configs/section-{id}.php`. +6. Loads `section-upsell.php` (Plus marketing). +7. Fires `onepress_customize_after_register` action. +8. If WooCommerce is active, bumps the WC panel priority to 300 (sits at the bottom). + +## Reading settings + +All theme options use `get_theme_mod( 'onepress_*' )`: + +```php +$disable = get_theme_mod( 'onepress_hero_disable' ); +$style = get_theme_mod( 'onepress_gallery_display', 'grid' ); // with default +``` + +## Custom controls + +Located in [../inc/customize-controls/](../inc/customize-controls/): + +| Class | File | Use case | +|---|---|---| +| `OnePress_Alpha_Color_Control` | `control-color-alpha.php` | Color picker with alpha channel | +| `OnePress_Category_Control` | `control-category.php` | Category checkboxes (for news section) | +| `OnePress_Editor_Custom_Control` | `control-editor.php` | TinyMCE / wp.editor field | +| `OnePress_Media_Control` | `control-media.php` | Image/video media picker | +| `OnePress_Media_Url_Control` | `control-media.php` | Media picker returning URL only | +| `OnePress_Misc_Control` | `control-misc.php` | Custom HTML messages, upsells, dividers | +| `OnePress_Pages_Control` | `control-pages.php` | Page selector | +| `Onepress_Customize_Repeatable_Control` | `control-repeater.php` | Repeatable rows (team members, features, slides) — SVG/icon-aware | +| `OnePress_Theme_Support` | `control-theme-support.php` | Read-only "feature available?" notice | +| `One_Press_Textarea_Custom_Control` | `control-custom-textarea.php` | Multi-line textarea | +| `OnePress_Section_Plus` | `section-plus.php` | Customizer **section** subclass that shows a Plus upsell badge | + +Plus a helper [`onepress_add_upsell_for_section($wp_customize, $section_id)`](../inc/customizer.php) that injects a "Upgrade to Plus" message at the bottom of a section. Suppress globally with filter `onepress_add_upsell_for_section`. + +## Sanitizers + +All settings must declare `sanitize_callback`. Common helpers (in [../inc/sanitize.php](../inc/sanitize.php)): + +| Function | For | +|---|---| +| `onepress_sanitize_text` | Plain text | +| `onepress_sanitize_html_input` | Limited inline HTML | +| `onepress_sanitize_checkbox` | Checkbox (0/1) | +| `onepress_sanitize_select` | Select (validates against control choices) | +| `onepress_sanitize_number` | Numeric | +| `onepress_sanitize_hex_color` | `#rrggbb` | +| `onepress_sanitize_color_alpha` | `rgba()`/`hsla()` with alpha | +| `onepress_sanitize_css_color` | Any safe CSS `` (incl. `var()`, modern color funcs) — XSS-hardened | +| `onepress_sanitize_css` | CSS code blob | +| `onepress_sanitize_image` | Attachment URL/ID | +| `onepress_sanitize_file_url` | File URL | +| `onepress_sanitize_repeatable_data_field` | Rows from repeater control (handles SVG icons) | +| `onepress_sanitize_news_layout` / `onepress_sanitize_news_grid_columns` | News section layout/columns | + +Validators (`validate_callback`): + +- `onepress_hero_fullscreen_callback` +- `onepress_gallery_source_validate` + +## Selective refresh + +[../inc/customizer-selective-refresh.php](../inc/customizer-selective-refresh.php) registers partials so the preview re-renders without a full reload. Two things to remember: + +1. In any section template, wrap "is this disabled?" checks with: + + ```php + if ( onepress_is_selective_refresh() ) { + $disable = false; + } + ``` + +2. Template-part lookup falls back to `ONEPRESS_PLUS_PATH` if the template isn't in the theme (Plus integration). + +Live preview JS: `src/admin/customizer-liveview.js` → `assets/admin/customizer-liveview.js`, enqueued by `onepress_customize_preview_js()` with deps `customize-preview`, `customize-selective-refresh`. + +Customizer-only controls JS: `src/admin/customizer.js` + `src/admin/customizer/*` (alpha color, repeater, icon picker, modal editor, plus-section upsell). + +## Icon picker + +`customize_controls_enqueue_scripts` localizes `C_Icon_Picker` with FontAwesome 6 metadata read from [../inc/list-icon-v6.php](../inc/list-icon-v6.php). Filter `c_icon_picker_js_setup` to add fonts or change behavior. + +--- + +## Theme supports + +Declared in [`onepress_setup()`](../functions.php): + +- `title-tag` +- `post-thumbnails` +- `automatic-feed-links` +- `html5` (search-form, comment-form, comment-list, gallery, caption) +- `custom-logo` (flex, 160×36) +- `customize-selective-refresh-widgets` +- `editor-styles`, `align-wide`, `wp-block-styles` +- `woocommerce` + `wc-product-gallery-zoom`, `wc-product-gallery-lightbox`, `wc-product-gallery-slider` +- `recommend-plugins` (see below) +- `post-type-support('page', 'excerpt')` — adds excerpt box on pages + +## Recommended plugins + +Surfaced on the admin dashboard: + +- `wpforms-lite` +- `famethemes-demo-importer` +- When WooCommerce is active: `currency-switcher-for-woocommerce`, `bulk-edit-for-woocommerce` + +## Image sizes + +Registered in [`onepress_setup()`](../functions.php): + +- `onepress-blog-small` — 300×150 cropped +- `onepress-small` — 480×300 cropped +- `onepress-medium` — 640×400 cropped + +## Menu locations + +- `primary` + +## Sidebars + +Registered in [`onepress_widgets_init()`](../functions.php): + +- `sidebar-1` — main sidebar +- `sidebar-shop` — only when WooCommerce is active +- `footer-1` … `footer-4` — footer columns + +## Dynamic `$content_width` + +Default 800, driven by theme mod `single_layout_content_width`. Filter with `onepress_content_width`. diff --git a/docs/spec-hooks.md b/docs/spec-hooks.md new file mode 100644 index 00000000..3deabb53 --- /dev/null +++ b/docs/spec-hooks.md @@ -0,0 +1,77 @@ +# spec-hooks — Actions, Filters, Loop Props + +Use this as the canonical hook reference for child themes and integrations. + +## Custom actions + +| Action | Where it fires | +|---|---| +| `onepress_before_site_start` / `onepress_after_site_end` | Outside `#page` in [../header.php](../header.php) / [../footer.php](../footer.php) | +| `onepress_header_end` | After the site header; default callback `onepress_load_hero_section` loads the hero | +| `onepress_frontpage_before_section_parts` | Before the front-page section loop | +| `onepress_frontpage_section_parts` | **Replaces** the default loop if anything hooks it | +| `onepress_frontpage_after_section_parts` | After the front-page section loop | +| `onepress_before_section_{id}` / `onepress_after_section_{id}` | Around a specific section | +| `onepress_before_section_part` / `onepress_after_section_part` | Around any non-hero section | +| `onepress_page_before_content` | Before page/WC content; default callback prints page title via `onepress_display_page_title` | +| `onepress_before_site_info` | Before footer site-info block | +| `onepress_footer_site_info` | Inside footer site-info block; renders the credit line | +| `onepress_site_end` | At the very end of `#page` | +| `onepress_customize_before_register` / `onepress_customize_after_register` | Customizer wiring extension points | + +## Custom filters + +| Filter | Purpose | +|---|---| +| `onepress_frontpage_sections_order` | Reorder or restrict front-page sections | +| `onepress_get_sections` | Add/remove section definitions | +| `onepress_sections_navigation_get_sections` | Add/remove dots-nav entries | +| `onepress_content_width` | Override the dynamic `$content_width` | +| `onepress_js_settings` | Extend the localized `onepress_js_settings` object | +| `onepress_loop_get_prop` | Intercept loop-scoped properties (excerpt type/length, etc.) | +| `onepress_get_layout` | Override page/WC layout (`right-sidebar`, `left-sidebar`, `no-sidebar`, `fullwidth`) | +| `c_icon_picker_js_setup` | Customize icon-picker fonts/icons | +| `onepress_add_upsell_for_section` | Suppress Plus upsell rows per section | + +## Loop properties + +Use these to pass per-loop / per-section render hints without polluting globals: + +- [`onepress_loop_set_prop( $prop, $value )`](../functions.php) — ~line 525 +- [`onepress_loop_get_prop( $prop, $default = false )`](../functions.php) — ~line 542 +- `onepress_loop_remove_prop( $prop )` + +Common props read by partials: + +- `excerpt_type` — `excerpt` / `more_tag` / `content` / `''` (uses post excerpt or trimmed content) +- `excerpt_length` — integer word count + +Set in section templates before calling `the_post()` / template parts, then remove after the loop. + +## Hook recipes + +**Replace the default front-page loop entirely:** + +```php +add_action( 'onepress_frontpage_section_parts', function () { + onepress_load_section( 'hero' ); + onepress_load_section( 'about' ); + onepress_load_section( 'contact' ); +} ); +``` + +**Inject extra markup after the hero:** + +```php +add_action( 'onepress_after_section_hero', function () { + echo '
'; +} ); +``` + +**Force a specific layout on the shop page:** + +```php +add_filter( 'onepress_get_layout', function ( $layout ) { + return is_shop() ? 'no-sidebar' : $layout; +} ); +``` diff --git a/docs/spec-line-endings.md b/docs/spec-line-endings.md new file mode 100644 index 00000000..f52e6b74 --- /dev/null +++ b/docs/spec-line-endings.md @@ -0,0 +1,85 @@ +# spec-line-endings — LF-only Policy & Audit Playbook + +The project standard is **LF (`\n`) only**. CRLF, or mixed CR/LF + LF in the same file, is forbidden because it: + +- Breaks WordPress.org SVN submission. +- Poisons webpack bundles — webpack preserves source line endings, so a single CRLF-shipping dependency contaminates the whole emitted bundle. +- Makes git diffs unreadable and noisy. + +## Where the rule is enforced + +| Layer | Mechanism | +|---|---| +| Editor / IDE | [`.editorconfig`](../.editorconfig) declares `end_of_line = lf`. | +| Build output (`assets/`) | [`webpack.config.js`](../webpack.config.js) installs `normalizeLineEndingsPlugin` (search `NormalizeLineEndingsPlugin`) — every emitted `.js` asset is rewritten to LF before being written to disk. This is the only defense against CRLF-shipping npm dependencies (e.g. `@dnd-kit/utilities`). **Do not remove it.** | +| Source tree (`src/`, PHP, configs) | Manual audit + the playbook below. | + +## Audit & normalize playbook + +Run from the theme root (`wp-content/themes/onepress/`). + +### 1. Audit the working tree + +Skip generated/vendor dirs: + +```bash +find . -type f \ + -not -path "./node_modules/*" \ + -not -path "./vendor/*" \ + -not -path "./assets/*" \ + -not -path "./release-staging/*" \ + -not -path "./languages/*" \ + -not -path "./.git/*" \ + -exec file {} \; | grep -iE "crlf|cr line" +``` + +Empty output ⇒ working tree is clean, **stop**. + +> Why skip `assets/`: it is regenerated by webpack with the LF normalizer; fixing source then rebuilding is the supported path. +> Why skip `languages/`: contains binary `.mo` files and machine-generated `.pot` that should not be hand-touched here. + +### 2. Normalize each flagged file in place + +Use Perl (portable across macOS / Linux): + +```bash +perl -i -pe 's/\r\n/\n/g' path/to/file +``` + +**Do not use `sed -i`** — its `-i` flag behaves differently on macOS vs Linux and silently corrupts files. + +### 3. Rebuild if `src/` was touched + +If any flagged file lives under `src/`, rebuild so `assets/` regenerates from clean source: + +```bash +npm run build +``` + +### 4. Confirm `assets/` is also clean + +```bash +find assets -type f \( -name "*.js" -o -name "*.css" \ + -o -name "*.map" -o -name "*.php" \) \ + -exec file {} \; | grep -iE "crlf|cr line" +``` + +Must return empty. If it doesn't, `normalizeLineEndingsPlugin` has been removed or disabled — restore it from [../webpack.config.js](../webpack.config.js). + +### 5. Stage only affected files by name + +Never `git add -A` — the project may have unrelated work-in-progress. + +```bash +git add +git commit -m "chore: normalize CRLF -> LF in " +``` + +Commit conventions: see [spec-commits.md](spec-commits.md). + +## Rules + +- **Never edit files in `assets/` directly.** Fix the source under `src/` and rebuild. +- **Never modify files inside `node_modules/`.** Upstream-shipped CRLF is handled at bundle time by the webpack plugin; leave it alone. +- **Never disable `normalizeLineEndingsPlugin`** without a documented replacement. +- **Never run the audit with `sed -i`** — use `perl -i -pe`. diff --git a/docs/spec-naming.md b/docs/spec-naming.md new file mode 100644 index 00000000..2071e573 --- /dev/null +++ b/docs/spec-naming.md @@ -0,0 +1,429 @@ +# spec-naming — Naming Conventions + +Naming rules for PHP, JS, CSS/SCSS, and WordPress slugs in OnePress. + +## Why naming is API + +> **Every name that ships becomes part of the public API.** Per the [additive-only mandate](spec-conventions.md#additive-only-mandate), names cannot be renamed or removed without a major-version migration. Pick names carefully on first ship; once shipped, they're frozen. + +This spec exists so: + +1. New code uses **consistent** names so child themes and integrators can predict the shape of new APIs. +2. Existing **inconsistencies** (frozen by additive-only) are documented so nobody "fixes" them and silently breaks customer sites. +3. Reviewers can flag naming drift in PR review against an explicit reference. + +--- + +## PHP + +### Functions + +- `snake_case`. +- **Always** prefixed `onepress_` for anything that lives at file scope (no namespace, no class). +- Imperative verb-first (`get_`, `set_`, `is_`, `has_`, `render_`, `enqueue_`, `register_`, `load_`, `sanitize_`). +- One purpose per function — no `onepress_do_a_and_b()`. + +```php +function onepress_get_layout( $default = 'right-sidebar' ) { … } +function onepress_is_wc_active() { … } +function onepress_load_section( $section_id ) { … } +function onepress_sanitize_hex_color( $color ) { … } +``` + +### Pluggable functions + +Anything wrapped in `if ( ! function_exists( 'onepress_*' ) )` is **pluggable** — child themes may override it. The **signature** is part of the API: + +```php +if ( ! function_exists( 'onepress_header' ) ) { + function onepress_header() { … } +} +``` + +For new pluggable helpers, always wrap in the `function_exists` guard. For non-pluggable internal helpers, do not wrap — but understand that once shipped they're still public per additive-only. + +### Classes + +- `PascalCase_With_Underscores` (WordPress-style PSR-0-ish). +- Prefix `OnePress_` for new code (see [Known inconsistencies](#known-inconsistencies-frozen-by-additive-only) below). +- File name mirrors class: `class-{kebab-case-name}.php` → `class-config.php` for `Onepress_Config`, or `control-{name}.php` for control subclasses. + +```php +class OnePress_MetaBox { … } +class OnePress_Alpha_Color_Control extends WP_Customize_Control { … } +class OnePress_Editor { … } +``` + +Singletons use a static `get_instance()`: + +```php +class Onepress_Dashboard { + private static $_instance = null; + public static function get_instance() { + if ( is_null( self::$_instance ) ) { + self::$_instance = new self(); + } + return self::$_instance; + } +} +``` + +### Methods + +- `snake_case` (matches WordPress core style, not PSR-12). +- Visibility explicit (`public`, `protected`, `private`). + +```php +public function render_meta_box_content( $post ) { … } +private function get_recommended_actions() { … } +``` + +### Constants + +- `UPPER_SNAKE_CASE` with prefix `ONEPRESS_`. +- Defined at file scope or inside a class. + +```php +define( 'ONEPRESS_THEME_PATH', dirname( __FILE__ ) ); +// Plus plugin: +defined( 'ONEPRESS_PLUS_PATH' ); +``` + +### Local variables + +- `$snake_case`. +- Descriptive — no `$x`, `$tmp`, `$arr` for anything that lives longer than 3 lines. +- Boolean flags read like English: `$is_shop`, `$has_logo`, `$can_edit`. + +```php +$custom_logo_id = get_theme_mod( 'custom_logo' ); +$is_shop = onepress_is_wc_active() && is_shop(); +$image_alt = get_post_meta( $custom_logo_id, '_wp_attachment_image_alt', true ); +``` + +### Globals + +- `$snake_case` with prefix `$onepress_`. +- Avoid where possible — prefer the loop-prop helpers (`onepress_loop_set_prop()` / `_get_prop()`) for loop-scoped state. + +```php +global $onepress_loop_props; +``` + +### Theme mods + +- `onepress_{area}_{setting}` snake_case. +- Group prefix matches the section or feature: `onepress_hero_*`, `onepress_gallery_*`, `onepress_header_*`, `onepress_footer_*`. +- Boolean toggles end in `_disable` (off by default) or `_enable` (on by default) — match what already exists in the area; **don't mix** within the same group. +- Color settings end in `_color`. Image settings end in `_image` (or `_logo` for logos). Multi-row settings (repeaters) named with plural noun: `onepress_hero_images`, `onepress_team_members`. + +```php +get_theme_mod( 'onepress_hero_disable' ); +get_theme_mod( 'onepress_hero_option_animation', 'flipInX' ); +get_theme_mod( 'onepress_gallery_display', 'grid' ); +get_theme_mod( 'onepress_disable_g_font' ); // ← legacy: prefix-first, kept frozen +get_theme_mod( 'onepress_btt_disable' ); // ← "back to top" abbreviation; kept frozen +``` + +### Options + +- `onepress_*` snake_case. +- One option per concern; prefer post meta or theme mods for everything else. + +```php +get_option( 'onepress_sections_settings' ); +get_option( 'onepress_actions_dismiss' ); +get_option( 'onepress_dismiss_switch_theme_notice' ); +``` + +### Post meta keys + +- **Leading underscore** + `snake_case` — WordPress convention for "hidden from default UI". +- All `_hide_*` keys are boolean (`'1'` / `''`). + +```php +get_post_meta( $post_id, '_hide_page_title', true ); +get_post_meta( $post_id, '_hide_header', true ); +get_post_meta( $post_id, '_hide_footer', true ); +get_post_meta( $post_id, '_hide_breadcrumb', true ); +``` + +### Hooks (actions and filters) + +- `onepress_*` snake_case. +- **Position adverbs encoded in the name**, not in priority: `onepress_before_*`, `onepress_after_*`, `*_start`, `*_end`. +- Sectional hooks include the ID as a suffix or interpolated middle: `onepress_before_section_{id}`, `onepress_after_section_{id}`. + +```php +do_action( 'onepress_before_site_start' ); +do_action( 'onepress_header_end' ); +do_action( 'onepress_before_section_' . $section_id ); +do_action( 'onepress_after_section_part', $section_id ); + +apply_filters( 'onepress_get_sections', $sections ); +apply_filters( 'onepress_js_settings', $settings ); +apply_filters( 'onepress_content_width', $width ); +``` + +For deprecation, use the legacy name as-is and add a new canonical filter — see [spec-conventions.md → Pattern 4](spec-conventions.md#the-four-patterns-for-improving-without-removing). + +### Nonces + +- Action key in `snake_case` (no prefix mandatory, but recommend `onepress_` going forward). +- Field name mirrors the action with `_nonce` suffix. + +```php +wp_nonce_field( 'onepress_page_settings', 'onepress_page_settings_nonce' ); +wp_nonce_field( 'onepres_save_settings', 'onepress_settings_nonce' ); // ← legacy typo 'onepres', frozen +``` + +### Sanitize / validate callbacks + +- `onepress_sanitize_{type}` / `onepress_{area}_validate`. +- See full list in [spec-customizer.md → Sanitizers](spec-customizer.md#sanitizers). + +--- + +## JavaScript + +### Localized globals (from PHP via `wp_localize_script`) + +- `snake_case` with prefix `onepress_`, or a `C_*` prefix for cross-cutting libs (icon picker uses `C_Icon_Picker`). +- Documented in PHP source — treat the **key set** as API: do not remove keys; only add additively. + +```js +window.onepress_js_settings // localized from functions.php +window.onepress_customizer_settings // localized from inc/customizer.php +window.C_Icon_Picker // localized from inc/customizer.php +``` + +### Module / file naming under `src/` + +- `kebab-case.js` for files: `customizer-liveview.js`, `gallery-isotope.js`, `lightgallery.js`. +- `PascalCase.js` for vendored libraries that ship that way: `src/frontend/libs/FitVids.js`, `src/frontend/libs/Morphext/`. +- One feature per file. + +### Variables in modern module code (`src/admin/`, `src/frontend/`) + +- `camelCase` for variables and functions. +- `PascalCase` for React components and classes. +- `UPPER_SNAKE_CASE` for constants. + +```js +const galleryDisplay = onepress_js_settings.gallery_display; +function initHeroSlider() { … } +class IconPicker { … } +const MAX_SLIDES = 20; +``` + +### Legacy jQuery / plugin libs (`src/frontend/libs/*`) + +- Existing files follow each library's own convention (jQuery plugins use `$.fn.pluginName`, etc.). +- **Do not refactor** — those files are vendored or near-vendored. Touch only to fix bugs or upgrade. + +### Event names (custom DOM events) + +- `kebab-case`, prefix with `onepress:` namespace for new events. + +```js +$(document).trigger('onepress:section-loaded', { id: 'hero' }); +``` + +--- + +## CSS / SCSS + +### Class names + +- `kebab-case`. +- **BEM-lite** — block, element, modifier separated by `-` (not `__` / `--`). The codebase is not strict BEM; match the surrounding code. + +```css +.site-header { … } +.hero-section { … } +.hero-content-style1 { … } +.custom-logo-transparent { … } +.footer-widget { … } +.btt .back-to-top { … } +``` + +### Section wrappers + +Every front-page section's root markup uses `.section-{id}`: + +```html +
+
+``` + +**Both** `.section` and `.section-{id}` are emitted — users target both in Additional CSS. + +### State and utility classes + +- `is-*` for boolean state: `.is-customize-preview`, `.is-sticky`. +- `has-*` for presence: `.has-logo`, `.has-sidebar`. +- Descriptive utilities: `.no-sidebar`, `.animation-disable`, `.hiding-page-title`, `.group-blog`. + +### Body classes + +Added by [`onepress_body_classes()`](../inc/extras.php) on the `body_class` filter: + +- `kebab-case`, descriptive. +- Examples: `.group-blog`, `.template-fullwidth-stretched`, `.is-customize-preview`, `.hiding-page-title`, `.animation-disable`. + +### SCSS partials + +- Files in `src/frontend/styles/` use **leading underscore + kebab-case** (Sass partial convention): `_variables.scss`, `_layout.scss`, `_sections.scss`, `_widgets.scss`. +- Entry files: no leading underscore (`style.scss`, `editor.scss`, `lightgallery.scss`, `animate.scss`). + +### SCSS variables + +- `$kebab-case`. +- Color variables grouped at the top of `_variables.scss`. + +```scss +$primary-color: #f55; +$text-color: #333; +$base-font-size: 14px; +``` + +### Style handles (registered via `wp_enqueue_style`) + +- `onepress-{key}` kebab-case. Match the webpack output key. +- Inline CSS attaches to `onepress-style` (the main theme handle). + +```php +wp_register_style( 'onepress-style', … ); +wp_register_style( 'onepress-fonts', … ); +wp_register_style( 'onepress-gallery-isotope', … ); +wp_add_inline_style( 'onepress-style', $custom_css ); +``` + +### Script handles + +Same rule — `onepress-{key}`. Set by [`onepress_load_build_script()`](../functions.php). + +```php +wp_register_script( 'onepress-theme', … ); +wp_register_script( 'onepress-customizer-liveview', … ); +``` + +--- + +## WordPress slugs + +### Image sizes + +- `onepress-{descriptor}` kebab-case. +- Once registered, never rename — users' uploaded media has thumbnails at these slugs. + +```php +add_image_size( 'onepress-blog-small', 300, 150, true ); +add_image_size( 'onepress-small', 480, 300, true ); +add_image_size( 'onepress-medium', 640, 400, true ); +``` + +### Sidebars + +- `sidebar-{n|name}` or `footer-{n}` kebab-case. +- Inherited from `_s` starter — don't reorganize. + +```php +register_sidebar( [ 'id' => 'sidebar-1', … ] ); +register_sidebar( [ 'id' => 'sidebar-shop', … ] ); // ← only when WC active +register_sidebar( [ 'id' => 'footer-' . $i, … ] ); // ← 1..4 +``` + +### Menu locations + +- `snake_case`, no prefix needed (WP convention). + +```php +register_nav_menus( [ 'primary' => esc_html__( 'Primary Menu', 'onepress' ) ] ); +``` + +### Text domain + +- Single word `onepress`, lowercase. Never `one-press`, never `OnePress`. +- Used in every `__()`, `esc_html__()`, `_x()`, `_n()` call. + +```php +__( 'Skip to content', 'onepress' ); +esc_html__( 'Primary Menu', 'onepress' ); +_x( 'on', 'Open Sans font: on or off', 'onepress' ); +``` + +### Customizer panel / section / setting IDs + +- Panel: `onepress_{group}`. +- Section: `onepress_{group}_{section}` or `onepress_section_{id}` for front-page sections. +- Setting: same as the underlying theme mod key (`onepress_*`). + +### Admin pages + +- Slug: `ft_onepress` (FameThemes prefix — frozen, do not rename). +- URL: `themes.php?page=ft_onepress`. + +--- + +## Cross-cutting prefix matrix + +Reference table — at a glance, what prefix goes where: + +| Prefix | Where | Example | +|---|---|---| +| `onepress_` | PHP functions, hooks, theme mods, options, JS globals, style/script handles, image sizes | `onepress_setup`, `onepress_get_sections`, `onepress_hero_disable`, `onepress-style`, `onepress-medium` | +| `OnePress_` | PHP classes (preferred for new code) | `OnePress_MetaBox`, `OnePress_Editor` | +| `Onepress_` | PHP classes (legacy, frozen) | `Onepress_Config`, `Onepress_Dashboard` | +| `ONEPRESS_` | PHP constants | `ONEPRESS_THEME_PATH`, `ONEPRESS_PLUS_PATH` | +| `_` (leading) | Post meta keys (WP "hidden" convention) | `_hide_page_title`, `_hide_footer` | +| `ft_` | Admin page slug (FameThemes legacy) | `ft_onepress` | +| `C_` | Cross-cutting JS lib globals | `C_Icon_Picker` | +| `onepress:` | Custom DOM event namespace | `onepress:section-loaded` | +| `is-` / `has-` | CSS state classes | `.is-customize-preview`, `.has-logo` | +| `section-` | CSS section wrapper | `.section-hero`, `.section-about` | +| `$onepress-` (SCSS) — *not currently used, but allowed for new namespaced vars* | SCSS variables | `$onepress-primary-color` | + +--- + +## Known inconsistencies (frozen by additive-only) + +These exist in the shipped codebase and **cannot be fixed** without violating [additive-only](spec-conventions.md#additive-only-mandate). Documented here so nobody "cleans them up": + +| Inconsistency | Shipped form | Don't do | +|---|---|---| +| Class prefix capitalization | Both `OnePress_*` (e.g. `OnePress_MetaBox`, `OnePress_Editor`, `OnePress_Alpha_Color_Control`) and `Onepress_*` (e.g. `Onepress_Config`, `Onepress_Dashboard`, `Onepress_Dots_Navigation`, `Onepress_Customize_Repeatable_Control`) exist. | Don't rename `Onepress_Config` → `OnePress_Config` — child themes and Plus call the existing name. | +| One control class uses snake_case-ish | `One_Press_Textarea_Custom_Control` (note `One_Press_` split) | Don't rename. | +| Nonce action typo | `onepres_save_settings` (missing `s`) | Don't fix the typo — sites with the old action key in localStorage / cached forms would break submission. | +| Theme mod ordering convention | `onepress_disable_g_font` (prefix-first) vs `onepress_animation_disable` (suffix-last) — both shipped | Don't normalize. | +| Some abbreviations baked into keys | `onepress_btt_disable` ("back to top"), `onepress_hero_pdtop` / `onepress_hero_pdbotom` ("padding top/bottom", note `pdbotom` typo) | Don't rename, don't fix typos. | +| Webpack output handle exception | `theme` entry's style handle is `onepress-style` (not `onepress-theme`) | Special-cased in [`onepress_load_build_script()`](../functions.php) — leave as-is. | +| `theme.json` palette slug `secondary` is `#333333` (= heading color) | `theme.json` ships secondary=#333333; SCSS `$secondary`=`#00aeef`; theme mod `onepress_secondary_color` defaults to `#00aeef` | Don't change the theme.json value — user posts with `.has-secondary-background-color` render at `#333333` on existing sites. Treat as a frozen naming collision with the SCSS / theme-mod meaning of "secondary". See [spec-block-editor.md → Known inconsistencies](spec-block-editor.md#known-inconsistencies). | +| `theme.json` palette slug `light` vs SCSS `$meta` | Same color `#f8f9f9`, two different names | Don't rename — both are public; treat as aliases. | + +When you find a new inconsistency in the codebase, **add it to this table** rather than fixing it. + +--- + +## Naming for new code (recommended forms) + +When introducing new symbols, pick from these preferred forms: + +| Surface | Preferred new form | +|---|---| +| Class | `OnePress_*` (uppercase `P`) | +| Theme mod | `onepress_{area}_{setting}` — area first, setting last | +| Boolean theme mod | `onepress_{area}_{setting}_enable` or `_disable` — match existing in same area | +| Filter | `onepress_{noun}` or `onepress_get_{noun}` for getters, `onepress_{verb}_{noun}` for transformers | +| Action | `onepress_{position}_{noun}` (`before_`, `after_`, `*_start`, `*_end`) | +| JS module file | `kebab-case.js` | +| JS function/variable | `camelCase` | +| CSS class | `kebab-case`, BEM-lite, `.section-{id}` for section wrappers | +| SCSS partial | `_kebab-case.scss` | +| SCSS variable (new) | `$onepress-{kebab-case}` to namespace away from generic Bootstrap variables | +| Custom DOM event | `onepress:kebab-case` | +| Style/script handle | `onepress-{key}` matching webpack output | +| Image size | `onepress-{descriptor}` | +| Constant | `ONEPRESS_{NOUN}` | + +**Naming is part of the additive-only contract.** Pick the right name on first ship — you live with it forever. diff --git a/docs/spec-sections.md b/docs/spec-sections.md new file mode 100644 index 00000000..24ac86bb --- /dev/null +++ b/docs/spec-sections.md @@ -0,0 +1,105 @@ +# spec-sections — Front-Page Sections + +The front-page template iterates a configurable, filterable list of **sections**, each a self-contained unit of **markup + customizer settings + on/off toggle**. This is the core feature of OnePress. + +## Render flow + +Entry: [../template-frontpage.php](../template-frontpage.php). + +1. `onepress_frontpage_before_section_parts` action fires. +2. Section IDs come from `apply_filters('onepress_frontpage_sections_order', [...])`. +3. For each ID, [`Onepress_Config::is_section_active($id)`](../inc/class-config.php) is checked. +4. [`onepress_load_section($id)`](../inc/template-tags.php) (~line 1879) loads `section-parts/section-{id}.php`, wrapping it in `onepress_before_section_{id}` / `onepress_after_section_{id}` actions. +5. The `hero` section is loaded earlier — from `onepress_header_end` via `onepress_load_hero_section()`. +6. `onepress_frontpage_after_section_parts` action fires. + +If anything hooks `onepress_frontpage_section_parts`, that hook **replaces** the default loop (the theme respects whoever hooks it). + +## Built-in sections + +From [`Onepress_Config::get_sections()`](../inc/class-config.php): + +`hero`, `about`, `services`, `videolightbox`, `gallery`, `counter`, `features`, `team`, `news`, `contact` + +Each section has: + +- Markup at `section-parts/section-{id}.php` +- Customizer config at `inc/customize-configs/section-{id}.php` (loaded by [customizer.php](../inc/customizer.php) **only when active**) +- A title/label in `get_sections()` used by the dots navigation + +## Plus-only sections + +Registered by the OnePress Plus plugin via [`Onepress_Config::get_plus_sections()`](../inc/class-config.php): + +`slider`, `clients`, `cta`, `map`, `pricing`, `projects`, `testimonials` + +Plus is detected via `defined('ONEPRESS_PLUS_PATH')` and `class_exists('OnePress_Plus')`. Selective-refresh partials also fall back to `ONEPRESS_PLUS_PATH` when a template part isn't in the theme (see [customizer-selective-refresh.php](../inc/customizer-selective-refresh.php)). + +## Activation state + +Persisted as a **single option** `onepress_sections_settings` (managed by [`Onepress_Config::save_settings()`](../inc/class-config.php)). The form lives on the admin dashboard (nonce action `onepres_save_settings`); see [spec-admin.md](spec-admin.md). + +When no setting exists yet, sections default to **active** (`is_section_active()` returns `1` for an empty config). + +## Sections-navigation (dots) + +[`Onepress_Dots_Navigation`](../inc/class-sections-navigation.php) (singleton) renders the right-side dot navigation on the front page. For each section, it injects a per-section checkbox `onepress_sections_nav___enable` into the Customizer (sanitizer `onepress_sanitize_text`). + +Filter `onepress_sections_navigation_get_sections` to add/remove dots without touching the section list itself. + +## Adding a new section (child theme) + +1. Add markup at `section-parts/section-foo.php`. +2. Register controls at `inc/customize-configs/section-foo.php`. +3. Append the ID via `onepress_get_sections` filter (so it shows up in the dashboard toggle list + dots nav) **and/or** `onepress_frontpage_sections_order` (so it actually renders). + +Example: + +```php +add_filter( 'onepress_get_sections', function ( $sections ) { + $sections['foo'] = [ + 'label' => __( 'Section: Foo', 'mychild' ), + 'title' => __( 'Foo', 'mychild' ), + 'default' => false, + 'inverse' => false, + ]; + return $sections; +} ); + +add_filter( 'onepress_frontpage_sections_order', function ( $order ) { + $order[] = 'foo'; + return $order; +} ); +``` + +## Section template conventions + +Every section template should: + +- Read its enable/disable mod (e.g. `get_theme_mod( 'onepress_foo_disable' )`) and short-circuit. +- Wrap the short-circuit so the Customizer preview can still re-enable it: + + ```php + if ( onepress_is_selective_refresh() ) { + $disable = false; + } + ``` + + See [`onepress_is_selective_refresh()`](../inc/template-tags.php) ~line 1686. + +- Pull all settings via `get_theme_mod( 'onepress_*' )`. +- Echo escaped output (`esc_html`, `esc_attr`, `esc_url`, `wp_kses($val, onepress_allowed_tags())`). + +## Section IDs are public API + +Section IDs and `onepress_*` theme-mod keys are **stable public API**. OnePress has 60,000+ active installs — every section ID is referenced in user databases under `onepress_sections_settings`, every theme mod is filled with user customizations, and child themes hook into both. **Renaming is a breaking change** that requires a major-version bump + a migration that backfills the new key from the old. + +Full BC contract: [spec-conventions.md → Backward Compatibility](spec-conventions.md#backward-compatibility). + +Practical rules when working on sections: + +- Adding a new section ID = safe. +- Renaming `hero`/`about`/etc. = **breaks every existing site** (they vanish from the front page; user settings stay in the DB but no longer wire to anything). +- Removing a section = breaks sites that had it active. +- Changing the default `is_section_active()` return for an empty option (currently `1` = active) = retroactively hides sections on sites that never saved the dashboard form. **Never touch this default.** +- Reordering the default section order = changes the rendered front page on sites that never customized order. Avoid; if essential, gate on a version marker. diff --git a/functions.php b/functions.php index b4247347..9c0aad8e 100644 --- a/functions.php +++ b/functions.php @@ -178,10 +178,20 @@ function onepress_setup() add_theme_support('wp-block-styles'); + /** + * Block editor UX additions (since 2.4.0). All additive — no existing + * theme support is removed. Each surfaces a control in the editor + * sidebar; none changes how saved posts render unless the user opts in. + */ + add_theme_support('responsive-embeds'); + add_theme_support('custom-line-height'); + add_theme_support('custom-spacing'); + add_theme_support('custom-units'); + /* * This theme styles the visual editor to resemble the theme style. */ - add_editor_style(array('assets/build/admin/editor.css', onepress_fonts_url())); + add_editor_style(array('assets/admin/editor.css', onepress_fonts_url())); if (get_theme_mod('onepress_gallery_disable')) { /** @@ -271,8 +281,8 @@ function onepress_widgets_init() function onepress_load_build_script($key, $deps = [], $is_admin = false) { $min_ext = defined('WP_DEBUG') && WP_DEBUG ? '' : '.minified'; - $dir = get_template_directory() . '/assets/build/'; - $dir_url = get_template_directory_uri() . '/assets/build/'; + $dir = get_template_directory() . '/assets/'; + $dir_url = get_template_directory_uri() . '/assets/'; if (!$is_admin) { $dir .= 'frontend/'; $dir_url .= 'frontend/'; @@ -281,27 +291,46 @@ function onepress_load_build_script($key, $deps = [], $is_admin = false) $dir_url .= 'admin/'; } - $f = $dir . $key . '.asset.php'; + // Webpack emits BOTH `{key}.asset.php` (dev build) and + // `{key}.minified.asset.php` (prod build) — pick the one that matches + // the current `$min_ext` so prod-only entries don't silently fall + // through the existence check. + $f = $dir . $key . $min_ext . '.asset.php'; if (!file_exists($f)) { - return; + // Back-compat: fall back to the non-suffixed file when the + // suffixed variant is missing. + $f = $dir . $key . '.asset.php'; + if (!file_exists($f)) { + return; + } } $asset = include $f; $asset['dependencies'] = array_merge($asset['dependencies'], $deps); - $url = $dir_url . $key . $min_ext . '.js'; + $js_file = $key . $min_ext . '.js'; + $url_js = false; + if (file_exists($dir . $js_file)) { + $url_js = $dir_url . $js_file; + } $handle = 'onepress-' . $key; $handle_css = 'onepress-' . $key; $url_css = false; if ($key === 'theme') { $handle_css = 'onepress-style'; - if (file_exists($dir . $key . '.css')) { - $url_css = $dir_url . $key . '.css'; - } } - wp_register_script($handle, $url, $asset['dependencies'], $asset['version'], true); + if (file_exists($dir . $key . '.css')) { + $url_css = $dir_url . $key . '.css'; + } + + if ($url_js) { + wp_register_script($handle, $url_js, $asset['dependencies'], $asset['version'], true); + wp_enqueue_script($handle); + } + if ($url_css) { wp_register_style($handle_css, $url_css, [], $asset['version']); + wp_enqueue_style($handle_css); } return $handle; } @@ -318,15 +347,11 @@ function onepress_scripts() if (!get_theme_mod('onepress_disable_g_font')) { $google_font_url = onepress_fonts_url(); - // var_dump($google_font_url); die(); if ($google_font_url) { wp_enqueue_style('onepress-fonts', onepress_fonts_url(), array(), $version); } } - wp_enqueue_style('onepress-animate', get_template_directory_uri() . '/assets/css/animate.min.css', array(), $version); - wp_enqueue_style('onepress-fa', get_template_directory_uri() . '/assets/fontawesome-v6/css/all.min.css', array(), '6.5.1'); - wp_enqueue_style('onepress-fa-shims', get_template_directory_uri() . '/assets/fontawesome-v6/css/v4-shims.min.css', array(), '6.5.1'); $deps = array('jquery'); // Animation from settings. @@ -383,15 +408,16 @@ function onepress_scripts() $deps[] = onepress_load_build_script('gallery-carousel'); } } - wp_enqueue_style('onepress-gallery-lightgallery', get_template_directory_uri() . '/assets/css/lightgallery.css'); + onepress_load_build_script('lightgallery', ['jquery']); + // wp_enqueue_style('onepress-gallery-lightgallery', get_template_directory_uri() . '/assets/css/lightgallery.css'); } if (defined('ONEPRESS_PLUS_PATH')) { $deps[] = onepress_load_build_script('gallery-carousel'); } - $handle = onepress_load_build_script('theme', $deps); - wp_enqueue_script($handle); + onepress_load_build_script('theme', $deps); + $custom_css = onepress_custom_inline_style(); wp_add_inline_style('onepress-style', $custom_css); @@ -689,3 +715,10 @@ function onepress_the_excerpt($type = false, $length = false) * @since 2.2.1 */ require get_template_directory() . '/inc/admin/class-editor.php'; + +/** + * theme.json palette bridge (Customizer → CSS vars). + * + * @since 2.4.1 + */ +require get_template_directory() . '/inc/theme-json-bridge.php'; diff --git a/home.php b/home.php index 30dea5e5..2a569af1 100644 --- a/home.php +++ b/home.php @@ -13,7 +13,8 @@ get_header(); -$layout = onepress_get_layout(); +$layout = onepress_get_layout(); +$blog_loop = onepress_get_blog_posts_loop_layout_config(); /** * @since 2.0.0 @@ -25,7 +26,7 @@
-
+
@@ -35,20 +36,41 @@ - - - - - /* - * Include the Post-Format-specific template for the content. - * If you want to override this in a child theme, then include a file - * called content-___.php (where ___ is the Post Format name) and that will be used instead. - */ - get_template_part( 'template-parts/content', get_post_format() ); - ?> + +
+ - + + '; + } + /* + * Include the Post-Format-specific template for the content. + * If you want to override this in a child theme, then include a file + * called content-___.php (where ___ is the Post Format name) and that will be used instead. + */ + get_template_part( 'template-parts/content', get_post_format() ); + if ( $blog_loop['is_grid'] ) { + echo '
'; + } + endwhile; + ?> + + +
+ diff --git a/inc/admin/class-editor.php b/inc/admin/class-editor.php index 016591ef..9f8b386b 100644 --- a/inc/admin/class-editor.php +++ b/inc/admin/class-editor.php @@ -1,24 +1,30 @@ =' ) ) { - add_filter( 'block_editor_settings_all', array( $this, 'editor_settings' ) ); + if (version_compare($current_wp_version, '5.8', '>=')) { + add_filter('block_editor_settings_all', array($this, 'editor_settings')); } else { - add_filter( 'block_editor_settings', array( $this, 'editor_settings' ) ); + add_filter('block_editor_settings', array($this, 'editor_settings')); } // Add ajax action to load css file. - add_action( 'wp_ajax_' . $this->action, array( $this, 'css_file' ) ); + add_action('wp_ajax_' . $this->action, array($this, 'css_file')); // Add more editor assets. - add_action( 'enqueue_block_editor_assets', array( $this, 'assets' ) ); + add_action('enqueue_block_editor_assets', array($this, 'assets')); } /** @@ -28,34 +34,142 @@ public function __construct() { * * @return void */ - function assets() { - if ( function_exists( 'onepress_typography_render_style' ) ) { - $typo = onepress_typography_render_style( false, true ); - if ( $typo['url'] ) { - wp_register_style( 'onepress-editor-fonts', $typo['url'] ); // Font style url. - wp_enqueue_style( 'onepress-editor-fonts' ); // Font style url. + function assets() + { + if (function_exists('onepress_typography_render_style')) { + $typo = onepress_typography_render_style(false, true); + if ($typo['url']) { + wp_register_style('onepress-editor-fonts', $typo['url']); // Font style url. + wp_enqueue_style('onepress-editor-fonts'); // Font style url. } - wp_add_inline_style( 'wp-edit-post', $typo['code'] ); + wp_add_inline_style('wp-edit-post', $typo['code']); } - wp_add_inline_style( 'wp-edit-post', $this->css() ); + wp_add_inline_style('wp-edit-post', $this->css()); + + /** + * Since 2.4.1: live-update `--wp--style--global--content-size` in the + * editor iframe when the user switches the page template via the + * sidebar. Mirrors the priority chain used server-side + * (`onepress_get_layout_for_post_id()` / `onepress_resolve_content_width_css()`). + * + * JS source lives at `src/admin/editor-content-width.js` and is + * built to `assets/admin/editor-content-width(.minified).js` by + * webpack (entry in `webpack.config.js`). Theme reviewers reject + * inline JS strings in PHP — keeping the watcher as a real file + * satisfies that rule and lets ESLint / build-time analysis run + * over it. + */ + $config = $this->content_width_config(); + if ($config !== null && function_exists('onepress_load_build_script')) { + $handle = onepress_load_build_script( + 'editor-content-width', + array('wp-data', 'wp-edit-post'), + true + ); + if ($handle) { + wp_localize_script($handle, 'onepressEditorContentWidth', $config); + } + } } /** - * Add styling settings to editor. + * Build dynamic editor-only CSS. * - * @return string CSS code. + * Since 2.4.1: emits ONLY a `:root { --wp--style--global--content-size: px }` + * override — no literal `max-width: px` rule. The SCSS rule in + * `_gutenberg.scss` (`.editor-styles-wrapper .wp-block:not(…) { max-width: var(--wp--style--global--content-size, 1110px) }`) + * wins WP's auto-generated `.is-root-container > :where(…)` rule by + * specificity and consumes this var — so the visible cap reflects the + * resolved value through a single source of truth. + * + * The value is resolved by `onepress_resolve_content_width_px()` from + * the layout determined by `onepress_get_layout_for_post_id()`. Priority: + * 1) page template (full-width / left-sidebar) + * 2) Single Layout Sidebar mod (post type `post` only) + * 3) Site Layout mod (global) + * Then `post_type=post` lets `single_layout_content_width` win as an + * explicit user override. + * + * Theme.json `layout.contentSize` is intentionally NOT mutated (see + * `inc/theme-json-bridge.php`) because WP would bake the value into + * the literal `max-width` of the `is-root-container` rule. + * + * @return string CSS code (empty when the resolved value matches the + * theme.json default — no override needed). */ - public function css() { - $css = ''; + public function css() + { + $post_id = isset($_REQUEST['post']) ? absint(wp_unslash($_REQUEST['post'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ($post_id <= 0) { + return ''; + } - $content_width = absint( get_theme_mod( 'single_layout_content_width' ) ); - if ( $content_width > 0 ) { - $value = $content_width . 'px'; - $css .= '.editor-styles-wrapper .wp-block:not([data-align="full"]):not([data-align="wide"]) { max-width: ' . $value . '; }'; + $post_type = get_post_type($post_id); + if (! $post_type) { + return ''; } - return $css; + if (! function_exists('onepress_get_layout_for_post_id') || ! function_exists('onepress_resolve_content_width_css')) { + return ''; + } + + $layout = onepress_get_layout_for_post_id($post_id); + $value = onepress_resolve_content_width_css($layout, $post_type); + + $default = function_exists('onepress_get_no_sidebar_base_px') + ? onepress_get_no_sidebar_base_px() . 'px' + : '1110px'; + + // Skip when the resolved value already matches the theme.json default — + // WP-emitted `:root { --wp--style--global--content-size: px }` + // already provides it; emitting an identical override is dead weight. + if ($value === '' || $value === $default) { + return ''; + } + + return ':root { --wp--style--global--content-size: ' . $value . '; }'; + } + + /** + * Build the config object consumed by `src/admin/editor-content-width.js`. + * + * Mirrors the PHP priority chain (template → single_layout → onepress_layout) + * + the post-only user override (`single_layout_content_width`). All + * inputs are PHP-resolved once at editor load; the JS does pure + * dictionary lookups with no extra fetches. + * + * @since 2.4.1 + * @return array|null Config array, or `null` when no post context. + */ + protected function content_width_config() + { + $post_id = isset($_REQUEST['post']) ? absint(wp_unslash($_REQUEST['post'])) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ($post_id <= 0) { + return null; + } + + $post_type = get_post_type($post_id); + if (! $post_type) { + return null; + } + + return array( + 'postType' => $post_type, + 'sidebarBase' => function_exists('onepress_get_sidebar_base_px') ? onepress_get_sidebar_base_px() : 790, + 'noSidebarBase' => function_exists('onepress_get_no_sidebar_base_px') ? onepress_get_no_sidebar_base_px() : 1110, + 'siteLayout' => get_theme_mod('onepress_layout', 'right-sidebar'), + 'singleLayout' => get_theme_mod('single_layout', 'default'), + 'singleContentWidth' => absint(get_theme_mod('single_layout_content_width')), + // Keep this map in sync with `onepress_get_layout_for_post_id()` + // in `inc/extras.php`. `'stretched'` is the special key that + // resolves to `100vw` instead of a pixel base. + 'templateMap' => array( + 'template-fullwidth.php' => 'no-sidebar', + 'template-fullwidth-stretched.php' => 'stretched', + 'template-left-sidebar.php' => 'left-sidebar', + ), + ); } /** @@ -63,13 +177,14 @@ public function css() { * * @return string CSS URL */ - public function editor_style_url() { + public function editor_style_url() + { return add_query_arg( array( 'action' => $this->action, - 'nonce' => wp_create_nonce( $this->action ), + 'nonce' => wp_create_nonce($this->action), ), - admin_url( 'admin-ajax.php' ) + admin_url('admin-ajax.php') ); } @@ -81,21 +196,187 @@ public function editor_style_url() { * @param array $editor_settings * @return array */ - public function editor_settings( $editor_settings ) { + public function editor_settings($editor_settings) + { $editor_settings['styles'][] = array( 'css' => $this->load_style(), ); + + /** + * Since 2.4.1: inject the dynamic CSS (content-width override + the + * defense-in-depth max-width rule) through `editor_settings['styles']` + * so it reaches the iframe canvas. Previously this string was added + * via `wp_add_inline_style('wp-edit-post', $this->css())` in assets(), + * which only lands in the outer admin DOM — the iframe canvas in + * WP 5.9+ never received it, so `single_layout_content_width` from + * the Customizer was silently ignored. + * + * `css()` returns '' when no Customizer value is set; guard against + * pushing an empty styles entry. + */ + $dynamic_css = $this->css(); + if ($dynamic_css !== '') { + $editor_settings['styles'][] = array( + 'css' => $dynamic_css, + ); + } + + /** + * Phase 3 (since 2.4.0): inject dynamic color palette + font sizes from + * theme mods, but only when nothing else has set them already. This + * lets child themes that call add_theme_support('editor-color-palette') + * or add_theme_support('editor-font-sizes') keep winning. + * + * Read fresh from theme mods each request — this is why we use the + * block_editor_settings_all filter instead of add_theme_support() + * (which requires a static array resolved at after_setup_theme time). + */ + if (empty($editor_settings['colors'])) { + $editor_settings['colors'] = $this->get_editor_color_palette(); + } + if (empty($editor_settings['fontSizes'])) { + $editor_settings['fontSizes'] = $this->get_editor_font_sizes(); + } + return $editor_settings; } + /** + * Build the editor color palette from current theme mods. + * + * Each entry shape: ['name', 'slug', 'color']. Slugs become CSS classes + * (`.has-{slug}-color`, `.has-{slug}-background-color`) — keep them stable + * across releases because user posts may already reference them. + * + * Filterable via `onepress_editor_color_palette` so integrators can extend + * or replace the palette without subclassing. + * + * @since 2.4.0 + * @return array + */ + public function get_editor_color_palette() + { + $primary = get_theme_mod('onepress_primary_color', '#03c4eb'); + $secondary = get_theme_mod('onepress_secondary_color', '#00aeef'); + + if ($primary && strpos($primary, '#') !== 0) { + $primary = '#' . ltrim($primary, '#'); + } + if ($secondary && strpos($secondary, '#') !== 0) { + $secondary = '#' . ltrim($secondary, '#'); + } + + $palette = array( + array( + 'name' => esc_html__('Primary', 'onepress'), + 'slug' => 'onepress-primary', + 'color' => $primary, + ), + array( + 'name' => esc_html__('Secondary', 'onepress'), + 'slug' => 'onepress-secondary', + 'color' => $secondary, + ), + array( + 'name' => esc_html__('Heading', 'onepress'), + 'slug' => 'onepress-heading', + 'color' => '#333333', + ), + array( + 'name' => esc_html__('Text', 'onepress'), + 'slug' => 'onepress-text', + 'color' => '#777777', + ), + array( + 'name' => esc_html__('Border', 'onepress'), + 'slug' => 'onepress-border', + 'color' => '#e9e9e9', + ), + array( + 'name' => esc_html__('Light Background', 'onepress'), + 'slug' => 'onepress-meta', + 'color' => '#f8f9f9', + ), + array( + 'name' => esc_html__('White', 'onepress'), + 'slug' => 'onepress-white', + 'color' => '#ffffff', + ), + ); + + return apply_filters('onepress_editor_color_palette', $palette); + } + + /** + * Build the editor font-size scale. + * + * Each entry shape: ['name', 'slug', 'size', 'slug']. Slugs become CSS + * classes (`.has-{slug}-font-size`); keep stable across releases. + * + * Filterable via `onepress_editor_font_sizes`. + * + * @since 2.4.0 + * @return array + */ + public function get_editor_font_sizes() + { + $sizes = array( + array( + 'name' => esc_html__('Small', 'onepress'), + 'slug' => 'small', + 'size' => 13, + ), + array( + 'name' => esc_html__('Normal', 'onepress'), + 'slug' => 'normal', + 'size' => 14, + ), + array( + 'name' => esc_html__('Medium', 'onepress'), + 'slug' => 'medium', + 'size' => 18, + ), + array( + 'name' => esc_html__('Large', 'onepress'), + 'slug' => 'large', + 'size' => 24, + ), + array( + 'name' => esc_html__('Huge', 'onepress'), + 'slug' => 'huge', + 'size' => 32, + ), + ); + + return apply_filters('onepress_editor_font_sizes', $sizes); + } + /** * Render dynamic CSS content. * * @return void */ - public function css_file() { - header( 'Content-type: text/css; charset: UTF-8' ); - echo wp_kses_post($this->load_style()); + public function css_file() + { + + if (! current_user_can('edit_posts')) { + wp_die(esc_html__('You are not authorized to access this page.', 'onepress')); + die(); + } + + // Must match editor_style_url(): query arg name is `nonce` (not `none`). + $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : ''; + if (! wp_verify_nonce($nonce, $this->action)) { + wp_die(esc_html__('Security check!', 'onepress')); + } + + nocache_headers(); + header('Content-Type: text/css; charset=UTF-8'); + + // File contents from theme disk; not HTML. wp_kses_post() would strip valid CSS. + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $this->load_style(); + exit; } /** @@ -103,21 +384,20 @@ public function css_file() { * * @return string CSS code. */ - public function load_style() { + public function load_style() + { global $wp_filesystem; WP_Filesystem(); $file = get_template_directory() . '/' . $this->editor_file; $file_contents = ''; - if ( file_exists( $file ) ) { - $file_contents .= $wp_filesystem->get_contents( $file ); + if (file_exists($file)) { + $file_contents .= $wp_filesystem->get_contents($file); } $file_contents .= ''; return $file_contents; } - } -if ( is_admin() ) { +if (is_admin()) { new OnePress_Editor(); } - diff --git a/inc/admin/dashboard.php b/inc/admin/dashboard.php index 98d5184b..d93a7d87 100644 --- a/inc/admin/dashboard.php +++ b/inc/admin/dashboard.php @@ -31,7 +31,6 @@ function init() function save_settings() { - if (isset($_POST['onepress_settings_nonce'])) { if (!isset($_POST['onepress_settings_nonce']) || !wp_verify_nonce($_POST['onepress_settings_nonce'], $this->action_key)) { wp_die(esc_html__('Security check!', 'onepress')); @@ -88,9 +87,8 @@ function maybe_show_switch_theme_notice() function admin_scripts($hook) { if ($hook === 'widgets.php' || $hook === 'appearance_page_ft_onepress') { - $theme_directory_url = get_template_directory_uri(); - - wp_enqueue_style('onepress-admin-css', $theme_directory_url . '/assets/css/admin.css'); + + onepress_load_build_script('admin', [], true); // Add recommend plugin css wp_enqueue_style('plugin-install'); wp_enqueue_script('plugin-install'); @@ -180,7 +178,7 @@ function render_section_settings($key, $section, $see_only = false) ?> - +
@@ -200,7 +198,7 @@ function sections_settings() if ($this->save_status) { ?>
-

+

-
+

Name)); ?>

@@ -352,7 +351,7 @@ function theme_info_page() ?>

- +

@@ -433,7 +432,7 @@ function theme_info_page() -

Name)); ?>

+

Name)); ?>

@@ -717,18 +716,6 @@ function theme_info_page()
- $s ) { - $data[ $k ] = isset( $submitted_data['section_'.$k] ) && $submitted_data['section_'.$k] == 1 ? 1 : false; + foreach ($sections as $k => $s) { + $val = isset($submitted_data['section_' . $k]) ? absint($submitted_data['section_' . $k]) : 0; + $data[$k] = $val == 1 ? 1 : false; } - - update_option( self::$key, $data ); + update_option(self::$key, $data); } - } - static function get_settings( ){ - return get_option( self::$key ); + static function get_settings() + { + return get_option(self::$key); } - static function get_plus_sections(){ + static function get_plus_sections() + { $plugin_sections = array( 'slider' => array( - 'label' => __( 'Section: Slider', 'onepress' ), - 'title' => __( 'Slider', 'onepress' ), + 'label' => __('Section: Slider', 'onepress'), + 'title' => __('Slider', 'onepress'), 'default' => false, 'inverse' => false, ), 'clients' => array( - 'label' => __( 'Section: Clients', 'onepress' ), - 'title' => __( 'Our Clients', 'onepress' ), + 'label' => __('Section: Clients', 'onepress'), + 'title' => __('Our Clients', 'onepress'), 'default' => false, 'inverse' => false, ), 'cta' => array( - 'label' => __( 'Section: Call to Action', 'onepress' ), + 'label' => __('Section: Call to Action', 'onepress'), 'title' => '', 'default' => false, 'inverse' => false, ), 'map' => array( - 'label' => __( 'Section: Map', 'onepress' ), - 'title' => __( 'Map', 'onepress' ), + 'label' => __('Section: Map', 'onepress'), + 'title' => __('Map', 'onepress'), 'default' => false, 'inverse' => false, ), 'pricing' => array( - 'label' => __( 'Section: Pricing', 'onepress' ), - 'title' => __( 'Pricing Table', 'onepress' ), + 'label' => __('Section: Pricing', 'onepress'), + 'title' => __('Pricing Table', 'onepress'), 'default' => false, 'inverse' => false, ), 'projects' => array( - 'label' => __( 'Section: Projects', 'onepress' ), - 'title' => __( 'Highlight Projects', 'onepress' ), + 'label' => __('Section: Projects', 'onepress'), + 'title' => __('Highlight Projects', 'onepress'), 'default' => false, 'inverse' => false, ), 'testimonials' => array( - 'label' => __( 'Section: Testimonials', 'onepress' ), - 'title' => __( 'Testimonials', 'onepress' ), + 'label' => __('Section: Testimonials', 'onepress'), + 'title' => __('Testimonials', 'onepress'), 'default' => false, 'inverse' => false, ), @@ -99,70 +108,79 @@ static function get_plus_sections(){ * * @return array */ - static function get_sections(){ - - $sorted_sections = apply_filters( 'onepress_frontpage_sections_order', array( - 'features', 'about', 'services', 'videolightbox', 'gallery', 'counter', 'team', 'news', 'contact' - ) ); + static function get_sections() + { + + $sorted_sections = apply_filters('onepress_frontpage_sections_order', array( + 'features', + 'about', + 'services', + 'videolightbox', + 'gallery', + 'counter', + 'team', + 'news', + 'contact' + )); $sections_config = array( 'hero' => array( - 'label' => __( 'Section: Hero', 'onepress' ), - 'title' => __( 'Home', 'onepress' ), + 'label' => __('Section: Hero', 'onepress'), + 'title' => __('Home', 'onepress'), 'default' => false, 'inverse' => false, ), 'about' => array( - 'label' => __( 'Section: About', 'onepress' ), - 'title' => __( 'About Us', 'onepress' ), + 'label' => __('Section: About', 'onepress'), + 'title' => __('About Us', 'onepress'), 'default' => false, 'inverse' => false, ), 'contact' => array( - 'label' => __( 'Section: Contact', 'onepress' ), - 'title' => __( 'Get in touch', 'onepress' ), + 'label' => __('Section: Contact', 'onepress'), + 'title' => __('Get in touch', 'onepress'), 'default' => false, 'inverse' => false, ), 'counter' => array( - 'label' => __( 'Section: Counter', 'onepress' ), - 'title' => __( 'Our Numbers', 'onepress' ), + 'label' => __('Section: Counter', 'onepress'), + 'title' => __('Our Numbers', 'onepress'), 'default' => false, 'inverse' => false, ), 'features' => array( - 'label' => __( 'Section: Features', 'onepress' ), - 'title' => __( 'Features', 'onepress' ), + 'label' => __('Section: Features', 'onepress'), + 'title' => __('Features', 'onepress'), 'default' => false, 'inverse' => false, ), 'gallery' => array( - 'label' => __( 'Section: Gallery', 'onepress' ), - 'title' => __( 'Gallery', 'onepress' ), + 'label' => __('Section: Gallery', 'onepress'), + 'title' => __('Gallery', 'onepress'), 'default' => false, 'inverse' => false, ), 'news' => array( - 'label' => __( 'Section: News', 'onepress' ), - 'title' => __( 'Latest News', 'onepress' ), + 'label' => __('Section: News', 'onepress'), + 'title' => __('Latest News', 'onepress'), 'default' => false, 'inverse' => false, ), 'services' => array( - 'label' => __( 'Section: Services', 'onepress' ), - 'title' => __( 'Our Services', 'onepress' ), + 'label' => __('Section: Services', 'onepress'), + 'title' => __('Our Services', 'onepress'), 'default' => false, 'inverse' => false, ), 'team' => array( - 'label' => __( 'Section: Team', 'onepress' ), - 'title' => __( 'Our Team', 'onepress' ), + 'label' => __('Section: Team', 'onepress'), + 'title' => __('Our Team', 'onepress'), 'default' => false, 'inverse' => false, ), 'videolightbox' => array( - 'label' => __( 'Section: Video Lightbox', 'onepress' ), + 'label' => __('Section: Video Lightbox', 'onepress'), 'title' => '', 'default' => false, 'inverse' => false, @@ -173,14 +191,13 @@ static function get_sections(){ 'hero' => $sections_config['hero'] ); - foreach ( $sorted_sections as $id ) { - if ( isset( $sections_config[ $id ] ) ) { - $new[ $id ] = $sections_config[ $id ]; + foreach ($sorted_sections as $id) { + if (isset($sections_config[$id])) { + $new[$id] = $sections_config[$id]; } } // Filter to add more custom sections here - return apply_filters( 'onepress_get_sections', $new ); - + return apply_filters('onepress_get_sections', $new); } -} \ No newline at end of file +} diff --git a/inc/customize-configs/options-blog-posts.php b/inc/customize-configs/options-blog-posts.php index 6fd6aaf2..74a698ae 100644 --- a/inc/customize-configs/options-blog-posts.php +++ b/inc/customize-configs/options-blog-posts.php @@ -51,3 +51,61 @@ 'description' => esc_html__( 'Hide placeholder if the post thumbnail not exists.', 'onepress' ), ) ); + +$wp_customize->add_setting( + 'onepress_blog_posts_settings_hr_layout', + array( + 'sanitize_callback' => 'onepress_sanitize_text', + ) +); +$wp_customize->add_control( + new OnePress_Misc_Control( + $wp_customize, + 'onepress_blog_posts_settings_hr_layout', + array( + 'section' => 'onepress_blog_posts', + 'type' => 'hr', + ) + ) +); + +$wp_customize->add_setting( + 'onepress_blog_posts_layout', + array( + 'default' => 'list', + 'sanitize_callback' => 'onepress_sanitize_news_layout', + ) +); +$wp_customize->add_control( + 'onepress_blog_posts_layout', + array( + 'label' => esc_html__( 'Blog listing layout', 'onepress' ), + 'section' => 'onepress_blog_posts', + 'type' => 'select', + 'choices' => array( + 'list' => esc_html__( 'List', 'onepress' ), + 'grid' => esc_html__( 'Grid', 'onepress' ), + ), + 'description' => esc_html__( 'Applies to blog index, archives, and post listings that use the theme templates.', 'onepress' ), + ) +); + +$wp_customize->add_setting( + 'onepress_blog_posts_grid_columns', + array( + 'default' => '2 2 1', + 'sanitize_callback' => 'onepress_sanitize_news_grid_columns', + ) +); +$wp_customize->add_control( + 'onepress_blog_posts_grid_columns', + array( + 'label' => esc_html__( 'Grid: columns per breakpoint', 'onepress' ), + 'section' => 'onepress_blog_posts', + 'type' => 'text', + 'input_attrs' => array( + 'placeholder' => '3 2 1', + ), + 'description' => esc_html__( 'Three numbers separated by spaces: desktop, tablet, mobile (e.g. 3 2 1). Use 1, 2, 3, 4, 6, or 12 so columns divide the 12-column grid evenly.', 'onepress' ), + ) +); diff --git a/inc/customize-configs/options-colors.php b/inc/customize-configs/options-colors.php index b7451fae..bfec62c6 100644 --- a/inc/customize-configs/options-colors.php +++ b/inc/customize-configs/options-colors.php @@ -18,6 +18,12 @@ 'sanitize_callback' => 'sanitize_hex_color_no_hash', 'sanitize_js_callback' => 'maybe_hash_hex_color', 'default' => '#03c4eb', + // Since 2.4.1: live preview via `customizer-liveview.js`. The + // handler updates `--wp--preset--color--primary` on the iframe + // `:root` — every SCSS consumer (`variables.$primary`) and every + // inline rule emitted by `template-tags.php` reads through that + // var, so a single style.setProperty propagates instantly. + 'transport' => 'postMessage', ) ); $wp_customize->add_control( @@ -44,6 +50,8 @@ 'sanitize_callback' => 'sanitize_hex_color_no_hash', 'sanitize_js_callback' => 'maybe_hash_hex_color', 'default' => '#333333', + // Since 2.4.1: live preview — see note on `onepress_primary_color`. + 'transport' => 'postMessage', ) ); $wp_customize->add_control( diff --git a/inc/customize-configs/section-news.php b/inc/customize-configs/section-news.php index 5253a78a..ba9a7e6c 100644 --- a/inc/customize-configs/section-news.php +++ b/inc/customize-configs/section-news.php @@ -111,6 +111,47 @@ ) ) ); +$wp_customize->add_setting( + 'onepress_news_layout', + array( + 'default' => 'list', + 'sanitize_callback' => 'onepress_sanitize_news_layout', + ) +); +$wp_customize->add_control( + 'onepress_news_layout', + array( + 'label' => esc_html__( 'Blog layout', 'onepress' ), + 'section' => 'onepress_news_settings', + 'type' => 'select', + 'choices' => array( + 'list' => esc_html__( 'List', 'onepress' ), + 'grid' => esc_html__( 'Grid', 'onepress' ), + ), + 'description' => esc_html__( 'List shows one post per row. Grid shows multiple columns on wide screens.', 'onepress' ), + ) +); + +$wp_customize->add_setting( + 'onepress_news_grid_columns', + array( + 'default' => '2 2 1', + 'sanitize_callback' => 'onepress_sanitize_news_grid_columns', + ) +); +$wp_customize->add_control( + 'onepress_news_grid_columns', + array( + 'label' => esc_html__( 'Number columns to show', 'onepress' ), + 'section' => 'onepress_news_settings', + 'type' => 'text', + 'input_attrs' => array( + 'placeholder' => '3 2 1', + ), + 'description' => esc_html__( 'One string of three numbers separated by spaces: desktop, tablet, then mobile (e.g. 3 2 1). Use 1, 2, 3, 4, 6, or 12 so columns divide the 12-column grid evenly.', 'onepress' ), + ) +); + /** * @since 2.1.0 */ diff --git a/inc/customize-configs/section-videolightbox.php b/inc/customize-configs/section-videolightbox.php index 68654fd5..d6846734 100644 --- a/inc/customize-configs/section-videolightbox.php +++ b/inc/customize-configs/section-videolightbox.php @@ -84,6 +84,27 @@ ) ); +// Media Library: stores id + url (JSON). Parsed on the front end for the popup link (URL). +$wp_customize->add_setting( + 'onepress_videolightbox_media_url', + array( + 'sanitize_callback' => 'onepress_sanitize_media_control_mixed', + 'default' => '', + ) +); +$wp_customize->add_control( + new OnePress_Media_Control( + $wp_customize, + 'onepress_videolightbox_media_url', + array( + 'label' => esc_html__( 'Video / image from Media Library', 'onepress' ), + 'section' => 'onepress_videolightbox_settings', + 'storage' => 'mixed', + 'description' => esc_html__( 'Select an image or video file. Both attachment ID and file URL are saved. If a URL is available, it is used instead of the YouTube/Vimeo field above.', 'onepress' ), + ) + ) +); + // Parallax image $wp_customize->add_setting( 'onepress_videolightbox_image', array( diff --git a/inc/customize-controls/control-media.php b/inc/customize-controls/control-media.php new file mode 100644 index 00000000..c56dda75 --- /dev/null +++ b/inc/customize-controls/control-media.php @@ -0,0 +1,472 @@ + $url, + 'kind' => $kind, + ); +} + +class OnePress_Media_Control extends WP_Customize_Control { + + /** + * @var bool + */ + protected static $inline_css_added = false; + + /** + * @var string Control type. + */ + public $type = 'onepress_media'; + + /** + * How the setting is stored: 'url', 'id', or 'mixed' (JSON object). + * + * @var string + */ + public $storage = 'url'; + + /** + * @inheritdoc + */ + public function to_json() { + parent::to_json(); + $this->json['storage'] = $this->storage; + } + + /** + * Enqueue control scripts. + */ + public function enqueue() { + wp_enqueue_media(); + $css = ' + .onepress-media-wrap { margin-top: 6px; } + .onepress-media-preview { + margin-bottom: 10px; + border: 1px solid #c3c4c7; + border-radius: 2px; + background: #f6f7f7; + min-height: 120px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + } + .onepress-media-preview-empty { + color: #646970; + font-size: 13px; + padding: 16px; + text-align: center; + } + .onepress-media-preview.is-empty .onepress-media-preview-empty { display: block; } + .onepress-media-preview:not(.is-empty) .onepress-media-preview-empty { display: none !important; } + .onepress-media-preview-img, + .onepress-media-preview-video { + display: block; + max-width: 100%; + height: auto; + max-height: 220px; + margin: 0 auto; + vertical-align: middle; + } + .onepress-media-preview-video { width: 100%; background: #000; } + .onepress-media-preview.is-image .onepress-media-preview-img { display: block; } + .onepress-media-preview.is-image .onepress-media-preview-video { display: none !important; } + .onepress-media-preview.is-video .onepress-media-preview-video { display: block; } + .onepress-media-preview.is-video .onepress-media-preview-img { display: none !important; } + .onepress-media-preview.is-empty .onepress-media-preview-img, + .onepress-media-preview.is-empty .onepress-media-preview-video { display: none !important; } + '; + if ( ! self::$inline_css_added ) { + self::$inline_css_added = true; + wp_add_inline_style( 'customize-controls', $css ); + } + parent::enqueue(); + } + + /** + * Serialized value kept for Customizer transport (hidden, not shown). + * + * @param mixed $value Setting value. + * @return string + */ + protected function format_hidden_value( $value ) { + switch ( $this->storage ) { + case 'id': + return ( $value !== '' && $value !== null ) ? (string) absint( $value ) : ''; + case 'mixed': + if ( $value === '' || $value === null ) { + return ''; + } + if ( is_string( $value ) ) { + $decoded = json_decode( $value, true ); + if ( is_array( $decoded ) ) { + return wp_json_encode( + array( + 'id' => isset( $decoded['id'] ) ? absint( $decoded['id'] ) : 0, + 'url' => isset( $decoded['url'] ) ? (string) $decoded['url'] : '', + ) + ); + } + } + + return ''; + case 'url': + default: + return is_string( $value ) ? $value : ''; + } + } + + /** + * Render control markup. + */ + public function render_content() { + $setting_id = $this->setting->id; + $storage = in_array( $this->storage, array( 'url', 'id', 'mixed' ), true ) ? $this->storage : 'url'; + $hidden_val = $this->format_hidden_value( $this->value() ); + $preview = onepress_media_control_get_preview_data( $this->value(), $storage ); + $kind = $preview['kind']; + $purl = $preview['url']; + $wrap_class = 'onepress-media-preview is-empty'; + if ( 'image' === $kind ) { + $wrap_class = 'onepress-media-preview is-image'; + } elseif ( 'video' === $kind ) { + $wrap_class = 'onepress-media-preview is-video'; + } + ?> + label ) ) : ?> + label ); ?> + + description ) ) : ?> + description ); ?> + +
+
+ + + +
+ +

+ + +

+
+ + + defined_values; } elseif ( is_array( $this->defined_values ) && ! empty ( $this->defined_values ) ) { @@ -128,18 +131,35 @@ public function to_json() { } /** + * Always surface every row in the Section Order & Styling control. + * + * Previously this loop set `__visibility = 'hidden'` on any row whose + * `section_id` mapped to an inactive section in the theme's + * `onepress_sections_settings` option. The RepeatableItem component + * then promoted that to a `.visibility-hidden` CSS class which the + * Customizer stylesheet collapses to `height: 0`. Net effect: a row + * the user had previously disabled (via the Sections admin page) + * disappeared completely from the Customizer, with no in-Customizer + * way to re-enable, reorder, or edit it. The user had to bounce out + * to the Sections admin to flip the switch, then come back — + * trapping the only recovery path outside the Customizer. + * + * Inactive rows now stay visible. The per-row `show_section` + * checkbox inside the item is the user-facing toggle for hiding a + * section from the rendered page; the Customizer list itself stays + * a complete inventory. + * + * `__visibility` is still emitted (empty string) so any downstream + * code that reads the field doesn't see undefined. + * * @since 2.1.1 + * @since 2.4.2 Always-visible — no more `is_section_active` gating. */ if ( $this->id_key == 'section_id' ) { - foreach ( ( array ) $value as $k => $v ) { - - if ( ! Onepress_Config::is_section_active( $v['section_id'] ) ) { - $value[ $k ]['__visibility'] = 'hidden'; - } else { - $value[ $k ]['__visibility'] = ''; - } - } - } + foreach ( (array) $value as $k => $v ) { + $value[ $k ]['__visibility'] = ''; + } + } $this->json['live_title_id'] = $this->live_title_id; $this->json['title_format'] = $this->title_format; @@ -149,7 +169,22 @@ public function to_json() { $this->json['default_empty_title'] = $this->default_empty_title; $this->json['value'] = $value; $this->json['id_key'] = $this->id_key; - $this->json['fields'] = $this->fields; + + // Sanitize fields data before passing to JavaScript + $sanitized_fields = array(); + foreach ($this->fields as $key => $field) { + $sanitized_fields[$key] = $field; + if (isset($field['title'])) { + // Allow safe HTML tags in title (like , , etc.) + $sanitized_fields[$key]['title'] = wp_kses_post($field['title']); + } + if (isset($field['desc'])) { + // Allow safe HTML tags in description (like , , ,

, etc.) + // wp_kses_post() removes dangerous tags like - esc_html__( 'Please setup your main Latitude & Longitude first', 'onepress' ), + 'multiple_map_notice' => esc_html__('Please setup your main Latitude & Longitude first', 'onepress'), ); - wp_enqueue_script( 'onepress-customizer', get_template_directory_uri() . '/assets/js/customizer.js', array( 'customize-controls', 'wp-color-picker' ), time() ); - wp_enqueue_style( 'onepress-customizer', get_template_directory_uri() . '/assets/css/customizer.css' ); - - wp_localize_script( 'onepress-customizer', 'ONEPRESS_CUSTOMIZER_DATA', $customizer_data ); + $handle = onepress_load_build_script('customizer', ['customize-controls', 'wp-color-picker'], true); + wp_localize_script($handle, 'ONEPRESS_CUSTOMIZER_DATA', $customizer_data); + } -add_action( 'customize_controls_enqueue_scripts', 'onepres_customizer_control_scripts', 99 ); -add_action( 'customize_controls_enqueue_scripts', array( 'OnePress_Editor_Scripts', 'enqueue' ), 95 ); +add_action('customize_controls_enqueue_scripts', 'onepres_customizer_control_scripts', 99); +add_action('customize_controls_enqueue_scripts', array('OnePress_Editor_Scripts', 'enqueue'), 95); diff --git a/inc/customizer-selective-refresh.php b/inc/customizer-selective-refresh.php index dfe12842..87ef4352 100644 --- a/inc/customizer-selective-refresh.php +++ b/inc/customizer-selective-refresh.php @@ -137,6 +137,8 @@ function onepress_customizer_partials( $wp_customize ) { 'onepress_news_cat', 'onepress_news_orderby', 'onepress_news_order', + 'onepress_news_layout', + 'onepress_news_grid_columns', ), ), @@ -177,6 +179,7 @@ function onepress_customizer_partials( $wp_customize ) { 'settings' => array( 'onepress_videolightbox_title', 'onepress_videolightbox_url', + 'onepress_videolightbox_media_url', ), ), diff --git a/inc/customizer.php b/inc/customizer.php index 53236f33..4993296a 100644 --- a/inc/customizer.php +++ b/inc/customizer.php @@ -165,8 +165,9 @@ function onepress_customize_register($wp_customize) * Binds JS handlers to make Theme Customizer preview reload changes asynchronously. */ function onepress_customize_preview_js() -{ - wp_enqueue_script('onepress_customizer_liveview', get_template_directory_uri() . '/assets/js/customizer-liveview.js', array('customize-preview', 'customize-selective-refresh'), false, true); +{ + $handle = onepress_load_build_script('customizer-liveview', ['customize-preview', 'customize-selective-refresh'], true); + wp_enqueue_script($handle); } add_action('customize_preview_init', 'onepress_customize_preview_js', 65); @@ -203,6 +204,9 @@ function onepress_customize_controls_enqueue_scripts() 'c_icon_picker_js_setup', array( 'search' => esc_html__('Search', 'onepress'), + 'svg_code' => esc_html__('Svg Code', 'onepress'), + 'apply_svg' => esc_html__( 'Apply', 'onepress' ), + 'svg_placeholder' => esc_html__( 'Paste SVG markup here…', 'onepress' ), 'fonts' => array( 'font-awesome' => array( // Name of icon diff --git a/inc/extras.php b/inc/extras.php index 6ab0b3e6..77a24bdf 100644 --- a/inc/extras.php +++ b/inc/extras.php @@ -214,6 +214,163 @@ function onepress_get_layout( $default = 'right-sidebar' ) { } +if ( ! function_exists( 'onepress_get_layout_for_post_id' ) ) { + /** + * Resolve effective sidebar layout for a given post ID — editor-safe. + * + * Mirrors the priority order applied at frontend render time, but is + * callable in admin / REST contexts (no query loop required): + * + * 1. Page template (`_wp_page_template` meta, pages only): + * template-fullwidth.php → no-sidebar + * template-fullwidth-stretched.php → no-sidebar + * template-left-sidebar.php → left-sidebar + * 2. Single Layout Sidebar (`single_layout` mod, post type `post` + * only), unless its value is empty or `'default'`. + * 3. Site Layout (`onepress_layout` mod) — global default. + * + * Intentionally does NOT replicate `onepress_get_layout()`'s + * WooCommerce sidebar-empty fallback: in the editor we cannot inspect + * runtime widget state, and on the frontend that branch is already + * handled by `onepress_get_layout()` itself. + * + * @since 2.4.1 + * @param int $post_id + * @return string One of: `'no-sidebar' | 'left-sidebar' | 'right-sidebar'`. + */ + function onepress_get_layout_for_post_id( $post_id ) { + $post_id = absint( $post_id ); + if ( $post_id <= 0 ) { + return get_theme_mod( 'onepress_layout', 'right-sidebar' ); + } + + $post_type = get_post_type( $post_id ); + + // 1) Page template — highest priority (pages only). + if ( $post_type === 'page' ) { + $template = (string) get_post_meta( $post_id, '_wp_page_template', true ); + if ( $template === 'template-fullwidth-stretched.php' ) { + // Since 2.4.1: stretched is its own layout key so the + // content-size resolver can emit `100vw` instead of the + // sidebar/no-sidebar pixel bases. Existing template + // markup hardcodes `

` so other + // `.no-sidebar` CSS rules still apply at render time. + return 'stretched'; + } + if ( $template === 'template-fullwidth.php' ) { + return 'no-sidebar'; + } + if ( $template === 'template-left-sidebar.php' ) { + return 'left-sidebar'; + } + } + + // 2) Single Layout Sidebar — middle priority (posts only). + if ( $post_type === 'post' ) { + $single = get_theme_mod( 'single_layout', 'default' ); + if ( $single !== '' && $single !== 'default' ) { + return $single; + } + } + + // 3) Site Layout — global fallback. + return get_theme_mod( 'onepress_layout', 'right-sidebar' ); + } +} + + +if ( ! function_exists( 'onepress_resolve_content_width_css' ) ) { + /** + * Resolve the effective `--wp--style--global--content-size` CSS value + * (string with unit) for a given layout + post type. + * + * Layout → value mapping: + * - `'stretched'` → `'100vw'` + * - `'no-sidebar'` → `px` + * - `'left-sidebar'` / `'right-sidebar'` → `px` + * + * Base values live in `theme.json` (single source of truth): + * - No-sidebar base : `settings.layout.contentSize` (default 1110px) + * - Sidebar base : `settings.custom.sidebarContentSize` (default 790px, + * derived from + * grid math) + * + * Override layer: + * - Post type `post` with `single_layout_content_width` > 0 → user value + * wins, regardless of layout (matches the Customizer-as-explicit-override + * mental model). Posts cannot be on a stretched template so the user + * mod never clashes with the `100vw` branch. + * + * @since 2.4.1 + * @param string $layout One of `'no-sidebar' | 'left-sidebar' | 'right-sidebar' | 'stretched'`. + * @param string $post_type Post type slug. + * @return string CSS value including unit (e.g. `'790px'`, `'1110px'`, `'100vw'`). + * Callers should compare against `onepress_get_no_sidebar_base_px() . 'px'` + * to decide whether to skip emit (theme.json default). + */ + function onepress_resolve_content_width_css( $layout, $post_type ) { + // Stretched template → bleed to viewport edge. + if ( $layout === 'stretched' ) { + return '100vw'; + } + + // User override (single posts only) — wins over base. Posts can't + // be on a stretched template, so this branch never collides with + // the `100vw` path above. + if ( $post_type === 'post' ) { + $user = absint( get_theme_mod( 'single_layout_content_width' ) ); + if ( $user > 0 ) { + return $user . 'px'; + } + } + + // Pixel bases from theme.json. + $has_sidebar = ( $layout === 'left-sidebar' || $layout === 'right-sidebar' ); + $base = $has_sidebar + ? onepress_get_sidebar_base_px() + : onepress_get_no_sidebar_base_px(); + + return $base . 'px'; + } +} + + +if ( ! function_exists( 'onepress_get_no_sidebar_base_px' ) ) { + /** + * Read the no-sidebar content-size base (theme.json `layout.contentSize`) + * as an integer pixel value. Used by emit channels to decide whether + * to skip the `:root` override (when the resolved value equals the + * theme.json default, WP's auto-emitted rule already provides it). + * + * @since 2.4.1 + * @return int + */ + function onepress_get_no_sidebar_base_px() { + $settings = function_exists( 'wp_get_global_settings' ) ? wp_get_global_settings() : array(); + return isset( $settings['layout']['contentSize'] ) + ? (int) $settings['layout']['contentSize'] + : 1110; + } +} + + +if ( ! function_exists( 'onepress_get_sidebar_base_px' ) ) { + /** + * Read the sidebar content-size base (theme.json + * `settings.custom.sidebarContentSize`) as an integer pixel value. + * + * @since 2.4.1 + * @return int + */ + function onepress_get_sidebar_base_px() { + $settings = function_exists( 'wp_get_global_settings' ) ? wp_get_global_settings() : array(); + return isset( $settings['custom']['sidebarContentSize'] ) + ? (int) $settings['custom']['sidebarContentSize'] + : 790; + } +} + + /** * Woocommerce Support */ @@ -248,6 +405,219 @@ function onepress_get_video_lightbox_image() { } } +if ( ! function_exists( 'onepress_is_self_hosted_video_file_url' ) ) { + /** + * True when URL points to a direct video file (not YouTube/Vimeo/etc. embed). + * + * @param string $url URL. + * @return bool + */ + function onepress_is_self_hosted_video_file_url( $url ) { + if ( ! is_string( $url ) || $url === '' ) { + return false; + } + if ( preg_match( '#youtu(\.be|be\.com)|vimeo\.com|dai\.ly|dailymotion\.com/embed|vk\.com/video_ext#i', $url ) ) { + return false; + } + $path = wp_parse_url( $url, PHP_URL_PATH ); + $ext = strtolower( pathinfo( (string) $path, PATHINFO_EXTENSION ) ); + + return in_array( $ext, array( 'mp4', 'webm', 'ogv', 'ogg', 'mov', 'm4v', 'avi', 'mkv' ), true ); + } +} + +if ( ! function_exists( 'onepress_videolightbox_mime_for_video_url' ) ) { + /** + * @param string $url Video file URL. + * @return string MIME type for HTML5 . + */ + function onepress_videolightbox_mime_for_video_url( $url ) { + $path = wp_parse_url( (string) $url, PHP_URL_PATH ); + $ext = strtolower( pathinfo( (string) $path, PATHINFO_EXTENSION ) ); + $map = array( + 'mp4' => 'video/mp4', + 'webm' => 'video/webm', + 'ogv' => 'video/ogg', + 'ogg' => 'video/ogg', + 'mov' => 'video/quicktime', + 'm4v' => 'video/x-m4v', + 'avi' => 'video/x-msvideo', + 'mkv' => 'video/x-matroska', + ); + + return isset( $map[ $ext ] ) ? $map[ $ext ] : 'video/mp4'; + } +} + +if ( ! function_exists( 'onepress_videolightbox_lightgallery_data_html' ) ) { + /** + * Markup for lightGallery 1.x HTML5 video (stored in data-html; requires lg-html5 class). + * + * @param string $src_url Absolute URL to video file. + * @param string $mime Optional MIME (from attachment); falls back from extension. + * @return string Raw HTML (escape with esc_attr() when placing in data-html). + */ + function onepress_videolightbox_lightgallery_data_html( $src_url, $mime = '' ) { + $safe_url = esc_url( $src_url ); + if ( $safe_url === '' ) { + return ''; + } + $mime = is_string( $mime ) && $mime !== '' ? $mime : onepress_videolightbox_mime_for_video_url( $safe_url ); + + return ''; + } +} + +if ( ! function_exists( 'onepress_videolightbox_poster_url' ) ) { + /** + * Optional poster for self-hosted lightbox (section background image mod). + * + * @return string URL or empty. + */ + function onepress_videolightbox_poster_url() { + $image_id = get_theme_mod( 'onepress_videolightbox_image' ); + $image_id = absint( $image_id ); + if ( ! $image_id ) { + return ''; + } + $url = wp_get_attachment_image_url( $image_id, 'full' ); + + return $url ? esc_url( $url ) : ''; + } +} + +if ( ! function_exists( 'onepress_sanitize_news_layout' ) ) { + /** + * @param string $value Raw value. + * @return string 'list'|'grid' + */ + function onepress_sanitize_news_layout( $value ) { + $value = is_string( $value ) ? $value : 'list'; + + return in_array( $value, array( 'list', 'grid' ), true ) ? $value : 'list'; + } +} + +if ( ! function_exists( 'onepress_news_snap_columns_per_row' ) ) { + /** + * Snap to a row count that divides the 12-column Bootstrap grid evenly. + * + * @param int $n Desired columns per row. + * @return int One of 1, 2, 3, 4, 6, 12. + */ + function onepress_news_snap_columns_per_row( $n ) { + $allowed = array( 1, 2, 3, 4, 6, 12 ); + $n = absint( $n ); + if ( $n < 1 ) { + $n = 1; + } + if ( in_array( $n, $allowed, true ) ) { + return $n; + } + $best = 1; + foreach ( $allowed as $a ) { + if ( abs( $a - $n ) < abs( $best - $n ) ) { + $best = $a; + } + } + + return $best; + } +} + +if ( ! function_exists( 'onepress_news_columns_per_row_to_span' ) ) { + /** + * Bootstrap 3 column span (out of 12) for equal columns per row. + * + * @param int $cols_per_row Columns per row (1–12, snapped). + * @return int Span 1–12. + */ + function onepress_news_columns_per_row_to_span( $cols_per_row ) { + $c = onepress_news_snap_columns_per_row( $cols_per_row ); + $map = array( + 1 => 12, + 2 => 6, + 3 => 4, + 4 => 3, + 6 => 2, + 12 => 1, + ); + + return isset( $map[ $c ] ) ? $map[ $c ] : 4; + } +} + +if ( ! function_exists( 'onepress_sanitize_news_grid_columns' ) ) { + /** + * Three integers "desktop tablet mobile" (space-separated), e.g. "3 2 1". + * + * @param string $input Raw. + * @return string Normalized string. + */ + function onepress_sanitize_news_grid_columns( $input ) { + $input = trim( preg_replace( '/\s+/', ' ', (string) $input ) ); + if ( $input === '' ) { + return '3 2 1'; + } + $parts = explode( ' ', $input ); + $parts = array_pad( $parts, 3, '1' ); + $out = array(); + foreach ( array_slice( $parts, 0, 3 ) as $p ) { + $out[] = (string) onepress_news_snap_columns_per_row( absint( $p ) ); + } + + return implode( ' ', $out ); + } +} + +if ( ! function_exists( 'onepress_parse_news_grid_columns' ) ) { + /** + * @param string $string Theme mod value. + * @return array{ lg: int, md: int, xs: int } Bootstrap span integers for col-lg-*, col-md-*, col-xs-*. + */ + function onepress_parse_news_grid_columns( $string ) { + $string = trim( (string) $string ); + if ( $string === '' ) { + $string = '3 2 1'; + } + $parts = preg_split( '/\s+/', $string ); + $parts = array_pad( $parts, 3, '1' ); + + return array( + 'lg' => onepress_news_columns_per_row_to_span( $parts[0] ), + 'md' => onepress_news_columns_per_row_to_span( $parts[1] ), + 'xs' => onepress_news_columns_per_row_to_span( $parts[2] ), + ); + } +} + +if ( ! function_exists( 'onepress_get_blog_posts_loop_layout_config' ) ) { + /** + * Blog / archive listing layout (Customizer: Blog Posts). + * + * @return array{ layout: string, is_grid: bool, grid_col_class: string } + */ + function onepress_get_blog_posts_loop_layout_config() { + $layout = onepress_sanitize_news_layout( get_theme_mod( 'onepress_blog_posts_layout', 'list' ) ); + $grid_col_class = ''; + if ( $layout === 'grid' ) { + $spans = onepress_parse_news_grid_columns( get_theme_mod( 'onepress_blog_posts_grid_columns', '2 2 1' ) ); + $grid_col_class = sprintf( + 'col-lg-%d col-md-%d col-xs-%d blog-posts-loop__col', + (int) $spans['lg'], + (int) $spans['md'], + (int) $spans['xs'] + ); + } + + return array( + 'layout' => $layout, + 'is_grid' => ( $layout === 'grid' ), + 'grid_col_class' => $grid_col_class, + ); + } +} + if ( ! function_exists( 'onepress_before_section' ) ) { /** diff --git a/inc/sanitize.php b/inc/sanitize.php index eaa698fb..d53c115a 100644 --- a/inc/sanitize.php +++ b/inc/sanitize.php @@ -1,279 +1,1110 @@ -]*?>.*?@si', '', $string); - $string = wp_strip_all_tags($string); - return trim($string); -} - - -function onepress_sanitize_color_alpha($color) -{ - $color = str_replace('#', '', $color); - if ('' === $color) { - return ''; - } - - // 3 or 6 hex digits, or the empty string. - if (preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', '#' . $color)) { - // convert to rgb - $colour = $color; - if (strlen($colour) == 6) { - list($r, $g, $b) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]); - } elseif (strlen($colour) == 3) { - list($r, $g, $b) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]); - } else { - return false; - } - $r = hexdec($r); - $g = hexdec($g); - $b = hexdec($b); - return 'rgba(' . join( - ',', - array( - 'r' => $r, - 'g' => $g, - 'b' => $b, - 'a' => 1, - ) - ) . ')'; - } - - return strpos(trim($color), 'rgb') !== false ? $color : false; -} - - -/** - * Sanitize repeatable data - * - * @param $input - * @param $setting object $wp_customize - * @return bool|mixed|string|void - */ -function onepress_sanitize_repeatable_data_field($input, $setting) -{ - $control = $setting->manager->get_control($setting->id); - - $fields = $control->fields; - if (is_string($input)) { - $input = json_decode(wp_unslash($input), true); - } - $data = wp_parse_args($input, array()); - - if (!is_array($data)) { - return false; - } - if (!isset($data['_items'])) { - return false; - } - $data = $data['_items']; - - foreach ($data as $i => $item_data) { - foreach ($item_data as $id => $value) { - - if (isset($fields[$id])) { - switch (strtolower($fields[$id]['type'])) { - case 'text': - $data[$i][$id] = sanitize_text_field($value); - break; - case 'textarea': - case 'editor': - $data[$i][$id] = wp_kses_post($value); - break; - case 'color': - $data[$i][$id] = sanitize_hex_color_no_hash($value); - break; - case 'coloralpha': - $data[$i][$id] = onepress_sanitize_color_alpha($value); - break; - case 'checkbox': - $data[$i][$id] = onepress_sanitize_checkbox($value); - break; - case 'select': - $data[$i][$id] = ''; - if (is_array($fields[$id]['options']) && !empty($fields[$id]['options'])) { - // if is multiple choices - if (is_array($value)) { - foreach ($value as $k => $v) { - if (isset($fields[$id]['options'][$v])) { - $value[$k] = $v; - } - } - $data[$i][$id] = $value; - } else { // is single choice - if (isset($fields[$id]['options'][$value])) { - $data[$i][$id] = $value; - } - } - } - - break; - case 'radio': - $data[$i][$id] = sanitize_text_field($value); - break; - case 'media': - $value = wp_parse_args( - $value, - array( - 'url' => '', - 'id' => false, - ) - ); - $value['id'] = absint($value['id']); - $data[$i][$id]['url'] = sanitize_text_field($value['url']); - - if ($url = wp_get_attachment_url($value['id'])) { - $data[$i][$id]['id'] = $value['id']; - $data[$i][$id]['url'] = $url; - } else { - $data[$i][$id]['id'] = ''; - } - - break; - default: - $data[$i][$id] = wp_kses_post($value); - } - } else { - $data[$i][$id] = wp_kses_post($value); - } - - if (is_array($data) && is_array($fields) && count($data[$i]) != count($fields)) { - foreach ($fields as $k => $f) { - if (!isset($data[$i][$k])) { - $data[$i][$k] = ''; - } - } - } - } - } - - return $data; -} - - -function onepress_sanitize_file_url($file_url) -{ - $output = ''; - $filetype = wp_check_filetype($file_url); - if ($filetype['ext']) { - $output = esc_url($file_url); - } - return $output; -} - - -/** - * Conditional to show more hero settings - * - * @param $control - * @return bool - */ -function onepress_hero_fullscreen_callback($control) -{ - $value = $control->manager->get_setting('onepress_hero_fullscreen')->value(); - if ('' == $value || !$value) { - return true; - } else { - return false; - } -} - -/** - * Sanitize select choices - * - * @param $input - * @param null $setting - * - * @return string - */ -function onepress_sanitize_select($input, $setting = null) -{ - - // input must be a slug: lowercase alphanumeric characters, dashes and underscores are allowed only - $input = sanitize_key($input); - - // get the list of possible select options - if ($setting) { - $choices = $setting->manager->get_control($setting->id)->choices; - - // return input if valid or return default option - return (array_key_exists($input, $choices) ? $input : $setting->default); - } else { - return $input; - } -} - - -function onepress_sanitize_number($input) -{ - return balanceTags($input); -} - -function onepress_sanitize_hex_color($color) -{ - if ($color === '') { - return ''; - } - if (preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $color)) { - return $color; - } - return null; -} - -function onepress_sanitize_checkbox($input) -{ - if ($input == 1) { - return 1; - } else { - return 0; - } -} - -function onepress_sanitize_text($string) -{ - return wp_kses_post(balanceTags($string)); -} - -function onepress_sanitize_html_input($string) -{ - return wp_kses_allowed_html($string); -} - -function onepress_showon_frontpage() -{ - return is_page_template('template-frontpage.php'); -} - -function onepress_gallery_source_validate($validity, $value) -{ - if (!class_exists('OnePress_Plus')) { - if ($value != 'page') { - $validity->add('notice', sprintf( - /* translators: 1: feature name */ - esc_html__('Upgrade to %1s to unlock this feature.', 'onepress'), 'OnePress Plus')); - } - } - return $validity; -} +]*?>.*?@si', '', $string); + $string = wp_strip_all_tags($string); + return trim($string); +} + + +/** + * Whether a string contains patterns disallowed inside a single CSS value (XSS / injection). + * + * @param string $color Color candidate. + * @return bool + */ +function onepress_css_color_has_injection($color) +{ + if ($color === '') { + return false; + } + if (preg_match('/[\x00-\x1F\x7F]/', $color)) { + return true; + } + if (preg_match('/[<>"\'\\\\]|\/\*|\*\/|!important|@import|expression\s*\(|url\s*\(|javascript\s*:/i', $color)) { + return true; + } + if (strpos($color, ';') !== false || strpos($color, '!') !== false) { + return true; + } + return false; +} + +/** + * Root functions allowed for CSS (Level 4+), plus safe var(). + * + * @return array + */ +function onepress_css_color_allowed_functions_map() +{ + static $map = null; + + if ($map === null) { + $funcs = array( + 'rgb', + 'rgba', + 'hsl', + 'hsla', + 'hwb', + 'lab', + 'lch', + 'oklab', + 'oklch', + 'color', + 'device-cmyk', + 'color-mix', + 'light-dark', + 'gray', + ); + $map = array_fill_keys($funcs, true); + } + + return $map; +} + +/** + * Sanitize a CSS value: supports hex, transparent/currentColor, rgb/hsl/hwb/lab/lch/oklab/oklch/color/device-cmyk, + * color-mix, light-dark, gray(), and var(--custom-property). Named colors (e.g. red, aliceblue) are not accepted. + * + * @param mixed $color Raw input. + * @param int $depth Internal recursion guard (e.g. var() fallbacks). + * @return string Safe color or empty string if invalid. + */ +function onepress_sanitize_css_color($color, $depth = 0) +{ + $color = is_string($color) ? trim($color) : ''; + if ($color === '') { + return ''; + } + + if ($depth > 5) { + return ''; + } + + if (strlen($color) > 512) { + return ''; + } + + if (onepress_css_color_has_injection($color)) { + return ''; + } + + $lower = strtolower($color); + if ('transparent' === $lower) { + return 'transparent'; + } + if ('currentcolor' === $lower) { + return 'currentColor'; + } + + if (preg_match('/^var\(\s*--[a-zA-Z0-9_-]+\s*\)$/', $color)) { + return $color; + } + + if (preg_match('/^var\(\s*(--[a-zA-Z0-9_-]+)\s*,\s*(.+)\)$/s', $color, $vm)) { + $fallback = onepress_sanitize_css_color(trim($vm[2]), $depth + 1); + if ('' === $fallback) { + return ''; + } + return 'var(' . $vm[1] . ', ' . $fallback . ')'; + } + + if (preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color)) { + return $color; + } + + if (preg_match('/^([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color)) { + return '#' . $color; + } + + if (! preg_match('/^([a-zA-Z][-a-zA-Z0-9]*)\s*\((.*)\)\s*$/s', $color, $m)) { + return ''; + } + + $fname = strtolower($m[1]); + $inner = $m[2]; + + if (! isset(onepress_css_color_allowed_functions_map()[$fname])) { + return ''; + } + + if (! onepress_css_color_parens_balanced($inner)) { + return ''; + } + + // Allow numbers, keywords (in, none, from, srgb, display-p3, etc.), whitespace, calc operators, underscores (e.g. var fallbacks / custom idents). + if (preg_match('/[^a-zA-Z0-9\s.,%#\/+()_*_-]/', $inner)) { + return ''; + } + + return $color; +} + +/** + * @param string $s Inner of parentheses. + * @return bool + */ +function onepress_css_color_parens_balanced($s) +{ + $depth = 0; + $len = strlen($s); + for ($i = 0; $i < $len; $i++) { + $c = $s[ $i ]; + if ('(' === $c) { + $depth++; + } elseif (')' === $c) { + $depth--; + if ($depth < 0) { + return false; + } + } + } + return 0 === $depth; +} + +/** + * Sanitize color values used in theme options / repeatable fields (alpha-capable CSS colors). + * + * @param mixed $color Raw input. + * @return string + */ +function onepress_sanitize_color_alpha($color) +{ + return onepress_sanitize_css_color($color); +} + + +/** + * Allowed HTML for inline SVG in repeatable icon field (Customizer + front). + * Broad allowlist for common icon sets (Tabler, Heroicons, Feather, MDI, sprites, gradients). + * Excludes foreignObject, script, animation (SMIL), feImage (arbitrary URLs). `style` uses Core CSS sanitizer. + * + * @return array> + */ +function onepress_svg_allowed_html() +{ + $id_class = array( + 'id' => true, + 'class' => true, + ); + // wp_kses runs safecss_filter_attr() on style values. + $style_transform = array( + 'style' => true, + 'transform' => true, + ); + $stroke_attrs = array( + 'stroke' => true, + 'stroke-width' => true, + 'stroke-linecap' => true, + 'stroke-linejoin' => true, + 'stroke-dasharray' => true, + 'stroke-dashoffset' => true, + 'stroke-miterlimit' => true, + 'stroke-opacity' => true, + ); + $fill_paint = array( + 'fill' => true, + 'opacity' => true, + 'fill-opacity' => true, + 'fill-rule' => true, + 'clip-rule' => true, + 'color' => true, + 'paint-order' => true, + 'vector-effect' => true, + ); + $ref_clip = array( + 'clip-path' => true, + 'mask' => true, + ); + $effects = array( + 'filter' => true, + 'marker-start' => true, + 'marker-mid' => true, + 'marker-end' => true, + ); + + $shape_core = array_merge( $id_class, $style_transform, $stroke_attrs, $fill_paint, $ref_clip, $effects ); + + return array( + 'svg' => array_merge( + array( + 'xmlns' => true, + 'xmlns:xlink' => true, + 'version' => true, + 'viewbox' => true, + 'viewBox' => true, + 'preserveaspectratio' => true, + 'overflow' => true, + 'x' => true, + 'y' => true, + 'aria-hidden' => true, + 'aria-label' => true, + 'role' => true, + 'focusable' => true, + ), + $shape_core + ), + 'g' => $shape_core, + 'path' => array_merge( + array( 'd' => true ), + $shape_core + ), + 'circle' => array_merge( + array( + 'cx' => true, + 'cy' => true, + 'r' => true, + ), + $shape_core + ), + 'rect' => array_merge( + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'rx' => true, + 'ry' => true, + ), + $shape_core + ), + 'line' => array_merge( + array( + 'x1' => true, + 'y1' => true, + 'x2' => true, + 'y2' => true, + ), + $shape_core + ), + 'polyline' => array_merge( + array( 'points' => true ), + $shape_core + ), + 'polygon' => array_merge( + array( 'points' => true ), + $shape_core + ), + 'ellipse' => array_merge( + array( + 'cx' => true, + 'cy' => true, + 'rx' => true, + 'ry' => true, + ), + $shape_core + ), + 'text' => array_merge( + $shape_core, + array( + 'x' => true, + 'y' => true, + 'dx' => true, + 'dy' => true, + 'text-anchor' => true, + 'dominant-baseline' => true, + 'textlength' => true, + 'lengthadjust' => true, + ) + ), + 'tspan' => array_merge( + $shape_core, + array( + 'x' => true, + 'y' => true, + 'dx' => true, + 'dy' => true, + 'text-anchor' => true, + 'dominant-baseline' => true, + ) + ), + 'title' => array(), + 'desc' => array(), + 'metadata' => array(), + 'defs' => array_merge( + $id_class, + array( 'style' => true ) + ), + 'symbol' => array_merge( + $id_class, + $style_transform, + $stroke_attrs, + $fill_paint, + $ref_clip, + $effects, + array( + 'viewbox' => true, + 'viewBox' => true, + 'overflow' => true, + 'preserveaspectratio' => true, + ) + ), + 'use' => array_merge( + $id_class, + $style_transform, + array( + 'href' => true, + 'xlink:href' => true, + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'fill' => true, + 'opacity' => true, + ), + $stroke_attrs + ), + 'image' => array_merge( + $id_class, + $style_transform, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'href' => true, + 'xlink:href' => true, + 'preserveaspectratio' => true, + ) + ), + 'clippath' => array_merge( + $id_class, + $style_transform, + array( 'clippathunits' => true ) + ), + 'mask' => array_merge( + $id_class, + $style_transform, + array( + 'maskunits' => true, + 'maskcontentunits' => true, + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + ) + ), + 'pattern' => array_merge( + $id_class, + $style_transform, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'patternunits' => true, + 'patterncontentunits' => true, + 'patterntransform' => true, + 'preserveaspectratio' => true, + 'overflow' => true, + ) + ), + 'marker' => array_merge( + $id_class, + $style_transform, + array( + 'markerwidth' => true, + 'markerheight' => true, + 'refx' => true, + 'refy' => true, + 'orient' => true, + 'overflow' => true, + 'preserveaspectratio' => true, + ) + ), + 'lineargradient' => array_merge( + $id_class, + array( + 'x1' => true, + 'x2' => true, + 'y1' => true, + 'y2' => true, + 'gradientunits' => true, + 'gradienttransform' => true, + 'spreadmethod' => true, + 'href' => true, + 'xlink:href' => true, + ) + ), + 'radialgradient' => array_merge( + $id_class, + array( + 'cx' => true, + 'cy' => true, + 'r' => true, + 'fx' => true, + 'fy' => true, + 'fr' => true, + 'gradientunits' => true, + 'gradienttransform' => true, + 'spreadmethod' => true, + 'href' => true, + 'xlink:href' => true, + ) + ), + 'switch' => $shape_core, + 'filter' => array_merge( + $id_class, + $style_transform, + array( + 'x' => true, + 'y' => true, + 'width' => true, + 'height' => true, + 'filterunits' => true, + 'primitiveunits' => true, + 'color-interpolation-filters' => true, + ) + ), + 'fegaussianblur' => array( + 'stddeviation' => true, + 'in' => true, + 'result' => true, + ), + 'fecolormatrix' => array( + 'in' => true, + 'type' => true, + 'values' => true, + 'result' => true, + ), + 'feoffset' => array( + 'dx' => true, + 'dy' => true, + 'in' => true, + 'result' => true, + ), + 'feblend' => array( + 'in' => true, + 'in2' => true, + 'mode' => true, + 'result' => true, + ), + 'fecomposite' => array( + 'in' => true, + 'in2' => true, + 'operator' => true, + 'k1' => true, + 'k2' => true, + 'k3' => true, + 'k4' => true, + 'result' => true, + ), + 'feflood' => array( + 'flood-color' => true, + 'flood-opacity' => true, + 'result' => true, + ), + 'femerge' => array(), + 'femergenode' => array( + 'in' => true, + ), + 'stop' => array_merge( + $id_class, + array( + 'offset' => true, + 'stop-color' => true, + 'stop-opacity' => true, + ) + ), + ); +} + +/** + * Whether a string is inline SVG used for icon fields (Customizer / front-end). + * + * @param mixed $value Value to test. + * @return bool + */ +function onepress_is_svg_icon_markup($value) +{ + if (!is_string($value)) { + return false; + } + $v = trim($value); + $v = preg_replace('/^\xEF\xBB\xBF/', '', $v); + $v = preg_replace('/^\x{FEFF}/u', '', $v); + $v = preg_replace('/^\s*<\?xml\b[^>]*>\s*/i', '', $v); + $v = preg_replace('/^\s*]*>\s*/i', '', $v); + return (bool) preg_match('/^\s*<\s*svg[\s>]/i', $v); +} + +/** + * Undo extra backslashes before quotes and "]*>\s*/i', '', $out); + $out = preg_replace('/^\s*]*>\s*/i', '', $out); + $out = preg_replace('~(?i)]*>/is', $sanitized, $san_m) || !preg_match('/]*>/is', $original, $orig_m)) { + return $sanitized; + } + $san_open = $san_m[0]; + $orig_tag = $orig_m[0]; + $names = array( + 'stroke', + 'stroke-width', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-miterlimit', + 'stroke-opacity', + 'fill', + 'fill-opacity', + 'fill-rule', + 'color', + 'opacity', + ); + $parts = array(); + foreach ($names as $name) { + $qn = preg_quote($name, '/'); + if (preg_match('/' . $qn . '\s*=/i', $san_open)) { + continue; + } + if (preg_match('/' . $qn . '\s*=\s*"([^"]*)"/i', $orig_tag, $m) + || preg_match('/' . $qn . "\s*=\s*'([^']*)'/i", $orig_tag, $m)) { + $parts[] = $name . '="' . esc_attr($m[1]) . '"'; + } + } + if ($parts === array()) { + return $sanitized; + } + $blob = implode(' ', $parts); + return preg_replace('/|null Decoded array or null on failure. + */ +function onepress_repeatable_sanitize_decode_json_string($raw) +{ + if (!is_string($raw) || $raw === '') { + return null; + } + $flags = defined('JSON_INVALID_UTF8_SUBSTITUTE') ? JSON_INVALID_UTF8_SUBSTITUTE : 0; + // Customize passes values already wp_unslash()'d; unslashing again strips JSON's \" and breaks decode. + $decoded = json_decode($raw, true, 512, $flags); + if (JSON_ERROR_NONE !== json_last_error()) { + $decoded = json_decode(wp_unslash($raw), true, 512, $flags); + } + if (JSON_ERROR_NONE !== json_last_error()) { + return null; + } + return is_array($decoded) ? $decoded : null; +} + +/** + * Normalize repeatable payload to a list of row arrays. + * Accepts JS shape { _items: [...] } or a plain list [...] (e.g. after theme_mod unserialize). + * + * @param mixed $data Decoded array. + * @return array>|null Rows or null if invalid. + */ +function onepress_repeatable_sanitize_extract_rows($data) +{ + if (!is_array($data)) { + return null; + } + if (isset($data['_items']) && is_array($data['_items'])) { + return $data['_items']; + } + if ($data === array()) { + return array(); + } + foreach ($data as $key => $row) { + $list_key = is_int($key) || (is_string($key) && ctype_digit($key)); + if (!$list_key || !is_array($row)) { + return null; + } + } + return array_values($data); +} + +/** + * Decode a repeatable theme_mod: JSON string or array, and unwrap JS shape { _items: [...] }. + * + * @param mixed $value Raw get_theme_mod value. + * @return array> + */ +function onepress_normalize_repeatable_theme_mod_rows($value) +{ + if (is_string($value)) { + $decoded = json_decode($value, true); + $value = (JSON_ERROR_NONE === json_last_error() && is_array($decoded)) ? $decoded : array(); + } + if (!is_array($value)) { + return array(); + } + if (isset($value['_items']) && is_array($value['_items'])) { + return $value['_items']; + } + return $value; +} + +/** + * Sanitize repeatable data + * + * @param $input + * @param $setting object $wp_customize + * @return bool|mixed|string|void + */ +function onepress_sanitize_repeatable_data_field($input, $setting) +{ + $control = $setting->manager->get_control($setting->id); + if (!$control || !is_array($control->fields)) { + return false; + } + + $fields = $control->fields; + + if (is_string($input)) { + $decoded = onepress_repeatable_sanitize_decode_json_string($input); + if ($decoded === null) { + return false; + } + $input = $decoded; + } elseif (is_object($input)) { + $encoded = wp_json_encode($input); + $input = is_string($encoded) ? json_decode($encoded, true) : null; + } + + if (!is_array($input)) { + return false; + } + + $data = onepress_repeatable_sanitize_extract_rows($input); + if ($data === null) { + return false; + } + + foreach ($data as $i => $item_data) { + if (!is_array($item_data)) { + $item_data = array(); + $data[$i] = array(); + } + foreach ($item_data as $id => $value) { + + if (isset($fields[$id])) { + switch (strtolower($fields[$id]['type'])) { + case 'text': + $data[$i][$id] = sanitize_text_field($value); + break; + case 'textarea': + case 'editor': + $data[$i][$id] = wp_kses_post($value); + break; + case 'color': + $data[$i][$id] = sanitize_hex_color_no_hash($value); + break; + case 'coloralpha': + $data[$i][$id] = onepress_sanitize_color_alpha($value); + break; + case 'checkbox': + $data[$i][$id] = onepress_sanitize_checkbox($value); + break; + case 'select': + $data[$i][$id] = ''; + if (is_array($fields[$id]['options']) && !empty($fields[$id]['options'])) { + // if is multiple choices + if (is_array($value)) { + foreach ($value as $k => $v) { + if (isset($fields[$id]['options'][$v])) { + $value[$k] = $v; + } + } + $data[$i][$id] = $value; + } else { // is single choice + if (isset($fields[$id]['options'][$value])) { + $data[$i][$id] = $value; + } + } + } + + break; + case 'radio': + $data[$i][$id] = sanitize_text_field($value); + break; + case 'add_by': + // Only "click" marks user-added rows; empty = theme default sections. + $v = is_string($value) ? trim($value) : ''; + $data[$i][$id] = ( 'click' === $v ) ? 'click' : ''; + break; + case 'icon': + $data[$i][$id] = onepress_sanitize_repeatable_icon($value); + break; + case 'media': + $value = wp_parse_args( + $value, + array( + 'url' => '', + 'id' => false, + ) + ); + $value['id'] = absint($value['id']); + + // Validate and sanitize URL + $url = sanitize_text_field($value['url']); + // Only allow http/https URLs for security + if (!empty($url) && !preg_match('/^https?:\/\//', $url)) { + $url = ''; + } + $url = esc_url_raw($url); + $data[$i][$id]['url'] = $url; + + if ($url && $value['id'] && ($attachment_url = wp_get_attachment_url($value['id']))) { + $data[$i][$id]['id'] = $value['id']; + $data[$i][$id]['url'] = esc_url_raw($attachment_url); + } else { + if (empty($url)) { + $data[$i][$id]['id'] = ''; + } else { + $data[$i][$id]['id'] = $value['id']; + } + } + + break; + default: + $data[$i][$id] = wp_kses_post($value); + } + } else { + $data[$i][$id] = wp_kses_post($value); + } + + if (is_array($data) && is_array($fields) && count($data[$i]) != count($fields)) { + foreach ($fields as $k => $f) { + if (!isset($data[$i][$k])) { + $data[$i][$k] = ''; + } + } + } + } + } + + return $data; +} + + +function onepress_sanitize_file_url($file_url) +{ + $output = ''; + $filetype = wp_check_filetype($file_url); + if ($filetype['ext']) { + $output = esc_url($file_url); + } + return $output; +} + + +/** + * Conditional to show more hero settings + * + * @param $control + * @return bool + */ +function onepress_hero_fullscreen_callback($control) +{ + $value = $control->manager->get_setting('onepress_hero_fullscreen')->value(); + if ('' == $value || !$value) { + return true; + } else { + return false; + } +} + +/** + * Sanitize select choices + * + * @param $input + * @param null $setting + * + * @return string + */ +function onepress_sanitize_select($input, $setting = null) +{ + + // input must be a slug: lowercase alphanumeric characters, dashes and underscores are allowed only + $input = sanitize_key($input); + + // get the list of possible select options + if ($setting) { + $choices = $setting->manager->get_control($setting->id)->choices; + + // return input if valid or return default option + return (array_key_exists($input, $choices) ? $input : $setting->default); + } else { + return $input; + } +} + + +function onepress_sanitize_number($input) +{ + return balanceTags($input); +} + +function onepress_sanitize_hex_color($color) +{ + if ($color === '') { + return ''; + } + if (preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', $color)) { + return $color; + } + return null; +} + +function onepress_sanitize_text($string) +{ + return wp_kses_post(balanceTags($string)); +} + +function onepress_sanitize_html_input($string) +{ + return wp_kses_allowed_html($string); +} + +if (! function_exists('onepress_sanitize_media_control_value')) { + /** + * Sanitize OnePress_Media_Control values for theme_mod / customize. + * + * @param mixed $input Raw value from Customizer (string, array, or JSON string). + * @param string $storage One of 'url', 'id', 'mixed'. + * @return string|int For 'id' returns int; otherwise string (URL or JSON for mixed). + */ + function onepress_sanitize_media_control_value($input, $storage = 'url') + { + $storage = in_array($storage, array('url', 'id', 'mixed'), true) ? $storage : 'url'; + + if ($input === null || $input === false || $input === '') { + return 'id' === $storage ? 0 : ''; + } + + switch ($storage) { + case 'id': + return absint($input); + case 'mixed': + $id = 0; + $url = ''; + if (is_array($input)) { + $id = isset($input['id']) ? absint($input['id']) : 0; + $url = isset($input['url']) ? esc_url_raw($input['url']) : ''; + } elseif (is_string($input)) { + $decoded = json_decode(wp_unslash($input), true); + if (is_array($decoded)) { + $id = isset($decoded['id']) ? absint($decoded['id']) : 0; + $url = isset($decoded['url']) ? esc_url_raw($decoded['url']) : ''; + } + } + if ($id <= 0 && $url === '') { + return ''; + } + + return wp_json_encode( + array( + 'id' => $id, + 'url' => $url, + ) + ); + case 'url': + default: + return esc_url_raw($input); + } + } +} + +if (! function_exists('onepress_sanitize_media_control_url')) { + function onepress_sanitize_media_control_url($input) + { + return onepress_sanitize_media_control_value($input, 'url'); + } +} + +if (! function_exists('onepress_sanitize_media_control_id')) { + function onepress_sanitize_media_control_id($input) + { + return onepress_sanitize_media_control_value($input, 'id'); + } +} + +if (! function_exists('onepress_sanitize_media_control_mixed')) { + function onepress_sanitize_media_control_mixed($input) + { + return onepress_sanitize_media_control_value($input, 'mixed'); + } +} + +if (! function_exists('onepress_parse_media_control_value')) { + /** + * Normalize stored media control value to id + url (for templates). + * + * @param mixed $raw theme_mod or similar. + * @return array{ id: int, url: string } + */ + function onepress_parse_media_control_value($raw) + { + $out = array( + 'id' => 0, + 'url' => '', + ); + if ($raw === null || $raw === false || $raw === '') { + return $out; + } + if (is_array($raw)) { + $out['id'] = isset($raw['id']) ? absint($raw['id']) : 0; + $out['url'] = isset($raw['url']) ? esc_url_raw($raw['url']) : ''; + + return $out; + } + if (is_numeric($raw)) { + $out['id'] = absint($raw); + if ($out['id']) { + $u = wp_get_attachment_url($out['id']); + $out['url'] = $u ? esc_url_raw($u) : ''; + } + + return $out; + } + if (is_string($raw)) { + $trim = trim($raw); + if ($trim === '') { + return $out; + } + $decoded = json_decode($raw, true); + if (is_array($decoded) && (isset($decoded['id']) || isset($decoded['url']))) { + $out['id'] = isset($decoded['id']) ? absint($decoded['id']) : 0; + $out['url'] = isset($decoded['url']) ? esc_url_raw($decoded['url']) : ''; + if ($out['url'] === '' && $out['id']) { + $u = wp_get_attachment_url($out['id']); + $out['url'] = $u ? esc_url_raw($u) : ''; + } + + return $out; + } + if (preg_match('/^\d+$/', $trim)) { + $out['id'] = absint($trim); + if ($out['id']) { + $u = wp_get_attachment_url($out['id']); + $out['url'] = $u ? esc_url_raw($u) : ''; + } + + return $out; + } + $out['url'] = esc_url_raw($raw); + } + + return $out; + } +} + +function onepress_showon_frontpage() +{ + return is_page_template('template-frontpage.php'); +} + +function onepress_gallery_source_validate($validity, $value) +{ + if (!class_exists('OnePress_Plus')) { + if ($value != 'page') { + $validity->add('notice', sprintf( + /* translators: 1: feature name */ + esc_html__('Upgrade to %1s to unlock this feature.', 'onepress'), 'OnePress Plus')); + } + } + return $validity; +} diff --git a/inc/template-tags.php b/inc/template-tags.php index 2e7a5949..0ceb9279 100644 --- a/inc/template-tags.php +++ b/inc/template-tags.php @@ -129,9 +129,9 @@ function onepress_site_logo() if (! $hide_sitetile) { $classes['title'] = 'has-title'; if (is_front_page() && ! is_home()) { - $html .= '

'; + $html .= '

'; } else { - $html .= '

'; + $html .= '

'; } } @@ -139,7 +139,7 @@ function onepress_site_logo() $description = get_bloginfo('description', 'display'); if ($description || is_customize_preview()) { $classes['desc'] = 'has-desc'; - $html .= '

' . $description . '

'; + $html .= '

' . esc_html($description) . '

'; } } else { $classes['desc'] = 'no-desc'; @@ -542,41 +542,34 @@ function onepress_comment($comment, $args, $depth) */ function onepress_hex_to_rgba($color, $alpha = 1) { - $color = str_replace('#', '', $color); - if ('' === $color) { + if (! function_exists('onepress_sanitize_css_color')) { return ''; } - if (strpos(trim($color), 'rgb') !== false) { - return $color; + $safe = onepress_sanitize_css_color($color); + if ('' === $safe) { + return ''; } - // 3 or 6 hex digits, or the empty string. - if (preg_match('|^#([A-Fa-f0-9]{3}){1,2}$|', '#' . $color)) { - // convert to rgb - $colour = $color; - if (strlen($colour) == 6) { - list($r, $g, $b) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]); - } elseif (strlen($colour) == 3) { - list($r, $g, $b) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]); + // 3- or 6-digit hex: combine with $alpha (legacy hero overlay behaviour). + if (preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $safe)) { + $hex = substr($safe, 1); + if (3 === strlen($hex)) { + $r = hexdec($hex[0] . $hex[0]); + $g = hexdec($hex[1] . $hex[1]); + $b = hexdec($hex[2] . $hex[2]); } else { - return false; + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); } - $r = hexdec($r); - $g = hexdec($g); - $b = hexdec($b); - return 'rgba(' . join( - ',', - array( - 'r' => $r, - 'g' => $g, - 'b' => $b, - 'a' => $alpha, - ) - ) . ')'; + $alpha = is_numeric($alpha) ? (float) $alpha : 1; + $alpha = max(0, min(1, $alpha)); + return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $alpha . ')'; } - return false; + // 4/8-digit hex, rgb/hsl/color()/var(), etc.: already a full CSS color. + return $safe; } } @@ -660,6 +653,13 @@ function onepress_custom_inline_style() )` + * instead of the saved hex. Customizer changes now propagate via the + * CSS var (server-side: `inc/theme-json-bridge.php`; client-side + * live preview: `customizer-liveview.js`). The wrapping IF guards, + * variable computation, and selector groups are kept as-is to + * minimise diff. */ $primary = sanitize_hex_color_no_hash(get_theme_mod('onepress_primary_color')); if ($primary != '') { ?> @@ -670,8 +670,7 @@ function onepress_custom_inline_style() .btn-theme-primary-outline, .sidebar .widget a:hover, .section-services .service-item .service-image i, .counter_item .counter__number, .team-member .member-thumb .member-profile a:hover, .icon-background-default { - color: #; + color: var(--wp--preset--color--primary, #03c4eb); } input[type="reset"], input[type="submit"], input[type="submit"], input[type="reset"]:hover, input[type="submit"]:hover, input[type="submit"]:hover .nav-links a:hover, .btn-theme-primary, .btn-theme-primary-outline:hover, .section-testimonials .card-theme-primary, .woocommerce #respond input#submit, .woocommerce a.button, .woocommerce button.button, .woocommerce input.button, .woocommerce button.button.alt, @@ -682,13 +681,18 @@ function onepress_custom_inline_style() .nav-links .page-numbers:hover, .nav-links .page-numbers.current { - background: #; + background: var(--wp--preset--color--primary, #03c4eb); } .btn-theme-primary-outline, .btn-theme-primary-outline:hover, .pricing__item:hover, .section-testimonials .card-theme-primary, .entry-content blockquote { - border-color : #; + border-color : var(--wp--preset--color--primary, #03c4eb); + } + /* Feature item icon (FA stack + SVG variants) — set the CSS + variable so both .icon-background-default (color) and + .feature-icon-svg-wrap (background-color) pick up the primary + color via _sections.scss's var(--icon-bg-color). */ + .feature-item { + --icon-bg-color: var(--wp--preset--color--primary, #03c4eb); } @@ -696,15 +700,13 @@ function onepress_custom_inline_style() .woocommerce a.button.alt, .woocommerce button.button.alt, .woocommerce input.button.alt { - background-color: #; + background-color: var(--wp--preset--color--primary, #03c4eb); } .woocommerce #respond input#submit.alt:hover, .woocommerce a.button.alt:hover, .woocommerce button.button.alt:hover, .woocommerce input.button.alt:hover { - background-color: #; + background-color: var(--wp--preset--color--primary, #03c4eb); } px; } 0) { - $value = $content_width . 'px'; - echo '.single-post .site-main, .single-post .entry-content > * { max-width: ' . $value . '; }'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + // Since 2.4.1: emit a `:root` override for `--wp--style--global--content-size` + // based on the resolved sidebar layout + post type. Sidebar layouts + // shrink the default cap from 1110 → 790 (theme.json `custom.sidebarContentSize`); + // single posts let `single_layout_content_width` override either base. + // + // Gated by `is_singular()` so 404 / search / archive / home keep + // the theme.json default — per spec, those contexts don't take + // a sidebar-aware override. + if ( is_singular() ) { + $post_id = (int) get_queried_object_id(); + if ( $post_id > 0 + && function_exists( 'onepress_get_layout_for_post_id' ) + && function_exists( 'onepress_resolve_content_width_css' ) + && function_exists( 'onepress_get_no_sidebar_base_px' ) + ) { + $layout = onepress_get_layout_for_post_id( $post_id ); + $post_type = get_post_type( $post_id ); + $value = onepress_resolve_content_width_css( $layout, $post_type ); + $default = onepress_get_no_sidebar_base_px() . 'px'; + + // Skip when the resolved value matches the theme.json + // default — WP's auto-emitted `:root` already provides it. + // Note: `100vw` (stretched template) never matches the + // `px` default so it always emits. + if ( $value !== '' && $value !== $default ) { + // Whitelist allowed unit suffixes (`px` and `vw`) for + // belt-and-braces output safety, even though the + // resolver only ever returns these values. + if ( preg_match( '/^\d+(?:px|vw)$/', $value ) ) { + echo ':root { --wp--style--global--content-size: ' . esc_attr( $value ) . '; }'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + } } $css = ob_get_clean(); @@ -1081,10 +1116,7 @@ function onepress_custom_inline_style() */ function onepress_get_section_about_data() { - $boxes = get_theme_mod('onepress_about_boxes'); - if (is_string($boxes)) { - $boxes = json_decode($boxes, true); - } + $boxes = onepress_normalize_repeatable_theme_mod_rows(get_theme_mod('onepress_about_boxes')); $page_ids = array(); if (! empty($boxes) && is_array($boxes)) { foreach ($boxes as $k => $v) { @@ -1116,10 +1148,7 @@ function onepress_get_section_about_data() */ function onepress_get_section_counter_data() { - $boxes = get_theme_mod('onepress_counter_boxes'); - if (is_string($boxes)) { - $boxes = json_decode($boxes, true); - } + $boxes = onepress_normalize_repeatable_theme_mod_rows(get_theme_mod('onepress_counter_boxes')); if (empty($boxes) || ! is_array($boxes)) { $boxes = array(); } @@ -1135,10 +1164,7 @@ function onepress_get_section_counter_data() */ function onepress_get_section_services_data() { - $services = get_theme_mod('onepress_services'); - if (is_string($services)) { - $services = json_decode($services, true); - } + $services = onepress_normalize_repeatable_theme_mod_rows(get_theme_mod('onepress_services')); $page_ids = array(); if (! empty($services) && is_array($services)) { foreach ($services as $k => $v) { @@ -1171,10 +1197,7 @@ function onepress_get_section_services_data() */ function onepress_get_section_team_data() { - $members = get_theme_mod('onepress_team_members'); - if (is_string($members)) { - $members = json_decode($members, true); - } + $members = onepress_normalize_repeatable_theme_mod_rows(get_theme_mod('onepress_team_members')); if (! is_array($members)) { $members = array(); } @@ -1191,10 +1214,7 @@ function onepress_get_section_team_data() */ function onepress_get_features_data() { - $array = get_theme_mod('onepress_features_boxes'); - if (is_string($array)) { - $array = json_decode($array, true); - } + $array = onepress_normalize_repeatable_theme_mod_rows(get_theme_mod('onepress_features_boxes')); if (! empty($array) && is_array($array)) { foreach ($array as $k => $v) { $array[$k] = wp_parse_args( @@ -1207,9 +1227,9 @@ function onepress_get_features_data() ) ); - // Get/Set social icons + // Get/Set icon class prefix (skip for inline SVG). $array[$k]['icon'] = trim($array[$k]['icon']); - if ($array[$k]['icon'] != '' && strpos($array[$k]['icon'], 'fa') !== 0) { + if ($array[$k]['icon'] !== '' && ! onepress_is_svg_icon_markup($array[$k]['icon']) && strpos($array[$k]['icon'], 'fa') !== 0) { $array[$k]['icon'] = 'fa-' . $array[$k]['icon']; } } @@ -1251,6 +1271,10 @@ function onepress_get_social_profiles() $icons = array(); $array[$k]['icon'] = trim($array[$k]['icon']); + if ($array[$k]['icon'] !== '' && onepress_is_svg_icon_markup($array[$k]['icon'])) { + continue; + } + if ($array[$k]['icon'] != '' && strpos($array[$k]['icon'], 'fa') !== 0) { $icons[$array[$k]['icon']] = 'fa-' . $array[$k]['icon']; } else { @@ -1268,7 +1292,11 @@ function onepress_get_social_profiles() foreach ((array) $array as $s) { if ($s && $s['icon'] != '') { - $html .= ''; + if (onepress_is_svg_icon_markup($s['icon'])) { + $html .= '' . onepress_sanitize_inline_svg_markup($s['icon']) . ''; + } else { + $html .= ''; + } } } @@ -1576,7 +1604,7 @@ function onepress_gallery_generate($echo = true) if ($html) { $div .= ''; } @@ -1586,7 +1614,7 @@ function onepress_gallery_generate($echo = true) if ($html) { $div .= ''; } @@ -1595,7 +1623,7 @@ function onepress_gallery_generate($echo = true) $html = onepress_gallery_html($data, true, 'full'); if ($html) { $div .= ''; } break; @@ -1606,7 +1634,7 @@ function onepress_gallery_generate($echo = true) $row_height = absint(get_theme_mod('onepress_g_row_height', 120)); $div .= ''; } @@ -1616,7 +1644,7 @@ function onepress_gallery_generate($echo = true) if ($html) { $div .= ''; } diff --git a/inc/theme-json-bridge.php b/inc/theme-json-bridge.php new file mode 100644 index 00000000..d3c29e6d --- /dev/null +++ b/inc/theme-json-bridge.php @@ -0,0 +1,122 @@ + :where(…) { max-width: ; … }` + * from theme.json's `contentSize` value. Bridging the user's + * `single_layout_content_width` here bakes the user's number into that + * literal — which is exactly what we want to avoid. Instead we keep the + * theme.json default (`1110px`) intact and inject a single + * `:root { --wp--style--global--content-size: px; }` override into + * the editor canvas (see `inc/admin/class-editor.php::css()`) and the + * frontend (see `body.single-post` rule in `inc/template-tags.php`). + * Our editor SCSS rule (`.editor-styles-wrapper .wp-block:not(…)` in + * `_gutenberg.scss`) wins WP's `is-root-container` rule by specificity + * and consumes the var — so the visible cap reflects the user value + * without any literal `px` appearing in CSS. + * + * @package OnePress + */ + +if (! function_exists('onepress_filter_theme_json_palette')) { + + /** + * Override the `primary` and `secondary` palette entries from the + * Customizer mods. + * + * @param WP_Theme_JSON_Data $theme_json The merged theme.json data wrapper. + * @return WP_Theme_JSON_Data + */ + function onepress_filter_theme_json_palette($theme_json) + { + if (! is_object($theme_json) || ! method_exists($theme_json, 'get_data')) { + return $theme_json; + } + + // Map theme.json palette slug → Customizer mod name. Add new + // entries here when extending; nothing else in this function needs + // to change. + $bridge = array( + 'primary' => 'onepress_primary_color', + 'secondary' => 'onepress_secondary_color', + ); + + $overrides = array(); + foreach ($bridge as $slug => $mod) { + $raw = get_theme_mod($mod, ''); + if ($raw === '' || $raw === null) { + continue; + } + $hex = sanitize_hex_color('#' . ltrim($raw, '#')); + if ($hex) { + $overrides[ $slug ] = $hex; + } + } + + if (empty($overrides)) { + return $theme_json; + } + + $data = $theme_json->get_data(); + + if (! isset($data['settings']['color']['palette']) || ! is_array($data['settings']['color']['palette'])) { + return $theme_json; + } + + // WP wraps the palette by origin (`theme`, `default`, `user`). + // Mutate the `theme` array if present; otherwise fall back to + // treating the value as a flat list (older WP versions or test + // fixtures). + $palette_ref = &$data['settings']['color']['palette']; + $target = (isset($palette_ref['theme']) && is_array($palette_ref['theme'])) + ? $palette_ref['theme'] + : $palette_ref; + + $mutated = false; + foreach ($target as $i => $color) { + if (isset($color['slug']) && isset($overrides[ $color['slug'] ])) { + $target[ $i ]['color'] = $overrides[ $color['slug'] ]; + $mutated = true; + } + } + + if (! $mutated) { + return $theme_json; + } + + if (isset($palette_ref['theme']) && is_array($palette_ref['theme'])) { + $palette_ref['theme'] = $target; + } else { + $palette_ref = $target; + } + unset($palette_ref); + + if (method_exists($theme_json, 'update_with')) { + return $theme_json->update_with($data); + } + + return $theme_json; + } +} + +add_filter('wp_theme_json_data_theme', 'onepress_filter_theme_json_palette'); diff --git a/index.php b/index.php index 37aebcc9..3b079d80 100644 --- a/index.php +++ b/index.php @@ -14,14 +14,15 @@ get_header(); -$layout = onepress_get_layout(); +$layout = onepress_get_layout(); +$blog_loop = onepress_get_blog_posts_loop_layout_config(); ?>
-
+
@@ -31,20 +32,41 @@ - - - - - /* - * Include the Post-Format-specific template for the content. - * If you want to override this in a child theme, then include a file - * called content-___.php (where ___ is the Post Format name) and that will be used instead. - */ - get_template_part( 'template-parts/content', get_post_format() ); - ?> + +
+ - + + '; + } + /* + * Include the Post-Format-specific template for the content. + * If you want to override this in a child theme, then include a file + * called content-___.php (where ___ is the Post Format name) and that will be used instead. + */ + get_template_part( 'template-parts/content', get_post_format() ); + if ( $blog_loop['is_grid'] ) { + echo '
'; + } + endwhile; + ?> + + +
+ diff --git a/package.json b/package.json index 62fb3193..1d769711 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "onepress", - "version": "2.3.14", + "version": "2.3.19", "main": "Gruntfile.js", "engines": { "node": ">= 0.10.0" diff --git a/section-parts/section-about.php b/section-parts/section-about.php index a9b00b8e..02a6ba66 100644 --- a/section-parts/section-about.php +++ b/section-parts/section-about.php @@ -30,7 +30,7 @@ echo '

' . esc_html( $title ) . '

'; } ?> ' . wp_kses_post(apply_filters( 'onepress_the_content', $desc )) . '
'; + echo '
' . apply_filters( 'onepress_the_content', wp_kses_post($desc )) . '
'; } ?>
@@ -99,7 +99,7 @@ if ( $settings['enable_link'] ) { echo ''; } - echo wp_kses_post( get_the_title( $post ) ); + echo get_the_title( $post ); if ( $settings['enable_link'] ) { echo ''; } @@ -109,11 +109,11 @@ post_content ); + $content = apply_filters( 'the_content', wp_kses_post($post->post_content )); $content = str_replace( ']]>', ']]>', $content ); - echo wp_kses_post($content); + echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?> diff --git a/section-parts/section-contact.php b/section-parts/section-contact.php index 8c9e0525..452ba5e8 100644 --- a/section-parts/section-contact.php +++ b/section-parts/section-contact.php @@ -36,7 +36,7 @@ echo '

' . esc_html($onepress_contact_title) . '

'; } ?> ' . wp_kses_post(apply_filters('onepress_the_content', $desc)) . '
'; + echo '
' . apply_filters('onepress_the_content', wp_kses_post($desc)) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?>
@@ -59,7 +59,7 @@


diff --git a/section-parts/section-counter.php b/section-parts/section-counter.php index 10518bab..321bd886 100644 --- a/section-parts/section-counter.php +++ b/section-parts/section-counter.php @@ -27,7 +27,7 @@ class="' . esc_html( $title ) . '';} ?> ' . wp_kses_post(apply_filters( 'onepress_the_content', $desc )) . '
'; + echo '
' . apply_filters( 'onepress_the_content', wp_kses_post( $desc )) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?>
diff --git a/section-parts/section-features.php b/section-parts/section-features.php index 187dbf04..a009a705 100644 --- a/section-parts/section-features.php +++ b/section-parts/section-features.php @@ -21,7 +21,7 @@ class="' . esc_html($subtitle) . ''; ?> ' . esc_html($title) . ''; ?> ' . wp_kses_post(apply_filters( 'onepress_the_content', $desc ) ). '
'; + echo '
' . apply_filters( 'onepress_the_content', wp_kses_post( $desc ) ). '
'; } ?>
@@ -47,18 +47,25 @@ class=" '; + if ( onepress_is_svg_icon_markup( $f['icon'] ) ) { + $media = '' . onepress_sanitize_inline_svg_markup( $f['icon'] ) . ''; + } else { + $media = ' '; + } } ?> ' . esc_html($subtitle) . ''; ?> ' . esc_html($title) . ''; ?> ' . wp_kses_post( apply_filters( 'onepress_the_content', $desc ) ) . '
'; + echo '
' . apply_filters( 'onepress_the_content', wp_kses_post( $desc ) ) . '
'; } ?>
diff --git a/section-parts/section-hero.php b/section-parts/section-hero.php index cc8e3e7d..81e354c9 100644 --- a/section-parts/section-hero.php +++ b/section-parts/section-hero.php @@ -75,12 +75,12 @@ class="hero-slideshow-wrapper ">
' . wp_kses_post(apply_filters( 'the_content', do_shortcode( $hcl2_content ) ) ) . '
'; + echo '
' . apply_filters( 'the_content', do_shortcode( wp_kses_post( $hcl2_content ) ) ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped }; ?>
'; + echo ''.esc_attr( get_bloginfo( 'name' ) ).''; }; ?>
diff --git a/section-parts/section-news.php b/section-parts/section-news.php index 39b7dad9..21eea4e6 100644 --- a/section-parts/section-news.php +++ b/section-parts/section-news.php @@ -16,11 +16,20 @@ $more_text = get_theme_mod('onepress_news_more_text', esc_html__('Read Our Blog', 'onepress')); $desc = get_theme_mod('onepress_news_desc'); + $news_layout = onepress_sanitize_news_layout( get_theme_mod( 'onepress_news_layout', 'list' ) ); + $grid_columns = get_theme_mod( 'onepress_news_grid_columns', '2 2 1' ); + $section_class = apply_filters( 'onepress_section_class', 'section-news section-padding onepage-section', 'news' ); + if ( $news_layout === 'grid' ) { + $section_class .= ' section-news--layout-grid'; + } else { + $section_class .= ' section-news--layout-list'; + } + ?>
class=""> + } ?>" class="">
@@ -33,14 +42,14 @@ echo '

' . esc_html($title) . '

'; } ?> ' . wp_kses_post(apply_filters('onepress_the_content', $desc)) . '
'; + echo '
' . apply_filters('onepress_the_content', wp_kses_post($desc)) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?>
-
+
have_posts()) : ?> + + + +
+ + have_posts()) : $query->the_post(); ?> '; + } /* * Include the Post-Format-specific template for the content. * If you want to override this in a child theme, then include a file * called content-___.php (where ___ is the Post Format name) and that will be used instead. */ get_template_part('template-parts/content', 'list'); + if ( $news_layout === 'grid' ) { + echo '
'; + } ?> + + +
+ ' . esc_html( $title ) . '';} ?> ' .wp_kses_post( apply_filters( 'onepress_the_content', $desc ) ) . '
'; + echo '
' . apply_filters( 'onepress_the_content', wp_kses_post( $desc ) ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?>
@@ -69,11 +69,15 @@ } } elseif ( $settings['icon'] ) { $settings['icon'] = trim( $settings['icon'] ); - // Get/Set social icons - if ( $settings['icon'] != '' && strpos( $settings['icon'], 'fa' ) !== 0 ) { - $settings['icon'] = 'fa-' . $settings['icon']; + if ( onepress_is_svg_icon_markup( $settings['icon'] ) ) { + $media = '
' . onepress_sanitize_inline_svg_markup( $settings['icon'] ) . '
'; + } else { + $icon_class = $settings['icon']; + if ( $icon_class != '' && strpos( $icon_class, 'fa' ) !== 0 ) { + $icon_class = 'fa-' . $icon_class; + } + $media = '
'; } - $media = '
'; } if ( $layout == 12 ) { $classes = 'col-sm-12 col-lg-' . $layout; diff --git a/section-parts/section-team.php b/section-parts/section-team.php index 1b1aa7f7..d16e8f49 100644 --- a/section-parts/section-team.php +++ b/section-parts/section-team.php @@ -26,7 +26,7 @@ class="' . esc_html($subtitle) . ''; ?> ' . esc_html($title) . ''; ?> ' . wp_kses_post( apply_filters( 'onepress_the_content', $desc ) ) . '
'; + echo '
' . apply_filters( 'onepress_the_content', wp_kses_post( $desc ) ) . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } ?>
diff --git a/section-parts/section-videolightbox.php b/section-parts/section-videolightbox.php index 2522cb43..14b9e491 100644 --- a/section-parts/section-videolightbox.php +++ b/section-parts/section-videolightbox.php @@ -2,7 +2,11 @@ $id = get_theme_mod( 'onepress_videolightbox_id', 'videolightbox' ); $disable = get_theme_mod( 'onepress_videolightbox_disable' ) == 1 ? true : false; $heading = get_theme_mod( 'onepress_videolightbox_title' ); -$video = get_theme_mod( 'onepress_videolightbox_url' ); +$media = onepress_parse_media_control_value( get_theme_mod( 'onepress_videolightbox_media_url' ) ); +$video = $media['url']; +if ( $video === '' ) { + $video = get_theme_mod( 'onepress_videolightbox_url' ); +} if ( onepress_is_selective_refresh() ) { $disable = false; } @@ -15,16 +19,33 @@ class="
- -
- - - -
- - -

- + + + + +

+
diff --git a/src/admin/admin.js b/src/admin/admin.js new file mode 100644 index 00000000..84277f88 --- /dev/null +++ b/src/admin/admin.js @@ -0,0 +1,13 @@ +import './admin.scss'; + +jQuery(function ($) { + $('body').addClass('about-php'); + + $('.copy-settings-form').on('submit', function () { + const text = $(this).data('confirm'); + var c = confirm(text); + if (!c) { + return false; + } + }); +}); \ No newline at end of file diff --git a/assets/css/admin.css b/src/admin/admin.scss similarity index 100% rename from assets/css/admin.css rename to src/admin/admin.scss diff --git a/assets/js/customizer-liveview.js b/src/admin/customizer-liveview.js similarity index 64% rename from assets/js/customizer-liveview.js rename to src/admin/customizer-liveview.js index e0cb8b19..a5022252 100644 --- a/assets/js/customizer-liveview.js +++ b/src/admin/customizer-liveview.js @@ -104,5 +104,39 @@ } ); + /** + * Live preview for Site Colors (Primary / Secondary). + * + * Since 2.4.1: both mods use `transport: 'postMessage'`. Updating the + * `--wp--preset--color--{slug}` CSS variable on `:root` propagates to: + * - Every SCSS rule that references `variables.$primary` / `variables.$secondary` + * (they compile to `var(...)` consumers). + * - Every inline rule emitted by `template-tags.php` (refactored to + * reference the same var, no more hard-coded hex per request). + * + * The Customizer feeds either `#xxxxxx` or `xxxxxx` depending on the + * sanitize callbacks — normalise to a single leading `#` to keep CSS + * parsing happy. + */ + function onepressBindColorToCssVar( settingId, cssVarName ) { + wp.customize( settingId, function ( value ) { + value.bind( function ( to ) { + var hex = String( to || '' ).trim(); + if ( hex === '' ) { + document.documentElement.style.removeProperty( cssVarName ); + return; + } + if ( hex.charAt( 0 ) !== '#' ) { + hex = '#' + hex; + } + document.documentElement.style.setProperty( cssVarName, hex ); + } ); + } ); + } + + onepressBindColorToCssVar( 'onepress_primary_color', '--wp--preset--color--primary' ); + onepressBindColorToCssVar( 'onepress_secondary_color', '--wp--preset--color--secondary' ); + + } )( jQuery , wp.customize ); diff --git a/src/admin/customizer.js b/src/admin/customizer.js new file mode 100644 index 00000000..fe54f3a5 --- /dev/null +++ b/src/admin/customizer.js @@ -0,0 +1,31 @@ +import './customizer.scss'; +import '../frontend/fontawesome-v6/css/all.min.css'; + +import { installAlphaColorPicker } from './customizer/alpha-color-picker'; +import { registerAlphaColorControl } from './customizer/control-alpha-color'; +import { initControlBindings } from './customizer/control-bindings'; +import { registerRepeatableControl } from './customizer/control-repeatable'; +import { initIconPicker } from './customizer/icon-picker'; +import { installDeparam } from './customizer/jquery-deparam'; +import { initModalEditors } from './customizer/modal-editor'; +import { registerPlusSection } from './customizer/plus-section'; +import { installWpEditor } from './customizer/wp-editor'; + +const api = wp.customize; +const $ = jQuery; + +registerPlusSection(api); +installDeparam($); +installAlphaColorPicker($); +registerAlphaColorControl(api, $); +registerRepeatableControl(api, $); +installWpEditor($); +initModalEditors(api, $); + +jQuery(window).ready(function () { + initControlBindings($); +}); + +jQuery(document).ready(function () { + initIconPicker($); +}); diff --git a/src/admin/customizer.scss b/src/admin/customizer.scss new file mode 100644 index 00000000..ee1e8c4b --- /dev/null +++ b/src/admin/customizer.scss @@ -0,0 +1,678 @@ +li#accordion-panel-onepress_options > .accordion-section-title, +li#accordion-panel-onepress_typo > .accordion-section-title { + /* padding-left: 14px; */ +} + +.theme-action-count { + padding: 0 6px; + display: inline-block; + background-color: #d54e21; + color: #fff; + font-size: 9px; + line-height: 17px; + font-weight: 600; + margin: 1px 0 0 2px; + vertical-align: top; + border-radius: 10px; + z-index: 26; +} +.item-hidden, +.tem-add_by { + display: none; +} + +.no-changeable .remove-btn-wrapper, +.no-changeable .repeatable-actions { + display: none !important; +} + +.list-repeatable .sortable-placeholder { + height: 42px; + display: block; +} +.list-repeatable li { + margin-bottom: 0px; + padding: 5px 0px 5px; +} +.list-repeatable .widget { + margin-bottom: 0px; +} +.list-repeatable .widget-top:after { + clear: both; + display: block; + content: " "; +} +.list-repeatable .widget .widget-title-action .widget-action { + text-decoration: none; + position: relative; + font-size: 16px; + top: 12px; + outline: none !important; + box-shadow: none; + -webkit-box-shadow: none; +} +.list-repeatable .widget .widget-title-action .widget-action::after { + content: "\f140"; + font-family: "dashicons"; +} + +.list-repeatable .widget.explained .widget-title-action .widget-action::after { + content: "\f142"; +} + +.list-repeatable .widget .widget-inside { + display: none; +} +.list-repeatable .widget.explained .widget-inside { + display: block; +} +.list-repeatable .wp-picker-holder { + z-index: 99; +} +.list-repeatable .wp-picker-container.wp-picker-active label { + display: inline-block; +} +.list-repeatable .item { + margin-bottom: 15px; +} +.list-repeatable .item:first-child { + margin-top: 15px; +} +.list-repeatable .item label { + display: block; + margin-bottom: 10px; +} +.list-repeatable .item label.field-label { + font-weight: bold; + margin-bottom: 5px; +} +.list-repeatabl input:not([type="radio"]), +.list-repeatabl input:not([type="checkbox"]), +.list-repeatable .item select, +.list-repeatable .item textarea { + width: 100%; +} + +.list-repeatable .item-media .actions, +.repeatable-actions { + text-align: right; + margin-top: 10px; + display: flex; + gap: 10px; + flex-direction: column; +} +.media-actions { + flex-direction: row; +} +.list-repeatable .thumbnail-image { + margin-bottom: 10px; +} +.list-repeatable .thumbnail-image img { + width: 100%; + height: auto; +} + +/* Special element */ +.repeatable-customize-control.show-display-field-only + .widget-content + .item:not(.item-show_section) { + display: none !important; +} + +/* +.section-videolightbox .item-bg_video, +.section-videolightbox .item-bg_video, +.section-videolightbox .item-bg_video_webm, +.section-videolightbox .item-enable_parallax, +.section-videolightbox .item-bg_video_ogv { + display: none; +} +*/ + +.wp-customizer .preview-tablet .wp-full-overlay-main { + width: 768px; +} + +.wp-customizer .preview-mobile .wp-full-overlay-main { + height: 95dvh; +} + +.repeatable-customize-control.show-display-field-only + .widget-content + .item.item-show_section { + display: block; + margin-top: 15px; +} +.accordion-section-title .onepress-notice { + font-size: 10px; + text-transform: uppercase; + float: right; + margin-right: 25px; + line-height: 16px; + margin-top: 3px; + display: inline-block; + font-weight: normal; + background: #d54e21; + color: #ffffff; + padding: 0px 5px; +} + +/* Editor customize */ +body .wp-full-overlay { + z-index: 9999; +} + +.wp-js-editor-active textarea, +.onepress-editor-added textarea { + display: none; +} + +.wp-js-editor-preview { + background-color: #fff; + border: 1px solid #ddd; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07) inset; + color: #32373c; + outline: 0 none; + -webkit-transition: border-color 50ms ease-in-out 0s; + transition: border-color 50ms ease-in-out 0s; + min-height: 150px; + display: block; + padding: 2px 6px; +} +.wp-js-editor-preview.wpe-focus { + border-color: #5b9dd9; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07) inset; +} +.wp-js-editor-preview { + cursor: pointer; +} +.wp-js-editor-preview img { + max-width: 100%; + height: auto; +} +.modal-wp-js-editor * { + box-sizing: border-box; +} +.modal-wp-js-editor { + display: block; + position: absolute; + left: 0px; + width: 100%; + top: auto; + bottom: 0px; + height: 350px; + max-height: 100%; + background: #eeeeee; + padding: 15px; + border-top: 1px solid #ddd; + box-sizing: border-box; + -webkit-transition: all 0.5s; /* Safari */ + transition: all 0.5s; + + -ms-transform: translateY(100%); /* IE 9 */ + -webkit-transform: translateY(100%); /* Safari */ + transform: translateY(100%); +} +.modal-wp-js-editor .fullscreen-wp-editor span:before { + content: "\f211"; + font-family: "dashicons"; +} +.modal-wp-js-editor.fullscreen .fullscreen-wp-editor span:before { + content: "\f506"; + font-family: "dashicons"; +} +.modal-wp-js-editor.fullscreen { + height: 100%; +} +.modal-wp-js-editor .wp-editor-wrap { + height: 100%; + display: block; +} +.modal-wp-js-editor.wpe-active { + -ms-transform: translateY(0); /* IE 9 */ + -webkit-transform: translateY(0); /* Safari */ + transform: translateY(0); +} + +.modal-wp-js-editor textarea { + width: 100%; + display: block; +} +.wp-switch-editor.close-wp-editor { + color: #e34113; +} + +@media (max-width: 700px) { + .modal-wp-js-editor { + z-index: 99999; + border-top: 1px solid #ddd; + border-right: 0 none !important; + + -ms-transform: translateX(0); /* IE 9 */ + -webkit-transform: translateX(0); /* Safari */ + transform: translateX(0); + + -ms-transform: translateY(100%); /* IE 9 */ + -webkit-transform: translateY(100%); /* Safari */ + transform: translateY(100%); + } + .modal-wp-js-editor.wpe-active { + -ms-transform: translateY(0); /* IE 9 */ + -webkit-transform: translateY(0); /* Safari */ + transform: translateY(0); + } +} + +/* COLOR PICKER ALPHA */ + +/** + * Alpha Color Picker CSS + */ + +.customize-control-alpha-color .wp-picker-container .iris-picker { + border-bottom: none; +} + +.customize-control-alpha-color .wp-picker-container { + max-width: 257px; +} + +.customize-control-alpha-color .wp-picker-open + .wp-picker-input-wrap { + width: 100%; +} + +.customize-control-alpha-color + .wp-picker-input-wrap + input[type="text"].wp-color-picker.alpha-color-control { + float: left; + width: 195px; +} + +.customize-control-alpha-color .wp-picker-input-wrap .button { + margin-left: 0; + float: right; +} + +.wp-picker-container + .wp-picker-open + ~ .wp-picker-holder + .alpha-color-picker-container { + display: block; +} + +.alpha-color-picker-container { + border: 1px solid #dfdfdf; + border-top: none; + display: none; + background: #fff; + padding: 0 11px 10px; + position: relative; +} + +.alpha-color-picker-container .ui-widget-content, +.alpha-color-picker-container .ui-widget-header, +.alpha-color-picker-wrap .ui-state-focus { + background: transparent; + border: none; +} + +.alpha-color-picker-wrap a.iris-square-value:focus { + box-shadow: none; +} + +.alpha-color-picker-container .ui-slider { + position: relative; + z-index: 1; + height: 24px; + text-align: center; + margin: 0 auto; + width: 88%; + width: calc(100% - 28px); +} + +.alpha-color-picker-container .ui-slider-handle, +.alpha-color-picker-container .ui-widget-content .ui-state-default { + color: #777; + background-color: #fff; + text-shadow: 0 1px 0 #fff; + text-decoration: none; + position: absolute; + z-index: 2; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + border: 1px solid #aaa; + border-radius: 4px; + margin-top: -2px; + top: 0; + height: 26px; + width: 26px; + cursor: ew-resize; + font-size: 0; + padding: 0; + line-height: 27px; + margin-left: -14px; +} + +.alpha-color-picker-container .ui-slider-handle.show-opacity { + font-size: 12px; +} + +.alpha-color-picker-container .click-zone { + width: 14px; + height: 24px; + display: block; + position: absolute; + left: 10px; +} + +.alpha-color-picker-container .max-click-zone { + right: 10px; + left: auto; +} + +.alpha-color-picker-container .transparency { + height: 24px; + width: 100%; + background-color: #fff; + background-image: url(../images/transparency-grid.png); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.4) inset; + border-radius: 3px; + padding: 0; + margin-top: -24px; +} + +@media only screen and (max-width: 782px) { + .customize-control-alpha-color + .wp-picker-input-wrap + input[type="text"].wp-color-picker.alpha-color-control { + width: 184px; + } +} + +@media only screen and (max-width: 640px) { + .customize-control-alpha-color + .wp-picker-input-wrap + input[type="text"].wp-color-picker.alpha-color-control { + width: 172px; + height: 33px; + } +} + +/* Customizer ICON Picker */ +.icon-wrapper { + border: 1px solid #cccccc; + font-size: 24px; + height: 40px; + line-height: 40px; + text-align: center; + width: 40px; + cursor: pointer; +} +.icon-wrapper i:before { + font-size: 24px; + height: 40px; + line-height: 40px; + text-align: center; + width: 40px !important; +} +.c-icon-picker * { + box-sizing: border-box; +} +.c-icon-picker { + position: absolute; + top: 0px; + left: 0px; + bottom: 0px; + display: block; + width: 300px; + background: #eeeeee; + border-right: 1px solid #ddd; + + -webkit-transition: all 0.5s; /* Safari */ + transition: all 0.5s; + + -ms-transform: translateX(-100%); /* IE 9 */ + -webkit-transform: translateX(-100%); /* Safari */ + transform: translateX(-100%); +} +.c-icon-picker.ic-active { + -ms-transform: translateX(0); /* IE 9 */ + -webkit-transform: translateX(0); /* Safari */ + transform: translateX(0); +} +.c-icon-type { +} +.c-icon-type-wrap { + position: absolute; + top: 10px; + left: 10px; + right: 10px; +} +.c-icon-type-wrap select { + height: 30px; + width: 100%; +} +.c-icon-search { + position: absolute; + top: 44px; + left: 10px; + right: 10px; +} +.c-icon-search input { + width: 100%; + height: 25px; +} +.c-icon-list { + position: absolute; + top: 75px; + left: 0px; + right: 0px; + bottom: 10px; + padding: 0 8px 0; + overflow: auto; +} +.c-icon-list:after { + clear: both; + display: block; + content: " "; +} +.c-icon-list i { + font-size: 20px; + line-height: 35px; + color: #333333; +} +.c-icon-list span { + width: 35px; + height: 35px; + display: block; + float: left; + background: #ffffff; + text-align: center; + margin: 2px; + cursor: pointer; +} +.c-icon-list span:hover { + background: #e86240; +} +.c-icon-list span:hover i { + color: #ffffff; +} +.icon-wrapper.icon-editing { + background: #f5f5f5; + box-shadow: 0 0 2px rgba(30, 140, 190, 0.8); + border-color: #5b9dd9; +} + +.icon-wrapper .onepress-svg-preview { + display: inline-block; + vertical-align: middle; +} +.icon-wrapper .onepress-svg-preview svg { + display: block; + max-width: 40px; + max-height: 40px; +} +.icon-wrapper .onepress-svg-preview--invalid { + display: inline-block; + width: 24px; + height: 24px; + margin: 8px auto; + border: 1px dashed #c3c4c7; + border-radius: 2px; + vertical-align: middle; + box-sizing: border-box; +} + +.c-icon-svg-editor { + position: absolute; + top: 44px; + left: 10px; + right: 10px; + bottom: 10px; + display: flex; + flex-direction: column; +} +.c-icon-svg-textarea { + flex: 1; + min-height: 120px; + width: 100%; + resize: vertical; + font-family: monospace; + font-size: 12px; +} +.c-icon-svg-actions { + margin: 8px 0 0; +} + +@media (max-width: 700px) { + .c-icon-picker { + z-index: 99999; + width: 100%; + top: 50%; + left: 0px; + border-top: 1px solid #ddd; + border-right: 0 none !important; + + -ms-transform: translateX(0); /* IE 9 */ + -webkit-transform: translateX(0); /* Safari */ + transform: translateX(0); + + -ms-transform: translateY(100%); /* IE 9 */ + -webkit-transform: translateY(100%); /* Safari */ + transform: translateY(100%); + } + .c-icon-picker.ic-active { + -ms-transform: translateY(0); /* IE 9 */ + -webkit-transform: translateY(0); /* Safari */ + transform: translateY(0); + } +} + +#customize-controls + .control-section-onepress-plus + .accordion-section-title:hover, +#customize-controls + .control-section-onepress-plus + .accordion-section-title:focus { + background-color: #fff; +} + +#customize-controls + #accordion-section-onepress_order_styling_preview + .accordion-section-title { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFBAMAAAB/QTvWAAAAG1BMVEXw8fTw8vTx8vTy9PXz9Pb09fb09ff5+vv///+y0VPXAAAAHElEQVR42mOo6Ehg6GgyYOhgaWBo9GhgCO1oAAA/NgZNWnETxAAAAABJRU5ErkJggg=="); + background-repeat: repeat; +} +#customize-controls + #accordion-section-onepress_order_styling_preview + .accordion-section-title:after { + content: "\f160"; +} + +.control-section-onepress-plus .accordion-section-title .button { + margin-top: -4px; + font-weight: 400; + margin-left: 8px; +} + +.rtl .control-section-onepress-plus .accordion-section-title .button { + margin-left: 0; + margin-right: 8px; +} +/* OnePress Plus section */ +#customize-controls #accordion-section-onepress-plus { + border-top: 1px solid #ca4a1f; + border-bottom: 1px solid #ca4a1f; +} +#customize-controls #accordion-section-onepress-plus h3 { + margin: 0; + position: relative; +} +#customize-controls #accordion-section-onepress-plus h3 a { + padding: 10px 10px 11px 14px; + line-height: 20px; + display: block; + background: #ffffff; + color: #ca4a1f; + text-decoration: none; + position: relative; +} +#customize-controls #accordion-section-onepress-plus h3 a:hover { + background: #f5f5f5; +} +#customize-controls #accordion-section-onepress-plus h3 a:after { + content: "\f345"; + position: absolute; + color: #ca4a1f; + top: 11px; + right: 10px; + z-index: 1; + float: right; + border: none; + background: none; + font: normal 20px/1 dashicons; + speak: none; + display: block; + padding: 0; + text-indent: 0; + text-align: center; +} + +.onepress-c-heading { + padding: 5px 12px; + background: #bbbbbb; + margin: 10px -12px 2px; + text-transform: uppercase; + letter-spacing: 2px; + font-weight: 500; + font-size: 12px; + color: #ffffff; +} +/* MISC control */ +.customize-control-heading .customize-control-title { + margin: 0 -12px; + padding: 5px 12px; + background: #ccc; + text-transform: uppercase; + letter-spacing: 2px; + font-weight: 500; + font-size: 12px; + color: #ffffff; +} + +.repeatable-customize-control.visibility-hidden { + height: 0px !important; + display: block !important; + overflow: hidden !important; + box-shadow: none !important; + padding: 0px !important; + margin: 0px !important; +} + +.onepress-customizer-notice { + background: rgb(254, 247, 241); + color: rgb(51, 51, 51); + padding: 10px; + border-left: 4px solid rgb(213, 78, 33); + display: block; + margin-bottom: 20px; +} diff --git a/src/admin/customizer/alpha-color-picker.js b/src/admin/customizer/alpha-color-picker.js new file mode 100644 index 00000000..74c930da --- /dev/null +++ b/src/admin/customizer/alpha-color-picker.js @@ -0,0 +1,277 @@ +/** + * Alpha color picker: extends WP Color + jQuery plugin. + */ +export function installAlphaColorPicker($) { + + /** + * Override the stock color.js toString() method to add support for + * outputting RGBa or Hex. + */ + Color.prototype.toString = function (flag) { + + // If our no-alpha flag has been passed in, output RGBa value with 100% opacity. + // This is used to set the background color on the opacity slider during color changes. + if ('no-alpha' == flag) { + return this.toCSS('rgba', '1').replace(/\s+/g, ''); + } + + // If we have a proper opacity value, output RGBa. + if (1 > this._alpha) { + return this.toCSS('rgba', this._alpha).replace(/\s+/g, ''); + } + + // Proceed with stock color.js hex output. + var hex = parseInt(this._color, 10).toString(16); + if (this.error) { + return ''; + } + if (hex.length < 6) { + for (var i = 6 - hex.length - 1; i >= 0; i--) { + hex = '0' + hex; + } + } + + return '#' + hex; + }; + + /** + * Given an RGBa, RGB, or hex color value, return the alpha channel value. + */ + function acp_get_alpha_value_from_color(value) { + var alphaVal; + + // Remove all spaces from the passed in value to help our RGBa regex. + value = value.replace(/ /g, ''); + + if (value.match(/rgba\(\d+\,\d+\,\d+\,([^\)]+)\)/)) { + alphaVal = parseFloat(value.match(/rgba\(\d+\,\d+\,\d+\,([^\)]+)\)/)[1]).toFixed(2) * 100; + alphaVal = parseInt(alphaVal); + } else { + alphaVal = 100; + } + + return alphaVal; + } + + /** + * Force update the alpha value of the color picker object and maybe the alpha slider. + */ + function acp_update_alpha_value_on_color_input(alpha, $input, $alphaSlider, update_slider) { + var iris, colorPicker, color; + + iris = $input.data('a8cIris'); + colorPicker = $input.data('wpWpColorPicker'); + + // Set the alpha value on the Iris object. + iris._color._alpha = alpha; + + // Store the new color value. + color = iris._color.toString(); + + // Set the value of the input. + $input.val(color); + $input.trigger('color_change'); + + // Update the background color of the color picker. + colorPicker.toggler.css({ + 'background-color': color + }); + + // Maybe update the alpha slider itself. + if (update_slider) { + acp_update_alpha_value_on_alpha_slider(alpha, $alphaSlider); + } + + // Update the color value of the color picker object. + $input.wpColorPicker('color', color); + } + + /** + * Update the slider handle position and label. + */ + function acp_update_alpha_value_on_alpha_slider(alpha, $alphaSlider) { + $alphaSlider.slider('value', alpha); + $alphaSlider.find('.ui-slider-handle').text(alpha.toString()); + } + + $.fn.alphaColorPicker = function () { + + return this.each(function () { + + // Scope the vars. + var $input, startingColor, paletteInput, showOpacity, defaultColor, palette, + colorPickerOptions, $container, $alphaSlider, alphaVal, sliderOptions; + + // Store the input. + $input = $(this); + + // We must wrap the input now in order to get our a top level class + // around the HTML added by wpColorPicker(). + $input.wrap('
'); + + // Get some data off the input. + paletteInput = $input.attr('data-palette') || 'true'; + showOpacity = $input.attr('data-show-opacity') || 'true'; + defaultColor = $input.attr('data-default-color') || ''; + + // Process the palette. + if (paletteInput.indexOf('|') !== -1) { + palette = paletteInput.split('|'); + } else if ('false' == paletteInput) { + palette = false; + } else { + palette = true; + } + + // Get a clean starting value for the option. + startingColor = $input.val().replace(/\s+/g, ''); + //startingColor = $input.val().replace( '#', '' ); + //console.log( startingColor ); + + // If we don't yet have a value, use the default color. + if ('' == startingColor) { + startingColor = defaultColor; + } + + // Set up the options that we'll pass to wpColorPicker(). + colorPickerOptions = { + change: function (event, ui) { + var key, value, alpha, $transparency; + + key = $input.attr('data-customize-setting-link'); + value = $input.wpColorPicker('color'); + + // Set the opacity value on the slider handle when the default color button is clicked. + if (defaultColor == value) { + alpha = acp_get_alpha_value_from_color(value); + $alphaSlider.find('.ui-slider-handle').text(alpha); + } + + // If we're in the Customizer, send an ajax request to wp.customize + // to trigger the Save action. + if (typeof wp.customize != 'undefined') { + wp.customize(key, function (obj) { + obj.set(value); + }); + } + + $transparency = $container.find('.transparency'); + + // Always show the background color of the opacity slider at 100% opacity. + $transparency.css('background-color', ui.color.toString('no-alpha')); + $input.trigger('color_change'); + }, + clear: function () { + var key = $input.attr('data-customize-setting-link') || ''; + if (key && key !== '') { + if (typeof wp.customize != 'undefined') { + wp.customize(key, function (obj) { + obj.set(''); + }); + } + } + $input.val(''); + $input.trigger('color_change'); + }, + palettes: palette // Use the passed in palette. + }; + + // Create the colorpicker. + $input.wpColorPicker(colorPickerOptions); + + $container = $input.parents('.wp-picker-container:first'); + + // Insert our opacity slider. + $('
' + + '
' + + '
' + + '
' + + '
' + + '
').appendTo($container.find('.wp-picker-holder')); + + $alphaSlider = $container.find('.alpha-slider'); + + // If starting value is in format RGBa, grab the alpha channel. + alphaVal = acp_get_alpha_value_from_color(startingColor); + + // Set up jQuery UI slider() options. + sliderOptions = { + create: function (event, ui) { + var value = $(this).slider('value'); + + // Set up initial values. + $(this).find('.ui-slider-handle').text(value); + $(this).siblings('.transparency ').css('background-color', startingColor); + }, + value: alphaVal, + range: 'max', + step: 1, + min: 0, + max: 100, + animate: 300 + }; + + // Initialize jQuery UI slider with our options. + $alphaSlider.slider(sliderOptions); + + // Maybe show the opacity on the handle. + if ('true' == showOpacity) { + $alphaSlider.find('.ui-slider-handle').addClass('show-opacity'); + } + + // Bind event handlers for the click zones. + $container.find('.min-click-zone').on('click', function () { + acp_update_alpha_value_on_color_input(0, $input, $alphaSlider, true); + }); + $container.find('.max-click-zone').on('click', function () { + acp_update_alpha_value_on_color_input(100, $input, $alphaSlider, true); + }); + + // Bind event handler for clicking on a palette color. + $container.find('.iris-palette').on('click', function () { + var color, alpha; + + color = $(this).css('background-color'); + alpha = acp_get_alpha_value_from_color(color); + + acp_update_alpha_value_on_alpha_slider(alpha, $alphaSlider); + + // Sometimes Iris doesn't set a perfect background-color on the palette, + // for example rgba(20, 80, 100, 0.3) becomes rgba(20, 80, 100, 0.298039). + // To compensante for this we round the opacity value on RGBa colors here + // and save it a second time to the color picker object. + if (alpha != 100) { + color = color.replace(/[^,]+(?=\))/, (alpha / 100).toFixed(2)); + } + + $input.wpColorPicker('color', color); + }); + + // Bind event handler for clicking on the 'Default' button. + $container.find('.button.wp-picker-default').on('click', function () { + var alpha = acp_get_alpha_value_from_color(defaultColor); + + acp_update_alpha_value_on_alpha_slider(alpha, $alphaSlider); + }); + + // Bind event handler for typing or pasting into the input. + $input.on('input', function () { + var value = $(this).val(); + var alpha = acp_get_alpha_value_from_color(value); + + acp_update_alpha_value_on_alpha_slider(alpha, $alphaSlider); + }); + + // Update all the things when the slider is interacted with. + $alphaSlider.slider().on('slide', function (event, ui) { + var alpha = parseFloat(ui.value) / 100.0; + + acp_update_alpha_value_on_color_input(alpha, $input, $alphaSlider, false); + + // Change value shown on slider handle. + $(this).find('.ui-slider-handle').text(ui.value); + }); + }); + } + +} diff --git a/src/admin/customizer/control-alpha-color.js b/src/admin/customizer/control-alpha-color.js new file mode 100644 index 00000000..da0973f3 --- /dev/null +++ b/src/admin/customizer/control-alpha-color.js @@ -0,0 +1,13 @@ +/** + * Customizer control: alpha-color. + */ +export function registerAlphaColorControl(api, $) { + api.controlConstructor['alpha-color'] = api.Control.extend({ + ready: function () { + var control = this; + $('.alpha-color-control', control.container).alphaColorPicker({ + clear: function () {}, + }); + }, + }); +} diff --git a/src/admin/customizer/control-bindings.js b/src/admin/customizer/control-bindings.js new file mode 100644 index 00000000..ea0182e1 --- /dev/null +++ b/src/admin/customizer/control-bindings.js @@ -0,0 +1,98 @@ +/** + * Hero / gallery / theme action UI toggles. + */ +export function initControlBindings($) { + + if (typeof onepress_customizer_settings !== "undefined") { + if (onepress_customizer_settings.number_action > 0) { + $('.control-section-themes h3.accordion-section-title').append('' + onepress_customizer_settings.number_action + ''); + } + } + + /** + * For Hero layout content settings + */ + $('select[data-customize-setting-link="onepress_hero_layout"]').on('change on_custom_load', function () { + var v = $(this).val() || ''; + + $("li[id^='customize-control-onepress_hcl']").hide(); + $("li[id^='customize-control-onepress_hcl" + v + "']").show(); + + }); + $('select[data-customize-setting-link="onepress_hero_layout"]').trigger('on_custom_load'); + + + /** + * For Gallery content settings + */ + $('select[data-customize-setting-link="onepress_gallery_source"]').on('change on_custom_load', function () { + var v = $(this).val() || ''; + + $("li[id^='customize-control-onepress_gallery_source_']").hide(); + $("li[id^='customize-control-onepress_gallery_api_']").hide(); + $("li[id^='customize-control-onepress_gallery_settings_']").hide(); + $("li[id^='customize-control-onepress_gallery_source_" + v + "']").show(); + $("li[id^='customize-control-onepress_gallery_api_" + v + "']").show(); + $("li[id^='customize-control-onepress_gallery_settings_" + v + "']").show(); + + }); + + $('select[data-customize-setting-link="onepress_gallery_source"]').trigger('on_custom_load'); + + /** + * For Gallery display settings + */ + $('select[data-customize-setting-link="onepress_gallery_display"]').on('change on_custom_load', function () { + var v = $(this).val() || ''; + switch (v) { + case 'slider': + $("#customize-control-onepress_g_row_height, #customize-control-onepress_g_col, #customize-control-onepress_g_spacing").hide(); + break; + case 'justified': + $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").hide(); + $("#customize-control-onepress_g_row_height").show(); + break; + case 'carousel': + $("#customize-control-onepress_g_row_height, #customize-control-onepress_g_col").hide(); + $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").show(); + break; + case 'masonry': + $("#customize-control-onepress_g_row_height").hide(); + $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").show(); + break; + default: + $("#customize-control-onepress_g_row_height").hide(); + $("#customize-control-onepress_g_col, #customize-control-onepress_g_spacing").show(); + + } + + }); + $('select[data-customize-setting-link="onepress_gallery_display"]').trigger('on_custom_load'); + + /** + * News section: show column string only when Blog layout is Grid + */ + $('select[data-customize-setting-link="onepress_news_layout"]').on('change on_custom_load', function () { + var v = $(this).val() || ''; + if (v === 'grid') { + $('#customize-control-onepress_news_grid_columns').show(); + } else { + $('#customize-control-onepress_news_grid_columns').hide(); + } + }); + $('select[data-customize-setting-link="onepress_news_layout"]').trigger('on_custom_load'); + + /** + * Blog Posts (global): grid column string only when layout is Grid + */ + $('select[data-customize-setting-link="onepress_blog_posts_layout"]').on('change on_custom_load', function () { + var v = $(this).val() || ''; + if (v === 'grid') { + $('#customize-control-onepress_blog_posts_grid_columns').show(); + } else { + $('#customize-control-onepress_blog_posts_grid_columns').hide(); + } + }); + $('select[data-customize-setting-link="onepress_blog_posts_layout"]').trigger('on_custom_load'); + +} diff --git a/src/admin/customizer/control-repeatable.js b/src/admin/customizer/control-repeatable.js new file mode 100644 index 00000000..a90ab646 --- /dev/null +++ b/src/admin/customizer/control-repeatable.js @@ -0,0 +1,39 @@ +/** + * Customizer control: repeatable fields (React UI + wp.customize.Setting bridge). + */ +import { createElement } from '@wordpress/element'; +import { createRoot } from 'react-dom/client'; +import { RepeatableControlApp } from './repeatable/RepeatableControlApp'; +import { installRepeatableMediaBridge } from './repeatable/repeatable-media-bridge'; + +export function registerRepeatableControl(api, $) { + installRepeatableMediaBridge($); + + api.controlConstructor['repeatable'] = api.Control.extend({ + ready() { + const control = this; + const run = () => { + const ul = control.container.find('.form-data .list-repeatable').get(0); + if (!ul) { + return; + } + const root = createRoot(ul); + root.render( + createElement(RepeatableControlApp, { + api, + $, + control, + }) + ); + control._onepressRepeatableRoot = root; + }; + if (typeof window.requestAnimationFrame === 'function') { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(run); + }); + } else { + window.setTimeout(run, 50); + } + }, + }); +} diff --git a/src/admin/customizer/icon-picker.js b/src/admin/customizer/icon-picker.js new file mode 100644 index 00000000..dce19eaa --- /dev/null +++ b/src/admin/customizer/icon-picker.js @@ -0,0 +1,47 @@ +/** + * Icon picker (React) + footer layout columns visibility. + */ +import { createElement } from '@wordpress/element'; +import { createRoot } from 'react-dom/client'; +import { IconPickerApp } from './icon-picker/IconPickerApp'; +import { injectIconFontLinks } from './icon-picker/injectFontLinks'; + +function initFooterLayoutColumns($) { + const displayFooterLayout = function (l) { + $('li[id^="customize-control-footer_custom_"]').hide(); + $('li[id^="customize-control-footer_custom_' + l + '_columns"]').show(); + }; + + displayFooterLayout($('#customize-control-footer_layout select').val()); + $('#customize-control-footer_layout select').on('change', function () { + displayFooterLayout($(this).val()); + }); +} + +export function initIconPicker($) { + window.editing_icon = false; + + if (typeof C_Icon_Picker === 'undefined') { + initFooterLayoutColumns($); + return; + } + + const hasFonts = C_Icon_Picker.fonts && Object.keys(C_Icon_Picker.fonts).length > 0; + const hasSvgTab = Boolean(C_Icon_Picker.svg_code); + if (!hasFonts && !hasSvgTab) { + initFooterLayoutColumns($); + return; + } + + injectIconFontLinks($); + + const overlay = document.querySelector('.wp-full-overlay'); + const host = document.createElement('div'); + host.id = 'onepress-icon-picker-host'; + (overlay || document.body).appendChild(host); + + const root = createRoot(host); + root.render(createElement(IconPickerApp, { $ })); + + initFooterLayoutColumns($); +} diff --git a/src/admin/customizer/icon-picker/IconPickerApp.jsx b/src/admin/customizer/icon-picker/IconPickerApp.jsx new file mode 100644 index 00000000..cbf5de5c --- /dev/null +++ b/src/admin/customizer/icon-picker/IconPickerApp.jsx @@ -0,0 +1,228 @@ +/** + * Customizer floating icon picker (Font Awesome / C_Icon_Picker + SVG code). + */ +import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; +import { isSvgIconValue, normalizeSvgIconForStorage, ONEPRESS_ICON_COMMIT_EVENT } from '../repeatable/repeatable-values'; + +const SVG_KEY = 'svg'; + +function normalizeFontGroups() { + if (typeof C_Icon_Picker === 'undefined' || !C_Icon_Picker.fonts) { + return []; + } + return Object.keys(C_Icon_Picker.fonts).map((key) => { + const raw = C_Icon_Picker.fonts[key] || {}; + const prefix = raw.prefix || ''; + const icons = String(raw.icons || '') + .split('|') + .filter(Boolean) + .map((part) => (prefix ? `${prefix} ${part}`.trim() : part)); + return { + key, + name: raw.name || key, + icons, + }; + }); +} + +function dispatchIconCommit(wrapperEl, value) { + if (!wrapperEl) { + return; + } + window.dispatchEvent( + new CustomEvent(ONEPRESS_ICON_COMMIT_EVENT, { + bubbles: true, + detail: { wrapperEl, value: String(value) }, + }) + ); +} + +export function IconPickerApp({ $ }) { + const fontGroups = useMemo(normalizeFontGroups, []); + const searchPlaceholder = + typeof C_Icon_Picker !== 'undefined' && C_Icon_Picker.search ? C_Icon_Picker.search : 'Search'; + const showSvgOption = + typeof C_Icon_Picker !== 'undefined' && Boolean(C_Icon_Picker.svg_code); + const applySvgLabel = + typeof C_Icon_Picker !== 'undefined' && C_Icon_Picker.apply_svg + ? C_Icon_Picker.apply_svg + : 'Apply'; + const svgPlaceholder = + typeof C_Icon_Picker !== 'undefined' && C_Icon_Picker.svg_placeholder + ? C_Icon_Picker.svg_placeholder + : ''; + + const defaultKey = fontGroups[0]?.key || (showSvgOption ? SVG_KEY : ''); + const [activeKey, setActiveKey] = useState(defaultKey); + const [search, setSearch] = useState(''); + const [isPickerActive, setIsPickerActive] = useState(false); + const [svgCode, setSvgCode] = useState(''); + + const q = search.trim().toLowerCase(); + + const closePicker = useCallback(() => { + setIsPickerActive(false); + window.editing_icon = false; + $('body').find('.icon-wrapper').removeClass('icon-editing'); + }, [$]); + + const applySelection = useCallback( + (fullName) => { + const $wrap = window.editing_icon; + if ($wrap && $wrap.length) { + dispatchIconCommit($wrap.get(0), fullName); + } + closePicker(); + }, + [closePicker] + ); + + const applySvgCode = useCallback(() => { + const $wrap = window.editing_icon; + const raw = normalizeSvgIconForStorage(String(svgCode || '').trim()); + if ($wrap && $wrap.length) { + dispatchIconCommit($wrap.get(0), raw); + } + closePicker(); + }, [svgCode, closePicker]); + + useEffect(() => { + const onWrapperClick = (e) => { + e.preventDefault(); + const $icon = $(e.currentTarget); + window.editing_icon = $icon; + const raw = normalizeSvgIconForStorage(String($icon.find('input').val() || '').trim()); + if (showSvgOption && isSvgIconValue(raw)) { + setActiveKey(SVG_KEY); + setSvgCode(raw); + } else { + setActiveKey(fontGroups[0]?.key || SVG_KEY); + setSvgCode(''); + } + setSearch(''); + setIsPickerActive(true); + $('body').find('.icon-wrapper').removeClass('icon-editing'); + $icon.addClass('icon-editing'); + }; + $(document.body).on('click.onepressIconWrap', '.icon-wrapper', onWrapperClick); + return () => $(document.body).off('click.onepressIconWrap', '.icon-wrapper'); + }, [$, fontGroups, showSvgOption]); + + useEffect(() => { + const onPointerDownOutside = (e) => { + const $t = $(e.target); + if ($t.closest('.c-icon-picker').length || $t.closest('.icon-wrapper').length) { + return; + } + if (!$('.c-icon-picker').hasClass('ic-active')) { + return; + } + closePicker(); + }; + $(document).on('mousedown.onepressIconPickOut', onPointerDownOutside); + return () => $(document).off('mousedown.onepressIconPickOut', onPointerDownOutside); + }, [$, closePicker]); + + const onTypeChange = useCallback( + (e) => { + const v = e.target.value; + setActiveKey(v); + if (v === SVG_KEY) { + const $w = window.editing_icon; + if ($w && $w.length) { + const cur = String($w.find('input').val() || '').trim(); + setSvgCode(isSvgIconValue(cur) ? cur : ''); + } else { + setSvgCode(''); + } + } + }, + [] + ); + + if (!showSvgOption && fontGroups.length === 0) { + return null; + } + + const isSvgMode = showSvgOption && activeKey === SVG_KEY; + + return ( +
+
+ +
+ {isSvgMode ? ( +
+
'); + var content = control.editing_area.val(); + // Load default value + $('textarea', control.editing_editor).val(content); + try { + control.preview.html(window.switchEditors._wp_Autop(content)); + } catch (e) { + + } + + $('body').on('click', '#customize-controls, .customize-section-back', function (e) { + if (!$(e.target).is(control.preview)) { + /// e.preventDefault(); // Keep this AFTER the key filter above + control.editing_editor.removeClass('wpe-active'); + $('.wp-js-editor-preview').removeClass('wpe-focus'); + } + }); + + control.container.find('.wp-js-editor').addClass('wp-js-editor-active'); + control.preview.insertBefore(control.editing_area); + + control._init(); + + $(window).on('resize', function () { + control._resize(); + }); + + }, + + _add_editor: function () { + var control = this; + if (!this.editor_added) { + this.editor_added = true; + + $('body .wp-full-overlay').append(control.editing_editor); + + $('textarea', control.editing_editor).attr('data-editor-mod', (control.editing_area.attr('data-editor-mod') || '')).wp_js_editor({ + sync_id: control.editing_area, + init_instance_callback: function (editor) { + var w = $('#wp-' + control.editor_id + '-wrap'); + $('.wp-editor-tabs', w).append(''); + $('.wp-editor-tabs', w).append(''); + $('.wp-editor-tabs', w).append(''); + w.on('click', '.close-wp-editor', function (e) { + e.preventDefault(); + control.editing_editor.removeClass('wpe-active'); + $('.wp-js-editor-preview').removeClass('wpe-focus'); + }); + $('.preview-wp-editor', w).hover(function () { + w.closest('.modal-wp-js-editor').css({opacity: 0}); + }, function () { + w.closest('.modal-wp-js-editor').css({opacity: 1}); + }); + w.on('click', '.fullscreen-wp-editor', function (e) { + e.preventDefault(); + w.closest('.modal-wp-js-editor').toggleClass('fullscreen'); + setTimeout(function () { + $(window).resize(); + }, 600); + }); + } + }); + + + } + }, + + _init: function () { + + var control = this; + + control.editing_area.on('change', function () { + control.preview.html(window.switchEditors._wp_Autop($(this).val())); + }); + + control.preview.on('click', function (e) { + control._add_editor(); + $('.modal-wp-js-editor').removeClass('wpe-active'); + control.editing_editor.toggleClass('wpe-active'); + tinyMCE.get(control.editor_id).focus(); + control.preview.addClass('wpe-focus'); + control._resize(); + return false; + }); + + + control.container.on('click', '.wp-js-editor-preview', function (e) { + e.preventDefault(); + }); + + }, + + _resize: function () { + var control = this; + var w = $('#wp-' + control.editor_id + '-wrap'); + var height = w.innerHeight(); + var tb_h = w.find('.mce-toolbar-grp').eq(0).height(); + tb_h += w.find('.wp-editor-tools').eq(0).height(); + tb_h += 50; + //var width = $( window ).width(); + var editor = tinymce.get(control.editor_id); + if (editor) { + control.editing_editor.width(''); + editor.theme.resizeTo('100%', height - tb_h); + w.find('textarea.wp-editor-area').height(height - tb_h); + } + + } + + }; + + _editor.ready(container); + + } + + function _remove_editor($context) { + $('textarea', $context).each(function () { + var id = $(this).attr('id') || ''; + var editor_id = 'wpe-for-' + id; + try { + var editor = tinymce.get(editor_id); + if (editor) { + editor.remove(); + } + $('#wrap-' + editor_id).remove(); + $('#wrap-' + id).remove(); + + if (typeof tinyMCEPreInit.mceInit[editor_id] !== "undefined") { + delete tinyMCEPreInit.mceInit[editor_id]; + } + + if (typeof tinyMCEPreInit.qtInit[editor_id] !== "undefined") { + delete tinyMCEPreInit.qtInit[editor_id]; + } + + } catch (e) { + + } + + }); + } + + var _is_init_editors = {}; + + // jQuery( document ).ready( function( $ ){ + + api.bind('ready', function (e, b) { + + $('#customize-theme-controls .accordion-section').each(function () { + var section = $(this); + var id = section.attr('id') || ''; + if (id) { + if (typeof _is_init_editors[id] === "undefined") { + _is_init_editors[id] = true; + + setTimeout(function () { + if ($('.wp-js-editor', section).length > 0) { + $('.wp-js-editor', section).each(function () { + _the_editor($(this)); + }); + } + + if ($('.repeatable-customize-control:not(.no-changeable) .item-editor', section).length > 0) { + $('.repeatable-customize-control:not(.no-changeable) .item-editor', section).each(function () { + _the_editor($(this)); + }); + } + }, 10); + + } + } + }); + + // Check section when focus + if (_wpCustomizeSettings.autofocus) { + if (_wpCustomizeSettings.autofocus.section) { + var id = "sub-accordion-section-" + _wpCustomizeSettings.autofocus.section; + _is_init_editors[id] = true; + var section = $('#' + id); + setTimeout(function () { + if ($('.wp-js-editor', section).length > 0) { + $('.wp-js-editor', section).each(function () { + _the_editor($(this)); + }); + } + + if ($('.repeatable-customize-control:not(.no-changeable) .item-editor', section).length > 0) { + $('.repeatable-customize-control:not(.no-changeable) .item-editor', section).each(function () { + _the_editor($(this)); + }); + } + }, 1000); + + } else if (_wpCustomizeSettings.autofocus.panel) { + + } + } + + + $('body').on('repeater-control-init-item', function (e, container) { + $('.item-editor', container).each(function () { + _the_editor($(this)); + }); + }); + + $('body').on('repeat-control-remove-item', function (e, container) { + _remove_editor(container); + }); + }); + + +} diff --git a/src/admin/customizer/plus-section.js b/src/admin/customizer/plus-section.js new file mode 100644 index 00000000..cc2b6f4a --- /dev/null +++ b/src/admin/customizer/plus-section.js @@ -0,0 +1,11 @@ +/** + * OnePress Plus upsell section (always contextually active). + */ +export function registerPlusSection(api) { + api.sectionConstructor['onepress-plus'] = api.Section.extend({ + attachEvents: function () {}, + isContextuallyActive: function () { + return true; + }, + }); +} diff --git a/src/admin/customizer/repeatable/RepeatableControlApp.jsx b/src/admin/customizer/repeatable/RepeatableControlApp.jsx new file mode 100644 index 00000000..805960aa --- /dev/null +++ b/src/admin/customizer/repeatable/RepeatableControlApp.jsx @@ -0,0 +1,232 @@ +/** + * React root for Customizer `repeatable` control: mounts as children of `ul.list-repeatable`. + * + * Data flow (every user edit must follow this path): + * 1. Field UI changes → field `onChange(fieldId, value)` (RepeatableItem → RepeatableField → field component). + * 2. `setRow` merges the value into that row in React state and builds the next `items` array. + * 3. `commit(nextItems)` serializes rows to JSON (`serializeSetting`) and calls + * `pushRepeatablePayloadToCustomizer` → `setting.set(payload)` + hidden input + callbacks + * so wp.customize marks the setting dirty and preview/changeset update. + * + * Fields that update the DOM via jQuery only (e.g. modal TinyMCE → `.val().trigger("change")`) + * must still invoke the same `onChange` path (see TextareaField editor + jQuery listeners). + */ +import { arrayMoveImmutable } from 'array-move'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from '@wordpress/element'; +import { RepeatableItem } from './RepeatableItem'; +import { + buildRowsFromParams, + newEmptyRow, + repeatableSettingValuesEqual, + serializeSetting, +} from './repeatable-values'; + +/** + * Step 3: apply serialized repeater data to the Customizer setting (and linked hidden input). + * Core Value#set no-ops when _.isEqual(from, to) — e.g. object vs same JSON string + * — leaving _dirty false so refresh preview / changeset never see the edit. + * + * @param {jQuery} $ jQuery + * @param {object} control wp.customize.Control instance + * @param {string} payload JSON string for the setting + */ +function pushRepeatablePayloadToCustomizer($, control, payload) { + const setting = control.setting; + if (!setting || typeof setting.set !== 'function' || typeof setting.get !== 'function') { + return; + } + const before = setting.get(); + setting.set(payload); + + const $hidden = control.container.find('input[data-customize-setting-link]'); + if ($hidden.length) { + $hidden.val(payload); + $hidden.trigger('input').trigger('change'); + } + + const after = setting.get(); + const _ = typeof window !== 'undefined' ? window._ : null; + if (_ && typeof _.isEqual === 'function') { + const skipped = _.isEqual(before, after) && !_.isEqual(before, payload); + if (skipped) { + setting._value = payload; + setting._dirty = true; + if (setting.callbacks && typeof setting.callbacks.fireWith === 'function') { + setting.callbacks.fireWith(setting, [payload, before]); + } + } + } +} + +export function RepeatableControlApp({ control, $, api }) { + const fields = control.params.fields; + const fieldIds = useMemo(() => Object.keys(fields || {}), [fields]); + + const [items, setItems] = useState(() => buildRowsFromParams(control.params.value, fields)); + + const maxItem = control.params.max_item ? parseInt(control.params.max_item, 10) : 0; + const limitedMsg = control.params.limited_msg || ''; + const idKey = control.params.id_key || ''; + + const dragFrom = useRef(null); + + // Sync hidden input + setting only if payload differs from WP (avoids false “dirty” on load). + // Note: wp.customize.Value#set ignores a second-arg “silent”; every set marks the setting dirty. + useLayoutEffect(() => { + const payload = serializeSetting(items, fields); + if (typeof control.setting.set !== 'function' || typeof control.setting.get !== 'function') { + return; + } + const current = control.setting.get(); + if (!repeatableSettingValuesEqual(current, payload)) { + pushRepeatablePayloadToCustomizer($, control, payload); + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- bootstrap only + }, []); + + // Step 3: rows in memory → JSON payload → wp.customize.Setting. + const commit = useCallback( + (next) => { + const payload = serializeSetting(next, fields); + pushRepeatablePayloadToCustomizer($, control, payload); + }, + [control, fields, $] + ); + + // Step 2: patch one row, then commit the full list. + const setRow = useCallback( + (index, updater) => { + setItems((prev) => { + const prevRow = prev[index]; + const nextRow = typeof updater === 'function' ? updater(prevRow) : updater; + const next = prev.slice(); + next[index] = nextRow; + commit(next); + return next; + }); + }, + [commit] + ); + + const onRemove = useCallback( + (index) => { + setItems((prev) => { + const next = prev.filter((_, i) => i !== index); + commit(next); + return next; + }); + }, + [commit] + ); + + const onDragStart = useCallback((e, index) => { + dragFrom.current = index; + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', String(index)); + }, []); + + const onDragOver = useCallback((e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + }, []); + + const onDrop = useCallback( + (e, toIndex) => { + e.preventDefault(); + const fromStr = e.dataTransfer.getData('text/plain'); + let from = fromStr !== '' ? parseInt(fromStr, 10) : dragFrom.current; + if (from === null || from === undefined || Number.isNaN(from)) { + return; + } + setItems((prev) => { + if (from === toIndex) { + return prev; + } + const next = arrayMoveImmutable(prev, from, toIndex); + commit(next); + return next; + }); + dragFrom.current = null; + }, + [commit] + ); + + const addItem = useCallback(() => { + if (control.id === 'onepress_map_items_address') { + const mapLong = api('onepress_map_long').get(); + const mapLat = api('onepress_map_lat').get(); + if (mapLong === '' || mapLat === '') { + const $lab = $('#customize-control-onepress_map_items_address', document).find('label'); + $lab.append( + '' + + (typeof window.ONEPRESS_CUSTOMIZER_DATA !== 'undefined' + ? window.ONEPRESS_CUSTOMIZER_DATA.multiple_map_notice + : '') + + '' + ); + return; + } + $('#customize-control-onepress_map_items_address', document).find('.onepress-customizer-notice').remove(); + } + + setItems((prev) => { + if (maxItem > 0 && prev.length >= maxItem) { + return prev; + } + const row = newEmptyRow(fields, idKey); + const next = [...prev, row]; + commit(next); + return next; + }); + }, [api, commit, control.id, fields, idKey, maxItem]); + + useEffect(() => { + const $btn = control.container.find('.add-new-repeat-item'); + $btn.off('click.onepressR').on('click.onepressR', (e) => { + e.preventDefault(); + addItem(); + }); + return () => $btn.off('click.onepressR'); + }, [addItem, control.container]); + + useEffect(() => { + const $actions = control.container.find('.repeatable-actions'); + const n = items.length; + if (maxItem > 0 && n >= maxItem) { + $actions.hide(); + if (limitedMsg && control.container.find('.limited-msg').length === 0) { + $('

').html(limitedMsg).insertAfter($actions); + } + control.container.find('.limited-msg').show(); + } else { + $actions.show(); + control.container.find('.limited-msg').hide(); + } + }, [items.length, maxItem, limitedMsg, control.container]); + + return ( + <> + {items.map((row, index) => { + const itemKey = + idKey && row[idKey] ? String(row[idKey]) : `idx-${index}`; + return ( + + ); + })} + + ); +} diff --git a/src/admin/customizer/repeatable/RepeatableField.jsx b/src/admin/customizer/repeatable/RepeatableField.jsx new file mode 100644 index 00000000..91ebed1b --- /dev/null +++ b/src/admin/customizer/repeatable/RepeatableField.jsx @@ -0,0 +1,67 @@ +/** + * Single field inside a repeatable row (mirrors PHP `js_item` structure / classes). + */ +import { useLayoutEffect, useRef } from '@wordpress/element'; +import { getRepeatableFieldComponent } from './fields/fieldRegistry'; +import { fieldVisible } from './repeatable-logic'; + +export function RepeatableField({ field, value, onChange, rowValues, $, skipEditor }) { + const wrapRef = useRef(null); + const fieldType = field?.type; + const fieldId = field?.id; + const required = field?.required; + const visible = fieldType ? fieldVisible(required, rowValues) : false; + + // Modal WP editor (modal-editor.js) only runs on row mount via repeater-control-init-item. + // When an editor field appears later (required / visibility), init it against the row

  • . + useLayoutEffect(() => { + if (!visible || fieldType !== 'editor' || skipEditor) { + return; + } + const el = wrapRef.current; + if (!el) { + return; + } + const $row = $(el).closest('.repeatable-customize-control'); + if (!$row.length) { + return; + } + $('body').trigger('repeater-control-init-item', [$row]); + }, [visible, fieldType, fieldId, skipEditor, $]); + + if (!fieldType) { + return null; + } + + // Do not mount hidden fields (avoids editor/media init; state stays in row). + if (!visible) { + return null; + } + + const FieldType = getRepeatableFieldComponent(fieldType); + if (!FieldType) { + return null; + } + + const wrapClass = `field--item item item-${fieldType} item-${fieldId}`; + + const t = fieldType; + const showLabel = t !== 'checkbox'; + + return ( +
    + {showLabel && field.title ? ( +
    + ); +} diff --git a/src/admin/customizer/repeatable/RepeatableItem.jsx b/src/admin/customizer/repeatable/RepeatableItem.jsx new file mode 100644 index 00000000..b0d590ee --- /dev/null +++ b/src/admin/customizer/repeatable/RepeatableItem.jsx @@ -0,0 +1,178 @@ +/** + * One repeater row: widget chrome, fields, remove/close, drag handle. + */ +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from '@wordpress/element'; +import { RepeatableField } from './RepeatableField'; +import { fieldVisible } from './repeatable-logic'; + +export function RepeatableItem({ + $, + control, + fieldIds, + fields, + index, + itemKey, + row, + setRow, + onRemove, + onDragStart, + onDragOver, + onDrop, +}) { + const liRef = useRef(null); + const [expanded, setExpanded] = useState(false); + + const liveTitleId = control.params.live_title_id; + const titleFormat = control.params.title_format || ''; + const defaultEmptyTitle = control.params.default_empty_title || 'Item'; + + const rowValues = useMemo(() => { + const o = { ...row }; + return o; + }, [row]); + + const liveTitle = useMemo(() => { + if (!liveTitleId) { + return defaultEmptyTitle; + } + const elId = liveTitleId; + let v = ''; + const raw = row[elId]; + const fieldDef = fields[elId]; + if (fieldDef && fieldDef.type === 'select' && !fieldDef.multiple) { + const opts = fieldDef.options || {}; + v = opts[raw] !== undefined ? opts[raw] : raw || ''; + } else { + v = raw === undefined || raw === null ? '' : String(raw); + } + if (v === '') { + v = defaultEmptyTitle; + } + let format = titleFormat; + // Built-in sections (not added via "Add Section") show plain live title; user-added rows use full title_format. + if (control.id === 'onepress_section_order_styling' && row.add_by !== 'click') { + format = '[live_title]'; + } + if (format !== '') { + v = format.replace(/\[live_title\]/g, v); + } + return v; + }, [row, liveTitleId, titleFormat, defaultEmptyTitle, fields, control.id]); + + // Step 1→2→3: field value → repeater row state → commit() → Customizer setting (RepeatableControlApp). + const onFieldChange = useCallback( + (fieldId, val) => { + setRow(index, (prev) => ({ ...prev, [fieldId]: val })); + }, + [index, setRow] + ); + + const skipEditor = control.id === 'onepress_section_order_styling' && row.add_by !== 'click'; + + const liClass = ['repeatable-customize-control']; + if (row.__visibility === 'hidden') { + liClass.push('visibility-hidden'); + } + const sid = row.section_id !== undefined && row.section_id !== null ? String(row.section_id) : ''; + if (sid !== '') { + liClass.push(`section-${sid}`); + } + if (sid === 'map' || sid === 'slider') { + liClass.push('show-display-field-only'); + } + if (skipEditor) { + liClass.push('no-changeable'); + } + + useLayoutEffect(() => { + const $ctx = $(liRef.current); + if (!$ctx.length) { + return; + } + $('body').trigger('repeater-control-init-item', [$ctx]); + return () => { + $('body').trigger('repeat-control-remove-item', [$ctx]); + }; + }, [$, itemKey]); + + const toggle = useCallback((e) => { + e.preventDefault(); + setExpanded((x) => !x); + }, []); + + return ( +
  • + +
  • + ); +} diff --git a/src/admin/customizer/repeatable/fields/AlphaColorField.jsx b/src/admin/customizer/repeatable/fields/AlphaColorField.jsx new file mode 100644 index 00000000..028d9fa0 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/AlphaColorField.jsx @@ -0,0 +1,5 @@ +import { AlphaColorInput } from './AlphaColorInput'; + +export function AlphaColorField({ field, value, onChange, $ }) { + return ; +} diff --git a/src/admin/customizer/repeatable/fields/AlphaColorInput.jsx b/src/admin/customizer/repeatable/fields/AlphaColorInput.jsx new file mode 100644 index 00000000..abf354af --- /dev/null +++ b/src/admin/customizer/repeatable/fields/AlphaColorInput.jsx @@ -0,0 +1,72 @@ +import { useEffect, useLayoutEffect, useRef } from '@wordpress/element'; + +export function AlphaColorInput({ value, onChange, fieldId, $ }) { + const ref = useRef(null); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; + useLayoutEffect(() => { + const $el = $(ref.current); + if (!$el.length) { + return; + } + let c = value || ''; + c = String(c).replace(/^#/, ''); + $el.removeAttr('value'); + $el.prop('value', c); + // $.fn.alphaColorPicker() ignores passed options; it always uses internal wpColorPicker callbacks + // and triggers jQuery "color_change" (see alpha-color-picker.js). + const onColorPlugin = () => { + onChangeRef.current($el.val() || ''); + }; + $el.on('color_change.onepressRepeatable', onColorPlugin); + // alpha-color-picker.js binds "input" for the opacity slider only; typing does not always fire color_change. + $el.on('input.onepressRepeatable', onColorPlugin); + $el.alphaColorPicker(); + let raf = 0; + const pushRaf = () => { + if (raf) { + return; + } + raf = window.requestAnimationFrame(() => { + raf = 0; + onColorPlugin(); + }); + }; + const $picker = $el.closest('.wp-picker-container'); + if ($picker.length) { + $picker.on('mousemove.onepressRepeatable touchmove.onepressRepeatable', '.iris-picker', pushRaf); + } + return () => { + $picker.off('.onepressRepeatable'); + if (raf) { + window.cancelAnimationFrame(raf); + } + $el.off('color_change.onepressRepeatable', onColorPlugin); + $el.off('input.onepressRepeatable', onColorPlugin); + try { + $el.wpColorPicker('destroy'); + } catch (e) { + // ignore + } + try { + const $wrap = $el.parent('.alpha-color-picker-wrap'); + if ($wrap.length) { + $el.unwrap(); + } + } catch (e) { + // ignore + } + }; + }, [$, fieldId]); + useEffect(() => { + try { + const $el = $(ref.current); + if ($el.length && $el.data('wpWpColorPicker')) { + $el.wpColorPicker('color', value || ''); + } + } catch (e) { + // ignore + } + }, [value, $]); + return ; +} diff --git a/src/admin/customizer/repeatable/fields/CheckboxField.jsx b/src/admin/customizer/repeatable/fields/CheckboxField.jsx new file mode 100644 index 00000000..06938754 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/CheckboxField.jsx @@ -0,0 +1,15 @@ +export function CheckboxField({ field, value, onChange }) { + return ( + + ); +} diff --git a/src/admin/customizer/repeatable/fields/ColorField.jsx b/src/admin/customizer/repeatable/fields/ColorField.jsx new file mode 100644 index 00000000..fb2c9e5e --- /dev/null +++ b/src/admin/customizer/repeatable/fields/ColorField.jsx @@ -0,0 +1,9 @@ +import { ColorInput } from './ColorInput'; + +export function ColorField({ field, value, onChange, $ }) { + let display = value || ''; + if (display && String(display).indexOf('#') !== 0) { + display = '#' + String(display).replace(/^#/, ''); + } + return ; +} diff --git a/src/admin/customizer/repeatable/fields/ColorInput.jsx b/src/admin/customizer/repeatable/fields/ColorInput.jsx new file mode 100644 index 00000000..33d43f25 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/ColorInput.jsx @@ -0,0 +1,61 @@ +import { useEffect, useLayoutEffect, useRef } from '@wordpress/element'; + +export function ColorInput({ value, onChange, fieldId, $ }) { + const ref = useRef(null); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; + useLayoutEffect(() => { + const $el = $(ref.current); + if (!$el.length) { + return; + } + const readColor = () => { + try { + return $el.wpColorPicker('color') || $el.val() || ''; + } catch (e) { + return $el.val() || ''; + } + }; + const push = () => { + onChangeRef.current(readColor()); + }; + let raf = 0; + const pushRaf = () => { + if (raf) { + return; + } + raf = window.requestAnimationFrame(() => { + raf = 0; + push(); + }); + }; + $el.wpColorPicker({ + change: push, + clear() { + onChangeRef.current(''); + }, + }); + // wpColorPicker does not forward Iris drag events; while dragging, sync via the picker surface. + const $wrap = $el.closest('.wp-picker-container'); + $wrap.on('mousemove.onepressRepeatable touchmove.onepressRepeatable', '.iris-picker', pushRaf); + return () => { + $wrap.off('.onepressRepeatable'); + if (raf) { + window.cancelAnimationFrame(raf); + } + try { + $el.wpColorPicker('destroy'); + } catch (e) { + // ignore + } + }; + }, [$, fieldId]); + useEffect(() => { + try { + $(ref.current).wpColorPicker('color', value || ''); + } catch (e) { + // ignore + } + }, [value, $]); + return ; +} diff --git a/src/admin/customizer/repeatable/fields/HiddenField.jsx b/src/admin/customizer/repeatable/fields/HiddenField.jsx new file mode 100644 index 00000000..36f33109 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/HiddenField.jsx @@ -0,0 +1,12 @@ +export function HiddenField({ field, value, onChange }) { + const t = field.type; + return ( + onChange(e.target.value)} + className={t === 'add_by' ? 'add_by' : ''} + /> + ); +} diff --git a/src/admin/customizer/repeatable/fields/IconField.jsx b/src/admin/customizer/repeatable/fields/IconField.jsx new file mode 100644 index 00000000..987668ac --- /dev/null +++ b/src/admin/customizer/repeatable/fields/IconField.jsx @@ -0,0 +1,61 @@ +import { useEffect, useRef } from '@wordpress/element'; +import { + iconPreviewClass, + isSvgIconValue, + ONEPRESS_ICON_COMMIT_EVENT, + sanitizeSvgForCustomizerPreview, +} from '../repeatable-values'; + +export function IconField({ field, value, onChange }) { + const wrapRef = useRef(null); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; + + useEffect(() => { + const handler = (e) => { + if (!e.detail || e.detail.wrapperEl !== wrapRef.current) { + return; + } + onChangeRef.current(e.detail.value); + }; + window.addEventListener(ONEPRESS_ICON_COMMIT_EVENT, handler); + return () => window.removeEventListener(ONEPRESS_ICON_COMMIT_EVENT, handler); + }, []); + + const isSvg = isSvgIconValue(value); + const ic = isSvg ? '' : iconPreviewClass(value); + const svgPreview = isSvg ? sanitizeSvgForCustomizerPreview(value) : ''; + + return ( +
    +
    + {isSvg ? ( + svgPreview ? ( + + ) : ( +
    + { + e.preventDefault(); + onChange(''); + }} + > + Remove + +
    + ); +} diff --git a/src/admin/customizer/repeatable/fields/MediaField.jsx b/src/admin/customizer/repeatable/fields/MediaField.jsx new file mode 100644 index 00000000..a411fbdf --- /dev/null +++ b/src/admin/customizer/repeatable/fields/MediaField.jsx @@ -0,0 +1,94 @@ +import { useEffect, useRef } from '@wordpress/element'; +import { normalizeMediaValue } from '../repeatable-values'; + +export function MediaField({ field, value, onChange, $ }) { + const rootRef = useRef(null); + const m = normalizeMediaValue(value); + const isImage = !field.media || field.media === '' || field.media === 'image'; + + useEffect(() => { + const $root = $(rootRef.current); + if (!$root.length) { + return; + } + const sync = () => { + onChange({ + url: String($root.find('input.image_url').first().val() || ''), + id: String($root.find('input.image_id').first().val() || ''), + }); + }; + $root.on('change.onepressR', 'input.image_url, input.image_id', sync); + return () => $root.off('.onepressR'); + }, [onChange, $]); + + useEffect(() => { + const $root = $(rootRef.current); + if (!$root.length) { + return; + } + const next = normalizeMediaValue(value); + $root.find('input.image_url').first().val(next.url); + $root.find('input.image_id').first().val(next.id); + }, [value, $]); + + return ( +
    + {isImage ? ( + + ) : ( + onChange({ ...m, url: e.target.value })} + /> + )} + + {isImage ? ( +
    +
    +
    +
    {m.url ? : null}
    +
    +
    +
    + ) : null} +
    + + +
    +
    +
    + ); +} diff --git a/src/admin/customizer/repeatable/fields/RadioField.jsx b/src/admin/customizer/repeatable/fields/RadioField.jsx new file mode 100644 index 00000000..a0999a58 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/RadioField.jsx @@ -0,0 +1,16 @@ +export function RadioField({ field, value, onChange }) { + const opts = field.options || {}; + return Object.keys(opts).map((k) => ( + + )); +} diff --git a/src/admin/customizer/repeatable/fields/SelectField.jsx b/src/admin/customizer/repeatable/fields/SelectField.jsx new file mode 100644 index 00000000..0bee18c8 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/SelectField.jsx @@ -0,0 +1,41 @@ +export function SelectField({ field, value, onChange }) { + const opts = field.options || {}; + const keys = Object.keys(opts); + + if (field.multiple) { + const arr = Array.isArray(value) ? value : []; + return ( + + ); + } + + return ( + + ); +} diff --git a/src/admin/customizer/repeatable/fields/TextField.jsx b/src/admin/customizer/repeatable/fields/TextField.jsx new file mode 100644 index 00000000..1ddeb090 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/TextField.jsx @@ -0,0 +1,11 @@ +export function TextField({ field, value, onChange }) { + return ( + onChange(e.target.value)} + className="" + /> + ); +} diff --git a/src/admin/customizer/repeatable/fields/TextareaField.jsx b/src/admin/customizer/repeatable/fields/TextareaField.jsx new file mode 100644 index 00000000..5de2b602 --- /dev/null +++ b/src/admin/customizer/repeatable/fields/TextareaField.jsx @@ -0,0 +1,50 @@ +import { useEffect, useRef } from '@wordpress/element'; + +export function TextareaField({ field, value, onChange, skipEditor, $ }) { + const ref = useRef(null); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; + + // Modal TinyMCE (modal-editor.js + wp-editor.js) syncs with + // settings.sync_id.val(html).trigger("change") (jQuery). That does not invoke + // native addEventListener handlers, so a controlled React textarea never updates + // state or the Customizer setting — bind the same callback via jQuery as well. + useEffect(() => { + if (field.type !== 'editor' || skipEditor) { + return; + } + const el = ref.current; + if (!el) { + return; + } + const push = () => { + onChangeRef.current(el.value); + }; + el.addEventListener('change', push); + el.addEventListener('input', push); + let $el; + if ($ && typeof $.fn?.on === 'function') { + $el = $(el); + $el.on('change.onepressRepeaterEditor input.onepressRepeaterEditor', push); + } + return () => { + el.removeEventListener('change', push); + el.removeEventListener('input', push); + if ($el) { + $el.off('.onepressRepeaterEditor'); + } + }; + }, [field.type, skipEditor, $]); + + if (field.type === 'editor' && skipEditor) { + return null; + } + return ( +