@claspo/components 1.6.3 → 1.7.0-a11y

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.
Files changed (45) hide show
  1. package/SysButtonComponent/SysButtonComponent.js +1 -1
  2. package/SysCalendarComponent/SysCalendarComponent.js +1 -1
  3. package/SysCheckboxListComponent/SysCheckboxListComponent.js +1 -1
  4. package/SysChoiceButtonsComponent/SysChoiceButtonsComponent.js +1 -1
  5. package/SysConsentComponent/SysConsentComponent.js +1 -1
  6. package/SysContainerComponent/SysContainerComponent.js +1 -1
  7. package/SysCountdownTimerComponent/SysCountdownTimerComponent.js +1 -1
  8. package/SysDateComponent/SysDateComponent.js +1 -1
  9. package/SysDropdownInputComponent/SysDropdownInputComponent.js +1 -1
  10. package/SysImageComponent/SysImageComponent.js +1 -1
  11. package/SysInAppColumnsComponent/SysInAppColumnsComponent.js +1 -1
  12. package/SysInputComponent/SysInputComponent.js +1 -1
  13. package/SysPhoneInputComponent/SysPhoneInputComponent.js +1 -1
  14. package/SysPromoCodeComponent/SysPromoCodeComponent.js +1 -1
  15. package/SysRadioGroupComponent/SysRadioGroupComponent.js +1 -1
  16. package/SysSliderComponent/SysSliderComponent.js +1 -1
  17. package/SysSocialComponent/SysSocialComponent.js +1 -1
  18. package/SysTextAreaComponent/SysTextAreaComponent.js +1 -1
  19. package/SysTextComponent/SysTextComponent.js +1 -1
  20. package/package.json +5 -3
  21. package/script/SysButtonComponent/SysButton.manifest.js +126 -0
  22. package/script/SysButtonComponent/SysButton.styles.js +64 -0
  23. package/script/SysButtonComponent/SysButtonComponent.js +231 -0
  24. package/script/SysColumnComponent/SysColumn.manifest.js +17 -0
  25. package/script/SysColumnComponent/SysColumnComponent.js +107 -0
  26. package/script/SysColumnsComponent/SysColumns.manifest.js +17 -0
  27. package/script/SysColumnsComponent/SysColumnsComponent.js +53 -0
  28. package/script/SysColumnsComponent/getStyleElement.js +23 -0
  29. package/script/SysContainerComponent/SysBaseContainerComponent.js +41 -0
  30. package/script/SysContainerComponent/SysContainer.manifest.js +18 -0
  31. package/script/SysContainerComponent/SysContainerComponent.js +86 -0
  32. package/script/SysImageComponent/SysImage.manifest.js +18 -0
  33. package/script/SysImageComponent/SysImageComponent.js +378 -0
  34. package/script/SysImageComponent/getStyleElement.js +18 -0
  35. package/script/SysInputComponent/EmailSuggesting.js +252 -0
  36. package/script/SysInputComponent/InputFormControl.js +136 -0
  37. package/script/SysInputComponent/SysInput.manifest.js +728 -0
  38. package/script/SysInputComponent/SysInputComponent.js +86 -0
  39. package/script/SysInputComponent/emailProvidersList.js +158 -0
  40. package/script/SysInputComponent/getOverlayStyles.js +220 -0
  41. package/script/SysInputComponent/getStyleElement.js +69 -0
  42. package/script/SysInputComponent/inputValidators.js +293 -0
  43. package/script/SysTextComponent/SysText.manifest.js +29 -0
  44. package/script/SysTextComponent/SysTextComponent.js +147 -0
  45. package/script/SysTextComponent/TextRoller.js +298 -0
