@sequent-org/moodboard 1.4.32 → 1.4.34

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.
Files changed (137) hide show
  1. package/package.json +5 -1
  2. package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
  3. package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
  4. package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  5. package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  6. package/src/assets/icons/attachments.svg +3 -1
  7. package/src/assets/icons/comments.svg +2 -2
  8. package/src/assets/icons/connector.svg +6 -0
  9. package/src/assets/icons/emoji.svg +6 -1
  10. package/src/assets/icons/frame.svg +4 -1
  11. package/src/assets/icons/image.svg +5 -1
  12. package/src/assets/icons/laser.svg +1 -0
  13. package/src/assets/icons/lasso.svg +5 -0
  14. package/src/assets/icons/mindmap.svg +10 -2
  15. package/src/assets/icons/note.svg +4 -1
  16. package/src/assets/icons/pan.svg +5 -2
  17. package/src/assets/icons/pencil.svg +4 -1
  18. package/src/assets/icons/reactions.svg +5 -0
  19. package/src/assets/icons/redo.svg +3 -2
  20. package/src/assets/icons/select.svg +2 -8
  21. package/src/assets/icons/shapes.svg +5 -1
  22. package/src/assets/icons/text-add.svg +15 -1
  23. package/src/assets/icons/undo.svg +3 -2
  24. package/src/assets/reactions/1f44d.svg +20 -0
  25. package/src/assets/reactions/1f44e.svg +20 -0
  26. package/src/assets/reactions/2705.svg +20 -0
  27. package/src/assets/reactions/274c.svg +19 -0
  28. package/src/assets/reactions/2753.svg +20 -0
  29. package/src/assets/reactions/2764.svg +22 -0
  30. package/src/assets/reactions/2b50.svg +19 -0
  31. package/src/assets/reactions/plus-one.svg +25 -0
  32. package/src/core/PixiEngine.js +23 -0
  33. package/src/core/bootstrap/CoreInitializer.js +43 -0
  34. package/src/core/commands/GroupDeleteCommand.js +13 -1
  35. package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
  36. package/src/core/commands/UpdateTextStyleCommand.js +17 -6
  37. package/src/core/commands/index.js +3 -0
  38. package/src/core/events/Events.js +22 -0
  39. package/src/core/flows/LayerAndViewportFlow.js +1 -0
  40. package/src/core/flows/ObjectLifecycleFlow.js +155 -7
  41. package/src/core/index.js +28 -1
  42. package/src/grid/CrossGridZoomPhases.js +3 -3
  43. package/src/initNoBundler.js +1 -1
  44. package/src/moodboard/DataManager.js +28 -0
  45. package/src/moodboard/MoodBoard.js +27 -0
  46. package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
  47. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
  48. package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
  49. package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
  50. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
  51. package/src/objects/ConnectorObject.js +2 -2
  52. package/src/objects/FrameObject.js +119 -59
  53. package/src/objects/ShapeObject.js +49 -74
  54. package/src/objects/shape/ShapeDrawer.js +210 -0
  55. package/src/services/ConnectorBindingResolver.js +112 -0
  56. package/src/services/ConnectorRouter.js +210 -0
  57. package/src/services/ai/ChatSessionController.js +14 -8
  58. package/src/services/comments/CommentService.js +344 -0
  59. package/src/tools/object-tools/CommentTool.js +85 -0
  60. package/src/tools/object-tools/DrawingTool.js +110 -10
  61. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  62. package/src/tools/object-tools/SelectTool.js +25 -1
  63. package/src/tools/object-tools/TextTool.js +6 -1
  64. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  65. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  66. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  67. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  68. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  69. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  70. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  71. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  72. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  73. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  74. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  75. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  76. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  77. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  78. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  79. package/src/ui/CommentPopover.js +6 -0
  80. package/src/ui/CommentsBar.js +91 -0
  81. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  82. package/src/ui/ContextMenu.js +25 -0
  83. package/src/ui/DrawingPropertiesPanel.js +362 -0
  84. package/src/ui/FilePropertiesPanel.js +5 -0
  85. package/src/ui/FramePropertiesPanel.js +5 -0
  86. package/src/ui/HtmlTextLayer.js +246 -66
  87. package/src/ui/NotePropertiesPanel.js +6 -0
  88. package/src/ui/ShapePropertiesPanel.js +307 -0
  89. package/src/ui/TextPropertiesPanel.js +100 -1
  90. package/src/ui/Toolbar.js +25 -2
  91. package/src/ui/Topbar.js +2 -2
  92. package/src/ui/animation/HoverLiftController.js +6 -7
  93. package/src/ui/chat/ChatComposer.js +63 -9
  94. package/src/ui/chat/ChatWindow.js +329 -166
  95. package/src/ui/comments/CommentListPanel.js +213 -0
  96. package/src/ui/comments/CommentPinLayer.js +448 -0
  97. package/src/ui/comments/CommentThreadPopover.js +539 -0
  98. package/src/ui/comments/commentFormat.js +32 -0
  99. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  100. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  103. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  104. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  105. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  106. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  107. package/src/ui/connectors/ConnectorLayer.js +264 -57
  108. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  109. package/src/ui/handles/HandlesEventBridge.js +1 -0
  110. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  111. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  112. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  113. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  114. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  115. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  116. package/src/ui/styles/chat.css +710 -18
  117. package/src/ui/styles/index.css +1 -0
  118. package/src/ui/styles/panels.css +112 -2
  119. package/src/ui/styles/shape-properties-panel.css +250 -0
  120. package/src/ui/styles/toolbar.css +7 -2
  121. package/src/ui/styles/topbar.css +1 -1
  122. package/src/ui/styles/workspace.css +257 -6
  123. package/src/ui/text-properties/TextFormatControls.js +88 -0
  124. package/src/ui/text-properties/TextListRenderer.js +137 -0
  125. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  126. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  127. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  128. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  129. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  130. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  131. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  132. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  133. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  134. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  135. package/src/utils/iconLoader.js +17 -16
  136. package/src/utils/markdown.js +14 -0
  137. package/src/utils/richText.js +125 -0
