diff --git a/src/pat/autotoc/README.md b/src/pat/autotoc/README.md index 0abaf4ed8..08cda0712 100644 --- a/src/pat/autotoc/README.md +++ b/src/pat/autotoc/README.md @@ -10,17 +10,33 @@ Automatically create a table of contents. ## Configuration -| Option | Type | Default | Description | -| :------------------: | :----: | :---------------: | :-----------------------------------: | -| IDPrefix | string | 'autotoc-item-' | Prefix used to generate ID. | -| classActiveName | string | 'active' | Class used for active level. | -| classLevelPrefixName | string | 'autotoc-level-' | Class prefix used for the TOC levels. | -| classSectionName | string | 'autotoc-section' | Class used for section in TOC. | -| classTOCName | string | 'autotoc-nav' | Class used for the TOC. | -| levels | string | 'h1,h2,h3' | Selectors used to find levels. | -| scrollDuration | string | 'slow' | Speed of scrolling. | -| scrollEasing | string | 'swing' | Easing to use while scrolling. | -| section | string | 'section' | Tag type to use for TOC. | +| Option | Type | Default | Description | +| :------------------: | :----: | :---------------: | :-------------------------------------------------------------------------: | +| IDPrefix | string | 'autotoc-item-' | Prefix used to generate ID. | +| classActiveName | string | 'active' | Class used for active level. | +| classLevelPrefixName | string | 'autotoc-level-' | Class prefix used for the TOC levels. | +| classSectionName | string | 'autotoc-section' | Class used for section in TOC. | +| classTOCName | string | 'autotoc-nav' | Class used for the TOC. | +| levels | string | 'h1,h2,h3' | Selectors used to find levels. | +| scrollDuration | string | 'slow' | Speed of scrolling. | +| scrollEasing | string | 'swing' | Easing to use while scrolling. | +| section | string | 'section' | Tag type to use for TOC. | +| validationDelay | number | 200 | For tabbed forms: Delay time in ms after the validation marker gets active. | + +## Validation support for tabbed forms + +In case the autotoc pattern is used for tabbed forms together with +pat-validation (a quite common case for z3c forms in Plone) the autotoc +navigation items are marked with `required` and `invalid` classes, if +applicable. + +This allows for a quick overview in which tabs required input fields or invalid +data is present. + +The option `validationDelay` is set to twice the delay of pat-validation input +check delay. The default is 200ms after which the form's tab-navigation is +marked with the required and invalid CSS classes. + ## Examples diff --git a/src/pat/autotoc/autotoc.js b/src/pat/autotoc/autotoc.js index 602a61934..6a0ca5fa0 100644 --- a/src/pat/autotoc/autotoc.js +++ b/src/pat/autotoc/autotoc.js @@ -1,5 +1,6 @@ import $ from "jquery"; import Base from "@patternslib/patternslib/src/core/base"; +import utils from "@patternslib/patternslib/src/core/utils"; export default Base.extend({ name: "autotoc", @@ -15,7 +16,11 @@ export default Base.extend({ classActiveName: "active", scrollDuration: "slow", scrollEasing: "swing", + validationDelay: 200, // Note: This is set to twice as the default delay time for validation. }, + + tabs: [], + init: function () { import("./autotoc.scss"); @@ -42,10 +47,10 @@ export default Base.extend({ var activeId = null; $(self.options.levels, self.$el).each(function (i) { - var $level = $(this), - id = $level.prop("id") - ? $level.prop("id") - : $level.parents(self.options.section).prop("id"); + const section = this.closest(self.options.section); + const $level = $(this); + let id = $level.prop("id") ? $level.prop("id") : $(section).prop("id"); + if (!id || $("#" + id).length > 0) { id = self.options.IDPrefix + self.name + "-" + i; } @@ -56,8 +61,8 @@ export default Base.extend({ activeId = id; } $level.data("navref", id); - $("") - .appendTo(self.$toc) + const $nav = $(""); + $nav.appendTo(self.$toc) .text($level.text()) .attr("id", id) .attr("href", "#" + id) @@ -107,6 +112,12 @@ export default Base.extend({ } }); $level.data("autotoc-trigger-id", id); + + self.tabs.push({ + section: section, + id: id, + nav: $nav[0], + }); }); if (activeId) { @@ -120,7 +131,51 @@ export default Base.extend({ skipHash: true, }); } + + // After DOM tree is built, initialize eventual validation + this.initialize_validation(self.$el); + }, + + initialize_validation: function ($el) { + const el = $el[0]; + + // Initialize only on pat-validation forms. + const form = el.closest("form.pat-validation"); + if (!form) { + return; + } + + for (const tab of this.tabs) { + if (tab.section.querySelectorAll("[required]").length > 0) { + tab.nav.classList.add("required"); + } else { + tab.nav.classList.remove("required"); + } + } + + const debounced_validation_marker = utils.debounce(() => { + this.validation_marker(); + }, this.options.validationDelay); + + form.addEventListener("pat-update", (e) => { + if (e.detail?.pattern !== "validation") { + // Nothing to do. + return; + } + debounced_validation_marker(); + }); }, + + validation_marker: function () { + for (const tab of this.tabs) { + if (tab.section.querySelectorAll(":invalid").length > 0) { + tab.nav.classList.add("invalid"); + } else { + tab.nav.classList.remove("invalid"); + } + } + }, + getLevel: function ($el) { var elementLevel = 0; $.each(this.options.levels.split(","), function (level, levelSelector) { diff --git a/src/pat/autotoc/autotoc.test.js b/src/pat/autotoc/autotoc.test.js index 558a86f21..2720c8d5e 100644 --- a/src/pat/autotoc/autotoc.test.js +++ b/src/pat/autotoc/autotoc.test.js @@ -1,9 +1,11 @@ +import { jest } from "@jest/globals"; import "./autotoc"; import $ from "jquery"; +import events from "@patternslib/patternslib/src/core/events"; import registry from "@patternslib/patternslib/src/core/registry"; import utils from "@patternslib/patternslib/src/core/utils"; -describe("AutoTOC", function () { +describe("1 - AutoTOC", function () { beforeEach(function () { document.body.innerHTML = `