@madgex/design-system 13.5.1 → 13.6.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.
@@ -7,11 +7,18 @@ module.exports = {
7
7
  name: 'file',
8
8
  id: 'file',
9
9
  fileTypes: '.doc,.docx,.pdf,.rtf',
10
+ cropper: {
11
+ i18n: {
12
+ tBtnReset: 'Cancel',
13
+ tBtnCommit: 'Crop and save',
14
+ },
15
+ },
10
16
  },
11
17
  variants: [
12
18
  {
13
19
  name: 'Selected file version',
14
20
  context: {
21
+ labelText: 'Selected file version',
15
22
  value: 'myCV.doc',
16
23
  name: 'selected-file',
17
24
  id: 'selected-file',
@@ -24,13 +31,60 @@ module.exports = {
24
31
  name: 'Errored file input',
25
32
  context: {
26
33
  labelText: 'Error with uploaded file',
27
- name: 'selected-file',
28
- id: 'selected-file',
34
+ name: 'selected-file-with-error',
35
+ id: 'selected-file-with-error',
29
36
  validationError: 'There was an error',
30
37
  i18n: {
31
38
  fallbackSelectedFileText: 'Use my saved CV or upload a different one',
32
39
  },
33
40
  },
34
41
  },
42
+ {
43
+ name: 'Image upload 320px x 160px',
44
+ context: {
45
+ labelText: 'Image upload 320px x 160px',
46
+ name: 'image-upload',
47
+ id: 'image-upload',
48
+ helpText: 'Your file must be a .jpg or.png. No larger than 244KB',
49
+ fileTypes: '.jpg,.png',
50
+ i18n: {
51
+ fallbackSelectedFileText: 'Add an image',
52
+ },
53
+ cropper: {
54
+ outputWidth: 320,
55
+ outputHeight: 160,
56
+ restrictPosition: false,
57
+ minZoom: '0.5',
58
+ },
59
+ },
60
+ },
61
+ {
62
+ name: 'Image upload - No auto open image cropper modal',
63
+ context: {
64
+ labelText: 'Image upload - No auto open image cropper modal',
65
+ name: 'image-upload-no-auto-open',
66
+ id: 'image-upload-no-auto-open',
67
+ helpText: 'Your file must be a .jpg or.png. No larger than 244KB',
68
+ fileTypes: '.jpg,.png',
69
+ i18n: {
70
+ fallbackSelectedFileText: 'Add an image',
71
+ },
72
+ noAutoOpenImageCropper: true,
73
+ },
74
+ },
75
+ {
76
+ name: 'Image upload without cropper',
77
+ context: {
78
+ labelText: 'Image upload without cropper',
79
+ name: 'image-upload-no-cropper',
80
+ id: 'image-upload-no-cropper',
81
+ helpText: 'Your file must be a .jpg or.png. No larger than 244KB',
82
+ fileTypes: '.jpg,.png',
83
+ i18n: {
84
+ fallbackSelectedFileText: 'Add an image',
85
+ },
86
+ noImageCropper: true,
87
+ },
88
+ },
35
89
  ],
36
90
  };
@@ -1,63 +1,173 @@
1
- const fileUploadClass = '.mds-form-element--file';
2
- const selectedFileClass = 'mds-form-element--selected-file';
3
- const dragOverClass = 'mds-form-element--dragover';
4
- const fileNameContainerClass = '.mds-file-upload__file-name';
5
- const removeButtonClass = '.mds-file-upload__remove-button';
6
- const supportedClass = 'mds-form-element--file-supported';
1
+ /**
2
+ * MdsFileUpload
3
+ *
4
+ * Requires HTML rendered by MdsFileUpload Nunjucks macro.
5
+ *
6
+ *Image Cropper and Modal are optionally rendered by Nunjucks, this Web Component will gracefully continue if they do not exist.
7
+ */
8
+ export class MdsFileUpload extends HTMLElement {
9
+ static selectedFileClass = 'mds-file-upload--selected-file';
10
+ static selectedFileIsImageClass = 'mds-file-upload--selected-file-is-image';
11
+ static dragOverClass = 'mds-file-upload--dragover';
7
12
 
8
- const fileUpload = {
9
- init: () => {
10
- if (!fileUpload.isBrowserIE()) {
11
- const fileUploads = Array.from(document.querySelectorAll(fileUploadClass));
13
+ constructor() {
14
+ super();
15
+ }
16
+ connectedCallback() {
17
+ this.classList.add('mds-form-element--file-supported'); // useless?
12
18
 
13
- fileUploads.forEach((uploader) => {
14
- uploader.classList.add(supportedClass);
15
- const fileNameContainer = uploader.querySelector(fileNameContainerClass);
16
- const input = uploader.querySelector('input[type=file]');
17
- const removeButton = uploader.querySelector(removeButtonClass);
19
+ this.#elInput.addEventListener('change', this.#onChangeInput);
20
+ this.#elRemoveButton.addEventListener('click', this.#onClickRemoveButton);
21
+ this.#elInput.addEventListener('dragover', this.#onDragOver);
22
+ this.#elInput.addEventListener('dragenter', this.#onDragOver);
23
+ this.#elInput.addEventListener('dragleave', this.#onDragEnd);
24
+ this.#elInput.addEventListener('dragend', this.#onDragEnd);
25
+ this.#elInput.addEventListener('drop', this.#onDragEnd);
26
+ this.#imageCropper?.addEventListener('commit', this.#onCommitImageCropper);
27
+ this.#imageCropper?.addEventListener('reset', this.#onResetImageCropper);
18
28
 
19
- document.addEventListener('readystatechange', () => {
20
- if (input.files && input.files.length) {
21
- fileNameContainer.textContent = input.files[0].name;
22
- uploader.classList.add(selectedFileClass);
23
- }
24
- });
29
+ this.#syncFileInputPopulated(true);
30
+ }
31
+ disconnectedCallback() {
32
+ this.#elInput?.removeEventListener('change', this.#onChangeInput);
33
+ this.#elRemoveButton?.removeEventListener('click', this.#onClickRemoveButton);
34
+ this.#elInput?.removeEventListener('dragover', this.#onDragOver);
35
+ this.#elInput?.removeEventListener('dragenter', this.#onDragOver);
36
+ this.#elInput?.removeEventListener('dragleave', this.#onDragEnd);
37
+ this.#elInput?.removeEventListener('dragend', this.#onDragEnd);
38
+ this.#elInput?.removeEventListener('drop', this.#onDragEnd);
39
+ this.#imageCropper?.removeEventListener('commit', this.#onCommitImageCropper);
40
+ this.#imageCropper?.removeEventListener('reset', this.#onResetImageCropper);
41
+ }
25
42
 
26
- input.addEventListener('change', () => {
27
- if (input.files && input.files.length) {
28
- fileNameContainer.textContent = input.files[0].name;
29
- uploader.classList.add(selectedFileClass);
30
- removeButton.focus();
31
- }
32
- });
43
+ get rootNode() {
44
+ return this;
45
+ }
46
+ get #document() {
47
+ return this.getRootNode({ composed: true });
48
+ }
49
+ /** @type {File?} the file populated (we only handle single file) */
50
+ get #file() {
51
+ return this.#elInput?.files?.[0];
52
+ }
53
+ get #isFileImage() {
54
+ return this.#file?.type?.startsWith('image/');
55
+ }
56
+ /* Attributes */
57
+ get #noAutoOpenImageCropper() {
58
+ const attr = this.rootNode.getAttribute('no-auto-open-image-cropper');
59
+ return attr !== null && attr !== 'false';
60
+ }
61
+ /* Elements */
62
+ get #elFileNameContainer() {
63
+ return this.rootNode.querySelector('.mds-file-upload__file-name');
64
+ }
65
+ get #elInput() {
66
+ return this.rootNode.querySelector('input[type=file]');
67
+ }
68
+ get #elRemoveButton() {
69
+ return this.rootNode.querySelector('.mds-file-upload__remove-button');
70
+ }
71
+ get #elPreviewImage() {
72
+ return this.rootNode.querySelector('.mds-file-upload__preview-image');
73
+ }
74
+ get #imageCropperModalTriggerButton() {
75
+ return this.rootNode.querySelector('.mds-file-upload__open-modal');
76
+ }
77
+ get #imageCropperModal() {
78
+ return this.rootNode.querySelector('.mds-modal');
79
+ }
80
+ get #imageCropper() {
81
+ return this.rootNode.querySelector('mds-image-cropper');
82
+ }
33
83
 
