adt-js-components 2.0.3 → 2.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adt-js-components",
3
- "version": "2.0.3",
3
+ "version": "2.2.0",
4
4
  "description": "JavaScript components for Nette framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -11,6 +11,10 @@
11
11
  "url": "git+https://github.com/AppsDevTeam/js-components.git"
12
12
  },
13
13
  "dependencies": {
14
+ "nette.ajax.js": "^2.3.0"
15
+ },
16
+ "peerDependencies": {
17
+ "jquery": "^3.4.1",
14
18
  "select2": "^4.0.13",
15
19
  "select2-bootstrap-5-theme": "^1.1.1",
16
20
  "autonumeric": "^4.6.0",
@@ -23,14 +27,26 @@
23
27
  "scrollparent": "^2.0.1",
24
28
  "tinymce": "^5.7.0",
25
29
  "tinymce-i18n": "^20.12.25",
26
- "nette.ajax.js": "^2.3.0",
27
30
  "leaflet": "^1.9.4",
28
31
  "leaflet.markercluster": "^1.5.3",
29
- "firebase": "^12.10.0",
30
- "@firebase/messaging": "^0.12.24"
32
+ "firebase": "^12.10.0"
31
33
  },
32
- "peerDependencies": {
33
- "jquery": "^3.4.1"
34
+ "peerDependenciesMeta": {
35
+ "select2": { "optional": true },
36
+ "select2-bootstrap-5-theme": { "optional": true },
37
+ "autonumeric": { "optional": true },
38
+ "@here/flexpolyline": { "optional": true },
39
+ "flatpickr": { "optional": true },
40
+ "script-loader": { "optional": true },
41
+ "timepicker": { "optional": true },
42
+ "glightbox": { "optional": true },
43
+ "@googlemaps/js-api-loader": { "optional": true },
44
+ "scrollparent": { "optional": true },
45
+ "tinymce": { "optional": true },
46
+ "tinymce-i18n": { "optional": true },
47
+ "leaflet": { "optional": true },
48
+ "leaflet.markercluster": { "optional": true },
49
+ "firebase": { "optional": true }
34
50
  },
35
51
  "author": "Apps Dev Team",
36
52
  "license": "MIT",
@@ -107,26 +107,105 @@ function run(options) {
107
107
  this.$originalElement.select2('destroy');
108
108
  };
109
109
 
110
- function applyEventHandlers(el) {
111
- new AjaxSelect($(el));
110
+ /**
111
+ * Lazy initialization.
112
+ *
113
+ * Building select2 reads computed styles and layout (e.g. the mousewheel plugin bundled
114
+ * in select2.full measures the line height of every results list). Right after the
115
+ * previous select2 was inserted, each such read forces the browser to lay out the whole
116
+ * page again, so initializing many selects at once gets quadratically slower - a form
117
+ * with 300 rows took tens of seconds.
118
+ *
119
+ * Selects that are visible (or close to the viewport) are therefore initialized right
120
+ * away and the rest only when they are about to scroll into view. Visibility of all newly
121
+ * added selects is read in one pass before any of them is initialized, so the page is laid
122
+ * out once, not once per select. A select that is not initialized yet is initialized on
123
+ * focus as well (e.g. when the user tabs into it).
124
+ *
125
+ * A few selects added at once (a row added by a replicator) are always initialized right
126
+ * away - that is cheap, and the code that added the row may call select2('open') on it,
127
+ * even when the row ended up at the end of a long list out of the viewport.
128
+ */
129
+ const VISIBILITY_MARGIN = 200;
130
+ const EAGER_LIMIT = 10;
131
+
132
+ function initialize(el) {
133
+ if (el.adtAjaxSelect) {
134
+ return;
135
+ }
136
+ el.adtAjaxSelect = new AjaxSelect($(el));
137
+ }
138
+
139
+ const visibilityObserver = 'IntersectionObserver' in window
140
+ ? new IntersectionObserver(entries => {
141
+ entries.forEach(entry => {
142
+ if (entry.isIntersecting) {
143
+ visibilityObserver.unobserve(entry.target);
144
+ entry.target.removeEventListener('focus', initializeOnFocus);
145
+ initialize(entry.target);
146
+ }
147
+ });
148
+ }, { rootMargin: VISIBILITY_MARGIN + 'px' })
149
+ : null;
150
+
151
+ function initializeOnFocus(e) {
152
+ const el = e.currentTarget;
153
+ el.removeEventListener('focus', initializeOnFocus);
154
+ visibilityObserver.unobserve(el);
155
+ initialize(el);
156
+ $(el).data('select2')?.$selection.trigger('focus');
157
+ }
158
+
159
+ function isNearViewport(rect) {
160
+ // zero size = not rendered (display: none), it will be initialized once it shows up
161
+ return (rect.width > 0 || rect.height > 0)
162
+ && rect.bottom >= -VISIBILITY_MARGIN
163
+ && rect.top <= window.innerHeight + VISIBILITY_MARGIN
164
+ && rect.right >= -VISIBILITY_MARGIN
165
+ && rect.left <= window.innerWidth + VISIBILITY_MARGIN;
166
+ }
167
+
168
+ function applyEventHandlers(elements) {
169
+ elements = elements.filter(el => !el.adtAjaxSelect);
170
+
171
+ if (!visibilityObserver || elements.length <= EAGER_LIMIT) {
172
+ elements.forEach(initialize);
173
+ return;
174
+ }
175
+
176
+ // read all rects first (one layout), only then start building select2
177
+ const rects = elements.map(el => el.getBoundingClientRect());
178
+
179
+ elements.forEach((el, i) => {
180
+ if (isNearViewport(rects[i])) {
181
+ initialize(el);
182
+ } else {
183
+ visibilityObserver.observe(el);
184
+ el.addEventListener('focus', initializeOnFocus);
185
+ }
186
+ });
112
187
  }
113
188
 
114
189
  const observer = new MutationObserver(mutations => {
190
+ const elements = [];
191
+
115
192
  mutations.forEach(mutation => {
116
193
  if (mutation.type === "childList") {
117
194
  mutation.addedNodes.forEach(node => {
118
195
  if (node.nodeType === 1) {
119
196
  if (node.hasAttribute("data-adt-ajax-select")) {
120
- applyEventHandlers(node);
197
+ elements.push(node);
121
198
  }
122
199
 
123
- node.querySelectorAll('[data-adt-ajax-select]').forEach(child => {
124
- applyEventHandlers(child);
125
- });
200
+ elements.push(...node.querySelectorAll('[data-adt-ajax-select]'));
126
201
  }
127
202
  });
128
203
  }
129
204
  });
205
+
206
+ if (elements.length) {
207
+ applyEventHandlers(elements);
208
+ }
130
209
  });
131
210
 
132
211
  observer.observe(document.body, {
@@ -134,9 +213,7 @@ function run(options) {
134
213
  subtree: true,
135
214
  });
136
215
 
137
- document.querySelectorAll('[data-adt-ajax-select]').forEach(function(el) {
138
- applyEventHandlers(el);
139
- });
216
+ applyEventHandlers([...document.querySelectorAll('[data-adt-ajax-select]')]);
140
217
  }
141
218
 
142
219
  export default { run }
@@ -88,18 +88,48 @@ const run = (config) => {
88
88
  updateButtons();
89
89
  });
90
90
 
91
- // Initial state: check if notifications are already enabled
92
- if (Notification.permission === 'granted' && window.messaging) {
93
- getToken(window.messaging, { vapidKey: config.vapidKey })
94
- .then(function (currentToken) {
95
- if (currentToken) {
96
- window.adtNotificationsToken = currentToken;
91
+ // window.messaging se inicializuje async v Messaging komponentě, která nemusí
92
+ // doběhnout dřív než tento init (typicky po F5). Počkáme na ni, jinak by se
93
+ // getToken nikdy nezavolal a window.adtNotificationsToken by zůstal nenastavený.
94
+ var waitForMessaging = function (timeoutMs) {
95
+ return new Promise(function (resolve) {
96
+ if (window.messaging) {
97
+ resolve(window.messaging);
98
+ return;
99
+ }
100
+ var elapsed = 0;
101
+ var step = 100;
102
+ var interval = setInterval(function () {
103
+ if (window.messaging) {
104
+ clearInterval(interval);
105
+ resolve(window.messaging);
106
+ } else if ((elapsed += step) >= timeoutMs) {
107
+ clearInterval(interval);
108
+ resolve(null);
97
109
  }
110
+ }, step);
111
+ });
112
+ };
113
+
114
+ // Initial state: check if notifications are already enabled
115
+ if (Notification.permission === 'granted') {
116
+ waitForMessaging(5000).then(function (messaging) {
117
+ if (!messaging) {
98
118
  updateButtons();
99
- })
100
- .catch(function () {
101
- updateButtons();
102
- });
119
+ return;
120
+ }
121
+
122
+ getToken(messaging, { vapidKey: config.vapidKey })
123
+ .then(function (currentToken) {
124
+ if (currentToken) {
125
+ window.adtNotificationsToken = currentToken;
126
+ }
127
+ updateButtons();
128
+ })
129
+ .catch(function () {
130
+ updateButtons();
131
+ });
132
+ });
103
133
  } else {
104
134
  updateButtons();
105
135
  }
@@ -8,6 +8,14 @@ function run(options) {
8
8
  const selector = options.selector || 'select:not(' + noSelect2Class + ')';
9
9
 
10
10
  function applyEventHandlers(el) {
11
+ // AJAX selects are initialized by the AjaxSelect component with their own options.
12
+ // Initializing them here as well would build select2 twice and AjaxSelect would
13
+ // rebuild it right away. Each initialization reads computed styles, which is the
14
+ // most expensive part of rendering a form with many selects.
15
+ if (el.matches('[data-adt-ajax-select]')) {
16
+ return;
17
+ }
18
+
11
19
  $(el)
12
20
  .select2($.extend({theme: 'bootstrap-5', language: "en"}, $(el).data('adt-select2') || {}))
13
21
  .on('change', function(e) {
@@ -8,6 +8,30 @@ function getErrorElementId(el) {
8
8
  return isList(el) ? el.id.split('-').slice(0, -1).join('-') : el.id;
9
9
  }
10
10
 
11
+ function toggleInvalid(el, invalid) {
12
+ el.classList.toggle('is-invalid', invalid);
13
+ // because of radio lists and checkbox lists
14
+ if (isList(el)) {
15
+ el.parentNode.classList.toggle('is-invalid', invalid);
16
+ }
17
+ // because of https://github.com/twbs/bootstrap/issues/25110
18
+ if (el.parentNode.classList.contains('input-group')) {
19
+ el.parentNode.classList.toggle('has-validation', invalid);
20
+ el.parentNode.classList.toggle('is-invalid', invalid);
21
+ }
22
+ }
23
+
24
+ function hasErrors(errorElement) {
25
+ return Array.from(errorElement.childNodes).some(node => node.nodeName !== 'SCRIPT' && node.textContent.trim() !== '');
26
+ }
27
+
28
+ function markErrorsFor(errorElement) {
29
+ const el = document.getElementById(errorElement.getAttribute('data-adt-errors-for'));
30
+ if (el && el.tagName !== 'FORM') {
31
+ toggleInvalid(el, hasErrors(errorElement));
32
+ }
33
+ }
34
+
11
35
  function scrollToFirstError(form) {
12
36
  const el = $(form).find('.alert-danger:first, .is-invalid:first')[0];
13
37
 
@@ -28,10 +52,17 @@ function run(options) {
28
52
  if (isList(this)) {
29
53
  $(this).parent().parent().find('.is-invalid').removeClass('is-invalid');
30
54
  }
55
+ if (this.parentNode.classList.contains('input-group')) {
56
+ this.parentNode.classList.remove('is-invalid');
57
+ }
31
58
  });
32
59
  }
33
60
 
34
61
  $.nette.ext('live').after(function($el) {
62
+ $el.find('[data-adt-errors-for]').addBack('[data-adt-errors-for]').each(function() {
63
+ markErrorsFor(this);
64
+ });
65
+
35
66
  window.addEventListener('pageshow', (e) => {
36
67
  if (e.persisted) {
37
68
  $el.find('[data-adt-submit-form]').each(function(e) {
@@ -85,15 +116,7 @@ function run(options) {
85
116
  errorElement.innerHTML += `<div>${error.message}</div>`;
86
117
  }
87
118
 
88
- error.element.classList.add('is-invalid');
89
- // because of radio lists and checkbox lists
90
- if (isList(error.element)) {
91
- error.element.parentNode.classList.add('is-invalid');
92
- }
93
- // because of https://github.com/twbs/bootstrap/issues/25110
94
- if (error.element.parentNode.classList.contains('input-group')) {
95
- error.element.parentNode.classList.add('has-validation');
96
- }
119
+ toggleInvalid(error.element, true);
97
120
  }
98
121
 
99
122
  if (errors.length) {
@@ -155,7 +178,9 @@ function run(options) {
155
178
  },
156
179
  error: function (xhr, status, error, settings) {
157
180
  if (settings.nette && settings.nette.form && settings.nette.form.attr('data-adt-submit-form') !== undefined) {
158
- settings.nette.form.prepend('<div class="alert alert-danger">' + xhr.responseText + '</div>');
181
+ if (status !== 'abort') {
182
+ settings.nette.form.prepend('<div class="alert alert-danger">' + xhr.responseText + '</div>');
183
+ }
159
184
 
160
185
  settings.nette.el.html(settings.nette.el.data('originalContent'));
161
186
  settings.nette.form.data('enabledButtons').each(function () {