diff --git a/website_form_multi_column/README.rst b/website_form_multi_column/README.rst new file mode 100644 index 0000000000..63d4f5a066 --- /dev/null +++ b/website_form_multi_column/README.rst @@ -0,0 +1,94 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========================= +Website Form Multi Column +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:adb442723290f97dc26a63058fb34701252b7f39609dc553851af40af5cf514b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwebsite-lightgray.png?logo=github + :target: https://github.com/OCA/website/tree/19.0/website_form_multi_column + :alt: OCA/website +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/website-19-0/website-19-0-website_form_multi_column + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/website&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a "Columns" option to the website Form snippet. + +The website editor can split the form into 2, 3 or 4 columns and drag +any field between them. Column layout is saved to the page like any +other website edit; the form submission flow is unchanged. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Edit a page that contains a Form snippet. +2. Click the form to open its options in the sidebar. +3. Open the "Columns" row and pick 1, 2, 3 or 4. +4. Drag any field from one column to another. Drop targets highlight on + hover. Empty columns remain droppable thanks to a minimum height. +5. On mobile (<768px) columns stack vertically by default. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ForgeFlow + +Contributors +------------ + +- Jasmin Solanki jasmin.solanki@forgeflow.com + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/website `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_form_multi_column/__init__.py b/website_form_multi_column/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/website_form_multi_column/__manifest__.py b/website_form_multi_column/__manifest__.py new file mode 100644 index 0000000000..e93687d412 --- /dev/null +++ b/website_form_multi_column/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2026 ForgeFlow S.L. +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +{ + "name": "Website Form Multi Column", + "summary": "Lay out website form fields across configurable columns" + " with drag-and-drop.", + "version": "19.0.1.0.0", + "category": "Website", + "license": "LGPL-3", + "website": "https://github.com/OCA/website", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "application": False, + "installable": True, + "depends": ["html_editor", "website"], + "assets": { + "web.assets_frontend": [ + "website_form_multi_column/static/src/scss/form_columns.scss", + ], + "website.website_builder_assets": [ + "website_form_multi_column/static/src/builder/**/*", + ], + }, +} diff --git a/website_form_multi_column/pyproject.toml b/website_form_multi_column/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_form_multi_column/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_form_multi_column/readme/CONTRIBUTORS.md b/website_form_multi_column/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..5799e7b248 --- /dev/null +++ b/website_form_multi_column/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Jasmin Solanki diff --git a/website_form_multi_column/readme/DESCRIPTION.md b/website_form_multi_column/readme/DESCRIPTION.md new file mode 100644 index 0000000000..d117935ee3 --- /dev/null +++ b/website_form_multi_column/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module adds a "Columns" option to the website Form snippet. + +The website editor can split the form into 2, 3 or 4 columns and drag any +field between them. Column layout is saved to the page like any other +website edit; the form submission flow is unchanged. diff --git a/website_form_multi_column/readme/USAGE.md b/website_form_multi_column/readme/USAGE.md new file mode 100644 index 0000000000..71ed6b1fed --- /dev/null +++ b/website_form_multi_column/readme/USAGE.md @@ -0,0 +1,6 @@ +1. Edit a page that contains a Form snippet. +2. Click the form to open its options in the sidebar. +3. Open the "Columns" row and pick 1, 2, 3 or 4. +4. Drag any field from one column to another. Drop targets highlight on + hover. Empty columns remain droppable thanks to a minimum height. +5. On mobile (<768px) columns stack vertically by default. diff --git a/website_form_multi_column/static/description/index.html b/website_form_multi_column/static/description/index.html new file mode 100644 index 0000000000..8dd1ce311a --- /dev/null +++ b/website_form_multi_column/static/description/index.html @@ -0,0 +1,444 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Website Form Multi Column

+ +

Beta License: LGPL-3 OCA/website Translate me on Weblate Try me on Runboat

+

This module adds a “Columns” option to the website Form snippet.

+

The website editor can split the form into 2, 3 or 4 columns and drag +any field between them. Column layout is saved to the page like any +other website edit; the form submission flow is unchanged.

+

Table of contents

+ +
+

Usage

