vgapp 1.4.0 → 1.4.2

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.
@@ -0,0 +1,227 @@
1
+ import Backdrop from "../../../utils/js/components/backdrop";
2
+ import Selectors from "../../../utils/js/dom/selectors";
3
+ import EventHandler from "../../../utils/js/dom/event";
4
+ import {reflow} from "../../../utils/js/functions";
5
+
6
+ const NAME_KEY = 'vg.modal';
7
+
8
+ const OPEN_SELECTOR = '.vg-modal.show:not([data-vg-persistent="true"])';
9
+
10
+ const CLASS_NAME_OPEN = 'vg-modal-open';
11
+ const CLASS_NAME_SHOW = 'show';
12
+ const CLASS_NAME_MINIMIZED = 'vg-modal-minimized-state';
13
+ const CLASS_NAME_MINIMIZE_BUTTON = 'vg-btn-minimize';
14
+ const CLASS_NAME_MINIMIZED_CONTAINER = 'vg-modal-minimized-container';
15
+ const CLASS_NAME_MINIMIZED_ITEM = 'vg-modal-minimized';
16
+
17
+ const EVENT_KEY_MINIMIZE = `${NAME_KEY}.minimize`;
18
+ const EVENT_KEY_MINIMIZED = `${NAME_KEY}.minimized`;
19
+ const EVENT_KEY_RESTORE = `${NAME_KEY}.restore`;
20
+ const EVENT_KEY_RESTORED = `${NAME_KEY}.restored`;
21
+
22
+ class VGModalMinimized {
23
+ constructor(modal) {
24
+ this._modal = modal;
25
+ this._element = modal._element;
26
+ this._dialog = modal._dialog;
27
+ this._content = modal._content;
28
+ this._minimizedElement = null;
29
+ this._isMinimized = false;
30
+ }
31
+
32
+ setup() {
33
+ const minimizeConfig = this._getConfig();
34
+ if (!minimizeConfig.enable || !this._content) return;
35
+ if (Selectors.find(`.${CLASS_NAME_MINIMIZE_BUTTON}`, this._content)) return;
36
+
37
+ const button = document.createElement('button');
38
+ button.type = 'button';
39
+ button.className = CLASS_NAME_MINIMIZE_BUTTON;
40
+ button.setAttribute('aria-label', minimizeConfig.label || 'Minimize');
41
+ button.innerHTML = '<span aria-hidden="true"></span>';
42
+
43
+ const closeButton = Selectors.find('.vg-btn-close', this._content);
44
+ if (closeButton && closeButton.parentNode) {
45
+ closeButton.parentNode.insertBefore(button, closeButton);
46
+ } else {
47
+ this._content.prepend(button);
48
+ }
49
+
50
+ EventHandler.on(button, `click.${NAME_KEY}.minimize`, (event) => {
51
+ event.preventDefault();
52
+ this.minimize(button);
53
+ });
54
+ }
55
+
56
+ isMinimized() {
57
+ return this._isMinimized;
58
+ }
59
+
60
+ minimize(relatedTarget) {
61
+ if (!this._getConfig().enable || !this._modal._isShown || this._isMinimized || this._modal._isTransitioning) return;
62
+
63
+ const minimizeEvent = EventHandler.trigger(this._element, EVENT_KEY_MINIMIZE, { relatedTarget });
64
+ if (minimizeEvent.defaultPrevented) return;
65
+
66
+ this._isMinimized = true;
67
+ this._modal._disableInteractionHandlers();
68
+ this._element.classList.remove(CLASS_NAME_SHOW);
69
+ this._element.classList.add(CLASS_NAME_MINIMIZED);
70
+ this._element.style.display = 'none';
71
+ this._element.removeAttribute('aria-modal');
72
+ this._element.removeAttribute('role');
73
+
74
+ const remainingOpenModals = Selectors.findAll(OPEN_SELECTOR).filter(modal => modal !== this._element);
75
+ if (!remainingOpenModals.length) {
76
+ document.body.classList.remove(CLASS_NAME_OPEN);
77
+ }
78
+
79
+ const finish = () => {
80
+ if (!Backdrop.isActive()) {
81
+ this._modal._scrollBar.reset();
82
+ }
83
+
84
+ this._ensureElement(relatedTarget);
85
+ EventHandler.trigger(this._element, EVENT_KEY_MINIMIZED, { relatedTarget });
86
+ };
87
+
88
+ if (this._modal._backdropElement) {
89
+ this._modal._hideBackdrop(finish);
90
+ return;
91
+ }
92
+
93
+ finish();
94
+ }
95
+
96
+ restore(relatedTarget) {
97
+ if (!this._isMinimized || this._modal._isTransitioning) return;
98
+
99
+ const restoreEvent = EventHandler.trigger(this._element, EVENT_KEY_RESTORE, { relatedTarget });
100
+ if (restoreEvent.defaultPrevented) return;
101
+
102
+ const restoreElement = () => {
103
+ this.removeElement();
104
+ this._isMinimized = false;
105
+ this._element.classList.remove(CLASS_NAME_MINIMIZED);
106
+ this._element.style.display = 'block';
107
+ this._element.setAttribute('aria-modal', true);
108
+ this._element.setAttribute('role', 'dialog');
109
+ document.body.classList.add(CLASS_NAME_OPEN);
110
+
111
+ reflow(this._element);
112
+ this._element.classList.add(CLASS_NAME_SHOW);
113
+ this._modal._toggleInteractionHandlers();
114
+ this._modal._syncInteractiveBounds();
115
+ EventHandler.trigger(this._element, EVENT_KEY_RESTORED, { relatedTarget });
116
+ };
117
+
118
+ if (this._modal._params.backdrop) {
119
+ this._modal._scrollBar.hide();
120
+ this._modal._showBackdrop(restoreElement);
121
+ return;
122
+ }
123
+
124
+ restoreElement();
125
+ }
126
+
127
+ reset() {
128
+ this.removeElement();
129
+ this._isMinimized = false;
130
+ this._element.classList.remove(CLASS_NAME_MINIMIZED);
131
+ }
132
+
133
+ dispose() {
134
+ this.removeElement();
135
+ }
136
+
137
+ _getConfig() {
138
+ const value = this._modal._params.minimize;
139
+ const defaults = {
140
+ enable: false,
141
+ title: '',
142
+ text: '',
143
+ label: 'Minimize',
144
+ };
145
+
146
+ if (typeof value === 'boolean') {
147
+ return {...defaults, enable: value};
148
+ }
149
+
150
+ if (value && typeof value === 'object') {
151
+ const hasEnable = Object.prototype.hasOwnProperty.call(value, 'enable');
152
+ return {
153
+ ...defaults,
154
+ ...value,
155
+ enable: hasEnable ? Boolean(value.enable) : true,
156
+ };
157
+ }
158
+
159
+ return defaults;
160
+ }
161
+
162
+ _ensureElement(relatedTarget) {
163
+ if (this._minimizedElement && this._minimizedElement.isConnected) return this._minimizedElement;
164
+
165
+ const minimizeConfig = this._getConfig();
166
+ const container = VGModalMinimized.getContainer();
167
+ const button = document.createElement('button');
168
+ button.type = 'button';
169
+ button.className = CLASS_NAME_MINIMIZED_ITEM;
170
+ button.setAttribute('data-vg-modal-target', `#${this._element.id}`);
171
+
172
+ const title = document.createElement('span');
173
+ title.className = `${CLASS_NAME_MINIMIZED_ITEM}__title`;
174
+ title.textContent = this._getTitle(minimizeConfig);
175
+ button.append(title);
176
+
177
+ const text = String(minimizeConfig.text || '').trim();
178
+ if (text) {
179
+ const description = document.createElement('span');
180
+ description.className = `${CLASS_NAME_MINIMIZED_ITEM}__text`;
181
+ description.textContent = text;
182
+ button.append(description);
183
+ }
184
+
185
+ EventHandler.on(button, `click.${NAME_KEY}.restore`, (event) => {
186
+ event.preventDefault();
187
+ this.restore(button || relatedTarget);
188
+ });
189
+
190
+ container.append(button);
191
+ this._minimizedElement = button;
192
+ return button;
193
+ }
194
+
195
+ _getTitle(minimizeConfig) {
196
+ const configuredTitle = String(minimizeConfig.title || '').trim();
197
+ if (configuredTitle) return configuredTitle;
198
+
199
+ const titleElement = Selectors.find('.vg-modal-title, .vg-modal-chat__name, [data-vg-modal-title]', this._element);
200
+ const title = String(titleElement ? titleElement.textContent : '').trim();
201
+ return title || this._element.id || 'Modal';
202
+ }
203
+
204
+ removeElement() {
205
+ if (this._minimizedElement) {
206
+ this._minimizedElement.remove();
207
+ this._minimizedElement = null;
208
+ }
209
+
210
+ const container = Selectors.find(`.${CLASS_NAME_MINIMIZED_CONTAINER}`);
211
+ if (container && !container.children.length) {
212
+ container.remove();
213
+ }
214
+ }
215
+
216
+ static getContainer() {
217
+ let container = Selectors.find(`.${CLASS_NAME_MINIMIZED_CONTAINER}`);
218
+ if (container) return container;
219
+
220
+ container = document.createElement('div');
221
+ container.className = CLASS_NAME_MINIMIZED_CONTAINER;
222
+ document.body.append(container);
223
+ return container;
224
+ }
225
+ }
226
+
227
+ export default VGModalMinimized;
@@ -91,10 +91,74 @@
91
91
  border: 0;
