+ +Demo page + + + + + +The content below is rendered from Markdown
+
++ + diff --git a/src/pat/markdown/injected-page.html b/src/pat/markdown/injected-page.html deleted file mode 100644 index c743f20c6..000000000 --- a/src/pat/markdown/injected-page.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -# Header 1 ## Header 2 @@ -33,34 +28,38 @@+Autoloading content from a separate Markdown file
| Felis | dui | porttitor | |-------|-----------|-----------| | dui | porttitor | eros | - -
- -- Click here to inject more markdown content -
- -- The content rendered from markdown will appear here. -- - +Some code: + +```javascript +const pattern = registry.patterns[name]; +if (pattern.transform) { + try { + pattern.transform($content); + } catch (e) { + if (dont_catch) { + throw(e); + } + log.error("Transform error for pattern" + name, e); + } +} +``` +
+ ++
+- Inject markdown content with data-type declaration.
+- Inject markdown content, automatically detected as markdown based on the file name extension.
++ The content rendered from markdown will appear here. ++Demo page - - - - - - This content will be replaced by the source content of the link, - rendered as HTML. - - diff --git a/src/pat/markdown/markdown.js b/src/pat/markdown/markdown.js index ba0b21aaf..e8127df73 100644 --- a/src/pat/markdown/markdown.js +++ b/src/pat/markdown/markdown.js @@ -2,12 +2,13 @@ import $ from "jquery"; import logging from "../../core/logging"; import utils from "../../core/utils"; import Base from "../../core/base"; +import events from "../../core/events"; import inject from "../inject/inject"; -var log = logging.getLogger("pat.markdown"); -var is_markdown_resource = /\.md$/; +const log = logging.getLogger("pat.markdown"); +const is_markdown_resource = /\.md$/; -var Markdown = Base.extend({ +const Markdown = Base.extend({ name: "markdown", trigger: ".pat-markdown", @@ -17,45 +18,38 @@ var Markdown = Base.extend({ * to pat-inject. The following only applies to standalone, when * $el is explicitly configured with the pat-markdown trigger. */ - var source = this.$el.is(":input") ? this.$el.val() : this.$el.text(); + const source = this.$el.is(":input") + ? this.$el.val() + : this.$el[0].innerHTML; let rendered = await this.render(source); - rendered.replaceAll(this.$el); + this.el.innerHTML = ""; + this.el.append(...rendered[0].childNodes); } }, async render(text) { - const Showdown = (await import("showdown")).default; + const marked = (await import("marked")).marked; + const DOMPurify = (await import("dompurify")).default; + const SyntaxHighlight = (await import("../syntax-highlight/syntax-highlight")).default; // prettier-ignore - // Add support for syntax highlighting via pat-syntax-highlight - Showdown.extensions.prettify = function () { - return [ - { - type: "output", - filter: function (source) { - return source.replace(/()?/gi, function (match, pre) { - if (pre) { - return '+ +'; - } else { - return ''; - } - }); - }, - }, - ]; - }; - - const $rendering = $(""); - const converter = new Showdown.Converter({ - tables: true, - extensions: ["prettify"], - }); - $rendering.html(converter.makeHtml(text)); - return $rendering; + const wrapper = document.createElement("div"); + const parsed = DOMPurify.sanitize(marked.parse(text)); + wrapper.innerHTML = parsed; + for (const item of wrapper.querySelectorAll("pre > code")) { + const pre = item.parentElement; + pre.classList.add("pat-syntax-highlight"); + // If the code block language was set in a fenced code block, + // marked has already set the language as a class on the code tag. + // pat-syntax-highlight will understand this. + new SyntaxHighlight(pre); + await events.await_event(pre, "init.syntax-highlight.patterns"); + } + return $(wrapper); }, async renderForInjection(cfg, data) { - var header, - source = data; + let header; + let source = data; if (cfg.source && (header = /^#+\s*(.*)/.exec(cfg.source)) !== null) { source = this.extractSection(source, header[1]); if (source === null) { @@ -69,21 +63,18 @@ var Markdown = Base.extend({ }, extractSection(text, header) { - var pattern, level; + let pattern; header = utils.escapeRegExp(header); - var matcher = new RegExp( - "^((#+)\\s*@TEXT@\\s*|@TEXT@\\s*\\n([=-])+\\s*)$".replace( - /@TEXT@/g, - header - ), - "m" - ), - match = matcher.exec(text); + let matcher = new RegExp( + "^((#+)\\s*@TEXT@\\s*|@TEXT@\\s*\\n([=-])+\\s*)$".replace(/@TEXT@/g, header), + "m" + ); + let match = matcher.exec(text); if (match === null) { return null; } else if (match[2]) { // We have a ##-style header. - level = match[2].length; + const level = match[2].length; pattern = "^#{@LEVEL@}\\s*@TEXT@\\s*$\\n+((?:.|\\n)*?(?=^#{1,@LEVEL@}\\s)|.*(?:.|\\n)*)"; pattern = pattern.replace(/@LEVEL@/g, level); @@ -117,7 +108,7 @@ $(document).ready(function () { /* Identify injected URLs which point to markdown files and set their * datatype so that we can register a type handler for them. */ - var cfgs = $(this).data("pat-inject"); + const cfgs = $(this).data("pat-inject"); cfgs.forEach(function (cfg) { if (is_markdown_resource.test(cfg.url)) { cfg.dataType = "markdown"; @@ -131,7 +122,7 @@ inject.registerTypeHandler("markdown", { async sources(cfgs, data) { return await Promise.all( cfgs.map(async function (cfg) { - var pat = Markdown.init(cfg.$target); + const pat = new Markdown(cfg.$target[0]); const rendered = await pat.renderForInjection(cfg, data); return rendered; }) diff --git a/src/pat/markdown/markdown.test.js b/src/pat/markdown/markdown.test.js index 29a940d3c..06bc938c5 100644 --- a/src/pat/markdown/markdown.test.js +++ b/src/pat/markdown/markdown.test.js @@ -16,7 +16,7 @@ describe("pat-markdown", function () { afterEach(() => { jest.restoreAllMocks(); }); - it("replaces the DOM element with the rendered Markdown content.", async function () { + it("It renders content for elements with the pattern trigger.", async function () { var $el = $(''); $el.appendTo("#lab"); jest.spyOn(pattern.prototype, "render").mockImplementation(() => { @@ -24,10 +24,10 @@ describe("pat-markdown", function () { }); pattern.init($el); await utils.timeout(1); // wait a tick for async to settle. - expect($("#lab").html()).toBe("+Rendering
"); + expect($("#lab").html()).toBe('Rendering
'); }); - it("does not replace the DOM element if it doesn't have the pattern trigger", function () { + it("It does not render when the DOM element doesn't have the pattern trigger", function () { var $el = $(""); $el.appendTo("#lab"); jest.spyOn(pattern.prototype, "render").mockImplementation(() => { @@ -70,7 +70,7 @@ describe("pat-markdown", function () { it("converts markdown into HTML", async function () { const $rendering = await pattern.prototype.render("*This is markdown*"); - expect($rendering.html()).toBe("This is markdown
"); + expect($rendering.html()).toBe(`This is markdown
\n`); }); }); @@ -159,4 +159,29 @@ describe("pat-markdown", function () { ).toBe("My title\n-------\nContent\n\n"); }); }); + + describe("Code blocks", function () { + it("It correctly renders code blocks", async function () { + document.body.innerHTML = `+# Title + +some content + +\`\`\`javascript + const foo = "bar"; +\`\`\` ++`; + + new pattern(document.querySelector(".pat-markdown")); + await utils.timeout(1); // wait a tick for async to settle. + await utils.timeout(1); // wait a tick for async to settle. + + expect(document.body.querySelector(".pat-markdown > h1").textContent).toBe("Title"); // prettier-ignore + expect(document.body.querySelector(".pat-markdown > p").textContent).toBe("some content"); // prettier-ignore + expect(document.body.querySelector(".pat-markdown > pre code")).toBeTruthy(); // prettier-ignore + expect(document.body.querySelector(".pat-markdown > pre.language-javascript code.language-javascript")).toBeTruthy(); // prettier-ignore + expect(document.body.querySelector(".pat-markdown > pre code .hljs-keyword")).toBeTruthy(); // prettier-ignore + }); + }); }); diff --git a/src/pat/sortable/sortable.js b/src/pat/sortable/sortable.js index 6347854e3..8d6a9ea7a 100644 --- a/src/pat/sortable/sortable.js +++ b/src/pat/sortable/sortable.js @@ -15,9 +15,12 @@ export default Base.extend({ trigger: ".pat-sortable", init: function () { + if (window.__patternslib_import_styles) { + import("./_sortable.scss"); + } this.$form = this.$el.closest("form"); this.options = parser.parse(this.$el, false); - this.recordPositions().addHandles().initScrolling(); + this.recordPositions().initScrolling(); this.$el.on("pat-update", this.onPatternUpdate.bind(this)); }, @@ -38,7 +41,10 @@ export default Base.extend({ this.$sortables = this.$el.children().filter(this.options.selector); this.$sortables.each(function (idx) { $(this).data("patterns.sortable", { position: idx }); + // Add `.sortable-item` class to each sortable. + this.classList.add("sortable-item"); }); + this.addHandles(); return this; }, diff --git a/src/pat/switch/switch.js b/src/pat/switch/switch.js index 4688522b6..8dcdc9403 100644 --- a/src/pat/switch/switch.js +++ b/src/pat/switch/switch.js @@ -23,6 +23,7 @@ export default Base.extend({ this.options = this._validateOptions(parser.parse(this.el, this.options, true)); events.add_event_listener(this.el, "click", "pat-switch--on-click", (e) => { + // TODO: e.target.tagName if (e.tagName === "A") { e.preventDefault(); } diff --git a/src/pat/syntax-highlight/index.html b/src/pat/syntax-highlight/index.html index 4a3edbc6b..8dbc0401a 100644 --- a/src/pat/syntax-highlight/index.html +++ b/src/pat/syntax-highlight/index.html @@ -1,23 +1,46 @@ - -pat-syntax-highlight demo page - - - - - -Sample output:
--var pattern = registry.patterns[name]; + +pat-syntax-highlight demo page + + + + + +Syntax highlight for JavaScript
++- +} ++const pattern = registry.patterns[name]; if (pattern.transform) { try { pattern.transform($content); } catch (e) { - if (dont_catch) { throw(e); } + if (dont_catch) { + throw(e); + } log.error("Transform error for pattern" + name, e); } -Syntax highlight for HTML
+++ ++<h1>hello</h1> ++Syntax highlight for Python
+++ + diff --git a/src/pat/syntax-highlight/syntax-highlight.js b/src/pat/syntax-highlight/syntax-highlight.js index 75c7dd94f..2cdf3dd09 100644 --- a/src/pat/syntax-highlight/syntax-highlight.js +++ b/src/pat/syntax-highlight/syntax-highlight.js @@ -1,13 +1,88 @@ -import Base from "../../core/base"; +import { BasePattern } from "../../core/basepattern"; +import Parser from "../../core/parser"; +import registry from "../../core/registry"; import utils from "../../core/utils"; -export default Base.extend({ - name: "syntax-highlight", - trigger: ".pat-syntax-highlight", +export const parser = new Parser("syntax-highlight"); +parser.addArgument("language", "html"); +parser.addArgument("theme", "dark", ["dark", "light"]); +parser.addArgument("features", null, ["line-highlight", "line-numbers"], true); + +class Pattern extends BasePattern { + static name = "syntax-highlight"; + static trigger = ".pat-syntax-highlight"; + parser = parser; async init() { - const Prettify = (await import("google-code-prettify/src/prettify")).default; // prettier-ignore - this.$el.addClass("prettyprint"); - utils.debounce(Prettify.prettyPrint, 50)(); - }, -}); + let _el = this.el; + const code_el = [...this.el.children].filter((it) => it.tagName === "CODE")?.[0]; + if (code_el) { + _el = code_el; + } + + let theme; + if ( + this.options.theme === "light" || + _el.classList.contains("light-bg") || + this.el.classList.contains("light-bg") + ) { + theme = "stackoverflow-light"; + } else if (this.options.theme === "dark") { + theme = "stackoverflow-dark"; + } else { + theme = this.options.theme; + } + + import(`highlight.js/styles/${theme}.css`); + const hljs = (await import("highlight.js")).default; + + // Get the language + let language = [..._el.classList, ...this.el.classList] + .filter((it) => { + return it.startsWith("language-") || it.startsWith("lang-"); + })?.[0] + ?.replace("language-", "") + ?.replace("lang-", ""); + // CSS class language always win. + language = language || this.options.language || "html"; + // Set the language on the code element (ignored if already set) + this.el.classList.add(`language-${language}`); + _el.classList.add(`language-${language}`); + _el.classList.add("hljs"); + + let high; + const value = utils.unescape_html(_el.innerHTML).trim(); + if (language) { + try { + // language to import path mapping + const import_path_mapping = { + atom: "xml", + html: "xml", + plist: "xml", + rss: "xml", + svg: "xml", + wsf: "xml", + xhtml: "xml", + xjb: "xml", + xsd: "xml", + xsl: "xml", + }; + const lang_file = import_path_mapping[language] || language; + const hljs_language = ( + await import(`highlight.js/lib/languages/${lang_file}`) + ).default; + hljs.registerLanguage("javascript", hljs_language); + high = hljs.highlight(value, { language: language }).value; + } catch { + high = hljs.highlightAuto(value).value; + } + } else { + high = hljs.highlightAuto(value).value; + } + _el.innerHTML = high; + } +} + +registry.register(Pattern); +export default Pattern; +export { Pattern }; diff --git a/src/pat/syntax-highlight/syntax-highlight.test.js b/src/pat/syntax-highlight/syntax-highlight.test.js new file mode 100644 index 000000000..22cd45fef --- /dev/null +++ b/src/pat/syntax-highlight/syntax-highlight.test.js @@ -0,0 +1,56 @@ +import pattern from "./syntax-highlight"; +import utils from "../../core/utils"; + +describe("pat-markdown", function () { + beforeEach(function () { + document.body.innerHTML = ""; + }); + + afterEach(function () { + document.body.innerHTML = ""; + }); + + describe("when initialized", function () { + it("it does syntax highlighting.", async function () { + document.body.innerHTML = `+def foo(): + if not bar: + return True ++`; + new pattern(document.querySelector(".pat-syntax-highlight")); + await utils.timeout(1); + expect( + document.querySelector(".pat-syntax-highlight code .hljs-keyword") + ).toBeTruthy(); + // Also adds the .hljs class to the wrapper + expect( + document.querySelector(".pat-syntax-highlight code.hljs") + ).toBeTruthy(); + }); + + it("it does syntax highlighting on any element.", async function () { + document.body.innerHTML = `def foo():\n passdef foo():\n pass`; + new pattern(document.querySelector(".pat-syntax-highlight")); + await utils.timeout(1); + expect( + document.querySelector(".pat-syntax-highlight .hljs-keyword") + ).toBeTruthy(); + // Also adds the .hljs class to the wrapper + expect(document.querySelector(".pat-syntax-highlight.hljs")).toBeTruthy(); + }); + + it("it does syntax highlighting for html.", async function () { + // html code should be escaped for `<`,`>` and `&`. + document.body.innerHTML = `+`; + new pattern(document.querySelector(".pat-syntax-highlight")); + await utils.timeout(1); + expect( + document.querySelector(".pat-syntax-highlight code .hljs-tag") + ).toBeTruthy(); + // Also adds the .hljs class to the wrapper + expect( + document.querySelector(".pat-syntax-highlight code.hljs") + ).toBeTruthy(); + }); + }); +}); diff --git a/src/pat/toggle/documentation.md b/src/pat/toggle/documentation.md index 4cf6246e4..8dc32dff7 100644 --- a/src/pat/toggle/documentation.md +++ b/src/pat/toggle/documentation.md @@ -8,7 +8,12 @@ For instance to show or hide a sidebar with a CSS class on the body tag. The _toggle_ pattern can be used to toggle attribute values for objects. It is most commonly used to toggle a CSS class. - Start working + Start working+ <h1>foo</h1> ++Working…@@ -67,3 +72,18 @@ The possible values for the `store` parameter are: - `none`: do not remember the toggle state (default). - `local`: remember the state as part of the local storage. - `session`: remember the status as part of the session storage. + + +### Options reference + +You can customise the behaviour of a switches through options in the +`data-pat-toggle` attribute. + +| Property | Default value | Values | Description | Type | +| ---------- | ------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `selector` | | | CSS selector matching elements where a class or an attribute should be toggled. | String | +| `value` | | | One or more space seperated CSS class names to toggle on the element. Can only be used with `attr` set to class, which is the default. | String | +| `event` | `click` | | A JavaScript event which triggers the toggler. The default is to listen to `click` events. | String | +| `attr` | `class` | | The attribute which should be toggled. In case of the default `class`, class names are added or removed. In case of any other attribute the attribute as added or removed. | String | +| `store` | `none` | `none` `session` `local` | How to store the state of a toggle. `none` does not remember the toggle state, `local` stores the state as part of the local storage and `session` stores the status as part of the session storage. | Mutually exclusive | + diff --git a/src/pat/toggle/toggle.js b/src/pat/toggle/toggle.js index 38ccc0b87..735d4638a 100644 --- a/src/pat/toggle/toggle.js +++ b/src/pat/toggle/toggle.js @@ -19,6 +19,8 @@ parser.addArgument("attr", "class"); parser.addArgument("value"); parser.addArgument("store", "none", ["none", "session", "local"]); +parser.addAlias("attribute", "attr"); + export function ClassToggler(values) { this.values = values.slice(0); if (this.values.length > 1) this.values.push(values[0]); diff --git a/src/pat/toggle/toggle.test.js b/src/pat/toggle/toggle.test.js index b0c36ef84..d7b408f70 100644 --- a/src/pat/toggle/toggle.test.js +++ b/src/pat/toggle/toggle.test.js @@ -175,6 +175,16 @@ describe("Pattern implementation", function () { store: "none", }); }); + + it("1.6 - Accept attribute as an alias to attr.", function () { + const instance = new Pattern(document.createElement("div")); + const validated = instance._validateOptions([ + { attribute: "disabled", selector: "input" }, + ]); + expect(validated.length).toEqual(1); + expect(validated[0].attribute).toEqual("disabled"); + expect(validated[0].selector).toEqual("input"); + }); }); describe("2 - When clicking on a toggle", function () { @@ -226,6 +236,18 @@ describe("Pattern implementation", function () { await utils.timeout(1); expect(victims[0].disabled).toBe(false); }); + + it("2.3 - attributes are updated - use the alias", async function () { + var $trigger = $(trigger); + trigger.dataset.patToggle = ".victim; attribute: disabled"; + new Pattern($trigger); + $trigger.click(); + await utils.timeout(1); + expect(victims[0].disabled).toBe(true); + $trigger.click(); + await utils.timeout(1); + expect(victims[0].disabled).toBe(false); + }); }); describe("3 - Toggle event triggers", function () { diff --git a/src/pat/validation/validation.js b/src/pat/validation/validation.js index 4ee831c26..d5383a929 100644 --- a/src/pat/validation/validation.js +++ b/src/pat/validation/validation.js @@ -1,5 +1,6 @@ // Patterns validate - Form vlidation import "../../core/polyfills"; // SubmitEvent.submitter for Safari < 15.4 and jsDOM +import $ from "jquery"; import Base from "../../core/base"; import Parser from "../../core/parser"; import dom from "../../core/dom"; @@ -7,8 +8,8 @@ import events from "../../core/events"; import logging from "../../core/logging"; import utils from "../../core/utils"; -const log = logging.getLogger("pat-validation"); -//log.setLevel(logging.Level.DEBUG); +const logger = logging.getLogger("pat-validation"); +//logger.setLevel(logging.Level.DEBUG); export const parser = new Parser("validation"); parser.addArgument("disable-selector", "[type=submit], button:not([type=button])"); // Elements which must be disabled if there are errors @@ -44,6 +45,32 @@ export default Base.extend({ init() { this.options = parser.parse(this.el, this.options); + events.add_event_listener( + this.el, + "submit", + `pat-validation--submit--validator`, + (e) => { + // On submit, check all. + // Immediate, non-debounced check with submit. Otherwise submit + // is not cancelable. + for (const input of this.inputs) { + logger.debug("Checking input for submit", input, e); + this.check_input({ input: input, event: e }); + } + } + ); + + this.initialize_inputs(); + $(this.el).on("pat-update", () => { + this.initialize_inputs(); + }); + + // Set ``novalidate`` attribute to disable the browser's validation + // bubbles but not disable the validation API. + this.el.setAttribute("novalidate", ""); + }, + + initialize_inputs() { this.inputs = [ ...this.el.querySelectorAll("input[name], select[name], textarea[name]"), ]; @@ -51,24 +78,11 @@ export default Base.extend({ ...this.el.querySelectorAll(this.options.disableSelector), ]; - // Set ``novalidate`` attribute to disable the browser's validation - // bubbles but not disable the validation API. - this.el.setAttribute("novalidate", ""); - for (const [cnt, input] of this.inputs.entries()) { // Cancelable debouncer. const debouncer = utils.debounce((e) => { + logger.debug("Checking input for event", input, e); this.check_input({ input: input, event: e }); - if (this.disabled_elements.some((it) => it.disabled)) { - // If there are already any disabled elements, do a check - // for the whole form. - // This is necessary otherwise the submit button is already - // disabled and no other errors would be shown. - // This is debounced, so it should not disturb too much while typing. - for (const _input of this.inputs.filter((it) => it !== input)) { - this.check_input({ input: _input }); - } - } }, this.options.delay); events.add_event_listener( @@ -90,20 +104,6 @@ export default Base.extend({ (e) => debouncer(e) ); } - - events.add_event_listener( - this.el, - "submit", - `pat-validation--submit--validator`, - (e) => { - // On submit, check all. - // Immediate, non-debounced check with submit. Otherwise submit - // is not cancelable. - for (const input of this.inputs) { - this.check_input({ input: input, event: e }); - } - } - ); }, check_input({ input, event, stop = false }) { @@ -121,7 +121,7 @@ export default Base.extend({ return; } - log.debug(` + logger.debug(` validity_state.badInput ${validity_state.badInput} validity_state.customError ${validity_state.customError} validity_state.patternMismatch ${validity_state.patternMismatch} @@ -155,6 +155,8 @@ export default Base.extend({ }); } else if (input_options.not.after || input_options.not.before) { const msg = input_options.message.date || input_options.message.datetime; + const msg_default_not_before = "The date must be after %{attribute}"; + const msg_default_not_after = "The date must be before %{attribute}"; let not_after; let not_after_el; @@ -199,19 +201,60 @@ export default Base.extend({ const date = new Date(input.value); if (not_after && date > not_after) { - this.set_validity({ input: input, msg: msg }); + let msg_attr; + // Try to construct a meaningfull error message + if (!not_after_el && input_options.not.after) { + // fixed date case + msg_attr = input_options.not.after; + } else { + // Get the label + other text content within the + // label and replace all whitespace and newlines + // with a single space. + msg_attr = not_after_el?.labels?.[0]?.textContent.replace( + /\s\s+/g, // replace all whitespace + " " // with a single space + ); + msg_attr = msg_attr || not_after_el.name; + } + this.set_validity({ + input: input, + msg: msg || msg_default_not_after, + attribute: msg_attr.trim(), + }); } else if (not_before && date < not_before) { - this.set_validity({ input: input, msg: msg }); + let msg_attr; + // Try to construct a meaningfull error message + if (!not_before_el && input_options.not.before) { + // fixed date case + msg_attr = input_options.not.before; + } else { + // Get the label + other text content within the + // label and replace all whitespace and newlines + // with a single space. + msg_attr = not_before_el?.labels?.[0]?.textContent.replace( + /\s\s+/g, // replace all whitespace + " " // with a single space + ); + msg_attr = msg_attr || not_before_el.name; + } + this.set_validity({ + input: input, + msg: msg || msg_default_not_before, + attribute: msg_attr.trim(), + }); } } // always check the other input to clear/set errors - !stop && // do not re-check when stop is set to avoid infinite loops - not_after_el && + // do not re-check when stop is set to avoid infinite loops + if (!stop && not_after_el) { + logger.debug("Check `not-after` input.", not_after_el); this.check_input({ input: not_after_el, stop: true }); - !stop && - not_before_el && + } + if (!stop && not_before_el) { + logger.debug("Check `no-before` input.", not_after_el); this.check_input({ input: not_before_el, stop: true }); + } } if (!validity_state.customError) { @@ -358,10 +401,25 @@ export default Base.extend({ } input[KEY_ERROR_EL] = error_node; + let did_disable = false; for (const it of this.disabled_elements) { if (!it.disabled) { + did_disable = true; it.setAttribute("disabled", "disabled"); it.classList.add("disabled"); + logger.debug("Disable element", it); + } + } + + // Do an initial check of the whole form when a form element (e.g. the + // submit button) was disabled. We want to show the user all possible + // errors at once and after the submit button is disabled there is no + // way to check the whole form at once. ... well we also do not want to + // check the whole form when one input was changed.... + if (did_disable) { + logger.debug("Checking whole form after element was disabled."); + for (const _input of this.inputs.filter((it) => it !== input)) { + this.check_input({ input: _input, stop: true }); } } }, diff --git a/src/pat/validation/validation.test.js b/src/pat/validation/validation.test.js index 07c260e72..8d283e298 100644 --- a/src/pat/validation/validation.test.js +++ b/src/pat/validation/validation.test.js @@ -945,7 +945,7 @@ describe("pat-validation", function () { expect(el.querySelectorAll("em.warning").length).toBe(0); }); - it("5.4 - validates dates with before/after as pattern config attributes with custom error message.", async function () { + it("5.4.1 - validates dates with before/after as pattern config attributes with custom error message.", async function () { document.body.innerHTML = ` + `; + + const el = document.querySelector(".pat-validation"); + const inp = el.querySelector("[name=date]"); + + new Pattern(el); + await utils.timeout(1); // wait a tick for async to settle. + + // No error when left empty and not required. + inp.value = ""; + inp.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + expect(el.querySelectorAll("em.warning").length).toBe(0); + + inp.value = "2010-10-10"; + inp.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + expect(el.querySelectorAll("em.warning").length).toBe(1); + expect(el.querySelectorAll("em.warning")[0].textContent).toBe( + "The date must be after 2011-11-11" + ); + + inp.value = "2023-02-23"; + inp.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + expect(el.querySelectorAll("em.warning").length).toBe(1); + expect(el.querySelectorAll("em.warning")[0].textContent).toBe( + "The date must be before 2022-02-22" + ); + + inp.value = "2022-01-01"; + inp.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + + expect(el.querySelectorAll("em.warning").length).toBe(0); + }); + + it("5.4.3 - validates dates with before/after as pattern config attributes with NO custom error message, using labels.", async function () { + document.body.innerHTML = ` + + `; + + const el = document.querySelector(".pat-validation"); + const inp1 = el.querySelector("[name=date1]"); + const inp2 = el.querySelector("[name=date2]"); + + new Pattern(el); + await utils.timeout(1); // wait a tick for async to settle. + + inp1.value = "2010-10-10"; + inp2.value = "2001-01-01"; + + inp1.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + expect(el.querySelectorAll("em.warning").length).toBe(2); + + console.log(document.body.innerHTML); + expect(el.querySelectorAll("em.warning")[0].textContent).toBe( + "The date must be before woo date" + ); + + expect(el.querySelectorAll("em.warning")[1].textContent).toBe( + "The date must be after ye date" + ); + }); + + it("5.4.4 - validates dates with before/after as pattern config attributes with NO custom error message, using input names.", async function () { + document.body.innerHTML = ` + + `; + + const el = document.querySelector(".pat-validation"); + const inp1 = el.querySelector("[name=date1]"); + const inp2 = el.querySelector("[name=date2]"); + + new Pattern(el); + await utils.timeout(1); // wait a tick for async to settle. + + inp1.value = "2010-10-10"; + inp2.value = "2001-01-01"; + + inp1.dispatchEvent(events.change_event()); + await utils.timeout(1); // wait a tick for async to settle. + expect(el.querySelectorAll("em.warning").length).toBe(2); + + console.log(document.body.innerHTML); + expect(el.querySelectorAll("em.warning")[0].textContent).toBe( + "The date must be before date2" + ); + + expect(el.querySelectorAll("em.warning")[1].textContent).toBe( + "The date must be after date1" + ); + }); + it("5.5 - validates dates with before/after constraints", async function () { document.body.innerHTML = `