+
    +
  1. Edit a page that contains a Form snippet.
  2. +
  3. Click the form to open its options in the sidebar.
  4. +
  5. Open the “Columns” row and pick 1, 2, 3 or 4.
  6. +
  7. Drag any field from one column to another. Drop targets highlight on +hover. Empty columns remain droppable thanks to a minimum height.
  8. +
  9. On mobile (<768px) columns stack vertically by default.
  10. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/website project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/website_form_multi_column/static/src/builder/form/form_columns_option.xml b/website_form_multi_column/static/src/builder/form/form_columns_option.xml new file mode 100644 index 0000000000..cd2bf69365 --- /dev/null +++ b/website_form_multi_column/static/src/builder/form/form_columns_option.xml @@ -0,0 +1,15 @@ + + + + + + + 1 + 2 + 3 + 4 + + + + + diff --git a/website_form_multi_column/static/src/builder/form/form_columns_plugin.esm.js b/website_form_multi_column/static/src/builder/form/form_columns_plugin.esm.js new file mode 100644 index 0000000000..19c769f864 --- /dev/null +++ b/website_form_multi_column/static/src/builder/form/form_columns_plugin.esm.js @@ -0,0 +1,126 @@ +import {registry} from "@web/core/registry"; +import {Plugin} from "@html_editor/plugin"; +import {BaseOptionComponent} from "@html_builder/core/utils"; +import {BuilderAction} from "@html_builder/core/builder_action"; + +const COL_CLASSES = { + 1: "col-12", + 2: "col-md-6", + 3: "col-md-4", + 4: "col-md-3", +}; + +export class FormColumnsOption extends BaseOptionComponent { + static template = "website_form_multi_column.FormColumnsOption"; + static selector = ".s_website_form"; +} + +function getColumnCount(formEl) { + const value = parseInt(formEl.dataset.multiColumn, 10); + return Number.isFinite(value) && value >= 1 && value <= 4 ? value : 1; +} + +function ensureColumnWrappers(formEl, count) { + const rowsEl = formEl.querySelector(".s_website_form_rows"); + const colClass = COL_CLASSES[count]; + const existing = [...rowsEl.querySelectorAll(":scope > .o_form_column")]; + while (existing.length > count) { + existing.pop().remove(); + } + while (existing.length < count) { + const col = document.createElement("div"); + col.className = `o_form_column ${colClass}`; + rowsEl.appendChild(col); + existing.push(col); + } + for (const col of existing) { + col.classList.remove("col-12", "col-md-6", "col-md-4", "col-md-3"); + col.classList.add(...colClass.split(" ")); + } + return existing; +} + +// Keep-then-fill: preserve prior column per field, clamp on shrink, default to col 0. +function redistributeFields(formEl, columnEls, fieldEls, oldCount, newCount) { + for (const fieldEl of fieldEls) { + const prev = parseInt(fieldEl.parentElement.dataset.columnIndex, 10); + const target = Number.isFinite(prev) ? Math.min(prev, newCount - 1) : 0; + columnEls[target].appendChild(fieldEl); + } +} + +export class SetColumnCountAction extends BuilderAction { + static id = "setColumnCount"; + + apply({editingElement: formEl, value}) { + const newCount = parseInt(value, 10); + const oldCount = getColumnCount(formEl); + const rowsEl = formEl.querySelector(".s_website_form_rows"); + + // Tag current wrappers so redistributeFields can read prior placement. + for (const col of rowsEl.querySelectorAll(":scope > .o_form_column")) { + const idx = [...rowsEl.children].indexOf(col); + col.dataset.columnIndex = String(idx); + } + + const fieldEls = [ + ...rowsEl.querySelectorAll(":scope > .s_website_form_field"), + ...rowsEl.querySelectorAll( + ":scope > .o_form_column > .s_website_form_field" + ), + ].filter( + (el) => + !el.classList.contains("s_website_form_submit") && + !el.classList.contains("s_website_form_recaptcha") && + !el.classList.contains("s_website_form_dnone") + ); + + if (newCount === 1) { + for (const el of fieldEls) { + rowsEl.appendChild(el); + } + for (const col of rowsEl.querySelectorAll(":scope > .o_form_column")) { + col.remove(); + } + delete formEl.dataset.multiColumn; + return; + } + + const columnEls = ensureColumnWrappers(formEl, newCount); + redistributeFields(formEl, columnEls, fieldEls, oldCount, newCount); + + // Submit / recaptcha / hidden fields stay after the columns. + for (const tail of rowsEl.querySelectorAll( + ":scope > .s_website_form_submit, :scope > .s_website_form_recaptcha, :scope > .s_website_form_dnone" + )) { + rowsEl.appendChild(tail); + } + formEl.dataset.multiColumn = String(newCount); + } + + isApplied({editingElement: formEl, value}) { + return getColumnCount(formEl) === parseInt(value, 10); + } +} + +export class FormColumnsOptionPlugin extends Plugin { + static id = "formColumnsOption"; + resources = { + builder_options: [FormColumnsOption], + builder_actions: {SetColumnCountAction}, + dropzone_selector: [ + { + selector: ".s_website_form_field", + exclude: ".s_website_form_dnone", + dropIn: ".o_form_column", + dropNear: ".s_website_form_field", + dropLockWithin: "form", + }, + ], + content_editable_selectors: [".s_website_form form .o_form_column"], + }; +} + +registry + .category("website-plugins") + .add(FormColumnsOptionPlugin.id, FormColumnsOptionPlugin); diff --git a/website_form_multi_column/static/src/scss/form_columns.scss b/website_form_multi_column/static/src/scss/form_columns.scss new file mode 100644 index 0000000000..61f90af5eb --- /dev/null +++ b/website_form_multi_column/static/src/scss/form_columns.scss @@ -0,0 +1,28 @@ +.s_website_form { + // Strip nested col-12 gutter on fields so the wrapper column provides spacing. + .o_form_column { + min-height: 4rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + + > .s_website_form_field { + padding-left: 0; + padding-right: 0; + margin-bottom: 1rem; + + &:last-child { + margin-bottom: 0; + } + } + + @include media-breakpoint-down(md) { + margin-bottom: 1rem; + } + } + + &[data-multi-column] .o_form_column:empty { + outline: 1px dashed var(--bs-border-color); + outline-offset: -4px; + border-radius: 0.25rem; + } +}