34
- removeButton.addEventListener('click', (elem) => {
35
- elem.preventDefault();
36
- input.value = '';
37
- fileNameContainer.textContent = '';
38
- uploader.classList.remove(selectedFileClass);
39
- input.focus();
40
- });
41
-
42
- ['dragover', 'dragenter'].forEach((event) => {
43
- input.addEventListener(event, (e) => {
44
- e.stopPropagation();
45
- uploader.classList.add(dragOverClass);
46
- });
47
- });
48
-
49
- ['dragleave', 'dragend', 'drop'].forEach((event) => {
50
- input.addEventListener(event, (e) => {
51
- e.stopPropagation();
52
- uploader.classList.remove(dragOverClass);
53
- });
54
- });
55
- });
84
+ /* Event handlers */
85
+ #onChangeInput = (e) => {
86
+ const { dontOpenModal = false } = e?.detail || {};
87
+ this.#syncFileInputPopulated();
88
+ if (this.#file) {
89
+ this.#elRemoveButton.focus();
90
+ }
91
+ if (!dontOpenModal && this.#isFileImage && !this.#noAutoOpenImageCropper) {
92
+ // automatically open image cropper modal if file is image
93
+ this.#openModal();
56
94
  }
57
- },
58
- isBrowserIE: () => {
59
- return !!window.document.documentMode;
60
- },
61
- };
95
+ };
96
+ #onClickRemoveButton = (e) => {
97
+ e.preventDefault();
98
+ this.removeFile();
99
+ this.#elInput.focus();
100
+ };
101
+ #onDragOver = (e) => {
102
+ e.stopPropagation();
103
+ this.classList.add(MdsFileUpload.dragOverClass);
104
+ };
105
+ #onDragEnd = (e) => {
106
+ e.stopPropagation();
107
+ this.classList.remove(MdsFileUpload.dragOverClass);
108
+ };
109
+ /** on receiving a new Blob from image cropper, update file input with new data */
110
+ #onCommitImageCropper = (e) => {
111
+ /** @type {Blob?} */
112
+ const blob = e.detail;
113
+ if (blob) {
114
+ // load blob back into fileinput using DataTransfer
115
+ const croppedFile = new File([blob], this.#file?.name || 'cropped-image', { type: blob.type });
116
+ const dataTransfer = new DataTransfer();
117
+ dataTransfer.items.add(croppedFile);
118
+ this.#elInput.files = dataTransfer.files;
119
+ // trigger 'change' as if a user selected a file, which in turn will run `#onChangeInput`
120
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
121
+ this.#elInput.dispatchEvent(new CustomEvent('change', { detail: { dontOpenModal: true }, bubbles: true }));
122
+ // close modal
123
+ this.#closeModal();
124
+ }
125
+ };
126
+ #onResetImageCropper = () => {
127
+ // close modal - TODO: convert modal js into web component so we can call `close` method instead of knowing this classname?
128
+ this.#closeModal();
129
+ };
62
130
 