@@ -0,0 +1,378 @@
1
+ import addEventListenerToElement from '@claspo/common/utils/addEventListener';
2
+ import SysImageManifest from './SysImage.manifest.js';
3
+ import WcElement from '@claspo/renderer/sdk/WcElement';
4
+ import getStyleElement from './getStyleElement.js';
5
+ import omitKeys from '@claspo/common/object/omitKeys';
6
+ import { getAdaptiveStylesForPlatform, replaceStyleAttributes } from '@claspo/renderer/sdk/ModelStyleUtils';
7
+
8
+ const VerticalPosition = {
9
+ TOP: 'top',
10
+ CENTER: 'center',
11
+ BOTTOM: 'bottom'
12
+ };
13
+ const HorizontalPosition = {
14
+ LEFT: 'left',
15
+ CENTER: 'center',
16
+ RIGHT: 'right'
17
+ };
18
+
19
+ const PositioningModes = {
20
+ FIXED: 'fixed',
21
+ STICKY: 'sticky',
22
+ };
23
+
24
+ class SysImageComponent extends WcElement {
25
+ static define = {
26
+ name: 'sys-image',
27
+ model: SysImageManifest.name,
28
+ manifest: SysImageManifest,
29
+ };
30
+
31
+
32
+ constructor() {
33
+ super();
34
+ this.originalApplyAutoAdaptiveStyles = this.applyAutoAdaptiveStyles;
35
+ this.applyAutoAdaptiveStyles = this.applyStylesRespectingRelativePositioning;
36
+ this.getRootElement().innerHTML = `
37
+ ${getStyleElement()}
38
+ <img cl-element="image" draggable="false" alt="">
39
+ `;
40
+ /**
41
+ * height: auto; is a fix for webkit browsers that overrides the height of the host element, for more details
42
+ * @see: https://stackoverflow.com/questions/8468066/child-inside-parent-with-min-height-100-not-inheriting-height
43
+ */
44
+
45
+ }
46
+
47
+ connectedCallback() {
48
+ super.connectedCallback();
49
+ this.skipGameBlur = !this._isFloating();
50
+
51
+ this.componentResourceManager.getPending().increment();
52
+ this.recalculateStylesAfterImageLoad();
53
+
54
+ this.observeProps((prev, next) => {
55
+ this.applyAutoAdaptiveStyles(next.adaptiveStyles);
56
+ this.applyAltText(next);
57
+
58
+ const imageElement = this.getElement('image');
59
+
60
+ if (
61
+ next.control?.imageSource.url
62
+ && imageElement
63
+ // possible fix for TypeError: Cannot set properties of undefined (setting 'src')
64
+ && 'src' in imageElement
65
+ ) {
66
+ imageElement.src = next.control.imageSource.url;
67
+ }
68
+
69
+ const nextUrl = next.control.imageSource.url;
70
+ const prevUrl = prev && prev.control.imageSource.url;
71
+
72
+ const nextInlineSVGUrl = String(next.control.imageSource.url).endsWith('.svg') ? next.control.imageSource.url : null;
73
+ const prevInlineSVGUrl = prev && String(prev.control.imageSource.url).endsWith('.svg') ? prev.control.imageSource.url : null;
74
+
75
+ if (nextInlineSVGUrl) {
76
+
77
+ if (nextInlineSVGUrl === prevInlineSVGUrl) {
78
+ this.applyAdaptiveStyles(next);
79
+ return;
80
+ }
81
+
82
+ this.deleteElementIfPresent(this.getRootElement(), 'img');
83
+ this.upsertSvg(this.getRootElement(), nextInlineSVGUrl).then(() => {
84
+ this.applyAltText(next);
85
+ this.applyAdaptiveStyles(next);
86
+ });
87
+
88
+ } else if (nextUrl) {
89
+
90
+ if (nextUrl === prevUrl) {
91
+ this.applyAdaptiveStyles(next);
92
+ return;
93
+ }
94
+
95
+ this.deleteElementIfPresent(this.getRootElement(), '.svgOverflowContainer');
96
+ this.upsertImage(this.getRootElement(), nextUrl)
97
+ .then(() => {
98
+ this.applyAltText(next);
99
+ this.applyAdaptiveStyles(next);
100
+ // this.componentResourceManager.getPending().decrement();
101
+
102
+ });
103
+
104
+
105
+ } else {
106
+
107
+ this.deleteElementIfPresent(this.getRootElement(), '.svgOverflowContainer');
108
+ this.upsertImage(this.getRootElement(), this.assets('img/image-placeholder.svg'))
109
+ .then(() => {
110
+ this.applyAltText(next);
111
+ this.applyAdaptiveStyles(next);
112
+ });
113
+
114
+ }
115
+
116
+
117
+ });
118
+
119
+ }
120
+
121
+ disconnectedCallback() {
122
+ super.disconnectedCallback();
123
+ this.resourcesLoadedListener?.off();
124
+ }
125
+
126
+ applyAltText(props) {
127
+ const imageElement = this.getElement('image');
128
+ if (!imageElement) return;
129
+ const altText = props.control?.altText || '';
130
+ if (imageElement.nodeName === 'IMG') {
131
+ imageElement.alt = altText;
132
+ } else {
133
+ imageElement.setAttribute('aria-label', altText);
134
+ }
135
+ }
136
+
137
+ applyStylesRespectingRelativePositioning() {
138
+ const adaptiveStyles = this.processPositioningStyles(this.getProps());
139
+ this.originalApplyAutoAdaptiveStyles(adaptiveStyles);
140
+ this.fixFloatingImageSize(adaptiveStyles);
141
+ }
142
+
143
+ recalculateStylesAfterImageLoad() {
144
+ this.resourcesLoadedListener = this.services.eventEmitter.on('VIEW_COMPONENT_RESOURCES_LOADED', () => {
145
+ requestAnimationFrame(() => {
146
+ this.applyAdaptiveStyles(this.getProps());
147
+ });
148
+ });
149
+ }
150
+
151
+ processPositioningStyles(props) {
152
+ const adaptiveStyles = props.adaptiveStyles;
153
+ const mode = props.control?.positioningMode || PositioningModes.FIXED;
154
+
155
+ if (mode === PositioningModes.FIXED) {
156
+ return adaptiveStyles
157
+ }
158
+
159
+ const environment = this.getEnvironment();
160
+ const positioning = props.control?.positioning?.[environment] || {};
161
+ const diff = positioning?.diff || {};
162
+ const hasDiff = Boolean('x' in diff && 'y' in diff);
163
+
164
+ if (!hasDiff) {
165
+ return adaptiveStyles;
166
+ }
167
+
168
+ const { styleAttributes } = getAdaptiveStylesForPlatform(adaptiveStyles, environment, 'host');
169
+ const { width, height } = this._getHostNewDimensionsWithStyles(styleAttributes);
170
+ const newStyleAttributes = omitKeys(styleAttributes, ['left', 'right', 'top', 'bottom']);
171
+
172
+ // process x
173
+ const xCenter = width / 2;
174
+ const shiftedX = diff.x - xCenter;
175
+ if (positioning.horizontalPosition === HorizontalPosition.LEFT) {
176
+ newStyleAttributes.left = `${shiftedX}px`;
177
+ newStyleAttributes.right = 'auto';
178
+ } else if (positioning.horizontalPosition === HorizontalPosition.RIGHT) {
179
+ newStyleAttributes.right = `${-width - shiftedX}px`;
180
+ newStyleAttributes.left = 'auto';
181
+ } else {
182
+ newStyleAttributes.left = `calc(50% + ${shiftedX}px)`;
183
+ newStyleAttributes.right = 'auto';
184
+ }
185
+
186
+ // process y
187
+ const yCenter = height / 2;
188
+ const shiftedY = diff.y - yCenter;
189
+ if (positioning.verticalPosition === VerticalPosition.TOP) {
190
+ newStyleAttributes.top = `${shiftedY}px`;
191
+ newStyleAttributes.bottom = 'auto';
192
+ } else if (positioning.verticalPosition === VerticalPosition.BOTTOM) {
193
+ newStyleAttributes.bottom = `${-height - shiftedY}px`;
194
+ newStyleAttributes.top = 'auto';
195
+ } else {
196
+ newStyleAttributes.top = `calc(50% + ${shiftedY}px)`;
197
+ newStyleAttributes.bottom = 'auto';
198
+ }
199
+
200
+ return replaceStyleAttributes(adaptiveStyles, environment, 'host', newStyleAttributes);
201
+ }
202
+
203
+ deleteElementIfPresent(rootElement, cssSelector) {
204
+ const element = rootElement.querySelector(cssSelector);
205
+ if (element) {
206
+ element.remove();
207
+ }
208
+ }
209
+
210
+ upsertSvg(rootElement, inlineSVGUrl) {
211
+ const alreadyPresent = rootElement.querySelector('svg');
212
+
213
+ // if we have the same SVG then we do not have to redraw
214
+ if (alreadyPresent && alreadyPresent.getAttribute('inline-svg-url') === inlineSVGUrl) {
215
+ return Promise.resolve();
216
+
217
+ // if we have different SVG, then we have to redraw
218
+ } else if (alreadyPresent) {
219
+ this.deleteElementIfPresent(this.getRootElement(), '.svgOverflowContainer');
220
+ }
221
+
222
+ return fetch(inlineSVGUrl).then(r => r.text()).then((inlineSVG) => {
223
+ const htmlTemplateElement = document.createElement('template');
224
+ const containerWithOverflow = document.createElement('div');
225
+ containerWithOverflow.style.overflow = 'hidden';
226
+ containerWithOverflow.style.width = '100%';
227
+ containerWithOverflow.style.height = 'inherit';
228
+ containerWithOverflow.classList.add('svgOverflowContainer');
229
+
230
+ htmlTemplateElement.append(containerWithOverflow);
231
+ htmlTemplateElement.firstChild.innerHTML = inlineSVG.trim();
232
+
233
+ const svgElementContainer = htmlTemplateElement.firstChild;
234
+ const svgNode = svgElementContainer.querySelector('svg');
235
+ svgNode.setAttribute('cl-element', 'image');
236
+ svgNode.setAttribute('inline-svg-url', inlineSVGUrl);
237
+
238
+ this.componentResourceManager.getPending().decrement();
239
+ rootElement.append(svgElementContainer);
240
+ return rootElement;
241
+ }).catch((err) => {
242
+ console.error(err);
243
+ this.upsertImage(rootElement, null);
244
+ });
245
+
246
+ }
247
+
248
+ upsertImage(rootElement, imgUrl) {
249
+ const alreadyPresentImgElement = rootElement.querySelector('img');
250
+ if (alreadyPresentImgElement) {
251
+ alreadyPresentImgElement.src = imgUrl;
252
+ addEventListenerToElement(alreadyPresentImgElement, 'load', () => {
253
+ this.componentResourceManager.getPending().decrement();
254
+ });
255
+ addEventListenerToElement(alreadyPresentImgElement, 'error', () => {
256
+ this.componentResourceManager.onResourceLoadFailure(alreadyPresentImgElement.src);
257
+ });
258
+ return Promise.resolve();
259
+ }
260
+
261
+ const imageElement = new Image();
262
+ imageElement.setAttribute('cl-element', 'image');
263
+ imageElement.src = imgUrl;
264
+ addEventListenerToElement(imageElement, 'load', () => {
265
+ this.componentResourceManager.getPending().decrement();
266
+ });
267
+ addEventListenerToElement(imageElement, 'error', () => {
268
+ this.componentResourceManager.onResourceLoadFailure(imageElement.src);
269
+ });
270
+ rootElement.append(imageElement);
271
+
272
+ return Promise.resolve();
273
+ }
274
+
275
+ applyAdaptiveStyles(next) {
276
+ this.applyAutoAdaptiveStyles(next.adaptiveStyles);
277
+
278
+ const environment = this.getEnvironment();
279
+
280
+ const envModels = next.adaptiveStyles[environment];
281
+ const hostElementModel = envModels.find(e => e.element === 'host');
282
+ const imageElementModel = envModels.find(e => e.element === 'image');
283
+ const imageElement = this.getElement('image');
284
+
285
+ const svgObjectFitMapper = {
286
+ cover: 'xMidYMid slice', contain: 'xMidYMid meet', none: '',
287
+ };
288
+
289
+ const isImageNodeSVG = imageElement && imageElement.nodeName === 'svg';
290
+ const elementModelHasObjectFit = imageElementModel?.styleAttributes?.objectFit && svgObjectFitMapper.hasOwnProperty(imageElementModel.styleAttributes.objectFit);
291
+
292
+ if (isImageNodeSVG && elementModelHasObjectFit) {
293
+ imageElement.setAttribute('preserveAspectRatio', svgObjectFitMapper[imageElementModel.styleAttributes.objectFit]);
294
+
295
+ if (imageElementModel.styleAttributes.objectFit === 'none') {
296
+ imageElement.setAttribute('width', imageElement.viewBox.baseVal.width + 'px');
297
+ imageElement.setAttribute('height', imageElement.viewBox.baseVal.height + 'px');
298
+ } else {
299
+ const aspectRatio = imageElement.viewBox.baseVal.width / imageElement.viewBox.baseVal.height;
300
+
301
+ if (hostElementModel.styleAttributes.width !== 'auto' && hostElementModel.styleAttributes.height !== 'auto') {
302
+ imageElement.setAttribute('height', '100%');
303
+ imageElement.setAttribute('width', '100%');
304
+ return;
305
+ }
306
+
307
+ if (hostElementModel.styleAttributes.width === 'auto' && hostElementModel.styleAttributes.height === 'auto') {
308
+ imageElement.setAttribute('width', imageElement.viewBox.baseVal.width + 'px');
309
+ imageElement.setAttribute('height', imageElement.viewBox.baseVal.height + 'px');
310
+ return;
311
+ }
312
+
313
+ if (hostElementModel.styleAttributes.width === 'auto') {
314
+ imageElement.setAttribute('height', '100%');
315
+
316
+ if (hostElementModel.styleAttributes.height !== '100%' && hostElementModel.styleAttributes.height !== 'calc(100% - 0px)') {
317
+ imageElement.setAttribute('width', `${Math.round(parseInt(hostElementModel.styleAttributes.height, 10) * aspectRatio)}px`);
318
+ }
319
+ }
320
+
321
+ if (hostElementModel.styleAttributes.height === 'auto') {
322
+ imageElement.setAttribute('width', '100%');
323
+
324
+ if (hostElementModel.styleAttributes.width !== '100%' && hostElementModel.styleAttributes.width !== 'calc(100% - 0px)') {
325
+ imageElement.setAttribute('height', `${Math.round(parseInt(hostElementModel.styleAttributes.width, 10) / aspectRatio)}px`);
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ fixFloatingImageSize(adaptiveStyles) {
333
+ const imageElement = this.getElement('image');
334
+
335
+ // svg image
336
+ if (!imageElement) {
337
+ return;
338
+ }
339
+
340
+ if (!this._isFloating()) {
341
+ imageElement.style.width = '100%';
342
+ return;
343
+ }
344
+
345
+ const imageStyles = getAdaptiveStylesForPlatform(adaptiveStyles, this.getEnvironment(), 'host');
346
+
347
+ // when <img> has width 100% and floating host has width auto it causes image to change size depending on image position in widget:
348
+ // the closer image to the left side of a widget, the smaller it gets. We could not find source of the issue,
349
+ // it persists even at 10.11.23 revisions, so it's looks like browsers changed behaviour in recent versions
350
+ if (imageStyles.styleAttributes.width === 'auto') {
351
+ imageElement.style.width = '';
352
+ } else {
353
+ imageElement.style.width = '100%';
354
+ }
355
+ }
356
+
357
+ _getHostNewDimensionsWithStyles(styleAttributes = {}) {
358
+ const host = this.getHostElement();
359
+ const dimensionalStyleNames = ['height', 'width', 'minHeight', 'minWidth', 'maxHeight', 'maxWidth', 'display'];
360
+ dimensionalStyleNames.forEach((style) => {
361
+ if (styleAttributes[style]) {
362
+ host.style[style] = styleAttributes[style];
363
+ } else if (host.style[style]) {
364
+ host.style[style] = '';
365
+ }
366
+ });
367
+ const { offsetWidth: width, offsetHeight: height } = host;
368
+ return { width, height };
369
+ }
370
+
371
+ _isFloating() {
372
+ return this.getProps().floating
373
+ // for backward compatibility
374
+ || this.getModel().floating;
375
+ }
376
+ }
377
+
378
+ export { HorizontalPosition, PositioningModes, VerticalPosition, SysImageComponent as default };
@@ -0,0 +1,18 @@
1
+ function getStyleElement() {
2
+ return `
3
+ <style>
4
+ :host {
5
+ transform: rotate(var(--rotation, 0deg));
6
+ }
7
+
8
+ img {
9
+ display: block;
10
+ max-height: inherit;
11
+ height: inherit;
12
+ min-height: inherit;
13
+ }
14
+ </style>
15
+ `;
16
+ }
17
+
18
+ export { getStyleElement as default };
@@ -0,0 +1,252 @@
1
+ import getOverlayStyles from './getOverlayStyles.js';
2
+ import waitForKeyboardHide from '@claspo/renderer/common/WaitForKeyboardHide';
3
+ import { createMenuOverlay, getMenuOverlayContentClassName } from '@claspo/renderer/sdk/OverlayUtils';
4
+ import emailProvidersList from './emailProvidersList.js';
5
+
6
+ class EmailSuggesting {
7
+
8
+ lastEmailSuggestionCheckedValue = null;
9
+ emailSuggestionOverlayBackdrop = null;
10
+
11
+ suggestEmail(
12
+ props,
13
+ inputElement,
14
+ shouldSkipLastEmailSuggestionCheck,
15
+ formService,
16
+ control,
17
+ getCurrentLanguageMap,
18
+ htmlDocumentObject,
19
+ ) {
20
+ if (
21
+ !inputElement.value ||
22
+ (this.lastEmailSuggestionCheckedValue === inputElement.value && !shouldSkipLastEmailSuggestionCheck)
23
+ ) {
24
+ return;
25
+ }
26
+
27
+ if (props.control.validation && props.control.validation.restrictFreeDomains) {
28
+ return;
29
+ }
30
+
31
+ this.lastEmailSuggestionCheckedValue = inputElement.value;
32
+
33
+ if (!control.elementRef.classList.contains('invalid')) {
34
+ if (emailProvidersList.includes(inputElement.value.toLowerCase().split('@')[1])) {
35
+ return;
36
+ }
37
+
38
+ const suggestion = EmailSuggesting.suggestEmailSync(inputElement.value);
39
+ const createSuggestionOverlay = (suggestion) => {
40
+ this.createSuggestionOverlay(
41
+ suggestion,
42
+ inputElement,
43
+ control,
44
+ getCurrentLanguageMap,
45
+ htmlDocumentObject,
46
+ );
47
+ EmailSuggesting.preventSubmit(formService);
48
+ };
49
+
50
+ if (suggestion) {
51
+ createSuggestionOverlay(suggestion);
52
+ }
53
+ }
54
+ }
55
+
56
+ emailValueChanged() {
57
+ this.lastEmailSuggestionCheckedValue = null;
58
+ }
59
+
60
+ hideEmailSuggestion() {
61
+ if (this.emailSuggestionOverlayBackdrop) {
62
+ this.emailSuggestionOverlayBackdrop.click();
63
+ }
64
+ }
65
+
66
+ createSuggestionOverlay(
67
+ suggestion,
68
+ inputElement,
69
+ control,
70
+ getCurrentLanguageMap,
71
+ htmlDocumentObject
72
+ ) {
73
+ this.hideEmailSuggestion();
74
+
75
+ waitForKeyboardHide(() => {
76
+ const overlay = createMenuOverlay({
77
+ triggerElement: inputElement,
78
+ overlayStyles: getOverlayStyles(getMenuOverlayContentClassName()),
79
+ createOverlayContent: (backdrop, overlayContentContainer) => {
80
+ EmailSuggesting.createOverlayContent(suggestion, backdrop, overlayContentContainer, control, getCurrentLanguageMap);
81
+ },
82
+ overlayWidth: 220,
83
+ overlayHeight: 90,
84
+ positionByDefault: 'top',
85
+ isHorizontallyCentered: true,
86
+ isBackdropDisabledOnUI: true,
87
+ onDestroy: () => {
88
+ this.emailSuggestionOverlayBackdrop = null;
89
+ },
90
+ offset: 10,
91
+ htmlDocumentObject: htmlDocumentObject,
92
+ });
93
+
94
+ this.emailSuggestionOverlayBackdrop = overlay.backdrop;
95
+ });
96
+ }
97
+
98
+ static createOverlayContent(suggestion, backdrop, overlayContentContainer, control, getCurrentLanguageMap) {
99
+ const componentLanguageMap = getCurrentLanguageMap();
100
+
101
+ const textContainer = document.createElement('div');
102
+ textContainer.classList.add('suggestion-text-container');
103
+ const didYouMeanTextNode = document.createElement('div');
104
+ didYouMeanTextNode.classList.add('did-you-mean-text');
105
+ // Avoid injecting HTML from translations
106
+ didYouMeanTextNode.textContent = componentLanguageMap['content,suggestionLabel'];
107
+ const suggestionNode = document.createElement('div');
108
+ suggestionNode.classList.add('suggestion-text');
109
+ suggestionNode.textContent = `${suggestion}?`;
110
+ const acceptButtonNode = document.createElement('div');
111
+ acceptButtonNode.classList.add('accept-button');
112
+ const denyButtonNode = document.createElement('div');
113
+ denyButtonNode.classList.add('deny-button');
114
+
115
+ textContainer.appendChild(didYouMeanTextNode);
116
+ textContainer.appendChild(suggestionNode);
117
+
118
+ acceptButtonNode.addEventListener('click', () => {
119
+ const leftEmailPart = control.value.split('@')[0];
120
+ control.setValue(`${leftEmailPart}@${suggestion}`);
121
+ backdrop.click();
122
+ });
123
+
124
+ denyButtonNode.addEventListener('click', () => {
125
+ backdrop.click();
126
+ });
127
+
128
+ overlayContentContainer.appendChild(textContainer);
129
+ overlayContentContainer.appendChild(acceptButtonNode);
130
+ overlayContentContainer.appendChild(denyButtonNode);
131
+ }
132
+
133
+ static preventSubmit(formService) {
134
+ const submitBlockTime = 200; // To prevent submit if suggestion exist
135
+ formService.setPreventSubmit(true);
136
+ setTimeout(() => formService.setPreventSubmit(false), submitBlockTime);
137
+ }
138
+
139
+ static suggestEmailSync(inputValue) {
140
+ const parsedInputValue = inputValue.toLowerCase();
141
+ const domainPartOfInput = parsedInputValue.split('@')[1] || '';
142
+
143
+ // If the domain is already in the list, no suggestion needed
144
+ if (emailProvidersList.indexOf(domainPartOfInput) > -1) {
145
+ return null;
146
+ }
147
+
148
+ // Check for dot placement issues
149
+ const dotSuggestion = EmailSuggesting.getSuggestionAfterDotChecks(domainPartOfInput);
150
+ if (dotSuggestion) {
151
+ return dotSuggestion;
152
+ }
153
+
154
+ // If domain doesn't have a dot, it's likely incomplete
155
+ if (!domainPartOfInput.includes('.')) {
156
+ return null;
157
+ }
158
+
159
+ // Calculate Levenshtein distance for each provider domain
160
+ const distances = emailProvidersList.map(provider => ({
161
+ provider,
162
+ distance: EmailSuggesting.getLevenshteinDistance(provider, domainPartOfInput)
163
+ }));
164
+
165
+ // Find the minimum distance
166
+ const minDistance = Math.min(...distances.map(item => item.distance));
167
+
168
+ // If the minimum distance is too large, don't suggest anything
169
+ if (minDistance > 2) {
170
+ return null;
171
+ }
172
+
173
+ // Get all providers with the minimum distance
174
+ const closestProviders = distances
175
+ .filter(item => item.distance === minDistance)
176
+ .map(item => item.provider);
177
+
178
+ // Check for character transposition (two characters in wrong order)
179
+ const twoCharsWrongOrderSuggestion = EmailSuggesting.isTwoCharsWrongOrder(closestProviders, domainPartOfInput);
180
+
181
+ // If distance is small enough, suggest the closest provider
182
+ if (minDistance <= 1) {
183
+ return closestProviders[0];
184
+ }
185
+
186
+ // If distance is 2 but there's a character transposition, suggest it
187
+ if (minDistance === 2 && twoCharsWrongOrderSuggestion) {
188
+ return twoCharsWrongOrderSuggestion;
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ static getSuggestionAfterDotChecks(inputValue) {
195
+ return emailProvidersList.find(provider => EmailSuggesting.isDotLocatedInWrongPlace(provider, inputValue));
196
+ }
197
+
198
+ static isDotLocatedInWrongPlace(provider, inputValue) {
199
+ if (!inputValue) {
200
+ return;
201
+ }
202
+
203
+ return inputValue.replace('.', '') === provider.replace('.', '');
204
+ }
205
+
206
+ static getLevenshteinDistance(s, t) { // Levenshtein Distance algorithm
207
+ if (!s.length) return t.length;
208
+ if (!t.length) return s.length;
209
+ const arr = [];
210
+ for (let i = 0; i <= t.length; i++) {
211
+ arr[i] = [i];
212
+ for (let j = 1; j <= s.length; j++) {
213
+ arr[i][j] =
214
+ i === 0
215
+ ? j
216
+ : Math.min(
217
+ arr[i - 1][j] + 1,
218
+ arr[i][j - 1] + 1,
219
+ arr[i - 1][j - 1] + (s[j - 1] === t[i - 1] ? 0 : 1)
220
+ );
221
+ }
222
+ }
223
+ return arr[t.length][s.length];
224
+ }
225
+
226
+ static getAllMinimalDistanceDomains(minimalDistance, stringDistances, secondLevelDomainsList) {
227
+ const minimalDistanceIndexes = stringDistances.reduce((acc, el, i) => {
228
+ if (el === minimalDistance) {
229
+ acc.push(i);
230
+ }
231
+
232
+ return acc;
233
+ }, []);
234
+
235
+ return secondLevelDomainsList.filter((_, i) => minimalDistanceIndexes.includes(i));
236
+ }
237
+
238
+ static isTwoCharsWrongOrder(suggestionCandidates, domain) {
239
+ return suggestionCandidates.find(candidate => {
240
+ const firstDifferentCharIndex = EmailSuggesting.findIndexOfFirstDiffInTwoStrings(domain, candidate);
241
+
242
+ return domain.charAt(firstDifferentCharIndex) === candidate.charAt(firstDifferentCharIndex + 1) &&
243
+ candidate.charAt(firstDifferentCharIndex) === domain.charAt(firstDifferentCharIndex + 1);
244
+ });
245
+ }
246
+
247
+ static findIndexOfFirstDiffInTwoStrings(str1, str2) {
248
+ return [...str1].findIndex((el, i) => el !== str2[i]);
249
+ }
250
+ }
251
+
252
+ export { EmailSuggesting };