@@ -4,6 +4,7 @@ import {
4
4
  FONT_SIZE_OPTIONS,
5
5
  TEXT_COLOR_PRESETS,
6
6
  } from './TextPropertiesPanelMapper.js';
7
+ import { createTextFormatControls } from './TextFormatControls.js';
7
8
 
8
9
  export function createTextPropertiesPanelRenderer(panelInstance) {
9
10
  const panel = document.createElement('div');
@@ -148,6 +149,29 @@ function createFontControls(panelInstance, panel) {
148
149
  panel.appendChild(bgColorLabel);
149
150
 
150
151
  createCompactBackgroundSelector(panelInstance, panel);
152
+
153
+ const mdSeparator = document.createElement('div');
154
+ mdSeparator.style.cssText = 'width:1px;height:18px;background:#e0e0e0;margin:0 6px;flex-shrink:0;';
155
+ panel.appendChild(mdSeparator);
156
+
157
+ const mdId = `tpp-md-${Date.now()}`;
158
+ panelInstance.markdownToggle = document.createElement('input');
159
+ panelInstance.markdownToggle.type = 'checkbox';
160
+ panelInstance.markdownToggle.id = mdId;
161
+ panelInstance.markdownToggle.className = 'tpp-md-toggle';
162
+ panelInstance.markdownToggle.style.cssText = 'width:14px;height:14px;cursor:pointer;flex-shrink:0;';
163
+
164
+ const mdLabel = document.createElement('label');
165
+ mdLabel.htmlFor = mdId;
166
+ mdLabel.textContent = 'MD';
167
+ mdLabel.title = 'Отображать как Markdown';
168
+ mdLabel.className = 'tpp-label';
169
+ mdLabel.style.cssText = 'cursor:pointer;user-select:none;';
170
+
171
+ panel.appendChild(panelInstance.markdownToggle);
172
+ panel.appendChild(mdLabel);
173
+
174
+ createTextFormatControls(panelInstance, panel);
151
175
  }
152
176
 
153
177
  function createCompactColorSelector(panelInstance, panel) {
@@ -12,6 +12,14 @@ export function createTextPropertiesPanelState() {
12
12
  currentBgColorButton: null,
13
13
  bgColorDropdown: null,
14
14
  bgColorInput: null,
15
+ markdownToggle: null,
16
+ boldBtn: null,
17
+ italicBtn: null,
18
+ underlineBtn: null,
19
+ strikethroughBtn: null,
20
+ alignControl: null,
21
+ listControl: null,
22
+ lineHeightSlider: null,
15
23
  _bindingsAttached: false,
16
24
  _eventBridgeAttached: false,
17
25
  _eventBridgeHandlers: null,
@@ -0,0 +1,88 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+
3
+ export class ReactionsPopupController {
4
+ constructor(toolbar) {
5
+ this.toolbar = toolbar;
6
+ }
7
+
8
+ createReactionsPopup() {
9
+ this.toolbar.reactionsPopupEl = document.createElement('div');
10
+ this.toolbar.reactionsPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--reactions';
11
+ this.toolbar.reactionsPopupEl.style.display = 'none';
12
+
13
+ const grid = document.createElement('div');
14
+ grid.className = 'moodboard-reactions__grid';
15
+
16
+ const stickerUrls = import.meta.glob('../../assets/reactions/*.svg', { eager: true, query: '?url', import: 'default' });
17
+
18
+ const entries = Object.entries(stickerUrls).sort(([a], [b]) => a.localeCompare(b));
19
+
20
+ if (entries.length === 0) {
21
+ const msg = document.createElement('div');
22
+ msg.className = 'moodboard-reactions__empty';
23
+ msg.textContent = 'Стикеры не найдены';
24
+ grid.appendChild(msg);
25
+ } else {
26
+ entries.forEach(([path, src]) => {
27
+ const btn = document.createElement('button');
28
+ btn.className = 'moodboard-reactions__btn';
29
+ const fileName = path.split('/').pop().replace(/\.[^.]+$/, '');
30
+ btn.title = fileName;
31
+
32
+ const img = document.createElement('img');
33
+ img.className = 'moodboard-reactions__img';
34
+ img.src = src;
35
+ img.alt = fileName;
36
+ btn.appendChild(img);
37
+
38
+ btn.addEventListener('click', () => {
39
+ this.toolbar.animateButton(btn);
40
+ this.toolbar.eventBus.emit(Events.Place.Set, {
41
+ type: 'image',
42
+ properties: { src, width: 64, height: 64, isEmojiIcon: true, isReaction: true },
43
+ size: { width: 64, height: 64 }
44
+ });
45
+ this.closeReactionsPopup();
46
+ });
47
+
48
+ grid.appendChild(btn);
49
+ });
50
+ }
51
+
52
+ this.toolbar.reactionsPopupEl.appendChild(grid);
53
+ this.toolbar.container.appendChild(this.toolbar.reactionsPopupEl);
54
+ }
55
+
56
+ toggleReactionsPopup(anchorButton) {
57
+ if (!this.toolbar.reactionsPopupEl) return;
58
+ if (this.toolbar.reactionsPopupEl.style.display === 'none') {
59
+ this.openReactionsPopup(anchorButton);
60
+ } else {
61
+ this.closeReactionsPopup();
62
+ }
63
+ }
64
+
65
+ openReactionsPopup(anchorButton) {
66
+ if (!this.toolbar.reactionsPopupEl) return;
67
+ const toolbarRect = this.toolbar.container.getBoundingClientRect();
68
+ const buttonRect = anchorButton.getBoundingClientRect();
69
+ const left = this.toolbar.element.offsetWidth + 8;
70
+ this.toolbar.reactionsPopupEl.style.visibility = 'hidden';
71
+ this.toolbar.reactionsPopupEl.style.display = 'block';
72
+ const desiredTop = buttonRect.top - toolbarRect.top - 4;
73
+ const popupHeight = this.toolbar.reactionsPopupEl.offsetHeight;
74
+ const containerHeight = this.toolbar.container.clientHeight || toolbarRect.height;
75
+ const minTop = 8;
76
+ const maxTop = Math.max(minTop, containerHeight - popupHeight - 8);
77
+ const top = Math.min(Math.max(minTop, desiredTop), maxTop);
78
+ this.toolbar.reactionsPopupEl.style.top = `${Math.round(top)}px`;
79
+ this.toolbar.reactionsPopupEl.style.left = `${Math.round(left)}px`;
80
+ this.toolbar.reactionsPopupEl.style.visibility = 'visible';
81
+ }
82
+
83
+ closeReactionsPopup() {
84
+ if (this.toolbar.reactionsPopupEl) {
85
+ this.toolbar.reactionsPopupEl.style.display = 'none';
86
+ }
87
+ }
88
+ }
@@ -25,18 +25,48 @@ export class ToolbarActionRouter {
25
25
  this.toolbar.closeShapesPopup();
26
26
  this.toolbar.closeDrawPopup();
27
27
  this.toolbar.closeEmojiPopup();
28
+ this.toolbar.closeReactionsPopup();
28
29
  this.toolbar.eventBus.emit(Events.Place.Set, null);
29
30
  this.toolbar.placeSelectedButtonId = null;
31
+ this.toolbar.eventBus.emit(Events.Lasso.ModeSet, { active: false });
30
32
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
31
33
  this.toolbar.setActiveToolbarButton('select');
32
34
  return true;
33
35
  }
34
36
 
37
+ if (toolType === 'activate-lasso') {
38
+ this.toolbar.animateButton(button);
39
+ this.toolbar.closeShapesPopup();
40
+ this.toolbar.closeDrawPopup();
41
+ this.toolbar.closeEmojiPopup();
42
+ this.toolbar.closeReactionsPopup();
43
+ this.toolbar.eventBus.emit(Events.Place.Set, null);
44
+ this.toolbar.placeSelectedButtonId = null;
45
+ this.toolbar.eventBus.emit(Events.Lasso.ModeSet, { active: true });
46
+ this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'select' });
47
+ this.toolbar.setActiveToolbarButton('lasso');
48
+ return true;
49
+ }
50
+
51
+ if (toolType === 'activate-laser') {
52
+ this.toolbar.animateButton(button);
53
+ this.toolbar.closeShapesPopup();
54
+ this.toolbar.closeDrawPopup();
55
+ this.toolbar.closeEmojiPopup();
56
+ this.toolbar.closeReactionsPopup();
57
+ this.toolbar.eventBus.emit(Events.Place.Set, null);
58
+ this.toolbar.placeSelectedButtonId = null;
59
+ this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'laser' });
60
+ this.toolbar.setActiveToolbarButton('laser');
61
+ return true;
62
+ }
63
+
35
64
  if (toolType === 'activate-pan') {
36
65
  this.toolbar.animateButton(button);
37
66
  this.toolbar.closeShapesPopup();
38
67
  this.toolbar.closeDrawPopup();
39
68
  this.toolbar.closeEmojiPopup();
69
+ this.toolbar.closeReactionsPopup();
40
70
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'pan' });
41
71
  this.toolbar.setActiveToolbarButton('pan');
