@sequent-org/moodboard 1.4.37 → 1.4.38
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
package/src/ui/Toolbar.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ReactionsPopupController } from './toolbar/ReactionsPopupController.js'
|
|
|
10
10
|
import { ToolbarTooltipController } from './toolbar/ToolbarTooltipController.js';
|
|
11
11
|
import { ToolbarStateController } from './toolbar/ToolbarStateController.js';
|
|
12
12
|
import { ToolbarRenderer } from './toolbar/ToolbarRenderer.js';
|
|
13
|
+
import { ToolbarResponsiveController } from './toolbar/ToolbarResponsiveController.js';
|
|
13
14
|
|
|
14
15
|
export class Toolbar {
|
|
15
16
|
constructor(container, eventBus, theme = 'light', options = {}) {
|
|
@@ -33,6 +34,7 @@ export class Toolbar {
|
|
|
33
34
|
this.tooltipController = new ToolbarTooltipController(this);
|
|
34
35
|
this.stateController = new ToolbarStateController(this);
|
|
35
36
|
this.renderer = new ToolbarRenderer(this);
|
|
37
|
+
this.responsiveController = new ToolbarResponsiveController(this);
|
|
36
38
|
|
|
37
39
|
this.init();
|
|
38
40
|
}
|
|
@@ -57,6 +59,7 @@ export class Toolbar {
|
|
|
57
59
|
this.createToolbar();
|
|
58
60
|
this.attachEvents();
|
|
59
61
|
this.setupHistoryEvents();
|
|
62
|
+
this.responsiveController.attach();
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
/**
|
|
@@ -353,6 +356,11 @@ export class Toolbar {
|
|
|
353
356
|
* Очистка ресурсов
|
|
354
357
|
*/
|
|
355
358
|
destroy() {
|
|
359
|
+
if (this.responsiveController) {
|
|
360
|
+
this.responsiveController.destroy();
|
|
361
|
+
this.responsiveController = null;
|
|
362
|
+
}
|
|
363
|
+
|
|
356
364
|
// Удаляем document-level listener (предотвращение утечки памяти)
|
|
357
365
|
if (this._documentClickHandler) {
|
|
358
366
|
document.removeEventListener('click', this._documentClickHandler);
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/* Toolbar (split from workspace.css) */
|
|
2
2
|
.moodboard-workspace__toolbar {
|
|
3
3
|
position: absolute;
|
|
4
|
-
top:
|
|
4
|
+
top: 0;
|
|
5
|
+
bottom: 0;
|
|
5
6
|
left: 16px;
|
|
6
|
-
transform: translateY(-50%);
|
|
7
7
|
z-index: 3000;
|
|
8
8
|
pointer-events: none;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
align-items: flex-start;
|
|
13
|
+
box-sizing: border-box;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
.moodboard-toolbar {
|
|
@@ -68,8 +73,12 @@
|
|
|
68
73
|
.moodboard-toolbar__button--undo:hover:not(:disabled),
|
|
69
74
|
.moodboard-toolbar__button--redo:hover:not(:disabled) { background: #80D8FF !important; }
|
|
70
75
|
|
|
76
|
+
/* Overflow button hover — matching toolbar palette */
|
|
77
|
+
.moodboard-toolbar__button--overflow:hover { background: #80D8FF !important; }
|
|
78
|
+
|
|
71
79
|
/* Popups next to toolbar */
|
|
72
80
|
.moodboard-toolbar__popup { position: absolute; top: 0; left: 64px; background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; box-shadow: 0 12px 28px rgba(0,0,0,0.18); z-index: 3200; padding: 8px; pointer-events: auto; }
|
|
81
|
+
.moodboard-toolbar__popup--overflow { flex-direction: column; align-items: center; gap: 8px; min-width: 46px; }
|
|
73
82
|
.moodboard-toolbar__popup--shapes { width: auto; }
|
|
74
83
|
.moodboard-toolbar__popup--draw { width: auto; padding: 10px; }
|
|
75
84
|
.moodboard-toolbar__popup--emoji { width: 360px; max-height: 420px; overflow: auto; padding: 8px 10px; }
|
|
@@ -740,11 +740,16 @@
|
|
|
740
740
|
/* Toolbar Container */
|
|
741
741
|
.moodboard-workspace__toolbar {
|
|
742
742
|
position: absolute;
|
|
743
|
-
top:
|
|
743
|
+
top: 0;
|
|
744
|
+
bottom: 0;
|
|
744
745
|
left: 16px;
|
|
745
|
-
transform: translateY(-50%);
|
|
746
746
|
z-index: 3000;
|
|
747
|
-
pointer-events: none;
|
|
747
|
+
pointer-events: none;
|
|
748
|
+
display: flex;
|
|
749
|
+
flex-direction: column;
|
|
750
|
+
justify-content: center;
|
|
751
|
+
align-items: flex-start;
|
|
752
|
+
box-sizing: border-box;
|
|
748
753
|
}
|
|
749
754
|
|
|
750
755
|
.moodboard-workspace__topbar {
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Управляет прогрессивным схлопыванием кнопок тулбара в кнопку «Ещё»,
|
|
3
|
+
* когда высота экрана не позволяет отобразить все инструменты.
|
|
4
|
+
*
|
|
5
|
+
* Алгоритм: физический перенос DOM-узлов (не копии) — все handlers/попапы
|
|
6
|
+
* кнопок сохраняются без переподключения.
|
|
7
|
+
*/
|
|
8
|
+
export class ToolbarResponsiveController {
|
|
9
|
+
constructor(toolbar) {
|
|
10
|
+
this.toolbar = toolbar;
|
|
11
|
+
this._observer = null;
|
|
12
|
+
this._overflowBtn = null;
|
|
13
|
+
this._overflowMenu = null;
|
|
14
|
+
/** @type {Element[]} все схлопываемые элементы в исходном порядке */
|
|
15
|
+
this._collapsibleOrder = [];
|
|
16
|
+
this._documentClickHandler = null;
|
|
17
|
+
this._rafId = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
attach() {
|
|
21
|
+
this._buildCollapsibleOrder();
|
|
22
|
+
this._createOverflowButton();
|
|
23
|
+
this._createOverflowMenu();
|
|
24
|
+
|
|
25
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
26
|
+
this._observer = new ResizeObserver(() => {
|
|
27
|
+
if (this._rafId != null) return;
|
|
28
|
+
const schedule = typeof requestAnimationFrame !== 'undefined'
|
|
29
|
+
? requestAnimationFrame
|
|
30
|
+
: (fn) => setTimeout(fn, 16);
|
|
31
|
+
this._rafId = schedule(() => { this._rafId = null; this.recompute(); });
|
|
32
|
+
});
|
|
33
|
+
this._observer.observe(this.toolbar.container);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.recompute();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Строим список схлопываемых элементов один раз (всё кроме select). */
|
|
40
|
+
_buildCollapsibleOrder() {
|
|
41
|
+
const el = this.toolbar.element;
|
|
42
|
+
if (!el) return;
|
|
43
|
+
let pastSelect = false;
|
|
44
|
+
for (const child of el.children) {
|
|
45
|
+
if (child.dataset && child.dataset.toolId === 'select') {
|
|
46
|
+
pastSelect = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (pastSelect) this._collapsibleOrder.push(child);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_createOverflowButton() {
|
|
54
|
+
this._overflowBtn = document.createElement('button');
|
|
55
|
+
this._overflowBtn.className = 'moodboard-toolbar__button moodboard-toolbar__button--overflow';
|
|
56
|
+
this._overflowBtn.dataset.tool = 'overflow';
|
|
57
|
+
this._overflowBtn.dataset.toolId = 'overflow';
|
|
58
|
+
|
|
59
|
+
// Иконка «three dots vertical» (lucide MoreVertical, inline SVG)
|
|
60
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
61
|
+
const svg = document.createElementNS(NS, 'svg');
|
|
62
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
63
|
+
svg.setAttribute('width', '20');
|
|
64
|
+
svg.setAttribute('height', '20');
|
|
65
|
+
svg.setAttribute('fill', 'none');
|
|
66
|
+
svg.setAttribute('stroke', 'currentColor');
|
|
67
|
+
svg.setAttribute('stroke-width', '2');
|
|
68
|
+
svg.setAttribute('stroke-linecap', 'round');
|
|
69
|
+
svg.setAttribute('stroke-linejoin', 'round');
|
|
70
|
+
svg.style.display = 'block';
|
|
71
|
+
for (const cy of ['5', '12', '19']) {
|
|
72
|
+
const c = document.createElementNS(NS, 'circle');
|
|
73
|
+
c.setAttribute('cx', '12');
|
|
74
|
+
c.setAttribute('cy', cy);
|
|
75
|
+
c.setAttribute('r', '1');
|
|
76
|
+
svg.appendChild(c);
|
|
77
|
+
}
|
|
78
|
+
this._overflowBtn.appendChild(svg);
|
|
79
|
+
|
|
80
|
+
this.toolbar.createTooltip(this._overflowBtn, 'Ещё');
|
|
81
|
+
|
|
82
|
+
this._overflowBtn.addEventListener('click', (e) => {
|
|
83
|
+
e.stopPropagation();
|
|
84
|
+
this._toggleMenu();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this._overflowBtn.style.display = 'none';
|
|
88
|
+
this.toolbar.element.appendChild(this._overflowBtn);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_createOverflowMenu() {
|
|
92
|
+
this._overflowMenu = document.createElement('div');
|
|
93
|
+
this._overflowMenu.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--overflow';
|
|
94
|
+
this._overflowMenu.style.display = 'none';
|
|
95
|
+
this.toolbar.container.appendChild(this._overflowMenu);
|
|
96
|
+
|
|
97
|
+
// Клики по кнопкам внутри меню — маршрутизируем через actionRouter
|
|
98
|
+
this._overflowMenu.addEventListener('click', (e) => {
|
|
99
|
+
const button = e.target.closest('.moodboard-toolbar__button');
|
|
100
|
+
if (!button || button.disabled) return;
|
|
101
|
+
this.toolbar.actionRouter.routeToolbarAction(button, button.dataset.tool, button.dataset.toolId);
|
|
102
|
+
setTimeout(() => this._closeMenu(), 50);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
this._documentClickHandler = (e) => {
|
|
106
|
+
if (!this._overflowMenu || this._overflowMenu.style.display === 'none') return;
|
|
107
|
+
const inMenu = this._overflowMenu.contains(e.target);
|
|
108
|
+
const onBtn = this._overflowBtn && this._overflowBtn.contains(e.target);
|
|
109
|
+
if (!inMenu && !onBtn) this._closeMenu();
|
|
110
|
+
};
|
|
111
|
+
document.addEventListener('click', this._documentClickHandler);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_toggleMenu() {
|
|
115
|
+
if (this._overflowMenu.style.display === 'none') this._openMenu();
|
|
116
|
+
else this._closeMenu();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_openMenu() {
|
|
120
|
+
this._positionMenu();
|
|
121
|
+
this._overflowMenu.style.display = 'flex';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_closeMenu() {
|
|
125
|
+
if (this._overflowMenu) this._overflowMenu.style.display = 'none';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_positionMenu() {
|
|
129
|
+
const btnRect = this._overflowBtn.getBoundingClientRect();
|
|
130
|
+
const containerRect = this.toolbar.container.getBoundingClientRect();
|
|
131
|
+
const left = Math.round(this.toolbar.element.offsetWidth + 8);
|
|
132
|
+
const top = Math.round(btnRect.top - containerRect.top);
|
|
133
|
+
this._overflowMenu.style.left = `${left}px`;
|
|
134
|
+
this._overflowMenu.style.top = `${top}px`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Расстояние от верха контейнера до зоны, где можно рисовать тулбар (ниже топбара + 16px). */
|
|
138
|
+
_getTopReserve() {
|
|
139
|
+
const container = this.toolbar.container;
|
|
140
|
+
const cRect = container.getBoundingClientRect();
|
|
141
|
+
const workspace = container.parentElement;
|
|
142
|
+
if (!workspace) return 72;
|
|
143
|
+
|
|
144
|
+
const topbar = workspace.querySelector('.moodboard-topbar');
|
|
145
|
+
if (topbar) {
|
|
146
|
+
const r = topbar.getBoundingClientRect();
|
|
147
|
+
return Math.round(r.bottom - cRect.top) + 16;
|
|
148
|
+
}
|
|
149
|
+
const topbarWrapper = workspace.querySelector('.moodboard-workspace__topbar');
|
|
150
|
+
if (topbarWrapper) {
|
|
151
|
+
let maxBottom = cRect.top;
|
|
152
|
+
for (const child of topbarWrapper.children) {
|
|
153
|
+
const r = child.getBoundingClientRect();
|
|
154
|
+
if (r.bottom > maxBottom) maxBottom = r.bottom;
|
|
155
|
+
}
|
|
156
|
+
if (maxBottom > cRect.top + 10) return Math.round(maxBottom - cRect.top) + 16;
|
|
157
|
+
}
|
|
158
|
+
return 72;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Расстояние от низа контейнера до зоны, где можно рисовать тулбар (выше чата + 16px). */
|
|
162
|
+
_getBottomReserve() {
|
|
163
|
+
const container = this.toolbar.container;
|
|
164
|
+
const cRect = container.getBoundingClientRect();
|
|
165
|
+
const workspace = container.parentElement;
|
|
166
|
+
if (!workspace) return 64;
|
|
167
|
+
|
|
168
|
+
const chatEl = workspace.querySelector('.moodboard-chat');
|
|
169
|
+
if (chatEl) {
|
|
170
|
+
const r = chatEl.getBoundingClientRect();
|
|
171
|
+
return Math.round(cRect.bottom - r.top) + 16;
|
|
172
|
+
}
|
|
173
|
+
return 64;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
recompute() {
|
|
177
|
+
if (!this.toolbar.element || !this._overflowBtn) return;
|
|
178
|
+
const el = this.toolbar.element;
|
|
179
|
+
|
|
180
|
+
// 1. Возвращаем все схлопнутые элементы обратно в тулбар (в исходном порядке)
|
|
181
|
+
for (const item of this._collapsibleOrder) {
|
|
182
|
+
if (item.parentNode !== el) el.insertBefore(item, this._overflowBtn);
|
|
183
|
+
}
|
|
184
|
+
// Убеждаемся, что overflow-кнопка стоит последней
|
|
185
|
+
el.appendChild(this._overflowBtn);
|
|
186
|
+
this._overflowBtn.style.display = 'none';
|
|
187
|
+
|
|
188
|
+
// Восстанавливаем видимость разделителей
|
|
189
|
+
for (const item of this._collapsibleOrder) {
|
|
190
|
+
if (item.classList.contains('moodboard-toolbar__divider')) item.style.display = '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 2. Вычисляем доступную высоту и обновляем отступы контейнера
|
|
194
|
+
const topReserve = this._getTopReserve();
|
|
195
|
+
const bottomReserve = this._getBottomReserve();
|
|
196
|
+
this.toolbar.container.style.paddingTop = `${topReserve}px`;
|
|
197
|
+
this.toolbar.container.style.paddingBottom = `${bottomReserve}px`;
|
|
198
|
+
const availableHeight = Math.max(60, this.toolbar.container.clientHeight - topReserve - bottomReserve);
|
|
199
|
+
|
|
200
|
+
// 3. Схлопываем снизу вверх, пока тулбар не влезет
|
|
201
|
+
// Используем scrollHeight (естественная высота контента) а не offsetHeight,
|
|
202
|
+
// т.к. flex-shrink может сжать элемент до доступной высоты, скрыв реальное переполнение.
|
|
203
|
+
let overflowCount = 0;
|
|
204
|
+
while (overflowCount < this._collapsibleOrder.length && el.scrollHeight > availableHeight) {
|
|
205
|
+
const idx = this._collapsibleOrder.length - 1 - overflowCount;
|
|
206
|
+
if (idx < 0) break;
|
|
207
|
+
const item = this._collapsibleOrder[idx];
|
|
208
|
+
// Вставляем в начало меню → в меню порядок сверху вниз совпадает с исходным
|
|
209
|
+
this._overflowMenu.insertBefore(item, this._overflowMenu.firstChild);
|
|
210
|
+
overflowCount++;
|
|
211
|
+
this._overflowBtn.style.display = '';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 4. Скрываем висячие разделители в конце видимого набора
|
|
215
|
+
this._hideTrailingDividers();
|
|
216
|
+
|
|
217
|
+
// 5. Обновляем позицию меню если оно открыто
|
|
218
|
+
if (this._overflowMenu.style.display !== 'none') this._positionMenu();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_hideTrailingDividers() {
|
|
222
|
+
const el = this.toolbar.element;
|
|
223
|
+
const children = Array.from(el.children).filter(c => c !== this._overflowBtn);
|
|
224
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
225
|
+
if (children[i].classList.contains('moodboard-toolbar__divider')) {
|
|
226
|
+
children[i].style.display = 'none';
|
|
227
|
+
} else {
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
destroy() {
|
|
234
|
+
if (this._rafId != null) {
|
|
235
|
+
if (typeof cancelAnimationFrame !== 'undefined') cancelAnimationFrame(this._rafId);
|
|
236
|
+
else clearTimeout(this._rafId);
|
|
237
|
+
this._rafId = null;
|
|
238
|
+
}
|
|
239
|
+
if (this._observer) {
|
|
240
|
+
this._observer.disconnect();
|
|
241
|
+
this._observer = null;
|
|
242
|
+
}
|
|
243
|
+
if (this._documentClickHandler) {
|
|
244
|
+
document.removeEventListener('click', this._documentClickHandler);
|
|
245
|
+
this._documentClickHandler = null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Возвращаем схлопнутые элементы в тулбар перед удалением меню
|
|
249
|
+
if (this.toolbar.element && this._overflowMenu) {
|
|
250
|
+
for (const item of this._collapsibleOrder) {
|
|
251
|
+
if (item.parentNode === this._overflowMenu) {
|
|
252
|
+
this._overflowMenu.removeChild(item);
|
|
253
|
+
this.toolbar.element.appendChild(item);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (this._overflowMenu) { this._overflowMenu.remove(); this._overflowMenu = null; }
|
|
259
|
+
if (this._overflowBtn) { this._overflowBtn.remove(); this._overflowBtn = null; }
|
|
260
|
+
|
|
261
|
+
// Сбрасываем отступы контейнера
|
|
262
|
+
if (this.toolbar.container) {
|
|
263
|
+
this.toolbar.container.style.paddingTop = '';
|
|
264
|
+
this.toolbar.container.style.paddingBottom = '';
|
|
265
|
+
}
|
|
266
|
+
this._collapsibleOrder = [];
|
|
267
|
+
}
|
|
268
|
+
}
|