63
- export default fileUpload;
131
+ /* Methods */
132
+ /**
133
+ * sync filename, image preview and selected state based on `this.#elInput` files
134
+ * @param {boolean} [dontResetState=false] used on connectedCallback, if there is no real file, we still want to display a populated state!
135
+ * */
136
+ #syncFileInputPopulated(dontResetState = false) {
137
+ if (this.#file) {
138
+ // has file
139
+ this.#elFileNameContainer.textContent = this.#file.name;
140
+ this.classList.add(MdsFileUpload.selectedFileClass);
141
+ // if file is image, load and show preview, populate cropper src
142
+ if (this.#isFileImage) {
143
+ this.classList.add(MdsFileUpload.selectedFileIsImageClass);
144
+ const reader = new FileReader();
145
+ reader.onload = (e) => {
146
+ this.#elPreviewImage.src = e.target.result;
147
+ this.#imageCropper?.setAttribute('src', e.target.result);
148
+ };
149
+ reader.readAsDataURL(this.#file);
150
+ } else {
151
+ this.classList.remove(MdsFileUpload.selectedFileIsImageClass);
152
+ }
153
+ } else if (!dontResetState) {
154
+ // no file, empty state
155
+ this.#elFileNameContainer.textContent = '';
156
+ this.classList.remove(MdsFileUpload.selectedFileClass);
157
+ this.classList.remove(MdsFileUpload.selectedFileIsImageClass);
158
+ }
159
+ }
160
+ #openModal() {
161
+ // TODO: When `modal code` is Web Component, use that instead of triggering click events on DOM elements...
162
+ this.#imageCropperModalTriggerButton?.dispatchEvent(new Event('click'));
163
+ }
164
+ #closeModal() {
165
+ // TODO: When `modal code` is Web Component, use that instead of triggering click events on DOM elements...
166
+ this.#imageCropperModal?.querySelector('.js-mds-modal-close').dispatchEvent(new Event('click'));
167
+ }
168
+ /** remove file from input */
169
+ removeFile() {
170
+ this.#elInput.value = '';
171
+ this.#elInput.dispatchEvent(new Event('change', { bubbles: true }));
172
+ }
173
+ }
@@ -1,5 +1,4 @@
1
1
  {% from "./inputs/file-upload/_macro.njk" import MdsFileUpload %}