42
72
  return true;
@@ -47,6 +77,7 @@ export class ToolbarActionRouter {
47
77
  this.toolbar.closeShapesPopup();
48
78
  this.toolbar.closeDrawPopup();
49
79
  this.toolbar.closeEmojiPopup();
80
+ this.toolbar.closeReactionsPopup();
50
81
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
51
82
  this.toolbar.placeSelectedButtonId = 'text';
52
83
  this.toolbar.setActiveToolbarButton('place');
@@ -62,13 +93,14 @@ export class ToolbarActionRouter {
62
93
  this.toolbar.closeShapesPopup();
63
94
  this.toolbar.closeDrawPopup();
64
95
  this.toolbar.closeEmojiPopup();
96
+ this.toolbar.closeReactionsPopup();
65
97
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
66
98
  this.toolbar.placeSelectedButtonId = 'note';
67
99
  this.toolbar.setActiveToolbarButton('place');
68
100
  this.toolbar.eventBus.emit(Events.Place.Set, {
69
101
  type: 'note',
70
102
  properties: {
71
- content: 'Новая записка',
103
+ content: '',
72
104
  fontFamily: 'Caveat, Arial, cursive',
73
105
  fontSize: 32,
74
106
  width: 250,
@@ -85,6 +117,7 @@ export class ToolbarActionRouter {
85
117
  this.toolbar.closeShapesPopup();
86
118
  this.toolbar.closeDrawPopup();
87
119
  this.toolbar.closeEmojiPopup();
120
+ this.toolbar.closeReactionsPopup();
88
121
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
89
122
  this.toolbar.placeSelectedButtonId = 'mindmap';
90
123
  this.toolbar.setActiveToolbarButton('place');
@@ -117,6 +150,7 @@ export class ToolbarActionRouter {
117
150
  this.toolbar.closeShapesPopup();
118
151
  this.toolbar.closeDrawPopup();
119
152
  this.toolbar.closeEmojiPopup();
153
+ this.toolbar.closeReactionsPopup();
120
154
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
121
155
  this.toolbar.placeSelectedButtonId = 'frame';
122
156
  this.toolbar.setActiveToolbarButton('place');
@@ -130,6 +164,7 @@ export class ToolbarActionRouter {
130
164
  this.toolbar.closeShapesPopup();
131
165
  this.toolbar.closeDrawPopup();
132
166
  this.toolbar.closeEmojiPopup();
167
+ this.toolbar.closeReactionsPopup();
133
168
  this.toolbar.openImageDialog();
134
169
  return true;
135
170
  }
@@ -139,6 +174,7 @@ export class ToolbarActionRouter {
139
174
  this.toolbar.closeShapesPopup();
140
175
  this.toolbar.closeDrawPopup();
141
176
  this.toolbar.closeEmojiPopup();
177
+ this.toolbar.closeReactionsPopup();
142
178
  this.toolbar.openImageObject2Dialog();
143
179
  return true;
144
180
  }
@@ -148,10 +184,9 @@ export class ToolbarActionRouter {
148
184
  this.toolbar.closeShapesPopup();
149
185
  this.toolbar.closeDrawPopup();
150
186
  this.toolbar.closeEmojiPopup();
151
- this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
152
- this.toolbar.placeSelectedButtonId = 'comments';
153
- this.toolbar.setActiveToolbarButton('place');
154
- this.toolbar.eventBus.emit(Events.Place.Set, { type: 'comment', properties: { width: 72, height: 72 } });
187
+ this.toolbar.closeReactionsPopup();
188
+ this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'comment' });
189
+ this.toolbar.setActiveToolbarButton('comment');
155
190
  return true;
156
191
  }
157
192
 
@@ -160,6 +195,7 @@ export class ToolbarActionRouter {
160
195
  this.toolbar.closeShapesPopup();
161
196
  this.toolbar.closeDrawPopup();
162
197
  this.toolbar.closeEmojiPopup();
198
+ this.toolbar.closeReactionsPopup();
163
199
  this.toolbar.openFileDialog();
164
200
  return true;
165
201
  }
@@ -169,6 +205,7 @@ export class ToolbarActionRouter {
169
205
  this.toolbar.closeShapesPopup();
170
206
  this.toolbar.closeDrawPopup();
171
207
  this.toolbar.closeEmojiPopup();
208
+ this.toolbar.closeReactionsPopup();
172
209
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
173
210
  this.toolbar.placeSelectedButtonId = 'frame-tool';
174
211
  this.toolbar.setActiveToolbarButton('place');
@@ -179,11 +216,25 @@ export class ToolbarActionRouter {
179
216
  return true;
180
217
  }
181
218
 
219
+ if (toolType === 'connector-add') {
220
+ this.toolbar.animateButton(button);
221
+ this.toolbar.closeShapesPopup();
222
+ this.toolbar.closeDrawPopup();
223
+ this.toolbar.closeEmojiPopup();
224
+ this.toolbar.closeReactionsPopup();
225
+ this.toolbar.eventBus.emit(Events.Place.Set, null);
226
+ this.toolbar.placeSelectedButtonId = null;
227
+ this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'connector' });
228
+ this.toolbar.setActiveToolbarButton('connector');
229
+ return true;
230
+ }
231
+
182
232
  if (toolType === 'custom-shapes') {
183
233
  this.toolbar.animateButton(button);
184
234
  this.toolbar.toggleShapesPopup(button);
185
235
  this.toolbar.closeDrawPopup();
186
236
  this.toolbar.closeEmojiPopup();
237
+ this.toolbar.closeReactionsPopup();
187
238
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
188
239
  this.toolbar.placeSelectedButtonId = 'shapes';
189
240
  this.toolbar.setActiveToolbarButton('place');
@@ -195,6 +246,8 @@ export class ToolbarActionRouter {
195
246
  this.toolbar.toggleDrawPopup(button);
196
247
  this.toolbar.closeShapesPopup();
197
248
  this.toolbar.closeEmojiPopup();
249
+ this.toolbar.closeReactionsPopup();
250
+ this.toolbar.eventBus.emit(Events.Lasso.ModeSet, { active: false });
198
251
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'draw' });
199
252
  this.toolbar.setActiveToolbarButton('draw');
200
253
  return true;
@@ -205,12 +258,25 @@ export class ToolbarActionRouter {
205
258
  this.toolbar.toggleEmojiPopup(button);
206
259
  this.toolbar.closeShapesPopup();
207
260
  this.toolbar.closeDrawPopup();
261
+ this.toolbar.closeReactionsPopup();
208
262
  this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
209
263
  this.toolbar.placeSelectedButtonId = 'emoji';
210
264
  this.toolbar.setActiveToolbarButton('place');
211
265
  return true;
212
266
  }
213
267
 
268
+ if (toolType === 'custom-reactions') {
269
+ this.toolbar.animateButton(button);
270
+ this.toolbar.toggleReactionsPopup(button);
271
+ this.toolbar.closeShapesPopup();
272
+ this.toolbar.closeDrawPopup();
273
+ this.toolbar.closeEmojiPopup();
274
+ this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
275
+ this.toolbar.placeSelectedButtonId = 'reactions';
276
+ this.toolbar.setActiveToolbarButton('place');
277
+ return true;
278
+ }
279
+
214
280
  if (toolType === 'clear') {
215
281
  this.toolbar.animateButton(button);
216
282
  this.toolbar.showClearConfirmation();
@@ -199,153 +199,155 @@ export class ToolbarPopupsController {
199
199
  this.toolbar.drawPopupEl.className = 'moodboard-toolbar__popup moodboard-toolbar__popup--draw';
200
200
  this.toolbar.drawPopupEl.style.display = 'none';
201
201
 
202
- const grid = document.createElement('div');
203
- grid.className = 'moodboard-draw__grid';
202
+ const panel = document.createElement('div');
203
+ panel.className = 'moodboard-draw__panel';
204
+
205
+ // ── Ряд 1: инструменты ──────────────────────────────────────────────────
206
+ const TOOL_SVG = {
207
+ pencil: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>',
208
+ marker: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9.06 11.9 8.07-8.06a2.85 2.85 0 1 1 4.03 4.03l-8.06 8.08"/><path d="M7.07 14.94c-1.66 0-3 1.35-3 3.02 0 1.33-2.5 1.52-2 2.02 1 1 2.48 1.02 3.5 1.02 2.2 0 3-1.8 3-3.02 0-1.67-1.33-3.04-1.5-3.04z"/></svg>',
209
+ eraser: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>',
210
+ laser: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M12 2v3"/><path d="m4.93 4.93 2.12 2.12"/><path d="M2 12h3"/><path d="m4.93 19.07 2.12-2.12"/><path d="M12 19v3"/><path d="m19.07 19.07-2.12-2.12"/><path d="M22 12h-3"/><path d="m19.07 4.93-2.12 2.12"/></svg>'
211
+ };
204
212
 
205
- const tools = [
206
- { id: 'pencil-tool', tool: 'pencil', title: 'Карандаш', svg: '<svg width="20" height="20" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M14.492 3.414 8.921 8.985a4.312 4.312 0 0 0 6.105 6.09l5.564-5.562 1.414 1.414-5.664 5.664a6.002 6.002 0 0 1-2.182 1.392L3.344 21.94 2.06 20.656 6.02 9.845c.3-.82.774-1.563 1.391-2.18l.093-.092.01-.01L13.077 2l1.415 1.414ZM4.68 19.32l4.486-1.64a6.305 6.305 0 0 1-1.651-1.19 6.306 6.306 0 0 1-1.192-1.655L4.68 19.32Z" clip-rule="evenodd"/></svg>' },
207
- { id: 'marker-tool', tool: 'marker', title: 'Маркер', svg: '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" width="20" height="20" class="c-bxOhME c-bxOhME-dvzWZT-size-medium"><path fill="currentColor" fill-rule="evenodd" d="M12.737 2.676 8.531 7.264a1 1 0 0 0 .03 1.382l7.674 7.675a1 1 0 0 0 1.442-.029l4.589-4.97 1.468 1.357-4.588 4.97a3 3 0 0 1-3.46.689l-1.917 2.303-1.454.087-.63-.593-.828 1.38L10 22v-1l-.001-.001L10 22H1v-3l.18-.573 3.452-4.93-.817-.77.045-1.496 2.621-2.184a2.999 2.999 0 0 1 .577-3.134l4.205-4.589 1.474 1.352ZM3 19.315v.684h6.434l.76-1.268-4.09-3.85L3 19.314Zm3.007-7.27 6.904 6.498 1.217-1.46-6.667-6.25-1.454 1.212Z" clip-rule="evenodd"></path></svg>' },
208
- { id: 'eraser-tool', tool: 'eraser', title: 'Ластик', svg: '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" width="20" height="20" class="c-bxOhME c-bxOhME-dvzWZT-size-medium"><path fill="currentColor" fill-rule="evenodd" d="M12.63 3.957 4.319 12.27a3 3 0 0 0 0 4.242L7.905 20.1 8.612 20.394H21v-2h-5.6l6.629-6.63a3 3 0 0 0 0-4.242L17.858 3.42a3 3 0 0 0-4.242 0ZM5.12 14.293a1 1 0 0 0 0 1.414L8.414 19h3.172l3-3L9 10.414l-3.879 3.88Zm10.336-8.922a1 1 0 0 0-1.414 0l-3.629 3.63L16 14.585l3.63-3.629a1 1 0 0 0 0-1.414L15.457 5.37Z" clip-rule="evenodd"></path></svg>' }
213
+ const toolDefs = [
214
+ { id: 'pencil-tool', tool: 'pencil', title: 'Карандаш' },
215
+ { id: 'marker-tool', tool: 'marker', title: 'Маркер' },
216
+ { id: 'eraser-tool', tool: 'eraser', title: 'Ластик' }
209
217
  ];
210
- const row1 = document.createElement('div');
211
- row1.className = 'moodboard-draw__row';
212
- this.toolbar.drawRow1 = row1;
213
- tools.forEach((t) => {
218
+
219
+ const toolRow = document.createElement('div');
220
+ toolRow.className = 'moodboard-draw__tool-row';
221
+ this.toolbar.drawRow1 = toolRow;
222
+
223
+ toolDefs.forEach((t) => {
214
224
  const btn = document.createElement('button');
215
225
  btn.className = `moodboard-draw__btn moodboard-draw__btn--${t.id}`;
216
226
  btn.title = t.title;
217
227
  const icon = document.createElement('span');
218
228
  icon.className = 'draw-icon';
219
- icon.innerHTML = t.svg;
229
+ icon.innerHTML = TOOL_SVG[t.tool];
220
230
  btn.appendChild(icon);
221
231
  btn.addEventListener('click', () => {
222
232
  this.toolbar.animateButton(btn);
223
- row1.querySelectorAll('.moodboard-draw__btn--active').forEach((el) => el.classList.remove('moodboard-draw__btn--active'));
233
+ toolRow.querySelectorAll('.moodboard-draw__btn--active').forEach((el) => el.classList.remove('moodboard-draw__btn--active'));
224
234
  btn.classList.add('moodboard-draw__btn--active');
235
+
225
236
  this.toolbar.currentDrawTool = t.tool;
237
+ this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'draw' });
226
238
  this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: t.tool });
227
- this.toolbar.buildDrawPresets(row2);
239
+ this._updateDrawPanelForTool(t.tool);
228
240
  });
229
- row1.appendChild(btn);
241
+ toolRow.appendChild(btn);
242
+ });
243
+ panel.appendChild(toolRow);
244
+
245
+ // ── Слайдер толщины ──────────────────────────────────────────────────────
246
+ const thicknessSection = document.createElement('div');
247
+ thicknessSection.className = 'moodboard-draw__section moodboard-draw__section--thickness';
248
+
249
+ const thicknessHeader = document.createElement('div');
250
+ thicknessHeader.className = 'moodboard-draw__section-header';
251
+ const thicknessLabel = document.createElement('span');
252
+ thicknessLabel.textContent = 'Толщина';
253
+ thicknessLabel.className = 'moodboard-draw__section-label';
254
+ const thicknessValue = document.createElement('span');
255
+ thicknessValue.textContent = '2px';
256
+ thicknessValue.className = 'moodboard-draw__thickness-value';
257
+ thicknessHeader.appendChild(thicknessLabel);
258
+ thicknessHeader.appendChild(thicknessValue);
259
+
260
+ const slider = document.createElement('input');
261
+ slider.type = 'range';
262
+ slider.min = '1';
263
+ slider.max = '24';
264
+ slider.value = '2';
265
+ slider.className = 'moodboard-draw__slider';
266
+ slider.addEventListener('input', () => {
267
+ const w = parseInt(slider.value, 10);
268
+ thicknessValue.textContent = `${w}px`;
269
+ this.toolbar.eventBus.emit(Events.Draw.BrushSet, { width: w });
230
270
  });
231
271
 
232
- const row2 = document.createElement('div');
233
- row2.className = 'moodboard-draw__row';
234
- this.toolbar.drawRow2 = row2;
235
- const clearActivePresetButtons = () => {
236
- row2.querySelectorAll('.moodboard-draw__btn--active').forEach((el) => el.classList.remove('moodboard-draw__btn--active'));
237
- };
272
+ thicknessSection.appendChild(thicknessHeader);
273
+ thicknessSection.appendChild(slider);
274
+ panel.appendChild(thicknessSection);
275
+ this.toolbar._drawThicknessSlider = slider;
276
+ this.toolbar._drawThicknessValue = thicknessValue;
277
+
278
+ // ── Палитра цветов ───────────────────────────────────────────────────────
279
+ const colorSection = document.createElement('div');
280
+ colorSection.className = 'moodboard-draw__section';
281
+
282
+ const PALETTE = [
283
+ '#111827', '#374151', '#9ca3af', '#d1d5db', '#ffffff',
284
+ '#ef4444', '#f97316', '#facc15', '#22c55e', '#3b82f6',
285
+ '#fca5a5', '#fdba74', '#fde68a', '#86efac', '#93c5fd',
286
+ '#f9a8d4', '#e9d5ff', '#c4b5fd', '#a5f3fc', '#bfdbfe'
287
+ ];
238
288
 
239
- const pencilPresetEl = document.createElement('div');
240
- pencilPresetEl.className = 'moodboard-draw__row';
241
- const markerPresetEl = document.createElement('div');
242
- markerPresetEl.className = 'moodboard-draw__row';
243
- const eraserPresetEl = document.createElement('div');
244
- eraserPresetEl.className = 'moodboard-draw__row';
245
- for (let i = 0; i < 3; i++) {
246
- const ph = document.createElement('div');
247
- ph.className = 'moodboard-draw__placeholder';
248
- eraserPresetEl.appendChild(ph);
249
- }
289
+ const colorGrid = document.createElement('div');
290
+ colorGrid.className = 'moodboard-draw__color-grid';
250
291
 
251
- const sizes = [
252
- { id: 'size-thin-black', title: 'Тонкий черный', color: '#111827', dot: 4, width: 2 },
253
- { id: 'size-medium-red', title: 'Средний красный', color: '#ef4444', dot: 8, width: 4 },
254
- { id: 'size-thick-green', title: 'Толстый зеленый', color: '#16a34a', dot: 10, width: 6 }
255
- ];
256
- sizes.forEach((s) => {
292
+ PALETTE.forEach((hex) => {
257
293
  const btn = document.createElement('button');
258
- btn.className = `moodboard-draw__btn moodboard-draw__btn--${s.id}`;
259
- btn.title = s.title;
260
- btn.dataset.brushWidth = String(s.width);
261
- btn.dataset.brushColor = s.color;
262
- const holder = document.createElement('span');
263
- holder.className = 'draw-size';
264
- const dot = document.createElement('span');
265
- dot.className = 'draw-dot';
266
- dot.style.background = s.color;
267
- dot.style.width = `${s.dot}px`;
268
- dot.style.height = `${s.dot}px`;
269
- holder.appendChild(dot);
270
- btn.appendChild(holder);
294
+ btn.className = 'moodboard-draw__color-btn';
295
+ btn.title = hex;
296
+ btn.style.background = hex;
297
+ if (hex === '#ffffff') btn.style.border = '1.5px solid #d1d5db';
271
298
  btn.addEventListener('click', () => {
272
- this.toolbar.animateButton(btn);
273
- clearActivePresetButtons();
274
- btn.classList.add('moodboard-draw__btn--active');
275
- const width = s.width;
276
- const color = parseInt(s.color.replace('#', ''), 16);
277
- this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil', width, color });
299
+ colorGrid.querySelectorAll('.moodboard-draw__color-btn--active').forEach((el) => el.classList.remove('moodboard-draw__color-btn--active'));
300
+ customColorBtn.classList.remove('moodboard-draw__color-btn--active');
301
+ btn.classList.add('moodboard-draw__color-btn--active');
302
+ const color = parseInt(hex.replace('#', ''), 16);
303
+ this.toolbar.eventBus.emit(Events.Draw.BrushSet, { color });
278
304
  });
279
- pencilPresetEl.appendChild(btn);
305
+ colorGrid.appendChild(btn);
280
306
  });
281
307
 
282
- const swatches = [
283
- { id: 'marker-yellow', title: 'Жёлтый', color: '#facc15' },
284
- { id: 'marker-green', title: 'Светло-зелёный', color: '#22c55e' },
285
- { id: 'marker-pink', title: 'Розовый', color: '#ec4899' }
286
- ];
287
- swatches.forEach((s) => {
288
- const btn = document.createElement('button');
289
- btn.className = `moodboard-draw__btn moodboard-draw__btn--${s.id}`;
290
- btn.title = s.title;
291
- const sw = document.createElement('span');
292
- sw.className = 'draw-swatch';
293
- sw.style.background = s.color;
294
- btn.appendChild(sw);
295
- btn.addEventListener('click', () => {
296
- this.toolbar.animateButton(btn);
297
- clearActivePresetButtons();
298
- btn.classList.add('moodboard-draw__btn--active');
299
- const color = parseInt(s.color.replace('#', ''), 16);
300
- this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: 'marker', color, width: 8 });
301
- });
302
- markerPresetEl.appendChild(btn);
308
+ // Кастомный пикер (радужный кружок)
309
+ const customColorBtn = document.createElement('button');
310
+ customColorBtn.className = 'moodboard-draw__color-btn moodboard-draw__color-btn--custom';
311
+ customColorBtn.title = 'Выбрать цвет';
312
+ const colorInput = document.createElement('input');
313
+ colorInput.type = 'color';
314
+ colorInput.value = '#000000';
315
+ colorInput.className = 'moodboard-draw__color-input';
316
+ colorInput.addEventListener('input', () => {
317
+ colorGrid.querySelectorAll('.moodboard-draw__color-btn--active').forEach((el) => el.classList.remove('moodboard-draw__color-btn--active'));
318
+ customColorBtn.classList.add('moodboard-draw__color-btn--active');
319
+ const color = parseInt(colorInput.value.replace('#', ''), 16);
320
+ this.toolbar.eventBus.emit(Events.Draw.BrushSet, { color });
321
+ });
322
+ customColorBtn.appendChild(colorInput);
323
+ customColorBtn.addEventListener('click', (e) => {
324
+ e.stopPropagation();
325
+ colorInput.click();
303
326
  });
327
+ colorGrid.appendChild(customColorBtn);
304
328
 
305
- const movePresetToRow = (fromEl, toRow) => {
306
- while (fromEl.firstChild) toRow.appendChild(fromEl.firstChild);
307
- };
308
- const getPresetElForContents = (container) => {
309
- if (container.querySelector('.moodboard-draw__btn--size-thin-black')) return pencilPresetEl;
310
- if (container.querySelector('.moodboard-draw__btn--marker-yellow')) return markerPresetEl;
311
- return eraserPresetEl;
312
- };
313
- this.toolbar.buildDrawPresets = (container) => {
314
- movePresetToRow(container, getPresetElForContents(container));
315
- if (this.toolbar.currentDrawTool === 'pencil') {
316
- movePresetToRow(pencilPresetEl, container);
317
- const first = container.querySelector('.moodboard-draw__btn');
318
- if (first) {
319
- container.querySelectorAll('.moodboard-draw__btn--active').forEach((el) => el.classList.remove('moodboard-draw__btn--active'));
320
- first.classList.add('moodboard-draw__btn--active');
321
- const width = parseInt(first.dataset.brushWidth, 10) || 2;
322
- const color = parseInt((first.dataset.brushColor || '#111827').replace('#', ''), 16);
323
- this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil', width, color });
324
- }
325
- } else if (this.toolbar.currentDrawTool === 'marker') {
326
- movePresetToRow(markerPresetEl, container);
327
- const first = container.querySelector('.moodboard-draw__btn');
328
- if (first) {
329
- container.querySelectorAll('.moodboard-draw__btn--active').forEach((el) => el.classList.remove('moodboard-draw__btn--active'));
330
- first.classList.add('moodboard-draw__btn--active');
331
- const color = parseInt(swatches[0].color.replace('#', ''), 16);
332
- this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: 'marker', color, width: 8 });
333
- }
334
- } else if (this.toolbar.currentDrawTool === 'eraser') {
335
- movePresetToRow(eraserPresetEl, container);
336
- this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: 'eraser' });
337
- }
338
- };
329
+ colorSection.appendChild(colorGrid);
330
+ panel.appendChild(colorSection);
339
331
 
340
- grid.appendChild(row1);
341
- grid.appendChild(row2);
342
- this.toolbar.drawPopupEl.appendChild(grid);
332
+ this.toolbar.drawPopupEl.appendChild(panel);
343
333
  this.toolbar.container.appendChild(this.toolbar.drawPopupEl);
344
- const pencilBtn = row1.querySelector('.moodboard-draw__btn--pencil-tool');
334
+
335
+ // Метод для скрытия/показа секций в зависимости от инструмента
336
+ this._updateDrawPanelForTool = (tool) => {
337
+ const showControls = tool !== 'eraser';
338
+ thicknessSection.style.display = '';
339
+ colorSection.style.display = showControls ? '' : 'none';
340
+ };
341
+
342
+ // Начальное состояние — карандаш активен
343
+ const pencilBtn = toolRow.querySelector('.moodboard-draw__btn--pencil-tool');
345
344
  if (pencilBtn) pencilBtn.classList.add('moodboard-draw__btn--active');
346
345
  this.toolbar.currentDrawTool = 'pencil';
347
- this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil' });
348
- this.toolbar.buildDrawPresets(row2);
346
+ this.toolbar.eventBus.emit(Events.Draw.BrushSet, { mode: 'pencil', width: 2, color: 0x111827 });
347
+ this._updateDrawPanelForTool('pencil');
348
+ // Выделяем первый цвет (чёрный)
349
+ const firstColorBtn = colorGrid.querySelector('.moodboard-draw__color-btn');
350
+ if (firstColorBtn) firstColorBtn.classList.add('moodboard-draw__color-btn--active');
349
351
  }
350
352
 
351
353
  toggleDrawPopup(anchorButton) {
@@ -11,6 +11,8 @@ export class ToolbarRenderer {
11
11
 
12
12
  const newTools = [
13
13
  { id: 'select', iconName: 'select', title: 'Инструмент выделения (V)', type: 'activate-select' },
14
+ { id: 'lasso', iconName: 'lasso', title: 'Лассо — произвольное выделение', type: 'activate-lasso' },
15
+ { id: 'laser', iconName: 'laser', title: 'Лазерная указка', type: 'activate-laser' },
14
16
  { id: 'pan', iconName: 'pan', title: 'Панорамирование (Пробел)', type: 'activate-pan' },
15
17
  { id: 'divider', type: 'divider' },
16
18
  { id: 'text-add', iconName: 'text-add', title: 'Добавить текст', type: 'text-add' },
@@ -19,8 +21,13 @@ export class ToolbarRenderer {
19
21
  // { id: 'image2', iconName: 'image', title: 'Добавить картинку', type: 'image2-add' },
20
22
  { id: 'shapes', iconName: 'shapes', title: 'Фигуры', type: 'custom-shapes' },
21
23
  { id: 'pencil', iconName: 'pencil', title: 'Рисование', type: 'custom-draw' },
24
+ { id: 'connector', iconName: 'connector', title: 'Коннектор', type: 'connector-add' },
25
+ ...(this.toolbar.enableComments
26
+ ? [{ id: 'comments', iconName: 'comments', title: 'Комментарий', type: 'custom-comments' }]
27
+ : []),
22
28
  { id: 'attachments', iconName: 'attachments', title: 'Файлы', type: 'custom-attachments' },
23
- { id: 'emoji', iconName: 'emoji', title: 'Эмоджи', type: 'custom-emoji' }
29
+ { id: 'emoji', iconName: 'emoji', title: 'Эмоджи', type: 'custom-emoji' },
30
+ { id: 'reactions', iconName: 'reactions', title: 'Реакции', type: 'custom-reactions' }
24
31
  ];
25
32
 
26
33
  const existingTools = [
@@ -46,6 +53,7 @@ export class ToolbarRenderer {
46
53
  this.toolbar.createShapesPopup();
47
54
  this.toolbar.createDrawPopup();
48
55
  this.toolbar.createEmojiPopup();
56
+ this.toolbar.createReactionsPopup();
49
57
  this.toolbar.createFramePopup();
50
58
 
51
59
  this.toolbar.eventBus.on(Events.Tool.Activated, this.toolbar._toolActivatedHandler);