92
92
  border-radius: 0;
93
93
  }
94
- }
95
- }
96
-
97
- @media screen and (min-width: 576px){
94
+ }
95
+ }
96
+
97
+ .vg-btn-minimize {
98
+ position: absolute;
99
+ width: 30px;
100
+ height: 30px;
101
+ right: 55px;
102
+ top: 15px;
103
+ border: none;
104
+ background: transparent;
105
+ border-radius: 50%;
106
+ padding: 0;
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ cursor: pointer;
111
+ z-index: 2;
112
+
113
+ span {
114
+ width: 14px;
115
+ height: 2px;
116
+ background: currentColor;
117
+ display: block;
118
+ }
119
+ }
120
+
121
+ .vg-modal-minimized-container {
122
+ position: fixed;
123
+ right: 20px;
124
+ bottom: 20px;
125
+ z-index: calc(var(--vg-modal-z-index, #{$modal-index}) + 1);
126
+ display: flex;
127
+ flex-direction: column;
128
+ align-items: flex-end;
129
+ gap: 10px;
130
+ pointer-events: none;
131
+ }
132
+
133
+ .vg-modal-minimized {
134
+ min-width: 220px;
135
+ max-width: 320px;
136
+ border: var(--vg-modal-border-width) var(--vg-modal-border-style) var(--vg-modal-border-color);
137
+ border-radius: var(--vg-modal-border-radius);
138
+ background: var(--vg-modal-background-color);
139
+ color: var(--vg-modal-color);
140
+ box-shadow: var(--vg-modal-box-shadow);
141
+ padding: .75rem 1rem;
142
+ cursor: pointer;
143
+ pointer-events: auto;
144
+ text-align: left;
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: .25rem;
148
+
149
+ &__title {
150
+ font-weight: 600;
151
+ line-height: 1.2;
152
+ }
153
+
154
+ &__text {
155
+ font-size: .875rem;
156
+ opacity: .75;
157
+ line-height: 1.2;
158
+ }
159
+ }
160
+
161
+ @media screen and (min-width: 576px){
98
162
  .vg-modal {
99
163
  --vg-modal-margin: #{$modal-dialog-margin-y-sm-up};
100
164
  --vg-modal-box-shadow: #{$modal-content-box-shadow-sm-up};
@@ -49,38 +49,60 @@ class VGNav extends BaseModule {
49
49
  super(element);
50
50
 
51
51
  this._params = this._getParams(element, mergeDeepObject({
52
+ // Брейкпоинт, с которого навигация отображается в расширенном виде.
52
53
  breakpoint: 'lg',
54
+ // Направление меню: влияет на классы и поведение позиционирования.
53
55
  placement: 'horizontal',
56
+ // Включает открытие выпадающих пунктов при наведении на десктопе.
54
57
  hover: true,
58
+ // Включает ограничение высоты и overflow для списка внутри выпадающего меню.
59
+ dropListScroll: true,
60
+ // Настройки плавного переключения между соседними пунктами первого уровня.
55
61
  hoversmoothfirstlevel: {
62
+ // Включает режим плавного перехода без мгновенного закрытия соседнего дропа.
56
63
  enable: false,
64
+ // Ограничивает плавное переключение только горизонтальным движением курсора.
57
65
  horizontalOnly: true
58
66
  },
67
+ // Настройки анимации открытия и закрытия выпадающих пунктов.
59
68
  animation: {
69
+ // Включает анимацию переходов.
60
70
  enable: true,
71
+ // Длительность ожидания завершения анимации перед финальным состоянием.
61
72
  timeout: 700
62
73
  },
63
- toggle: '<span class="default"></span>',
64
- hamburger: {
65
- enable: true,
66
- always: false,
67
- title: '',
68
- body: null,
69
- target: '#sidebar-nav'
70
- },
71
- callbacks: {
72
- afterInit: noop,
73
- afterClick: noop,
74
- }
75
- }, params));
74
+ // HTML-разметка иконки-указателя для пунктов с выпадающим меню.
75
+ toggle: '<span class="default"></span>',
76
+ // Настройки кнопки-гамбургера для мобильной/адаптивной навигации.
77
+ hamburger: {
78
+ // Разрешает создавать и использовать кнопку-гамбургер.
79
+ enable: true,
80
+ // Принудительно показывает гамбургер всегда, независимо от брейкпоинта.
81
+ always: false,
82
+ // Текстовый заголовок рядом с иконкой гамбургера.
83
+ title: '',
84
+ // Пользовательская HTML-разметка тела гамбургера вместо стандартных линий.
85
+ body: null,
86
+ // CSS-селектор сайдбара, который открывает кнопка-гамбургер.
87
+ target: '#sidebar-nav'
88
+ },
89
+ // Пользовательские обработчики жизненного цикла навигации.
90
+ callbacks: {
91
+ // Вызывается после построения навигации.
92
+ afterInit: noop,
93
+ // Вызывается после клика по пункту навигации.
94
+ afterClick: noop,
95
+ }
96
+ }, params));
76
97
 
77
98
  this._classes = {
78
99
  hamburgerActive: 'vg-nav-hamburger-active',
79
100
  hamburgerAlways: 'vg-nav-hamburger-always',
80
- hamburger: 'vg-nav-hamburger',
81
- container: 'vg-nav-container',
82
- wrapper: 'vg-nav-wrapper',
83
- active: 'vg-nav-active',
101
+ hamburger: 'vg-nav-hamburger',
102
+ container: 'vg-nav-container',
103
+ wrapper: 'vg-nav-wrapper',
104
+ dropListScroll: 'vg-nav-drop-list-scroll',
105
+ active: 'vg-nav-active',
84
106
  expand: 'vg-nav-expand',
85
107
  cloned: 'vg-nav-cloned',
86
108
  hover: 'vg-nav-hover',
@@ -131,11 +153,12 @@ class VGNav extends BaseModule {
131
153
  let params = this._params,
132
154
  classes = this._classes;
133
155
 
134
- // Вешаем основные классы
135
- this._element.classList.add(classes.container);
136
- this._element.classList.add('vg-nav-' + params.placement);
137
-
138
- if (!params.hamburger.always) {
156
+ // Вешаем основные классы
157
+ this._element.classList.add(classes.container);
158
+ this._element.classList.add('vg-nav-' + params.placement);
159
+ this._element.classList.toggle(classes.dropListScroll, !!params.dropListScroll);
160
+
161
+ if (!params.hamburger.always) {
139
162
  if (!params.breakpoint) {
140
163
  this._element.classList.add(classes.expand);
141
164
  } else if (params.breakpoint !== false) {
@@ -70,15 +70,9 @@
70
70
  background-color: var(--vg-nav-drop-bg);
71
71
  overflow: visible;
72
72
 
73
- > .vg-nav-drop-list {
74
- max-height: var(--vg-nav-drop-max-height);
75
- overflow-y: var(--vg-nav-drop-overflow-y);
76
- overflow-x: var(--vg-nav-drop-overflow-x);
77
- }
78
-
79
- &:not(.show) {
80
- display: none;
81
- }
73
+ &:not(.show) {
74
+ display: none;
75
+ }
82
76
 
83
77
  &.fade {
84
78
  visibility: visible;
@@ -135,11 +129,25 @@
135
129
  }
136
130
  }
137
131
  }
138
- }
139
- }
140
-
141
- /** set placement **/
142
- @import "placement";
132
+ }
133
+ }
134
+
135
+ &.vg-nav-drop-list-scroll {
136
+ .vg-nav-wrapper {
137
+ .dropdown {
138
+ .dropdown-content {
139
+ > .vg-nav-drop-list {
140
+ max-height: var(--vg-nav-drop-max-height);
141
+ overflow-y: var(--vg-nav-drop-overflow-y);
142
+ overflow-x: var(--vg-nav-drop-overflow-x);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ /** set placement **/
150
+ @import "placement";
143
151
 
144
152
  /** set hamburger **/
145
153
  @import "hamburger";
@@ -69,16 +69,18 @@ const nestable = VGNestable.getOrCreateInstance("#myNestable", {
69
69
  Значения по умолчанию:
70
70
 
71
71
  ```js
72
- {
73
- listselector: ".vg-nestable-list",
72
+ {
73
+ disabled: false,
74
+ disabledattribute: "data-disabled",
75
+ listselector: ".vg-nestable-list",
74
76
  itemselector: ".vg-nestable-item",
75
77
  handleselector: ".vg-nestable-handle",
76
78
  idattribute: "data-id",
77
79
  childlistclass: "vg-nestable-list",
78
80
  handleicon: "",
79
- indent: 28,
81
+ indent: 18,
80
82
  maxdepth: 6,
81
- hoverthreshold: 0.18,
83
+ hoverthreshold: 0.25,
82
84
  neighborchangethreshold: 0,
83
85
  showplaceholder: true,
84
86
  group: "",
@@ -117,22 +119,49 @@ const nestable = VGNestable.getOrCreateInstance("#myNestable", {
117
119
  }
118
120
  ```
119
121
 
120
- ### Пояснение параметров
121
-
122
- - `listselector`: селектор корневого/вложенных списков.
122
+ ### Пояснение параметров
123
+
124
+ - `disabled`: полностью отключает изменение дерева мышью, touch и клавиатурой. Также читается из `data-disabled="true"` корневого элемента.
125
+ - `disabledattribute`: атрибут блокировки отдельных элементов и вложенных списков (`data-disabled` по умолчанию).
126
+ - `listselector`: селектор корневого/вложенных списков.
123
127
  - `itemselector`: селектор sortable-элемента.
124
128
  - `handleselector`: селектор зоны, за которую можно начинать drag.
125
129
  - `idattribute`: атрибут, из которого берется `id` в `serialize()`.
126
130
  - `childlistclass`: классы для автосозданного дочернего списка.
127
131
  - `handleicon`: HTML/SVG иконки хэндла (проходит SVG-санитизацию).
128
- - `indent`: смещение по X (px), после которого режим дропа переключается в вложение (`child`).
132
+ - `indent`: ширина одного горизонтального шага вложенности. Каждый следующий шаг вправо переносит элемент ещё на один доступный уровень под предыдущий элемент.
129
133
  - `maxdepth`: максимальная глубина дерева.
130
- - `hoverthreshold`: вертикальный порог (0.05..0.45 фактически), определяет зоны `before/after/keep`.
134
+ - `hoverthreshold`: половина высоты центральной зоны (0.05..0.45), используемой для вложения; выше и ниже неё находятся зоны `before/after`.
131
135
  - `neighborchangethreshold`: порог в процентах (0..49), альтернативная логика смены позиции по краям элемента.
132
136
  - `showplaceholder`: показывать/скрывать placeholder во время drag.
133
137
  - `group`: имя группы списков для межспискового dnd.
134
138
  - `connect`: включить связь списков внутри группы.
135
- - `accept(item, sourceInstance, targetInstance)`: функция-фильтр разрешения дропа в target.
139
+ - `accept(item, sourceInstance, targetInstance)`: функция-фильтр разрешения дропа в target.
140
+
141
+ ### Блокировка перетаскивания
142
+
143
+ Заблокировать всё дерево, в том числе запретить перенос в него из связанного списка:
144
+
145
+ ```html
146
+ <div class="vg-nestable" data-vg-toggle="nestable" data-disabled="true">
147
+ <ol class="vg-nestable-list">...</ol>
148
+ </div>
149
+ ```
150
+
151
+ Заблокировать только конкретный элемент. Другие элементы по-прежнему могут вставляться до/после него, поэтому его индекс в результате может измениться:
152
+
153
+ ```html
154
+ <li class="vg-nestable-item" data-id="2" data-disabled="true">...</li>
155
+ ```
156
+
157
+ Заблокировать внутреннюю группу. Её элементы нельзя переносить отдельно или добавлять в эту группу, но родительскую ветку можно перемещать целиком:
158
+
159
+ ```html
160
+ <li class="vg-nestable-item" data-id="1">
161
+ <div class="vg-nestable-inner">...</div>
162
+ <ol class="vg-nestable-list" data-disabled="true">...</ol>
163
+ </li>
164
+ ```
136
165
 
137
166
  #### `collapse`
138
167