@sequent-org/moodboard 1.4.31 → 1.4.33
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 +5 -1
- package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/src/assets/icons/attachments.svg +3 -1
- package/src/assets/icons/comments.svg +2 -2
- package/src/assets/icons/connector.svg +6 -0
- package/src/assets/icons/emoji.svg +6 -1
- package/src/assets/icons/frame.svg +4 -1
- package/src/assets/icons/image.svg +5 -1
- package/src/assets/icons/laser.svg +1 -0
- package/src/assets/icons/lasso.svg +5 -0
- package/src/assets/icons/mindmap.svg +10 -2
- package/src/assets/icons/note.svg +4 -1
- package/src/assets/icons/pan.svg +5 -2
- package/src/assets/icons/pencil.svg +4 -1
- package/src/assets/icons/reactions.svg +5 -0
- package/src/assets/icons/redo.svg +3 -2
- package/src/assets/icons/select.svg +2 -8
- package/src/assets/icons/shapes.svg +5 -1
- package/src/assets/icons/text-add.svg +15 -1
- package/src/assets/icons/undo.svg +3 -2
- package/src/assets/reactions/1f44d.svg +20 -0
- package/src/assets/reactions/1f44e.svg +20 -0
- package/src/assets/reactions/2705.svg +20 -0
- package/src/assets/reactions/274c.svg +19 -0
- package/src/assets/reactions/2753.svg +20 -0
- package/src/assets/reactions/2764.svg +22 -0
- package/src/assets/reactions/2b50.svg +19 -0
- package/src/assets/reactions/plus-one.svg +25 -0
- package/src/core/PixiEngine.js +23 -0
- package/src/core/bootstrap/CoreInitializer.js +43 -0
- package/src/core/commands/GroupDeleteCommand.js +13 -1
- package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
- package/src/core/commands/UpdateTextStyleCommand.js +17 -6
- package/src/core/commands/index.js +3 -0
- package/src/core/events/Events.js +22 -0
- package/src/core/flows/LayerAndViewportFlow.js +1 -0
- package/src/core/flows/ObjectLifecycleFlow.js +155 -7
- package/src/core/index.js +28 -1
- package/src/grid/CrossGridZoomPhases.js +3 -3
- package/src/initNoBundler.js +1 -1
- package/src/moodboard/DataManager.js +28 -0
- package/src/moodboard/MoodBoard.js +27 -0
- package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
- package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
- package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
- package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
- package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
- package/src/objects/ConnectorObject.js +2 -2
- package/src/objects/FrameObject.js +119 -59
- package/src/objects/ShapeObject.js +49 -74
- package/src/objects/shape/ShapeDrawer.js +210 -0
- package/src/services/ConnectorBindingResolver.js +112 -0
- package/src/services/ConnectorRouter.js +210 -0
- package/src/services/comments/CommentService.js +344 -0
- package/src/tools/object-tools/CommentTool.js +85 -0
- package/src/tools/object-tools/DrawingTool.js +110 -10
- package/src/tools/object-tools/LaserPointerTool.js +121 -0
- package/src/tools/object-tools/SelectTool.js +25 -1
- package/src/tools/object-tools/TextTool.js +6 -1
- package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
- package/src/tools/object-tools/connector/connectorGesture.js +33 -19
- package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
- package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
- package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
- package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
- package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
- package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
- package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
- package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
- package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
- package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
- package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
- package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
- package/src/ui/CommentPopover.js +6 -0
- package/src/ui/CommentsBar.js +91 -0
- package/src/ui/ConnectorPropertiesPanel.js +150 -0
- package/src/ui/ContextMenu.js +25 -0
- package/src/ui/DrawingPropertiesPanel.js +362 -0
- package/src/ui/FilePropertiesPanel.js +5 -0
- package/src/ui/FramePropertiesPanel.js +5 -0
- package/src/ui/HtmlTextLayer.js +246 -66
- package/src/ui/NotePropertiesPanel.js +6 -0
- package/src/ui/ShapePropertiesPanel.js +307 -0
- package/src/ui/TextPropertiesPanel.js +100 -1
- package/src/ui/Toolbar.js +25 -2
- package/src/ui/Topbar.js +2 -2
- package/src/ui/animation/HoverLiftController.js +6 -7
- package/src/ui/chat/ChatComposer.js +59 -12
- package/src/ui/chat/ChatExtendedPromptModal.js +1 -12
- package/src/ui/chat/ChatWindow.js +60 -144
- package/src/ui/chat/ChatWindowRenderer.js +1 -8
- package/src/ui/chat/icons.js +0 -4
- package/src/ui/comments/CommentListPanel.js +213 -0
- package/src/ui/comments/CommentPinLayer.js +448 -0
- package/src/ui/comments/CommentThreadPopover.js +539 -0
- package/src/ui/comments/commentFormat.js +32 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
- package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
- package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
- package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
- package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
- package/src/ui/connectors/ConnectorLayer.js +264 -57
- package/src/ui/handles/HandlesDomRenderer.js +5 -13
- package/src/ui/handles/HandlesEventBridge.js +1 -0
- package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
- package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
- package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
- package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
- package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
- package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
- package/src/ui/styles/chat.css +682 -28
- package/src/ui/styles/index.css +1 -0
- package/src/ui/styles/panels.css +112 -2
- package/src/ui/styles/shape-properties-panel.css +250 -0
- package/src/ui/styles/toolbar.css +7 -2
- package/src/ui/styles/topbar.css +1 -1
- package/src/ui/styles/workspace.css +257 -6
- package/src/ui/text-properties/TextFormatControls.js +88 -0
- package/src/ui/text-properties/TextListRenderer.js +137 -0
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
- package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
- package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
- package/src/ui/toolbar/ReactionsPopupController.js +88 -0
- package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
- package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
- package/src/ui/toolbar/ToolbarRenderer.js +9 -1
- package/src/ui/toolbar/ToolbarStateController.js +4 -1
- package/src/utils/iconLoader.js +17 -16
- package/src/utils/markdown.js +14 -0
- package/src/utils/richText.js +125 -0
|
@@ -146,10 +146,11 @@ export function createTextEditorFinalize(controller, {
|
|
|
146
146
|
});
|
|
147
147
|
} else {
|
|
148
148
|
if (isNewCreation) {
|
|
149
|
-
|
|
150
|
-
controller.eventBus.emit(Events.Object.
|
|
149
|
+
const oldContent = typeof initialContent === 'string' ? initialContent : '';
|
|
150
|
+
controller.eventBus.emit(Events.Object.ContentChange, {
|
|
151
151
|
objectId,
|
|
152
|
-
|
|
152
|
+
oldContent,
|
|
153
|
+
newContent: value,
|
|
153
154
|
});
|
|
154
155
|
} else {
|
|
155
156
|
const oldContent = typeof initialContent === 'string' ? initialContent : '';
|
|
@@ -167,9 +168,11 @@ export function bindTextEditorInteractions(controller, {
|
|
|
167
168
|
textarea,
|
|
168
169
|
isNewCreation,
|
|
169
170
|
isNote,
|
|
171
|
+
isShape,
|
|
170
172
|
autoSize,
|
|
171
173
|
updateNoteEditor,
|
|
172
174
|
finalize,
|
|
175
|
+
listType,
|
|
173
176
|
}) {
|
|
174
177
|
const blurHandler = () => {
|
|
175
178
|
const editorObjectId = controller?.textEditor?.objectId || null;
|
|
@@ -190,18 +193,48 @@ export function bindTextEditorInteractions(controller, {
|
|
|
190
193
|
};
|
|
191
194
|
|
|
192
195
|
const keydownHandler = (e) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
+
const isList = listType && listType !== 'none';
|
|
197
|
+
if (e.key === 'Enter') {
|
|
198
|
+
if (isList && !e.shiftKey) {
|
|
199
|
+
e.preventDefault();
|
|
200
|
+
const start = textarea.selectionStart;
|
|
201
|
+
const end = textarea.selectionEnd;
|
|
202
|
+
const value = textarea.value;
|
|
203
|
+
const lineStart = value.lastIndexOf('\n', start - 1) + 1;
|
|
204
|
+
const lineEnd = value.indexOf('\n', start);
|
|
205
|
+
const curLineEnd = lineEnd === -1 ? value.length : lineEnd;
|
|
206
|
+
const currentLine = value.slice(lineStart, curLineEnd);
|
|
207
|
+
if (currentLine.trim() === '' && start === end) {
|
|
208
|
+
finalize(true);
|
|
209
|
+
} else {
|
|
210
|
+
textarea.setRangeText('\n', start, end, 'end');
|
|
211
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
212
|
+
}
|
|
213
|
+
} else if (isList && e.shiftKey) {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
finalize(true);
|
|
216
|
+
} else if (!e.shiftKey) {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
finalize(true);
|
|
219
|
+
}
|
|
220
|
+
// Shift+Enter в режиме без списка: нативный перевод строки
|
|
196
221
|
} else if (e.key === 'Escape') {
|
|
197
222
|
e.preventDefault();
|
|
198
223
|
finalize(false);
|
|
199
224
|
}
|
|
200
225
|
};
|
|
201
226
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
227
|
+
// Записка и фигура имеют фиксированные границы — autoSize по вводу им не нужен.
|
|
228
|
+
// Для фигуры autoSize раздувал поле до minWBound (120px) и при textAlign:center
|
|
229
|
+
// уносил текст вправо от фигуры. Поэтому ввод в фигуре границы не меняет.
|
|
230
|
+
let inputHandler;
|
|
231
|
+
if (isNote) {
|
|
232
|
+
inputHandler = () => { try { if (updateNoteEditor) updateNoteEditor(); } catch (_) {} };
|
|
233
|
+
} else if (isShape) {
|
|
234
|
+
inputHandler = () => {};
|
|
235
|
+
} else {
|
|
236
|
+
inputHandler = autoSize;
|
|
237
|
+
}
|
|
205
238
|
|
|
206
239
|
textarea.addEventListener('blur', blurHandler);
|
|
207
240
|
textarea.addEventListener('keydown', keydownHandler);
|
|
@@ -292,10 +325,11 @@ export function closeTextEditorFromState(controller, commit) {
|
|
|
292
325
|
});
|
|
293
326
|
} else {
|
|
294
327
|
if (isNewCreation) {
|
|
295
|
-
|
|
296
|
-
controller.eventBus.emit(Events.Object.
|
|
328
|
+
const oldContent = typeof initialContent === 'string' ? initialContent : '';
|
|
329
|
+
controller.eventBus.emit(Events.Object.ContentChange, {
|
|
297
330
|
objectId,
|
|
298
|
-
|
|
331
|
+
oldContent,
|
|
332
|
+
newContent: value,
|
|
299
333
|
});
|
|
300
334
|
} else {
|
|
301
335
|
controller.eventBus.emit(Events.Object.ContentChange, {
|
|
@@ -95,6 +95,7 @@ export function registerNoteEditorSync(controller, { objectId, updateNoteEditor
|
|
|
95
95
|
const listeners = [
|
|
96
96
|
[Events.UI.ZoomPercent, onZoom],
|
|
97
97
|
[Events.Tool.PanUpdate, onPan],
|
|
98
|
+
[Events.Viewport.Changed, onPan],
|
|
98
99
|
[Events.Tool.DragUpdate, onDrag],
|
|
99
100
|
[Events.Tool.ResizeUpdate, onResize],
|
|
100
101
|
[Events.Tool.RotateUpdate, onRotate],
|
|
@@ -51,6 +51,7 @@ export function openTextEditor(object, create = false) {
|
|
|
51
51
|
|
|
52
52
|
// Определяем тип объекта
|
|
53
53
|
const isNote = objectType === 'note';
|
|
54
|
+
const isShape = objectType === 'shape';
|
|
54
55
|
|
|
55
56
|
// Проверяем, что position существует
|
|
56
57
|
if (!position) {
|
|
@@ -156,6 +157,9 @@ export function openTextEditor(object, create = false) {
|
|
|
156
157
|
|
|
157
158
|
// Вычисляем межстрочный интервал; подгоняем к реальным значениям HTML-отображения
|
|
158
159
|
let lhInitial = computeTextEditorLineHeightPx(effectiveFontPx);
|
|
160
|
+
if (typeof properties.lineHeight === 'number') {
|
|
161
|
+
lhInitial = Math.round(effectiveFontPx * properties.lineHeight);
|
|
162
|
+
}
|
|
159
163
|
try {
|
|
160
164
|
if (objectId) {
|
|
161
165
|
if (objectType === 'note') {
|
|
@@ -207,6 +211,15 @@ export function openTextEditor(object, create = false) {
|
|
|
207
211
|
effectiveFontPx,
|
|
208
212
|
lineHeightPx: lhInitial,
|
|
209
213
|
});
|
|
214
|
+
if (properties.textAlign) {
|
|
215
|
+
textarea.style.textAlign = properties.textAlign;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Shape: текст по центру, textarea покрывает весь bounds фигуры
|
|
219
|
+
if (isShape) {
|
|
220
|
+
textarea.style.textAlign = 'center';
|
|
221
|
+
textarea.placeholder = '';
|
|
222
|
+
}
|
|
210
223
|
|
|
211
224
|
wrapper.appendChild(textarea);
|
|
212
225
|
// Убрана зелёная рамка вокруг поля ввода по требованию
|
|
@@ -311,6 +324,49 @@ export function openTextEditor(object, create = false) {
|
|
|
311
324
|
toScreen,
|
|
312
325
|
});
|
|
313
326
|
updateNoteEditor = noteSetup.updateNoteEditor;
|
|
327
|
+
} else if (isShape) {
|
|
328
|
+
// Shape: редактор занимает весь bounds фигуры, текст вертикально центрирован
|
|
329
|
+
const viewResShape = (this.app?.renderer?.resolution) ||
|
|
330
|
+
(view.width && view.clientWidth ? view.width / view.clientWidth : 1);
|
|
331
|
+
const worldLayerShape = this.textEditor.world || this.app?.stage;
|
|
332
|
+
const sShape = worldLayerShape?.scale?.x || 1;
|
|
333
|
+
const sCssShape = sShape / viewResShape;
|
|
334
|
+
|
|
335
|
+
let shapeW = 100, shapeH = 100;
|
|
336
|
+
if (initialSize) {
|
|
337
|
+
shapeW = initialSize.width;
|
|
338
|
+
shapeH = initialSize.height;
|
|
339
|
+
} else if (objectId) {
|
|
340
|
+
const sizeData = { objectId, size: null };
|
|
341
|
+
this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
|
|
342
|
+
if (sizeData.size) { shapeW = sizeData.size.width; shapeH = sizeData.size.height; }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const shapeCssW = Math.max(1, Math.round(shapeW * sCssShape));
|
|
346
|
+
const shapeCssH = Math.max(1, Math.round(shapeH * sCssShape));
|
|
347
|
+
// Центрируем каретку по вертикали: padding-top = (высота фигуры - высота одной строки) / 2
|
|
348
|
+
const oneLinePx = Math.max(1, Math.round(effectiveFontPx * 1.4));
|
|
349
|
+
const paddingTopShape = Math.max(0, Math.round((shapeCssH - oneLinePx) / 2));
|
|
350
|
+
|
|
351
|
+
wrapper.style.left = `${Math.round(screenPos.x)}px`;
|
|
352
|
+
wrapper.style.top = `${Math.round(screenPos.y)}px`;
|
|
353
|
+
wrapper.style.width = `${shapeCssW}px`;
|
|
354
|
+
wrapper.style.height = `${shapeCssH}px`;
|
|
355
|
+
|
|
356
|
+
textarea.style.width = `${shapeCssW}px`;
|
|
357
|
+
textarea.style.height = `${shapeCssH}px`;
|
|
358
|
+
textarea.style.boxSizing = 'border-box';
|
|
359
|
+
// display:block убирает baseline-смещение inline-block textarea внутри wrapper'а
|
|
360
|
+
// (wrapper наследует line-height ~24px от body, из-за чего на сильном отдалении
|
|
361
|
+
// textarea съезжает вниз от фигуры на фиксированные ~11px независимо от зума).
|
|
362
|
+
// padding-bottom:0 перебивает CSS .moodboard-text-input (5px), чтобы не раздувать
|
|
363
|
+
// высоту поля относительно крошечной фигуры.
|
|
364
|
+
textarea.style.display = 'block';
|
|
365
|
+
textarea.style.paddingTop = `${paddingTopShape}px`;
|
|
366
|
+
textarea.style.paddingBottom = '0px';
|
|
367
|
+
|
|
368
|
+
this.textEditor._cssLeftPx = Math.round(screenPos.x);
|
|
369
|
+
this.textEditor._cssTopPx = Math.round(screenPos.y);
|
|
314
370
|
} else {
|
|
315
371
|
const {
|
|
316
372
|
leftPx,
|
|
@@ -377,7 +433,7 @@ export function openTextEditor(object, create = false) {
|
|
|
377
433
|
}
|
|
378
434
|
|
|
379
435
|
// Если создаём новый текст — длина поля ровно как placeholder
|
|
380
|
-
if (create && !isNote) {
|
|
436
|
+
if (create && !isNote && !isShape) {
|
|
381
437
|
// +25% — запас на Caveat vs Arial: при незагруженном Caveat span рендерится в Arial,
|
|
382
438
|
// а Caveat (рукописный шрифт) заметно шире для кириллицы.
|
|
383
439
|
const startWidth = Math.max(1, Math.ceil(measureTextEditorPlaceholderWidth(textarea, 'Напишите что-нибудь') * 1.25));
|
|
@@ -391,8 +447,8 @@ export function openTextEditor(object, create = false) {
|
|
|
391
447
|
minHBound = startHeight;
|
|
392
448
|
}
|
|
393
449
|
|
|
394
|
-
// Для записок размеры уже установлены выше, пропускаем эту логику
|
|
395
|
-
if (!isNote && !create) {
|
|
450
|
+
// Для записок и фигур размеры уже установлены выше, пропускаем эту логику
|
|
451
|
+
if (!isNote && !isShape && !create) {
|
|
396
452
|
if (initialWpx) {
|
|
397
453
|
textarea.style.width = `${initialWpx}px`;
|
|
398
454
|
wrapper.style.width = `${initialWpx}px`;
|
|
@@ -403,7 +459,7 @@ export function openTextEditor(object, create = false) {
|
|
|
403
459
|
}
|
|
404
460
|
}
|
|
405
461
|
// Автоподгон
|
|
406
|
-
const syncRegularTextSizeToObject = !isNote && objectId
|
|
462
|
+
const syncRegularTextSizeToObject = !isNote && !isShape && objectId
|
|
407
463
|
? ({ widthPx, heightPx }) => {
|
|
408
464
|
try {
|
|
409
465
|
const scaleX = (worldLayerRef?.scale?.x) || 1;
|
|
@@ -428,8 +484,8 @@ export function openTextEditor(object, create = false) {
|
|
|
428
484
|
onSizeChange: syncRegularTextSizeToObject,
|
|
429
485
|
});
|
|
430
486
|
|
|
431
|
-
//
|
|
432
|
-
if (!isNote) {
|
|
487
|
+
// autoSize только для обычного текста; shape имеет фиксированные bounds фигуры
|
|
488
|
+
if (!isNote && !isShape) {
|
|
433
489
|
autoSize();
|
|
434
490
|
}
|
|
435
491
|
textarea.focus();
|
|
@@ -448,6 +504,7 @@ export function openTextEditor(object, create = false) {
|
|
|
448
504
|
position,
|
|
449
505
|
properties: { fontSize },
|
|
450
506
|
objectType,
|
|
507
|
+
listType: properties.listType || 'none',
|
|
451
508
|
_phStyle: styleEl,
|
|
452
509
|
initialContent: content,
|
|
453
510
|
isNewCreation: !!create,
|
|
@@ -488,9 +545,11 @@ export function openTextEditor(object, create = false) {
|
|
|
488
545
|
textarea,
|
|
489
546
|
isNewCreation,
|
|
490
547
|
isNote,
|
|
548
|
+
isShape,
|
|
491
549
|
autoSize,
|
|
492
550
|
updateNoteEditor,
|
|
493
551
|
finalize,
|
|
552
|
+
listType: properties.listType || 'none',
|
|
494
553
|
});
|
|
495
554
|
}
|
|
496
555
|
|
package/src/ui/CommentPopover.js
CHANGED
|
@@ -40,6 +40,8 @@ export class CommentPopover {
|
|
|
40
40
|
this.eventBus.on(Events.Tool.RotateUpdate, () => this.reposition());
|
|
41
41
|
this.eventBus.on(Events.UI.ZoomPercent, () => this.reposition());
|
|
42
42
|
this.eventBus.on(Events.Tool.PanUpdate, () => this.reposition());
|
|
43
|
+
this._onViewportChanged = () => this.reposition();
|
|
44
|
+
this.eventBus.on(Events.Viewport.Changed, this._onViewportChanged);
|
|
43
45
|
this.eventBus.on(Events.Object.Deleted, ({ objectId }) => {
|
|
44
46
|
if (this.currentId && objectId === this.currentId) this.hide();
|
|
45
47
|
// По желанию можно очистить: this.commentsById.delete(objectId);
|
|
@@ -48,6 +50,10 @@ export class CommentPopover {
|
|
|
48
50
|
|
|
49
51
|
destroy() {
|
|
50
52
|
this.hide();
|
|
53
|
+
if (this._onViewportChanged && this.eventBus?.off) {
|
|
54
|
+
this.eventBus.off(Events.Viewport.Changed, this._onViewportChanged);
|
|
55
|
+
this._onViewportChanged = null;
|
|
56
|
+
}
|
|
51
57
|
if (this.layer) this.layer.remove();
|
|
52
58
|
this.layer = null;
|
|
53
59
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
|
|
3
|
+
const ICONS = {
|
|
4
|
+
eye: '<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
|
|
5
|
+
eyeOff: '<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>',
|
|
6
|
+
messages: '<path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2Z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/>',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function svg(paths) {
|
|
10
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Нижняя правая панель управления комментариями.
|
|
15
|
+
* Источник правды для UI-фильтра «показывать решённые»: эмитит
|
|
16
|
+
* Comment.ResolvedFilterChanged, CommentPinLayer применяет фильтр к пинам.
|
|
17
|
+
*/
|
|
18
|
+
export class CommentsBar {
|
|
19
|
+
constructor(container, eventBus) {
|
|
20
|
+
this.container = container;
|
|
21
|
+
this.eventBus = eventBus;
|
|
22
|
+
this.element = null;
|
|
23
|
+
this.filterBtn = null;
|
|
24
|
+
this.filterIcon = null;
|
|
25
|
+
this.filterLabel = null;
|
|
26
|
+
/** true — решённые комментарии видны (по умолчанию) */
|
|
27
|
+
this.showResolved = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
attach() {
|
|
31
|
+
this.element = document.createElement('div');
|
|
32
|
+
this.element.className = 'moodboard-commentsbar';
|
|
33
|
+
|
|
34
|
+
this.filterBtn = document.createElement('button');
|
|
35
|
+
this.filterBtn.type = 'button';
|
|
36
|
+
this.filterBtn.className = 'moodboard-commentsbar__filter';
|
|
37
|
+
this.filterIcon = document.createElement('span');
|
|
38
|
+
this.filterIcon.className = 'moodboard-commentsbar__icon';
|
|
39
|
+
this.filterLabel = document.createElement('span');
|
|
40
|
+
this.filterLabel.className = 'moodboard-commentsbar__label';
|
|
41
|
+
this.filterBtn.appendChild(this.filterIcon);
|
|
42
|
+
this.filterBtn.appendChild(this.filterLabel);
|
|
43
|
+
this.filterBtn.addEventListener('click', () => this._toggleResolved());
|
|
44
|
+
|
|
45
|
+
const divider = document.createElement('span');
|
|
46
|
+
divider.className = 'moodboard-commentsbar__divider';
|
|
47
|
+
|
|
48
|
+
this.listBtn = document.createElement('button');
|
|
49
|
+
this.listBtn.type = 'button';
|
|
50
|
+
this.listBtn.className = 'moodboard-commentsbar__button';
|
|
51
|
+
this.listBtn.title = 'Все комментарии';
|
|
52
|
+
this.listBtn.setAttribute('aria-label', 'Все комментарии');
|
|
53
|
+
this.listBtn.innerHTML = svg(ICONS.messages);
|
|
54
|
+
this.listBtn.addEventListener('click', () => {
|
|
55
|
+
this.eventBus.emit(Events.Comment.ListOpened, {});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.element.appendChild(this.filterBtn);
|
|
59
|
+
this.element.appendChild(divider);
|
|
60
|
+
this.element.appendChild(this.listBtn);
|
|
61
|
+
this.container.appendChild(this.element);
|
|
62
|
+
|
|
63
|
+
this._syncFilter();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_toggleResolved() {
|
|
67
|
+
this.showResolved = !this.showResolved;
|
|
68
|
+
this._syncFilter();
|
|
69
|
+
this.eventBus.emit(Events.Comment.ResolvedFilterChanged, {
|
|
70
|
+
showResolved: this.showResolved,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_syncFilter() {
|
|
75
|
+
if (!this.filterBtn) return;
|
|
76
|
+
const label = this.showResolved ? 'Скрыть решённые' : 'Показать все';
|
|
77
|
+
this.filterIcon.innerHTML = svg(this.showResolved ? ICONS.eyeOff : ICONS.eye);
|
|
78
|
+
this.filterLabel.textContent = label;
|
|
79
|
+
this.filterBtn.title = label;
|
|
80
|
+
this.filterBtn.setAttribute('aria-pressed', String(!this.showResolved));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
destroy() {
|
|
84
|
+
if (this.element) this.element.remove();
|
|
85
|
+
this.element = null;
|
|
86
|
+
this.filterBtn = null;
|
|
87
|
+
this.filterIcon = null;
|
|
88
|
+
this.filterLabel = null;
|
|
89
|
+
this.listBtn = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { Events } from '../core/events/Events.js';
|
|
2
|
+
import {
|
|
3
|
+
createConnectorPropertiesPanelState,
|
|
4
|
+
clearConnectorPropertiesPanelState,
|
|
5
|
+
} from './connector-properties/ConnectorPropertiesPanelState.js';
|
|
6
|
+
import { createConnectorPropertiesPanelDom, updateConnectorPanelControls, updateLabelRow } from './connector-properties/ConnectorPropertiesPanelRenderer.js';
|
|
7
|
+
import { bindConnectorPropertiesPanelControls, unbindConnectorPropertiesPanelControls } from './connector-properties/ConnectorPropertiesPanelBindings.js';
|
|
8
|
+
import { attachConnectorPropertiesPanelEventBridge, detachConnectorPropertiesPanelEventBridge } from './connector-properties/ConnectorPropertiesPanelEventBridge.js';
|
|
9
|
+
import {
|
|
10
|
+
getSelectedConnectorId,
|
|
11
|
+
getConnectorData,
|
|
12
|
+
getConnectorMidpointScreen,
|
|
13
|
+
getConnectorStyle,
|
|
14
|
+
buildStyleUpdate,
|
|
15
|
+
} from './connector-properties/ConnectorPropertiesPanelMapper.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Панель свойств коннектора.
|
|
19
|
+
* Всплывает над серединой выделенной линии.
|
|
20
|
+
*/
|
|
21
|
+
export class ConnectorPropertiesPanel {
|
|
22
|
+
constructor(eventBus, container, core) {
|
|
23
|
+
Object.assign(this, createConnectorPropertiesPanelState());
|
|
24
|
+
this.eventBus = eventBus;
|
|
25
|
+
this.container = container;
|
|
26
|
+
this.core = core;
|
|
27
|
+
|
|
28
|
+
createConnectorPropertiesPanelDom(this);
|
|
29
|
+
container.appendChild(this.panel);
|
|
30
|
+
|
|
31
|
+
bindConnectorPropertiesPanelControls(this);
|
|
32
|
+
attachConnectorPropertiesPanelEventBridge(this);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Публичное API ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
updateFromSelection() {
|
|
38
|
+
const id = getSelectedConnectorId(this.core);
|
|
39
|
+
if (!id) { this.hide(); return; }
|
|
40
|
+
|
|
41
|
+
if (this.currentId === id && this.panel.style.display !== 'none') {
|
|
42
|
+
this.reposition();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.currentId = id;
|
|
47
|
+
this.panel.style.display = 'flex';
|
|
48
|
+
this._updateControlsFromObject();
|
|
49
|
+
this.reposition();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
hide() {
|
|
53
|
+
this.currentId = null;
|
|
54
|
+
this.panel.style.display = 'none';
|
|
55
|
+
this._closeDropdowns();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
reposition() {
|
|
59
|
+
if (!this.panel || !this.currentId || this.panel.style.display === 'none') return;
|
|
60
|
+
|
|
61
|
+
// Проверяем, что коннектор всё ещё выделен
|
|
62
|
+
const stillSelected = getSelectedConnectorId(this.core) === this.currentId;
|
|
63
|
+
if (!stillSelected) { this.hide(); return; }
|
|
64
|
+
|
|
65
|
+
const mid = getConnectorMidpointScreen(this.core, this.currentId);
|
|
66
|
+
if (!mid) return;
|
|
67
|
+
|
|
68
|
+
const panelW = this.panel.offsetWidth || 480;
|
|
69
|
+
const panelH = this.panel.offsetHeight || 40;
|
|
70
|
+
const GAP = 18;
|
|
71
|
+
|
|
72
|
+
let px = mid.x - Math.round(panelW / 2);
|
|
73
|
+
let py = mid.y - panelH - GAP;
|
|
74
|
+
|
|
75
|
+
// Если уходит вверх за контейнер — переносим ниже середины
|
|
76
|
+
if (py < 0) py = mid.y + GAP;
|
|
77
|
+
|
|
78
|
+
// Clamp по ширине контейнера
|
|
79
|
+
const cw = this.container.offsetWidth || window.innerWidth;
|
|
80
|
+
px = Math.max(8, Math.min(px, cw - panelW - 8));
|
|
81
|
+
py = Math.max(8, py);
|
|
82
|
+
|
|
83
|
+
this.panel.style.left = `${Math.round(px)}px`;
|
|
84
|
+
this.panel.style.top = `${Math.round(py)}px`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
destroy() {
|
|
88
|
+
detachConnectorPropertiesPanelEventBridge(this);
|
|
89
|
+
unbindConnectorPropertiesPanelControls(this);
|
|
90
|
+
|
|
91
|
+
if (this.panel?.parentNode) {
|
|
92
|
+
this.panel.parentNode.removeChild(this.panel);
|
|
93
|
+
}
|
|
94
|
+
clearConnectorPropertiesPanelState(this);
|
|
95
|
+
this.panel = null;
|
|
96
|
+
this.eventBus = null;
|
|
97
|
+
this.container = null;
|
|
98
|
+
this.core = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Внутренние методы ────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
_getConnector() {
|
|
104
|
+
return getConnectorData(this.core, this.currentId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_updateControlsFromObject() {
|
|
108
|
+
if (!this.currentId) return;
|
|
109
|
+
const connector = this._getConnector();
|
|
110
|
+
if (!connector) return;
|
|
111
|
+
const style = getConnectorStyle(connector);
|
|
112
|
+
updateConnectorPanelControls(this, style);
|
|
113
|
+
updateLabelRow(this, connector);
|
|
114
|
+
|
|
115
|
+
// Синхронизируем кнопку замка
|
|
116
|
+
if (this._lockBtn) {
|
|
117
|
+
const locked = connector.properties?.locked ?? false;
|
|
118
|
+
this._lockBtn.textContent = locked ? '🔒' : '🔓';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Эмитит StateChanged с частичным style-обновлением.
|
|
124
|
+
* Вызывается из Bindings.
|
|
125
|
+
*/
|
|
126
|
+
_emitStyle(partialStyle) {
|
|
127
|
+
if (!this.currentId) return;
|
|
128
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
129
|
+
objectId: this.currentId,
|
|
130
|
+
updates: buildStyleUpdate(partialStyle),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Эмитит StateChanged с полным объектом label (или null).
|
|
136
|
+
* Вызывается из Bindings при изменении цвета/размера метки.
|
|
137
|
+
*/
|
|
138
|
+
_emitLabel(label) {
|
|
139
|
+
if (!this.currentId) return;
|
|
140
|
+
this.eventBus.emit(Events.Object.StateChanged, {
|
|
141
|
+
objectId: this.currentId,
|
|
142
|
+
updates: { properties: { style: { label } } },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_closeDropdowns() {
|
|
147
|
+
if (this.strokeColorDropdown) this.strokeColorDropdown.style.display = 'none';
|
|
148
|
+
if (this._labelColorDropdown) this._labelColorDropdown.style.display = 'none';
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/ui/ContextMenu.js
CHANGED
|
@@ -13,6 +13,8 @@ export class ContextMenu {
|
|
|
13
13
|
// Флаг состояния объекта
|
|
14
14
|
this.destroyed = false;
|
|
15
15
|
|
|
16
|
+
this.enableComments = false;
|
|
17
|
+
|
|
16
18
|
this.createElement();
|
|
17
19
|
this.attachEvents();
|
|
18
20
|
}
|
|
@@ -303,6 +305,25 @@ export class ContextMenu {
|
|
|
303
305
|
});
|
|
304
306
|
list.appendChild(itemImg);
|
|
305
307
|
|
|
308
|
+
// Добавить комментарий (только если фича включена)
|
|
309
|
+
if (this.enableComments) {
|
|
310
|
+
const commentItem = document.createElement('div');
|
|
311
|
+
commentItem.className = 'moodboard-contextmenu__item';
|
|
312
|
+
const commentIcon = document.createElement('span');
|
|
313
|
+
commentIcon.className = 'moodboard-contextmenu__item-icon';
|
|
314
|
+
commentIcon.textContent = '💬';
|
|
315
|
+
const commentLabel = document.createElement('span');
|
|
316
|
+
commentLabel.className = 'moodboard-contextmenu__label';
|
|
317
|
+
commentLabel.textContent = 'Добавить комментарий';
|
|
318
|
+
commentItem.appendChild(commentIcon);
|
|
319
|
+
commentItem.appendChild(commentLabel);
|
|
320
|
+
commentItem.addEventListener('click', () => {
|
|
321
|
+
this.hide();
|
|
322
|
+
this.eventBus.emit(Events.Comment.OpenDraftAt, { screenX: this.lastX, screenY: this.lastY });
|
|
323
|
+
});
|
|
324
|
+
list.appendChild(commentItem);
|
|
325
|
+
}
|
|
326
|
+
|
|
306
327
|
// Разделитель
|
|
307
328
|
const divider = document.createElement('div');
|
|
308
329
|
divider.className = 'moodboard-contextmenu__divider';
|
|
@@ -342,6 +363,10 @@ export class ContextMenu {
|
|
|
342
363
|
this.element.innerHTML = '<div style="padding:8px 12px; color:#888;">(пусто)</div>';
|
|
343
364
|
}
|
|
344
365
|
|
|
366
|
+
setEnableComments(val) {
|
|
367
|
+
this.enableComments = !!val;
|
|
368
|
+
}
|
|
369
|
+
|
|
345
370
|
/**
|
|
346
371
|
* Уничтожение контекстного меню
|
|
347
372
|
*/
|