vgapp 1.5.6 → 1.5.7

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,5 +1,9 @@
1
- import BaseModule from "../../base-module";
2
- import { mergeDeepObject, getElement, isDisabled, isVisible } from "../../../utils/js/functions";
1
+ /**
2
+ * Описание: отслеживание активных секций и навигация по якорям.
3
+ * Возможности: Data API, нативная и виртуальная прокрутка, вложенные меню и обновление секций.
4
+ */
5
+ import BaseModule from "../../base-module";
6
+ import { getElement, isDisabled, isVisible, normalizeData } from "../../../utils/js/functions";
3
7
  import EventHandler from "../../../utils/js/dom/event";
4
8
  import Selectors from "../../../utils/js/dom/selectors";
5
9
 
@@ -52,7 +56,7 @@ class VGSpy extends BaseModule {
52
56
  * @property {number[]|string} threshold - пороги видимости (0.1, 0.5, 1)
53
57
  */
54
58
  this._params = this._configAfterMerge(
55
- mergeDeepObject(
59
+ Object.assign(
56
60
  {
57
61
  offset: null, // Устаревшее, для обратной совместимости
58
62
  rootMargin: '0px 0px -25%',
@@ -60,7 +64,8 @@ class VGSpy extends BaseModule {
60
64
  target: this._element,
61
65
  threshold: [0.1, 0.5, 1],
62
66
  },
63
- params
67
+ params,
68
+ this._getDataOptions()
64
69
  )
65
70
  );
66
71
 
@@ -130,15 +135,27 @@ class VGSpy extends BaseModule {
130
135
  * Ключ модуля (для хранения в data)
131
136
  * @returns {string}
132
137
  */
133
- static get NAME_KEY() {
134
- return NAME_KEY;
135
- }
138
+ static get NAME_KEY() {
139
+ return NAME_KEY;
140
+ }
141
+
142
+ _getDataOptions() {
143
+ const options = {};
144
+ for (const name of ['target', 'offset', 'rootMargin', 'smoothScroll', 'threshold']) {
145
+ const attribute = `data-${name.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`;
146
+ if (this._element.hasAttribute(attribute)) {
147
+ options[name] = normalizeData(this._element.getAttribute(attribute));
148
+ }
149
+ }
150
+ return options;
151
+ }
136
152
 
137
153
  /**
138
154
  * Инициализирует или перезапускает модуль: находит ссылки и секции, создаёт observer
139
155
  */
140
- refresh() {
141
- this._initializeTargetsAndObservables();
156
+ refresh() {
157
+ this._process(null);
158
+ this._initializeTargetsAndObservables();
142
159
  this._updateRootElement();
143
160
  this._maybeEnableSmoothScroll();
144
161
 
@@ -163,8 +180,8 @@ class VGSpy extends BaseModule {
163
180
 
164
181
  // Smooth Scrollbar can be initialized after the spy; retry once on the next tick.
165
182
  if (this._rootElement && !this._isScrollableSelf(this._rootElement) && window.Scrollbar) {
166
- setTimeout(() => {
167
- if (this._scrollbar) return;
183
+ setTimeout(() => {
184
+ if (!this._element || this._scrollbar) return;
168
185
  this._updateSmoothScrollbar();
169
186
  if (this._scrollbar) this._setupScrollbarTracking();
170
187
  }, 0);
@@ -356,7 +373,9 @@ class VGSpy extends BaseModule {
356
373
  return window.scrollY || document.documentElement.scrollTop || 0;
357
374
  }
358
375
 
359
- dispose() {
376
+ dispose() {
377
+ if (!this._element) return;
378
+ this._process(null);
360
379
  if (this._observer) {
361
380
  this._observer.disconnect();
362
381
  }
@@ -415,8 +434,9 @@ class VGSpy extends BaseModule {
415
434
  // If the target section is currently hidden (tabs/collapses/lazy layout),
416
435
  // don't hijack the click; allow UI to reveal it, then retry.
417
436
  if (!isVisible(section)) {
418
- setTimeout(() => {
419
- const revealed = Selectors.findID(id);
437
+ setTimeout(() => {
438
+ if (!this._element) return;
439
+ const revealed = Selectors.findID(id);
420
440
  if (!revealed || !isVisible(revealed)) return;
421
441
 
422
442
  this._targetLinks.set(revealed.id, link);
@@ -505,10 +525,10 @@ class VGSpy extends BaseModule {
505
525
  const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;
506
526
  this._previousScrollData.parentScrollTop = parentScrollTop;
507
527
 
508
- for (const entry of entries) {
509
- if (!entry.isIntersecting) {
510
- this._clearActiveClass(getTargetLink(entry));
511
- continue;
528
+ for (const entry of entries) {
529
+ if (!entry.isIntersecting) {
530
+ if (getTargetLink(entry) === this._activeTarget) this._process(null);
531
+ continue;
512
532
  }
513
533
 
514
534
  const entryTop = this._getSectionTop(entry.target);
@@ -516,7 +536,7 @@ class VGSpy extends BaseModule {
516
536
  const shouldActivate =
517
537
  (userScrollsDown && isEntryBelow) || (!userScrollsDown && !isEntryBelow);
518
538
 
519
- if (shouldActivate) {
539
+ if (shouldActivate || !this._activeTarget) {
520
540
  this._previousScrollData.visibleEntryTop = entryTop;
521
541
  this._process(getTargetLink(entry));
522
542
  }
@@ -595,7 +615,7 @@ class VGSpy extends BaseModule {
595
615
  parent.classList.remove(CLASS_NAME_ACTIVE);
596
616
 
597
617
  const activeLinks = Selectors.findAll(
598
- `[href].${CLASS_NAME_ACTIVE}, [data-vg-target].${CLASS_NAME_ACTIVE}`,
618
+ `[href].${CLASS_NAME_ACTIVE}, [data-vg-target].${CLASS_NAME_ACTIVE}, ${SELECTOR_DROPDOWN_TOGGLE}.${CLASS_NAME_ACTIVE}`,
599
619
  parent
600
620
  );
601
621
  for (const link of activeLinks) {
@@ -603,8 +623,9 @@ class VGSpy extends BaseModule {
603
623
  }
604
624
  }
605
625
 
606
- _getTargetIdFromTrigger(trigger) {
607
- if (!trigger || typeof trigger.getAttribute !== 'function') return null;
626
+ _getTargetIdFromTrigger(trigger) {
627
+ if (!trigger || typeof trigger.getAttribute !== 'function') return null;
628
+ if (isDisabled(trigger) || trigger.matches(SELECTOR_DROPDOWN_TOGGLE)) return null;
608
629
 
609
630
  const dataTarget = (trigger.getAttribute('data-vg-target') || '').trim();
610
631
  const href = (trigger.getAttribute('href') || '').trim();
@@ -1,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: вкладки VGTabs с декларативной и ручной инициализацией.
3
+ * Возможности: клавиатура, начальный hash, AJAX, отменяемые события и адаптивный индикатор.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import Selectors from "../../../utils/js/dom/selectors";
3
7
  import EventHandler from "../../../utils/js/dom/event";
4
8
  import {getNextActiveElement, isDisabled, mergeDeepObject} from "../../../utils/js/functions";
@@ -132,7 +136,7 @@ class VGTabs extends BaseModule {
132
136
  once: true,
133
137
  output: true,
134
138
  },
135
- }, this._params);
139
+ }, params || {});
136
140
 
137
141
  this._parent = this._element.closest(SELECTOR.TAB_PANEL);
138
142
  this._main_parent = this._parent?.closest(SELECTOR.TAB_CLASS) || null;
@@ -173,19 +177,19 @@ class VGTabs extends BaseModule {
173
177
  show() {
174
178
  const innerElem = this._element;
175
179
 
176
- if (this._elemIsActive(innerElem)) return;
180
+ if (!innerElem || isDisabled(innerElem) || this._elemIsActive(innerElem)) return;
177
181
 
178
182
  const activeElem = this._getActiveElem();
179
- const relatedTarget = innerElem;
180
183
 
181
184
  // События hide и show
182
- const hideEvent = activeElem ? EventHandler.trigger(activeElem, EVENT_HIDE, {relatedTarget}) : null;
183
- const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {relatedTarget});
185
+ const hideEvent = activeElem ? EventHandler.trigger(activeElem, EVENT_HIDE, {relatedTarget: innerElem}) : null;
186
+ const showEvent = EventHandler.trigger(innerElem, EVENT_SHOW, {relatedTarget: activeElem});
184
187
 
185
188
  if (showEvent.defaultPrevented || (hideEvent && hideEvent.defaultPrevented)) return;
186
189
 
187
190
  this._deactivate(activeElem, innerElem);
188
- this._activate(innerElem, relatedTarget);
191
+ this._activate(innerElem, activeElem);
192
+ this._updateSlider(innerElem);
189
193
  }
190
194
 
191
195
  /**
@@ -218,21 +222,28 @@ class VGTabs extends BaseModule {
218
222
  const target = Selectors.getElementFromSelector(element);
219
223
  if (target) this._activate(target, relatedTarget);
220
224
 
221
- const complete = () => {
225
+ const complete = () => {
226
+ if (!this._element || !element.classList.contains(CLASS_NAME.ACTIVE)) return;
222
227
  if (element.getAttribute('role') !== 'tab') {
223
228
  element.classList.add(CLASS_NAME.SHOW);
224
229
  return;
225
230
  }
226
231
 
227
- this._route((status, data) => {
228
- EventHandler.trigger(this._element, EVENT_LOADED, { stats: status, data });
229
- });
232
+ if (this._params.ajax.route && !this._isLoaded && !this._isLoading) {
233
+ this._isLoading = true;
234
+ const loaded = (status, data) => {
235
+ if (!this._element) return;
236
+ this._isLoading = false;
237
+ EventHandler.trigger(this._element, EVENT_LOADED, { stats: status, data });
238
+ };
239
+ this._route(loaded, error => loaded('error', error));
240
+ }
230
241
 
231
242
  element.removeAttribute('tabindex');
232
243
  element.setAttribute('aria-selected', 'true');
233
244
  this._toggleDropDown(element, true);
234
245
 
235
- EventHandler.trigger(element, EVENT_SHOWN, { relatedTarget }); // ← теперь relatedTarget определён
246
+ EventHandler.trigger(element, EVENT_SHOWN, { relatedTarget });
236
247
  };
237
248
 
238
249
  this._queueCallback(complete, element, element.classList.contains(CLASS_NAME.FADE));
@@ -252,7 +263,8 @@ class VGTabs extends BaseModule {
252
263
  const target = Selectors.getElementFromSelector(element);
253
264
  if (target) this._deactivate(target, relatedTarget);
254
265
 
255
- const complete = () => {
266
+ const complete = () => {
267
+ if (!this._element || element.classList.contains(CLASS_NAME.ACTIVE)) return;
256
268
  if (element.getAttribute('role') !== 'tab') {
257
269
  element.classList.remove(CLASS_NAME.SHOW);
258
270
  return;
@@ -300,13 +312,12 @@ class VGTabs extends BaseModule {
300
312
  _setTabHash() {
301
313
  if (!this._params.hash) return;
302
314
 
303
- const url = document.location.toString();
304
- if (!url.includes('#')) return;
305
-
306
- const id = url.split('#')[1];
307
- const element = Selectors.find(`[href="#${id}"]`, this._parent) ||
308
- Selectors.find(`[data-vg-target="#${id}"]`, this._element) ||
309
- null;
315
+ let hash = document.location.hash;
316
+ if (!hash) return;
317
+ try { hash = decodeURIComponent(hash); } catch { return; }
318
+ const element = this._getChildren().find(child =>
319
+ !isDisabled(child) && (child.getAttribute('href') === hash || child.getAttribute('data-vg-target') === hash)
320
+ );
310
321
 
311
322
  if (element) {
312
323
  VGTabs.getOrCreateInstance(element).show();
@@ -316,8 +327,8 @@ class VGTabs extends BaseModule {
316
327
  /**
317
328
  * Инициализация слайдера-индикатора под вкладками
318
329
  */
319
- _setInitialSlider() {
320
- if (!this._params.slide) return;
330
+ _setInitialSlider() {
331
+ if (!this._params.slide || !this._main_parent) return;
321
332
 
322
333
  let slider = Selectors.find(`.${CLASS_NAME.SLIDER}`, this._main_parent);
323
334
  if (!slider) {
@@ -328,45 +339,58 @@ class VGTabs extends BaseModule {
328
339
 
329
340
  this._main_parent.classList.add(CLASS_NAME.WITH_SLIDER);
330
341
 
331
- const activeLink = Selectors.find(`.${CLASS_NAME.ACTIVE}`, this._parent);
332
- if (!activeLink) return;
333
-
334
- const {width, height} = window.getComputedStyle(activeLink);
335
- activeLink.classList.add(CLASS_NAME.HOVER);
336
-
337
- slider.style.width = width;
338
- slider.style.height = height;
339
- slider.style.left = `${activeLink.offsetLeft}px`;
340
-
341
- // Наведение
342
- EventHandler.on(this._main_parent, EVENT_MOUSEOVER_DATA_API, SELECTOR.DATA_TOGGLE, (event) => {
343
- const target = event.target;
344
- if (['A', 'AREA'].includes(target.tagName)) event.preventDefault();
345
- if (isDisabled(target)) return;
346
-
347
- const hover = Selectors.find(`.${CLASS_NAME.HOVER}`, this._parent);
348
- if (hover) hover.classList.remove(CLASS_NAME.HOVER);
349
- target.classList.add(CLASS_NAME.HOVER);
350
-
351
- const {width, height} = window.getComputedStyle(target);
352
- slider.style.width = width;
353
- slider.style.height = height;
354
- slider.style.left = `${target.offsetLeft}px`;
355
- });
356
-
357
- // Уход курсора
358
- EventHandler.on(this._main_parent, EVENT_MOUSEOUT_DATA_API, SELECTOR.DATA_TOGGLE, () => {
359
- const active = Selectors.find(`.${CLASS_NAME.ACTIVE}`, this._parent);
360
- const {width, height} = window.getComputedStyle(active);
361
-
362
- Selectors.findAll(`.${CLASS_NAME.HOVER}`, this._parent).forEach(el => el.classList.remove(CLASS_NAME.HOVER));
363
- active.classList.add(CLASS_NAME.HOVER);
364
-
365
- slider.style.width = width;
366
- slider.style.height = height;
367
- slider.style.left = `${active.offsetLeft}px`;
368
- });
369
- }
342
+ this._updateSlider(this._getActiveElem());
343
+ this._sliderOver = event => {
344
+ const target = event.delegateTarget;
345
+ if (target.closest(SELECTOR.TAB_PANEL) === this._parent && !isDisabled(target)) this._updateSlider(target);
346
+ };
347
+ this._sliderOut = () => this._updateSlider(this._getActiveElem());
348
+ // Только один владелец общих обработчиков на группу вкладок.
349
+ const owner = this._getChildren().some(child => VGTabs.getInstance(child)?._sliderOver && child !== this._element);
350
+ if (owner) {
351
+ this._sliderOver = null;
352
+ this._sliderOut = null;
353
+ return;
354
+ }
355
+ EventHandler.on(this._main_parent, EVENT_MOUSEOVER_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOver);
356
+ EventHandler.on(this._main_parent, EVENT_MOUSEOUT_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOut);
357
+ this._sliderResize = () => this._updateSlider(this._getActiveElem());
358
+ window.addEventListener('resize', this._sliderResize);
359
+ if (typeof ResizeObserver !== 'undefined') {
360
+ this._sliderObserver = new ResizeObserver(this._sliderResize);
361
+ this._sliderObserver.observe(this._parent);
362
+ this._getChildren().forEach(child => this._sliderObserver.observe(child));
363
+ }
364
+ }
365
+
366
+ _updateSlider(target) {
367
+ if (!target || !this._main_parent) return;
368
+ const slider = this._main_parent.querySelector(`.${CLASS_NAME.SLIDER}`);
369
+ if (!slider) return;
370
+ this._getChildren().forEach(child => child.classList.toggle(CLASS_NAME.HOVER, child === target));
371
+ const {width, height} = window.getComputedStyle(target);
372
+ Object.assign(slider.style, {width, height, left: `${target.offsetLeft}px`, top: `${target.offsetTop}px`});
373
+ }
374
+
375
+ dispose() {
376
+ if (!this._element) return;
377
+ const parent = this._main_parent;
378
+ const successor = this._sliderOver && this._getChildren()
379
+ .map(child => VGTabs.getInstance(child))
380
+ .find(instance => instance && instance !== this && instance._params.slide);
381
+ if (this._sliderOver) {
382
+ EventHandler.off(parent, EVENT_MOUSEOVER_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOver);
383
+ EventHandler.off(parent, EVENT_MOUSEOUT_DATA_API, SELECTOR.DATA_TOGGLE, this._sliderOut);
384
+ window.removeEventListener('resize', this._sliderResize);
385
+ this._sliderObserver?.disconnect();
386
+ }
387
+ super.dispose();
388
+ if (successor) successor._setInitialSlider();
389
+ else if (parent && !Selectors.findAll(SELECTOR.DATA_TOGGLE, parent).some(child => VGTabs.getInstance(child)?._params.slide)) {
390
+ parent.querySelector(`.${CLASS_NAME.SLIDER}`)?.remove();
391
+ parent.classList.remove(CLASS_NAME.WITH_SLIDER);
392
+ }
393
+ }
370
394
 
371
395
  /**
372
396
  * Устанавливает базовые ARIA-атрибуты родителю
@@ -391,9 +415,9 @@ class VGTabs extends BaseModule {
391
415
  if (outerElem !== child) {
392
416
  this._setAttributeIfNotExists(outerElem, 'role', 'presentation');
393
417
  }
394
- if (!isActive) {
395
- child.setAttribute('tabindex', '-1');
396
- }
418
+ if (!isActive) {
419
+ child.setAttribute('tabindex', '-1');
420
+ } else child.removeAttribute('tabindex');
397
421
  this._setAttributeIfNotExists(child, 'role', 'tab');
398
422
  this._setInitialAttributesOnTargetPanel(child);
399
423
  }
@@ -429,7 +453,8 @@ class VGTabs extends BaseModule {
429
453
  * @returns {HTMLElement[]}
430
454
  */
431
455
  _getChildren() {
432
- return Selectors.findAll(SELECTOR.INNER_ELEM, this._parent);
456
+ return Selectors.findAll(SELECTOR.INNER_ELEM, this._parent)
457
+ .filter(child => child.closest(SELECTOR.TAB_PANEL) === this._parent);
433
458
  }
434
459
 
435
460
  /**
@@ -490,4 +515,4 @@ EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
490
515
  });
491
516
  });
492
517
 
493
- export default VGTabs;
518
+ export default VGTabs;
@@ -1,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: уведомления VGToast через Data API и JavaScript.
3
+ * Возможности: стек, AJAX, анимации, автоскрытие, перетаскивание и изменение размера.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import EventHandler from "../../../utils/js/dom/event";
3
7
  import {dismissTrigger, getSVG} from "../../module-fn";
4
8
  import Sanitize from "../../../utils/js/components/sanitize";
@@ -331,6 +335,7 @@ class VGToast extends BaseModule {
331
335
  */
332
336
  show(relatedTarget) {
333
337
  if (isDisabled(this._element)) return;
338
+ if (this._isShown() && !this._isHiding) return;
334
339
 
335
340
  const element = this._element;
336
341
  this._clearTimeout();
@@ -466,7 +471,7 @@ class VGToast extends BaseModule {
466
471
  const elmsShown = Selectors.findAll(`.vg-toast.show.${stackClass}`)
467
472
  .filter(el => {
468
473
  const instance = VGToast.getInstance(el);
469
- return instance?._params.stack.enable;
474
+ return instance && !instance._isHiding;
470
475
  });
471
476
 
472
477
  if (!this._params.stack.enable) {
@@ -478,8 +483,9 @@ class VGToast extends BaseModule {
478
483
  }
479
484
 
480
485
  // Ограничиваем по max
481
- if (elmsShown.length >= this._params.stack.max) {
482
- const excess = elmsShown.slice(0, elmsShown.length - this._params.stack.max + 1);
486
+ const max = Math.max(1, Number(this._params.stack.max) || 5);
487
+ if (elmsShown.length > max) {
488
+ const excess = elmsShown.slice(0, elmsShown.length - max);
483
489
  excess.forEach(el => VGToast.getInstance(el).hide());
484
490
  }
485
491
 
@@ -507,7 +513,7 @@ class VGToast extends BaseModule {
507
513
  const visibleStack = Selectors.findAll(`.vg-toast.show.${stackClass}`)
508
514
  .filter(el => {
509
515
  const instance = VGToast.getInstance(el);
510
- return instance?._params.stack.enable;
516
+ return instance && !instance._isHiding;
511
517
  });
512
518
  const elms = visibleStack.length ? visibleStack : stackItems.map(item => item.el);
513
519
  let offset = 0;
@@ -1,4 +1,8 @@
1
- import BaseModule from "../../base-module";
1
+ /**
2
+ * Описание: всплывающие подсказки и информационные popover VGApp.
3
+ * Возможности: Data API, позиционирование, события и очистка при удалении триггера.
4
+ */
5
+ import BaseModule from "../../base-module";
2
6
  import {isDisabled, makeRandomString, mergeDeepObject} from "../../../utils/js/functions";
3
7
  import EventHandler from "../../../utils/js/dom/event";
4
8
  import Selectors from "../../../utils/js/dom/selectors";
@@ -80,8 +84,13 @@ class VGTooltip extends BaseModule {
80
84
  constructor(element, params = {}) {
81
85
  super(element, params);
82
86
 
83
- this._params = this._getParams(element, mergeDeepObject(defaultParams, params));
84
- this._tooltip = null;
87
+ this._params = this._getParams(element, mergeDeepObject(defaultParams, params));
88
+ // Геометрические массивы заменяются целиком, а не дополняют defaults.
89
+ const dataParams = this._getParams(element, {});
90
+ ['offset', 'fallbackPlacements'].forEach(key => {
91
+ this._params[key] = [...(dataParams[key] ?? params[key] ?? defaultParams[key])];
92
+ });
93
+ this._tooltip = null;
85
94
  this._isHiding = false;
86
95
  this._showTimeout = null;
87
96
  this._hideTimeout = null;
@@ -249,10 +258,11 @@ class VGTooltip extends BaseModule {
249
258
  tooltip.classList.add('vg-tooltip-popover');
250
259
  }
251
260
 
252
- const inner = document.createElement('div');
253
- inner.classList.add('vg-tooltip-inner');
254
-
255
- if (this._params.content) {
261
+ const inner = document.createElement('div');
262
+ inner.classList.add('vg-tooltip-inner');
263
+ const content = this._params.content || this._element.dataset.vgContent || '';
264
+
265
+ if (content) {
256
266
  const titleBlock = document.createElement('div');
257
267
  titleBlock.classList.add('vg-tooltip-inner--title');
258
268
 
@@ -261,10 +271,10 @@ class VGTooltip extends BaseModule {
261
271
 
262
272
  if (this._params.html) {
263
273
  titleBlock.innerHTML = title;
264
- contentBlock.innerHTML = this._params.content;
274
+ contentBlock.innerHTML = content;
265
275
  } else {
266
276
  titleBlock.textContent = title;
267
- contentBlock.textContent = this._params.content;
277
+ contentBlock.textContent = content;
268
278
  }
269
279
 
270
280
  inner.append(titleBlock);
@@ -1,4 +1,8 @@
1
- const FILE_ICON_BY_EXT = {
1
+ /**
2
+ * Описание: определение категории файла и соответствующей SVG-иконки.
3
+ * Возможности: распознаёт MIME-тип и расширение, поддерживает явные имена иконок и общий fallback.
4
+ */
5
+ const FILE_ICON_BY_EXT = {
2
6
  pdf: 'file-pdf',
3
7
  doc: 'file-word',
4
8
  docx: 'file-word',
@@ -34,7 +38,8 @@ const FILE_ICON_BY_EXT = {
34
38
  tiff: 'file-image',
35
39
  heic: 'file-image',
36
40
  heif: 'file-image',
37
- avif: 'file-image',
41
+ avif: 'file-image',
42
+ ico: 'file-image',
38
43
  mp3: 'file-audio',
39
44
  wav: 'file-audio',
40
45
  ogg: 'file-audio',