2
-
3
2
  <div class="mds-grid-row">
4
3
  <div class="mds-grid-col-12 mds-grid-col-lg-9">
5
4
  <div class="mds-form-field">
@@ -18,7 +17,11 @@
18
17
  classes: classes,
19
18
  tooltipMessage: tooltipMessage,
20
19
  fileTypes: fileTypes,
21
- i18n: i18n
20
+ i18n: i18n,
21
+ siteContainerId:siteContainerId,
22
+ noImageCropper: noImageCropper,
23
+ noAutoOpenImageCropper: noAutoOpenImageCropper,
24
+ cropper: cropper
22
25
  }) }}
23
26
  </div>
24
27
  </div>
@@ -1,137 +1,129 @@
1
- .mds-file-upload__input .mds-file-upload__input-controls {
1
+ /* container for fancy file upload, hidden without `.js` */
2
+ .mds-file-upload__wrapper {
2
3
  display: none;
3
4
  }
4
5
 
5
- .js .mds-form-element--file-supported .mds-file-upload {
6
- display: flex;
7
- flex-direction: column;
8
- background-color: $constant-color-neutral-lightest;
9
- padding: $constant-size-baseline;
10
-
11
- @include mq($from: $constant-size-breakpoint-md) {
12
- flex-direction: row;
6
+ /* all styling is only relevant if JS is enabled */
7
+ .js {
8
+ .mds-file-upload__selected-file-fallback {
9
+ display: none;
13
10
  }
14
- }
15
-
16
- .js .mds-form-element--file-supported .mds-file-upload__input {
17
- position: relative;
18
- border: 1px dashed $constant-color-neutral-lighter;
19
- width: 100%;
20
- padding: ($constant-size-baseline * 4) ($constant-size-baseline * 2);
21
- display: flex;
22
- align-items: center;
23
- justify-content: center;
24
-
25
- & .mds-form-label {
11
+ .mds-file-upload {
26
12
  position: relative;
27
- @include z-index;
28
13
  display: block;
29
- cursor: pointer;
30
- margin-bottom: 0;
14
+ background-color: $constant-color-neutral-lightest;
15
+ max-width: 100%;
16
+ width: 100%;
17
+ padding: $constant-size-baseline;
18
+ .mds-form-label {
19
+ position: relative;
20
+ @include z-index;
21
+ display: block;
22
+ cursor: pointer;
23
+ margin-bottom: 0;
24
+ }
25
+ .mds-form-label__label {
26
+ @extend .mds-button;
27
+ @extend .mds-button--small;
28
+ font-weight: normal;
29
+ }
30
+ }
31
+ .mds-file-upload__wrapper {
32
+ display: block;
33
+ padding: ($constant-size-baseline * 3);
34
+ border: 1px dashed $constant-color-neutral-lighter;
35
+ }
36
+ .mds-file-upload--dragover {
37
+ .mds-file-upload__wrapper {
38
+ background-color: #fff;
39
+ }
40
+ .mds-form-label {
41
+ pointer-events: none; // avoid label blocking drag action
42
+ }
43
+ }
44
+ .mds-file-upload__state {
45
+ display: flex;
46
+ align-items: center;
47
+ }
48
+ .mds-file-upload__state--no-file {
49
+ flex-direction: column;
50
+ row-gap: $constant-size-baseline * 5;
51
+ & .mds-icon {
52
+ fill: var(--mds-color-button-bg-base, #000);
53
+ }
54
+ }
55
+ .mds-file-upload__state--selected {
56
+ display: none;
57
+ }
58
+ .mds-file-upload__preview .mds-file-upload__preview-image {
59
+ display: none;
60
+ max-width: 100%;
61
+ max-height: 100%;
62
+ }
63
+ .mds-file-upload:not(.mds-file-upload--selected-file-is-image) .mds-file-upload__open-modal {
64
+ display: none;
65
+ }
66
+ .mds-file-upload--selected-file-is-image .mds-file-upload__preview-icon {
67
+ display: none;
68
+ }
69
+ .mds-file-upload--selected-file-is-image .mds-file-upload__preview-image {
70
+ display: block;
71
+ width: 70px;
72
+ height: 70px;
73
+ object-fit: contain;
31
74
  }
32
75
 
33
- & label {
34
- @extend .mds-button;
35
- @extend .mds-button--small;
36
- font-weight: normal;
76
+ .mds-file-upload__file-name-container {
77
+ display: flex;
78
+ align-items: center;
37
79
  }
38
80
 
39
- & .mds-form-control {
81
+ .mds-file-upload__input {
40
82
  position: absolute;
41
83
  width: 100%;
42
84
  height: 100%;
43
85
  top: 0;
44
86
  opacity: 0;
45
87
  }
46
-
47
- & .mds-file-upload__input-controls {
48
- display: flex;
49
- flex-direction: column;
50
- align-items: center;
51
- justify-content: center;
52
- width: 100%;
53
- }
54
-
55
- & .mds-form-control:focus ~ .mds-file-upload__input-controls {
88
+ .mds-file-upload__input:focus ~ .mds-file-upload__wrapper {
56
89
  // Apply some focus styling to the 'button' when the input has focus and can be keyboard-activated
57
- label {
90
+ .mds-form-label__label {
58
91
  border: 1px solid $focus-color;
59
92
  outline-color: $focus-color;
60
93
  box-shadow: 0 0 4px 2px $focus-color;
61
94
  }
62
95
  }
63
- }
64
-
65
- .js .mds-form-element--file-supported .mds-file-upload__prompt {
66
- display: flex;
67
- align-items: center;
68
- justify-content: center;
69
- margin-bottom: $constant-size-baseline * 5;
70
-
71
- & .mds-icon {
72
- fill: var(--mds-color-button-bg-base, #000);
73
- }
74
- }
75
-
76
- .mds-file-upload__selected-state {
77
- display: none;
78
- }
79
- .mds-file-upload__file-name-container {
80
- display: flex;
81
- align-items: center;
82
- justify-content: center;
83
- padding: $constant-size-baseline * 2 $constant-size-baseline * 3;
84
- }
85
- .mds-file-upload__remove-button {
86
- @include z-index;
87
- }
88
-
89
- .js .mds-form-element--file-supported.mds-form-element--selected-file {
90
- @include mq($from: $constant-size-breakpoint-md) {
91
- width: 50%;
92
- }
93
96
 
94
- & .mds-file-upload {
97
+ /* Selected state */
98
+ .mds-file-upload--selected-file {
95
99
  border: $regular-border;
96
- }
97
- & .mds-file-upload__input {
98
- border: 0;
99
- }
100
- /* When a file is selected, we reduce the input size to 0 (.mds-form-control)
100
+ @include mq($from: $constant-size-breakpoint-md) {
101
+ width: 50%;
102
+ min-width: fit-content;
103
+ }
104
+ .mds-file-upload__wrapper {
105
+ border: none;
106
+ }
107
+
108
+ /* When a file is selected, we reduce the input size to 0 (.mds-form-control)
101
109
  * so the user can't click or drag and drop in the grey area anymore.
102
110
  * Will mostly be useful when used with the cloud services buttons
103
111
  * as the user will need to clear the input to see the buttons again.
104
112
  * We also remove the dotted border to indicate the change
105
113
  */
106
- & .mds-form-control {
107
- display: none;
108
- }
109
- & .mds-file-upload__input-controls {
110
- display: block;
111
- }
112
-
113
- & .mds-file-upload__selection-state {
114
- display: none;
115
- }
116
- & .mds-file-upload__selected-state {
117
- display: flex;
118
- align-items: center;
119
- justify-content: space-between;
120
- flex-wrap: wrap;
121
- }
122
- }
123
-
124
- .js .mds-form-element--file-supported.mds-form-element--dragover {
125
- & .mds-file-upload__input {
126
- background-color: #fff;
114
+ .mds-file-upload__input {
115
+ display: none;
116
+ }
117
+ .mds-file-upload__state--no-file {
118
+ display: none;
119
+ }
120
+ .mds-file-upload__state--selected {
121
+ display: flex;
122
+ flex-wrap: wrap;
123
+ gap: $constant-size-baseline * 4; // spacing between preview image/icon and filename, also works when flex wrapping occurs on small screens
124
+ .mds-button {
125
+ padding-left: 0;
126
+ }
127
+ }
127
128
  }
128
129
  }
129
-
130
- /* If the drag and drop is supported, we hide the "selected file" fallback sentence.
131
- * The text will show by default when there is no javascript
132
- * or for browsers like IE that don't support drag and drop,
133
- * as they will both get the default file input.
134
- */
135
- .js .mds-form-element--file-supported .mds-file-upload__selected-file-fallback {
136
- display: none;
137
- }
@@ -34,7 +34,7 @@
34
34
  <textarea
35
35
  class="mds-form-control"
36
36
  name="{{ params.id }}"
37
- id="text-editor-fallback-{{ params.id }}"
37
+ id="{{ params.id }}"
38
38
  {%- if params.validationError %}
39
39
  aria-invalid="true"
40
40
  {% endif -%}
@@ -14,7 +14,7 @@ module.exports = {
14
14
  name: 'Custom toolbar and translated editor',
15
15
  context: {
16
16
  variantTitle: 'Custom toolbar and translated editor',
17
- id: 'custom-toolbar',
17
+ id: 'custom.toolbar',
18
18
  labelText: 'Ajouter un commentaire',
19
19
  menuButtons: [
20
20
  {
@@ -61,10 +61,10 @@ const modals = {
61
61
  focusedElementBeforeModal = document.activeElement;
62
62
  modal.classList.add(modalActiveClass);
63
63
  firstTabStop.focus();
64
- siteContainer.setAttribute('aria-hidden', 'true');
64
+ siteContainer?.setAttribute('aria-hidden', 'true');
65
65
  },
66
66
  close: (modal, previousActiveElement, siteContainer) => {
67
- siteContainer.removeAttribute('aria-hidden');
67
+ siteContainer?.removeAttribute('aria-hidden');
68
68
  modal.classList.remove(modalActiveClass);
69
69
  previousActiveElement.focus();
70
70
  },
package/src/js/index.js CHANGED
@@ -5,7 +5,7 @@ import subnavigation from '../components/subnavigation/subnavigation';
5
5
  import checkboxList from '../components/inputs/checkbox-list/checkbox-list';
6
6
  import popovers from '../components/popover/popover';
7
7
  import modals from '../components/modal/modal';
8
- import fileUpload from '../components/inputs/file-upload/file-upload';
8
+ import { MdsFileUpload } from '../components/inputs/file-upload/file-upload';
9
9
  import characterCount from '../components/inputs/textarea/character-count';
10
10
  import button from '../components/button/button';
11
11
  import prose from '../helpers/prose/prose';
@@ -31,6 +31,9 @@ if (!window.customElements.get('mds-conditional-section')) {
31
31
  if (!window.customElements.get('mds-image-cropper')) {
32
32
  window.customElements.define('mds-image-cropper', MdsImageCropper);
33
33
  }
34
+ if (!window.customElements.get('mds-file-upload')) {
35
+ window.customElements.define('mds-file-upload', MdsFileUpload);
36
+ }
34
37
  if (!window.customElements.get('mds-category-picker')) {
35
38
  window.customElements.define('mds-category-picker', MdsCategoryPicker);
36
39
  }
@@ -41,7 +44,6 @@ const initAll = () => {
41
44
  subnavigation.init();
42
45
  checkboxList.init();
43
46
  modals.init();
44
- fileUpload.init();
45
47
  characterCount.init();
46
48
  popovers.init();
47
49
  button.init();