adt-js-components 2.1.1 → 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.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "JavaScript components for Nette framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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 }
@@ -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) {
@@ -178,7 +178,9 @@ function run(options) {
178
178
  },
179
179
  error: function (xhr, status, error, settings) {
180
180
  if (settings.nette && settings.nette.form && settings.nette.form.attr('data-adt-submit-form') !== undefined) {
181
- 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
+ }
182
184
 
183
185
  settings.nette.el.html(settings.nette.el.data('originalContent'));
184
186
  settings.nette.form.data('enabledButtons').each(function () {