@brightspace-ui/core 3.117.0 → 3.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,202 +0,0 @@
1
- import './form-errory-summary.js';
2
- import '../tooltip/tooltip.js';
3
- import '../link/link.js';
4
- import { isCustomFormElement, isNativeFormElement } from './form-helper.js';
5
- import { getComposedActiveElement } from '../../helpers/focus.js';
6
- import { getUniqueId } from '../../helpers/uniqueId.js';
7
- import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
8
- import { localizeFormElement } from './form-element-localize-helper.js';
9
-
10
- export const FormMixin = superclass => class extends LocalizeCoreElement(superclass) {
11
-
12
- static get properties() {
13
- return {
14
- /**
15
- * Indicates that the form should interrupt and warn on navigation if the user has unsaved changes on native elements.
16
- * @type {boolean}
17
- */
18
- trackChanges: { type: Boolean, attribute: 'track-changes', reflect: true },
19
- _errors: { type: Object },
20
- _hasErrors: { type: Boolean, attribute: '_has-errors', reflect: true },
21
- };
22
- }
23
-
24
- constructor() {
25
- super();
26
- this._onUnload = this._onUnload.bind(this);
27
- this._onNativeSubmit = this._onNativeSubmit.bind(this);
28
-
29
- this.trackChanges = false;
30
- this._errors = new Map();
31
- this._firstUpdateResolve = null;
32
- this._firstUpdatePromise = new Promise((resolve) => {
33
- this._firstUpdateResolve = resolve;
34
- });
35
- this._tooltips = new Map();
36
- this._validationCustoms = new Set();
37
-
38
- this.addEventListener('d2l-form-errors-change', this._onErrorsChange);
39
- this.addEventListener('d2l-form-element-errors-change', this._onErrorsChange);
40
- this.addEventListener('d2l-validation-custom-connected', this._validationCustomConnected);
41
- }
42
-
43
- connectedCallback() {
44
- super.connectedCallback();
45
- window.addEventListener('beforeunload', this._onUnload);
46
- }
47
-
48
- disconnectedCallback() {
49
- super.disconnectedCallback();
50
- window.removeEventListener('beforeunload', this._onUnload);
51
- }
52
-
53
- firstUpdated(changedProperties) {
54
- super.firstUpdated(changedProperties);
55
- this.addEventListener('change', this._onFormElementChange);
56
- this.addEventListener('input', this._onFormElementChange);
57
- this.addEventListener('focusout', this._onFormElementChange);
58
- this._firstUpdateResolve();
59
- }
60
-
61
- willUpdate(changedProperties) {
62
- super.willUpdate(changedProperties);
63
- if (changedProperties.has('_errors')) {
64
- this._hasErrors = this._errors.size > 0;
65
- }
66
- }
67
-
68
- // eslint-disable-next-line no-unused-vars
69
- async requestSubmit(submitter) {
70
- throw new Error('FormMixin.requestSubmit must be overridden');
71
- }
72
-
73
- async submit() {
74
- throw new Error('FormMixin.submit must be overridden');
75
- }
76
-
77
- async validate() {
78
- throw new Error('FormMixin.validate must be overridden');
79
- }
80
-
81
- _displayInvalid(ele, message) {
82
- let tooltip = this._tooltips.get(ele);
83
- if (!tooltip) {
84
- tooltip = document.createElement('d2l-tooltip');
85
- tooltip.for = ele.id;
86
- tooltip.align = 'start';
87
- tooltip.state = 'error';
88
- ele.parentNode.append(tooltip);
89
- this._tooltips.set(ele, tooltip);
90
-
91
- tooltip.appendChild(document.createTextNode(message));
92
- } else if (tooltip.innerText.trim() !== message.trim()) {
93
- tooltip.textContent = '';
94
- tooltip.appendChild(document.createTextNode(message));
95
- tooltip.updatePosition();
96
- }
97
- ele.setAttribute('aria-invalid', 'true');
98
- }
99
-
100
- _displayValid(ele) {
101
- const tooltip = this._tooltips.get(ele);
102
- if (tooltip) {
103
- this._tooltips.delete(ele);
104
- tooltip.remove();
105
- }
106
- ele.setAttribute('aria-invalid', 'false');
107
- }
108
-
109
- _onErrorsChange(e) {
110
- if (e.target === this) {
111
- return;
112
- }
113
- e.stopPropagation();
114
- this._updateErrors(e.target, e.detail.errors);
115
- }
116
-
117
- async _onFormElementChange(e) {
118
- const ele = e.target;
119
-
120
- if ((isNativeFormElement(ele) || isCustomFormElement(ele)) && e.type !== 'focusout') {
121
- this._dirty = true;
122
- /** Dispatched whenever any form element fires an `input` or `change` event. Can be used to track whether the form is dirty or not. */
123
- this.dispatchEvent(new CustomEvent('d2l-form-dirty'));
124
- }
125
-
126
- if (!isNativeFormElement(ele)) {
127
- return;
128
- }
129
- e.stopPropagation();
130
- const errors = await this._validateFormElement(ele, e.type === 'focusout');
131
- this._updateErrors(ele, errors);
132
- }
133
-
134
- _onNativeSubmit(e) {
135
- e.preventDefault();
136
- e.stopPropagation();
137
- const submitter = e.submitter || getComposedActiveElement();
138
- this.requestSubmit(submitter);
139
- }
140
-
141
- _onUnload(e) {
142
- if (this.trackChanges && this._dirty) {
143
- e.preventDefault();
144
- e.returnValue = false;
145
- }
146
- }
147
-
148
- _updateErrors(ele, errors) {
149
-
150
- if (!this._errors.has(ele)) {
151
- return false;
152
- }
153
- if (Array.from(errors).length === 0) {
154
- this._errors.delete(ele);
155
- } else {
156
- this._errors.set(ele, errors);
157
- }
158
- const detail = { bubbles: true, composed: true, detail: { errors: this._errors } };
159
- /** @ignore */
160
- this.dispatchEvent(new CustomEvent('d2l-form-errors-change', detail));
161
- this.requestUpdate('_errors');
162
- return true;
163
- }
164
-
165
- async _validateFormElement(ele, showNewErrors) {
166
- // if validation occurs before we've rendered,
167
- // localization may not have loaded yet
168
- await this._firstUpdatePromise;
169
- ele.id = ele.id || getUniqueId();
170
- if (isCustomFormElement(ele)) {
171
- return ele.validate(showNewErrors);
172
- } else if (isNativeFormElement(ele)) {
173
- const customs = [...this._validationCustoms].filter(custom => custom.forElement === ele);
174
- const results = await Promise.all(customs.map(custom => custom.validate()));
175
- const errors = customs.map(custom => custom.failureText).filter((_, i) => !results[i]);
176
- if (!ele.checkValidity()) {
177
- const validationMessage = localizeFormElement(this.localize.bind(this), ele);
178
- errors.unshift(validationMessage);
179
- }
180
- if (errors.length > 0 && (showNewErrors || ele.getAttribute('aria-invalid') === 'true')) {
181
- this._displayInvalid(ele, errors[0]);
182
- } else {
183
- this._displayValid(ele);
184
- }
185
- return errors;
186
- }
187
- return [];
188
- }
189
-
190
- _validationCustomConnected(e) {
191
- e.stopPropagation();
192
- const custom = e.composedPath()[0];
193
- this._validationCustoms.add(custom);
194
-
195
- const onDisconnect = () => {
196
- custom.removeEventListener('d2l-validation-custom-disconnected', onDisconnect);
197
- this._validationCustoms.delete(custom);
198
- };
199
- custom.addEventListener('d2l-validation-custom-disconnected', onDisconnect);
200
- }
201
-
202
- };
@@ -1,148 +0,0 @@
1
- import { css, html, LitElement } from 'lit';
2
- import { findFormElements, getFormElementData, isCustomFormElement, isNativeFormElement } from './form-helper.js';
3
- import { FormMixin } from './form-mixin.js';
4
- import { getUniqueId } from '../../helpers/uniqueId.js';
5
-
6
- /**
7
- * A component that can be used to build sections containing interactive controls that are validated and submitted as a group.
8
- * These interactive controls are submitted using a native HTML form submission.
9
- * @slot - The native and custom form elements that participate in validation and submission
10
- * @fires submit - Dispatched when the form is submitted. Cancelling this event will prevent form submission.
11
- */
12
- class FormNative extends FormMixin(LitElement) {
13
-
14
- static get properties() {
15
- return {
16
- /**
17
- * The URL that processes the form submission.
18
- * @type {string}
19
- */
20
- action: { type: String },
21
- /**
22
- * If the value of the method attribute is post, enctype is the MIME type of the form submission.
23
- * @type {'application/x-www-form-urlencoded'|'multipart/form-data'|'text/plain'}
24
- */
25
- enctype: { type: String },
26
- /**
27
- * The HTTP method to submit the form with.
28
- * @type {'get'|'post'}
29
- */
30
- method: { type: String },
31
- /**
32
- * Indicates where to display the response after submitting the form.
33
- * @type {'_self '|'_blank'|'_parent'|'_top'}
34
- */
35
- target: { type: String },
36
- };
37
- }
38
-
39
- static get styles() {
40
- return css`
41
- :host {
42
- display: block;
43
- }
44
- :host([hidden]) {
45
- display: none;
46
- }
47
- `;
48
- }
49
-
50
- constructor() {
51
- super();
52
- this.action = '';
53
- this.enctype = 'application/x-www-form-urlencoded';
54
- this.method = 'get';
55
- this.target = '_self';
56
- }
57
-
58
- render() {
59
- const errors = [...this._errors]
60
- .filter(([, eleErrors]) => eleErrors.length > 0)
61
- .map(([ele, eleErrors]) => ({ href: `#${ele.id}`, message: eleErrors[0], onClick: () => ele.focus() }));
62
- return html`
63
- <d2l-form-error-summary .errors=${errors}></d2l-form-error-summary>
64
- <slot></slot>
65
- `;
66
- }
67
-
68
- shouldUpdate(changedProperties) {
69
- if (!super.shouldUpdate(changedProperties)) {
70
- return false;
71
- }
72
- const ignoredProps = new Set(['action', 'enctype', 'method', 'target']);
73
- return [...changedProperties].filter(([prop]) => !ignoredProps.has(prop)).length > 0;
74
- }
75
-
76
- async requestSubmit(submitter) {
77
- const errors = await this.validate();
78
- if (errors.size > 0) {
79
- return;
80
- }
81
- this._dirty = false;
82
-
83
- const form = document.createElement('form');
84
- form.addEventListener('formdata', this._onFormData);
85
- form.id = getUniqueId();
86
- form.action = this.action;
87
- form.enctype = this.enctype;
88
- form.method = this.method;
89
- form.target = this.target;
90
- this.appendChild(form);
91
-
92
- let customFormData = {};
93
- const formElements = findFormElements(this);
94
- for (const ele of formElements) {
95
- const eleData = getFormElementData(ele, submitter);
96
- const isCustom = isCustomFormElement(ele);
97
- if (isCustom || ele === submitter) {
98
- customFormData = { ...customFormData, ...eleData };
99
- }
100
- if (!isCustom && isNativeFormElement(ele)) {
101
- ele.setAttribute('form', form.id);
102
- }
103
- }
104
- for (const entry of Object.entries(customFormData)) {
105
- const input = document.createElement('input');
106
- input.type = 'hidden';
107
- input.name = entry[0];
108
- input.value = entry[1];
109
- form.appendChild(input);
110
- }
111
- const submit = this.dispatchEvent(new CustomEvent('submit', { bubbles: true, cancelable: true }));
112
- /** Dispatched after the entry list representing the form's data is constructed. This happens when the form is submitted just prior to submission. The form data can be obtained from the `detail`'s `formData` property. */
113
- this.dispatchEvent(new CustomEvent('formdata', { detail: { formData: new FormData(form) } }));
114
- if (submit) {
115
- form.submit();
116
- }
117
- form.remove();
118
- }
119
-
120
- async submit() {
121
- return this.requestSubmit(null);
122
- }
123
-
124
- async validate() {
125
- let errors = [];
126
- const errorMap = new Map();
127
- const formElements = findFormElements(this);
128
- for (const ele of formElements) {
129
- const eleErrors = await this._validateFormElement(ele, true);
130
- if (eleErrors.length > 0) {
131
- errors = [...errors, ...eleErrors];
132
- errorMap.set(ele, eleErrors);
133
- }
134
- }
135
- this._errors = errorMap;
136
- if (this.shadowRoot && errorMap.size > 0) {
137
- const errorSummary = this.shadowRoot.querySelector('d2l-form-error-summary');
138
- this.updateComplete.then(() => errorSummary.focus());
139
- }
140
- return errorMap;
141
- }
142
-
143
- _onFormData(e) {
144
- e.stopPropagation();
145
- }
146
-
147
- }
148
- customElements.define('d2l-form-native', FormNative);