@sequent-org/moodboard 1.4.55 → 1.4.57
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 +1 -1
- package/src/core/commands/PasteObjectCommand.js +7 -1
- package/src/core/commands/UpdateContentCommand.js +49 -0
- package/src/core/flows/ObjectLifecycleFlow.js +10 -1
- package/src/initNoBundler.js +29 -5
- package/src/tools/object-tools/TextTool.js +9 -3
- package/src/tools/object-tools/selection/SelectInputRouter.js +1 -1
- package/src/tools/object-tools/selection/TextEditorInteractionController.js +86 -11
- package/src/tools/object-tools/selection/TextInlineEditorController.js +2 -2
- package/src/ui/FramePropertiesPanel.js +7 -0
- package/src/ui/HtmlTextLayer.js +79 -6
- package/src/ui/ImagePropertiesPanel.js +7 -0
- package/src/ui/TextPropertiesPanel.js +163 -3
- package/src/ui/styles/panels.css +471 -31
- package/src/ui/styles/workspace.css +31 -7
- package/src/ui/text-properties/TextFormatControls.js +351 -41
- package/src/ui/text-properties/TextLinkControl.js +255 -0
- package/src/ui/text-properties/TextLockMoreControls.js +173 -0
- package/src/ui/text-properties/TextPropertiesPanelBindings.js +105 -2
- package/src/ui/text-properties/TextPropertiesPanelMapper.js +23 -0
- package/src/ui/text-properties/TextPropertiesPanelRenderer.js +348 -56
- package/src/ui/text-properties/TextPropertiesPanelState.js +6 -0
- package/src/utils/styleLoader.js +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { getObjectProperties } from './TextPropertiesPanelMapper.js';
|
|
2
|
+
|
|
3
|
+
const ICON_LINK = `<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M10.5893 15.3024L9.4108 16.4809C7.78361 18.1081 5.14542 18.1081 3.51824 16.4809C1.89106 14.8537 1.89106 12.2155 3.51824 10.5883L4.69675 9.40982M15.3034 10.5883L16.4819 9.40982C18.109 7.78264 18.109 5.14445 16.4819 3.51726C14.8547 1.89008 12.2165 1.89008 10.5893 3.51726L9.4108 4.69577M7.08339 12.9157L12.9167 7.08239"></path></svg>`;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Создаёт кнопку «Ссылка» с всплывающей формой ввода URL.
|
|
7
|
+
*
|
|
8
|
+
* Поведение:
|
|
9
|
+
* - Кнопка disabled пока у объекта выключен MD-режим.
|
|
10
|
+
* - Клик сохраняет selection в textarea, показывает форму рядом с textarea.
|
|
11
|
+
* - Сабмит: если было выделение — [selected](url), иначе [весь текст](url).
|
|
12
|
+
* - Автоматически добавляет https:// если схема не указана.
|
|
13
|
+
* - Enter = OK, Esc = отмена, клик вне = отмена.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} panelInstance - экземпляр TextPropertiesPanel
|
|
16
|
+
* @returns {HTMLButtonElement} кнопка для вставки в панель
|
|
17
|
+
*/
|
|
18
|
+
export function createLinkButton(panelInstance) {
|
|
19
|
+
const btn = document.createElement('button');
|
|
20
|
+
btn.className = 'ipp-btn tpp-link-btn';
|
|
21
|
+
btn.title = 'Добавить ссылку';
|
|
22
|
+
btn.id = 'tpp-btn-link';
|
|
23
|
+
btn.dataset.id = 'tpp-btn-link';
|
|
24
|
+
btn.disabled = false;
|
|
25
|
+
btn.innerHTML = ICON_LINK;
|
|
26
|
+
|
|
27
|
+
btn.addEventListener('click', (e) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
e.stopPropagation();
|
|
30
|
+
_openLinkForm(panelInstance, btn);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
panelInstance._tppBtnLink = btn;
|
|
34
|
+
return btn;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Обновляет disabled-состояние кнопки в зависимости от MD-флага объекта.
|
|
39
|
+
* При включённом MD-режиме кнопка недоступна — ссылки добавляются markdown-синтаксисом вручную.
|
|
40
|
+
* Вызывается из TextPropertiesPanelBindings при смене markdownToggle.
|
|
41
|
+
*/
|
|
42
|
+
export function updateLinkButtonState(panelInstance, isMarkdown) {
|
|
43
|
+
if (!panelInstance._tppBtnLink) return;
|
|
44
|
+
const btn = panelInstance._tppBtnLink;
|
|
45
|
+
btn.disabled = isMarkdown;
|
|
46
|
+
btn.title = isMarkdown
|
|
47
|
+
? 'Ссылки в MD-режиме добавляются через синтаксис [текст](url)'
|
|
48
|
+
: 'Добавить ссылку к тексту';
|
|
49
|
+
btn.classList.toggle('tpp-link-btn--disabled', isMarkdown);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _getActiveTextarea(panelInstance) {
|
|
53
|
+
return panelInstance.core?.selectTool?.textEditor?.textarea ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _normalizeUrl(raw) {
|
|
57
|
+
const trimmed = raw.trim();
|
|
58
|
+
if (!trimmed) return '';
|
|
59
|
+
if (/^https?:\/\//i.test(trimmed) || /^mailto:/i.test(trimmed)) return trimmed;
|
|
60
|
+
return 'https://' + trimmed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _openLinkForm(panelInstance, triggerBtn) {
|
|
64
|
+
// Закрываем предыдущую форму, если она уже открыта
|
|
65
|
+
_closeExistingForm();
|
|
66
|
+
|
|
67
|
+
const textarea = _getActiveTextarea(panelInstance);
|
|
68
|
+
|
|
69
|
+
// Запоминаем id выделенного объекта ДО показа формы — фокус ухода в URL-input
|
|
70
|
+
// или клик мимо canvas может сбросить выделение и panelInstance.currentId станет null
|
|
71
|
+
const savedObjectId = panelInstance.currentId;
|
|
72
|
+
|
|
73
|
+
// Запоминаем выделение ДО показа формы (фокус уйдёт на input)
|
|
74
|
+
let savedStart = 0;
|
|
75
|
+
let savedEnd = 0;
|
|
76
|
+
let savedText = '';
|
|
77
|
+
if (textarea) {
|
|
78
|
+
savedStart = textarea.selectionStart ?? 0;
|
|
79
|
+
savedEnd = textarea.selectionEnd ?? 0;
|
|
80
|
+
savedText = textarea.value ?? '';
|
|
81
|
+
} else {
|
|
82
|
+
// Объект только выделен (без активного редактора) — применяем ссылку ко всему содержимому
|
|
83
|
+
const props = getObjectProperties(panelInstance.eventBus, savedObjectId);
|
|
84
|
+
savedText = props?.content ?? '';
|
|
85
|
+
}
|
|
86
|
+
const hasSelection = savedEnd > savedStart;
|
|
87
|
+
|
|
88
|
+
const form = _buildForm(panelInstance, textarea, {
|
|
89
|
+
savedStart,
|
|
90
|
+
savedEnd,
|
|
91
|
+
savedText,
|
|
92
|
+
hasSelection,
|
|
93
|
+
triggerBtn,
|
|
94
|
+
savedObjectId,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
document.body.appendChild(form);
|
|
98
|
+
|
|
99
|
+
// Позиционируем форму рядом с textarea или рядом с кнопкой
|
|
100
|
+
_positionForm(form, textarea, triggerBtn);
|
|
101
|
+
|
|
102
|
+
const urlInput = form.querySelector('.tpp-link-input');
|
|
103
|
+
urlInput.focus();
|
|
104
|
+
|
|
105
|
+
// Закрытие по клику вне формы
|
|
106
|
+
const closeOnOutside = (ev) => {
|
|
107
|
+
if (!form.contains(ev.target) && ev.target !== triggerBtn) {
|
|
108
|
+
_closeForm(form, closeOnOutside);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
setTimeout(() => document.addEventListener('mousedown', closeOnOutside), 0);
|
|
112
|
+
form._closeOnOutside = closeOnOutside;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _buildForm(panelInstance, textarea, ctx) {
|
|
116
|
+
const { savedStart, savedEnd, savedText, hasSelection, triggerBtn, savedObjectId } = ctx;
|
|
117
|
+
|
|
118
|
+
const form = document.createElement('div');
|
|
119
|
+
form.className = 'tpp-link-form';
|
|
120
|
+
form.id = 'tpp-link-form-active';
|
|
121
|
+
|
|
122
|
+
const label = document.createElement('span');
|
|
123
|
+
label.className = 'tpp-link-form-label';
|
|
124
|
+
label.textContent = hasSelection ? `Ссылка для «${_truncate(savedText.slice(savedStart, savedEnd), 24)}»` : 'URL ссылки';
|
|
125
|
+
form.appendChild(label);
|
|
126
|
+
|
|
127
|
+
const row = document.createElement('div');
|
|
128
|
+
row.className = 'tpp-link-form-row';
|
|
129
|
+
|
|
130
|
+
const urlInput = document.createElement('input');
|
|
131
|
+
urlInput.type = 'url';
|
|
132
|
+
urlInput.placeholder = 'https://example.com';
|
|
133
|
+
urlInput.className = 'tpp-link-input';
|
|
134
|
+
urlInput.autocomplete = 'url';
|
|
135
|
+
|
|
136
|
+
const okBtn = document.createElement('button');
|
|
137
|
+
okBtn.type = 'button';
|
|
138
|
+
okBtn.className = 'tpp-link-form-ok';
|
|
139
|
+
okBtn.textContent = 'OK';
|
|
140
|
+
|
|
141
|
+
row.appendChild(urlInput);
|
|
142
|
+
row.appendChild(okBtn);
|
|
143
|
+
form.appendChild(row);
|
|
144
|
+
|
|
145
|
+
const submit = () => {
|
|
146
|
+
const url = _normalizeUrl(urlInput.value);
|
|
147
|
+
if (!url) {
|
|
148
|
+
urlInput.classList.add('tpp-link-input--error');
|
|
149
|
+
urlInput.focus();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
urlInput.classList.remove('tpp-link-input--error');
|
|
153
|
+
|
|
154
|
+
// Запись ссылки как диапазона в properties.links (не markdown-синтаксис).
|
|
155
|
+
// objectId передаём явно — currentId на панели мог сброситься, пока пользователь
|
|
156
|
+
// печатал URL и фокус был в input.
|
|
157
|
+
if (typeof panelInstance._addLink === 'function') {
|
|
158
|
+
const contentLen = (savedText || '').length;
|
|
159
|
+
const start = hasSelection ? savedStart : 0;
|
|
160
|
+
const end = hasSelection ? savedEnd : contentLen;
|
|
161
|
+
panelInstance._addLink(url, start, end, savedObjectId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_closeForm(form, form._closeOnOutside);
|
|
165
|
+
// Возвращаем фокус в textarea
|
|
166
|
+
if (textarea) {
|
|
167
|
+
try { textarea.focus(); } catch (_) {}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
urlInput.addEventListener('keydown', (e) => {
|
|
172
|
+
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
173
|
+
if (e.key === 'Escape') { e.preventDefault(); _closeForm(form, form._closeOnOutside); }
|
|
174
|
+
urlInput.classList.remove('tpp-link-input--error');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
okBtn.addEventListener('click', (e) => {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
e.stopPropagation();
|
|
180
|
+
submit();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return form;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function _insertLink(textarea, { savedStart, savedEnd, savedText, hasSelection, url }) {
|
|
187
|
+
let newValue;
|
|
188
|
+
let newCursorPos;
|
|
189
|
+
|
|
190
|
+
if (hasSelection) {
|
|
191
|
+
const selected = savedText.slice(savedStart, savedEnd);
|
|
192
|
+
const mdLink = `[${selected}](${url})`;
|
|
193
|
+
newValue = savedText.slice(0, savedStart) + mdLink + savedText.slice(savedEnd);
|
|
194
|
+
newCursorPos = savedStart + mdLink.length;
|
|
195
|
+
} else {
|
|
196
|
+
const allText = savedText || url;
|
|
197
|
+
const mdLink = `[${allText}](${url})`;
|
|
198
|
+
newValue = mdLink;
|
|
199
|
+
newCursorPos = mdLink.length;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
textarea.value = newValue;
|
|
203
|
+
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
204
|
+
|
|
205
|
+
// Сигналим autoSize и commit-пайплайну
|
|
206
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function _positionForm(form, textarea, triggerBtn) {
|
|
210
|
+
// Временно делаем видимым для измерения
|
|
211
|
+
form.style.visibility = 'hidden';
|
|
212
|
+
form.style.display = 'flex';
|
|
213
|
+
|
|
214
|
+
const anchor = textarea ?? triggerBtn;
|
|
215
|
+
const rect = anchor.getBoundingClientRect();
|
|
216
|
+
const formH = form.offsetHeight || 80;
|
|
217
|
+
const winH = window.innerHeight;
|
|
218
|
+
|
|
219
|
+
let top;
|
|
220
|
+
if (rect.bottom + formH + 8 < winH) {
|
|
221
|
+
top = rect.bottom + 8;
|
|
222
|
+
} else {
|
|
223
|
+
top = rect.top - formH - 8;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const left = Math.max(8, Math.min(rect.left, window.innerWidth - 280));
|
|
227
|
+
|
|
228
|
+
form.style.top = `${Math.round(top)}px`;
|
|
229
|
+
form.style.left = `${Math.round(left)}px`;
|
|
230
|
+
form.style.visibility = '';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function _closeForm(form, outsideHandler) {
|
|
234
|
+
if (outsideHandler) {
|
|
235
|
+
document.removeEventListener('mousedown', outsideHandler);
|
|
236
|
+
}
|
|
237
|
+
if (form && form.parentNode) {
|
|
238
|
+
form.parentNode.removeChild(form);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function _closeExistingForm() {
|
|
243
|
+
const existing = document.getElementById('tpp-link-form-active');
|
|
244
|
+
if (existing) {
|
|
245
|
+
if (existing._closeOnOutside) {
|
|
246
|
+
document.removeEventListener('mousedown', existing._closeOnOutside);
|
|
247
|
+
}
|
|
248
|
+
existing.parentNode?.removeChild(existing);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _truncate(str, max) {
|
|
253
|
+
if (str.length <= max) return str;
|
|
254
|
+
return str.slice(0, max) + '…';
|
|
255
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Events } from '../../core/events/Events.js';
|
|
2
|
+
import { createLinkButton } from './TextLinkControl.js';
|
|
3
|
+
|
|
4
|
+
const ICON_LOCK = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5.75 11.75C5.75 11.1977 6.19772 10.75 6.75 10.75H17.25C17.8023 10.75 18.25 11.1977 18.25 11.75V17.25C18.25 18.3546 17.3546 19.25 16.25 19.25H7.75C6.64543 19.25 5.75 18.3546 5.75 17.25V11.75Z"></path><path d="M7.75008 10.5V10.3427C7.75008 8.78147 7.65615 7.04125 8.74654 5.9239C9.36837 5.2867 10.3746 4.75 12.0001 4.75C13.6256 4.75 14.6318 5.2867 15.2536 5.9239C16.344 7.04125 16.2501 8.78147 16.2501 10.3427V10.5"></path><path d="M12 14.25L12 15.75"></path></svg>`;
|
|
5
|
+
const ICON_UNLOCK = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5.75 11.75C5.75 11.1977 6.19772 10.75 6.75 10.75H17.25C17.8023 10.75 18.25 11.1977 18.25 11.75V17.25C18.25 18.3546 17.3546 19.25 16.25 19.25H7.75C6.64543 19.25 5.75 18.3546 5.75 17.25V11.75Z"></path><path d="M7.74972 10.5V9.84343C7.74972 8.61493 7.70065 7.29883 8.42388 6.30578C8.99834 5.51699 10.0565 4.75 11.9997 4.75C13.9997 4.75 15.2497 6.25 15.2497 6.25"></path><path d="M12 14.25L12 15.75"></path></svg>`;
|
|
6
|
+
const ICON_MORE = `<svg width="24" height="24" viewBox="0 0 22 22" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M16.2246 12.375C16.984 12.375 17.5996 11.7594 17.5996 11C17.5996 10.2406 16.984 9.625 16.2246 9.625C15.4652 9.625 14.8496 10.2406 14.8496 11C14.8496 11.7594 15.4652 12.375 16.2246 12.375Z"></path><path d="M11 12.375C11.7594 12.375 12.375 11.7594 12.375 11C12.375 10.2406 11.7594 9.625 11 9.625C10.2406 9.625 9.625 10.2406 9.625 11C9.625 11.7594 10.2406 12.375 11 12.375Z"></path><path d="M5.77539 12.375C6.53478 12.375 7.15039 11.7594 7.15039 11C7.15039 10.2406 6.53478 9.625 5.77539 9.625C5.016 9.625 4.40039 10.2406 4.40039 11C4.40039 11.7594 5.016 12.375 5.77539 12.375Z"></path></svg>`;
|
|
7
|
+
const ICON_COMMENT = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Добавляет к text-properties-panel кнопку lock + кнопку more с дропдауном.
|
|
11
|
+
* Поведение идентично lock / more в ImagePropertiesPanel; идентификаторы с префиксом tpp-.
|
|
12
|
+
*/
|
|
13
|
+
export function createTextLockMoreControls(panelInstance, panel) {
|
|
14
|
+
// Все уже добавленные дочерние элементы скрываются при locked
|
|
15
|
+
panelInstance._lockableEls = Array.from(panel.children);
|
|
16
|
+
panelInstance._tppLockIcon = ICON_LOCK;
|
|
17
|
+
panelInstance._tppUnlockIcon = ICON_UNLOCK;
|
|
18
|
+
|
|
19
|
+
const linkBtn = createLinkButton(panelInstance);
|
|
20
|
+
panel.appendChild(linkBtn);
|
|
21
|
+
|
|
22
|
+
const divider1 = document.createElement('div');
|
|
23
|
+
divider1.className = 'ipp-divider';
|
|
24
|
+
panel.appendChild(divider1);
|
|
25
|
+
|
|
26
|
+
const lockBtn = document.createElement('button');
|
|
27
|
+
lockBtn.className = 'ipp-btn';
|
|
28
|
+
lockBtn.title = 'Заблокировать';
|
|
29
|
+
lockBtn.id = 'tpp-btn-lock';
|
|
30
|
+
lockBtn.dataset.id = 'tpp-btn-lock';
|
|
31
|
+
lockBtn.innerHTML = ICON_LOCK;
|
|
32
|
+
lockBtn.addEventListener('click', (e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
e.stopPropagation();
|
|
35
|
+
panelInstance._toggleLocked();
|
|
36
|
+
});
|
|
37
|
+
panel.appendChild(lockBtn);
|
|
38
|
+
panelInstance._tppBtnLock = lockBtn;
|
|
39
|
+
|
|
40
|
+
const divider = document.createElement('div');
|
|
41
|
+
divider.className = 'ipp-divider';
|
|
42
|
+
panel.appendChild(divider);
|
|
43
|
+
|
|
44
|
+
const moreWrapper = _makeMoreWrapper(panelInstance);
|
|
45
|
+
panel.appendChild(moreWrapper);
|
|
46
|
+
panelInstance._tppMoreGroup = moreWrapper;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _makeMoreWrapper(panelInstance) {
|
|
50
|
+
const wrapper = document.createElement('div');
|
|
51
|
+
wrapper.className = 'tpp-btn-wrapper';
|
|
52
|
+
wrapper.dataset.id = 'tpp-btn-wrapper';
|
|
53
|
+
wrapper.style.cssText = 'position:relative;display:inline-flex;';
|
|
54
|
+
|
|
55
|
+
const mainBtn = document.createElement('button');
|
|
56
|
+
mainBtn.className = 'ipp-btn';
|
|
57
|
+
mainBtn.title = 'Ещё';
|
|
58
|
+
mainBtn.id = 'tpp-btn-more';
|
|
59
|
+
mainBtn.dataset.id = 'tpp-btn-more';
|
|
60
|
+
mainBtn.innerHTML = ICON_MORE;
|
|
61
|
+
|
|
62
|
+
const dropdown = document.createElement('div');
|
|
63
|
+
dropdown.className = 'tpp-more-dropdown';
|
|
64
|
+
|
|
65
|
+
const items = [
|
|
66
|
+
{ id: 'copy', label: 'Копировать', shortcut: 'Ctrl+C' },
|
|
67
|
+
{ id: 'copy-link', label: 'Копировать ссылку на объект' },
|
|
68
|
+
{ divider: true },
|
|
69
|
+
{ id: 'bring-front', label: 'На передний план', shortcut: ']' },
|
|
70
|
+
{ id: 'bring-forward', label: 'Переместить вперёд', shortcut: 'Ctrl+]' },
|
|
71
|
+
{ id: 'send-backward', label: 'Переместить назад', shortcut: 'Ctrl+[' },
|
|
72
|
+
{ id: 'send-back', label: 'На задний план', shortcut: '[' },
|
|
73
|
+
{ divider: true },
|
|
74
|
+
{ id: 'lock', label: 'Заблокировать', shortcut: 'Ctrl+Shift+L' },
|
|
75
|
+
{ id: 'duplicate', label: 'Дублировать', shortcut: 'Ctrl+D' },
|
|
76
|
+
{ divider: true },
|
|
77
|
+
{ id: 'add-comment', label: 'Добавить комментарий', icon: ICON_COMMENT },
|
|
78
|
+
{ divider: true },
|
|
79
|
+
{ id: 'delete', label: 'Удалить', shortcut: 'Delete' },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
items.forEach((item) => {
|
|
83
|
+
if (item.divider) {
|
|
84
|
+
const div = document.createElement('div');
|
|
85
|
+
div.className = 'tpp-dropdown-divider';
|
|
86
|
+
dropdown.appendChild(div);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const btn = document.createElement('button');
|
|
91
|
+
btn.className = 'tpp-dropdown-item';
|
|
92
|
+
if (item.id) btn.dataset.id = `tpp-more-${item.id}`;
|
|
93
|
+
|
|
94
|
+
if (item.icon) {
|
|
95
|
+
const iconSpan = document.createElement('span');
|
|
96
|
+
iconSpan.className = 'tpp-dropdown-icon';
|
|
97
|
+
iconSpan.innerHTML = item.icon;
|
|
98
|
+
btn.appendChild(iconSpan);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const labelSpan = document.createElement('span');
|
|
102
|
+
labelSpan.textContent = item.label;
|
|
103
|
+
btn.appendChild(labelSpan);
|
|
104
|
+
|
|
105
|
+
if (item.shortcut) {
|
|
106
|
+
const shortcutSpan = document.createElement('span');
|
|
107
|
+
shortcutSpan.className = 'tpp-dropdown-item-shortcut';
|
|
108
|
+
shortcutSpan.textContent = item.shortcut;
|
|
109
|
+
btn.appendChild(shortcutSpan);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
btn.addEventListener('click', (e) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
|
|
116
|
+
const { eventBus, currentId } = panelInstance;
|
|
117
|
+
|
|
118
|
+
if (item.id === 'copy') {
|
|
119
|
+
eventBus.emit(Events.Keyboard.Copy);
|
|
120
|
+
} else if (item.id === 'bring-front') {
|
|
121
|
+
if (currentId) eventBus.emit(Events.UI.LayerBringToFront, { objectId: currentId });
|
|
122
|
+
} else if (item.id === 'bring-forward') {
|
|
123
|
+
if (currentId) eventBus.emit(Events.UI.LayerBringForward, { objectId: currentId });
|
|
124
|
+
} else if (item.id === 'send-backward') {
|
|
125
|
+
if (currentId) eventBus.emit(Events.UI.LayerSendBackward, { objectId: currentId });
|
|
126
|
+
} else if (item.id === 'send-back') {
|
|
127
|
+
if (currentId) eventBus.emit(Events.UI.LayerSendToBack, { objectId: currentId });
|
|
128
|
+
} else if (item.id === 'lock') {
|
|
129
|
+
panelInstance._toggleLocked();
|
|
130
|
+
} else if (item.id === 'duplicate') {
|
|
131
|
+
panelInstance._duplicateText();
|
|
132
|
+
} else if (item.id === 'add-comment') {
|
|
133
|
+
if (currentId) eventBus.emit(Events.Comment.OpenImageDraft, { objectId: currentId });
|
|
134
|
+
} else if (item.id === 'delete') {
|
|
135
|
+
if (currentId) eventBus.emit(Events.Tool.ObjectsDelete, { objects: [currentId] });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
dropdown.classList.remove('is-open');
|
|
139
|
+
mainBtn.classList.remove('is-active');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
if (item.id === 'lock') {
|
|
143
|
+
panelInstance._moreLockLabel = labelSpan;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
dropdown.appendChild(btn);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
mainBtn.addEventListener('click', (e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
e.stopPropagation();
|
|
152
|
+
const isOpen = dropdown.classList.contains('is-open');
|
|
153
|
+
|
|
154
|
+
document.querySelectorAll('.tpp-more-dropdown.is-open').forEach((el) => el.classList.remove('is-open'));
|
|
155
|
+
document.querySelectorAll('.ipp-btn.is-active').forEach((el) => el.classList.remove('is-active'));
|
|
156
|
+
|
|
157
|
+
if (!isOpen) {
|
|
158
|
+
const rect = mainBtn.getBoundingClientRect();
|
|
159
|
+
dropdown.style.top = (rect.bottom + 6) + 'px';
|
|
160
|
+
dropdown.style.left = (rect.right - 220) + 'px';
|
|
161
|
+
dropdown.classList.add('is-open');
|
|
162
|
+
requestAnimationFrame(() => {
|
|
163
|
+
const left = Math.max(4, rect.right - dropdown.offsetWidth);
|
|
164
|
+
dropdown.style.left = left + 'px';
|
|
165
|
+
});
|
|
166
|
+
mainBtn.classList.add('is-active');
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
wrapper.appendChild(mainBtn);
|
|
171
|
+
wrapper.appendChild(dropdown);
|
|
172
|
+
return wrapper;
|
|
173
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { updateLinkButtonState } from './TextLinkControl.js';
|
|
2
|
+
|
|
1
3
|
function hidePresetTicks(buttons) {
|
|
2
4
|
buttons.forEach((button) => {
|
|
3
5
|
const tick = button.querySelector('i');
|
|
@@ -7,19 +9,80 @@ function hidePresetTicks(buttons) {
|
|
|
7
9
|
});
|
|
8
10
|
}
|
|
9
11
|
|
|
12
|
+
function setFontDropdownOpen(panel, isOpen) {
|
|
13
|
+
if (!panel.fontDropdown || !panel.fontSelect) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
panel.fontDropdown.classList.toggle('is-open', isOpen);
|
|
17
|
+
panel.fontSelect.classList.toggle('is-active', isOpen);
|
|
18
|
+
panel.fontSelect.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export function bindTextPropertiesPanelControls(panel) {
|
|
11
22
|
if (panel._bindingsAttached) {
|
|
12
23
|
return;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
panel.fontSelect.addEventListener('
|
|
16
|
-
|
|
26
|
+
panel.fontSelect.addEventListener('click', (event) => {
|
|
27
|
+
event.stopPropagation();
|
|
28
|
+
const willOpen = !panel.fontDropdown.classList.contains('is-open');
|
|
29
|
+
setFontDropdownOpen(panel, willOpen);
|
|
17
30
|
});
|
|
18
31
|
|
|
32
|
+
panel.fontSelect.addEventListener('keydown', (event) => {
|
|
33
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
const willOpen = !panel.fontDropdown.classList.contains('is-open');
|
|
36
|
+
setFontDropdownOpen(panel, willOpen);
|
|
37
|
+
} else if (event.key === 'Escape') {
|
|
38
|
+
setFontDropdownOpen(panel, false);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
panel.fontDropdown.querySelectorAll('.font-dropdown__item').forEach((item) => {
|
|
43
|
+
item.addEventListener('click', (event) => {
|
|
44
|
+
event.stopPropagation();
|
|
45
|
+
const value = item.dataset.value;
|
|
46
|
+
panel.fontSelect.value = value;
|
|
47
|
+
panel._changeFontFamily(value);
|
|
48
|
+
setFontDropdownOpen(panel, false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
panel._onFontDocumentClick = (event) => {
|
|
53
|
+
if (!panel._fontSelectWrapper || !event.target) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!panel._fontSelectWrapper.contains(event.target)) {
|
|
57
|
+
setFontDropdownOpen(panel, false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
document.addEventListener('click', panel._onFontDocumentClick);
|
|
61
|
+
|
|
19
62
|
panel.fontSizeSelect.addEventListener('change', (event) => {
|
|
20
63
|
panel._changeFontSize(parseInt(event.target.value, 10));
|
|
21
64
|
});
|
|
22
65
|
|
|
66
|
+
if (panel.fontSizeUpBtn) {
|
|
67
|
+
panel.fontSizeUpBtn.addEventListener('click', () => {
|
|
68
|
+
const select = panel.fontSizeSelect;
|
|
69
|
+
if (select.selectedIndex < select.options.length - 1) {
|
|
70
|
+
select.selectedIndex++;
|
|
71
|
+
select.dispatchEvent(new Event('change'));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (panel.fontSizeDownBtn) {
|
|
77
|
+
panel.fontSizeDownBtn.addEventListener('click', () => {
|
|
78
|
+
const select = panel.fontSizeSelect;
|
|
79
|
+
if (select.selectedIndex > 0) {
|
|
80
|
+
select.selectedIndex--;
|
|
81
|
+
select.dispatchEvent(new Event('change'));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
23
86
|
panel.currentColorButton.addEventListener('click', (event) => {
|
|
24
87
|
event.stopPropagation();
|
|
25
88
|
panel._toggleColorDropdown();
|
|
@@ -47,6 +110,35 @@ export function bindTextPropertiesPanelControls(panel) {
|
|
|
47
110
|
};
|
|
48
111
|
document.addEventListener('click', panel._onColorDocumentClick);
|
|
49
112
|
|
|
113
|
+
if (panel.currentHighlightButton) {
|
|
114
|
+
panel.currentHighlightButton.addEventListener('click', (event) => {
|
|
115
|
+
event.stopPropagation();
|
|
116
|
+
panel._toggleHighlightDropdown();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
panel._highlightPresetButtons.forEach((button) => {
|
|
120
|
+
button.addEventListener('click', () => {
|
|
121
|
+
hidePresetTicks(panel._highlightPresetButtons);
|
|
122
|
+
const tick = button.querySelector('i');
|
|
123
|
+
if (tick) {
|
|
124
|
+
tick.style.display = 'block';
|
|
125
|
+
}
|
|
126
|
+
panel._selectHighlightColor(button.dataset.colorValue);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
panel.highlightInput.addEventListener('change', (event) => {
|
|
131
|
+
panel._selectHighlightColor(event.target.value);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
panel._onHighlightDocumentClick = (event) => {
|
|
135
|
+
if (!panel._highlightSelectorContainer || !event.target || !panel._highlightSelectorContainer.contains(event.target)) {
|
|
136
|
+
panel._hideHighlightDropdown();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
document.addEventListener('click', panel._onHighlightDocumentClick);
|
|
140
|
+
}
|
|
141
|
+
|
|
50
142
|
panel.currentBgColorButton.addEventListener('click', (event) => {
|
|
51
143
|
event.stopPropagation();
|
|
52
144
|
panel._toggleBgColorDropdown();
|
|
@@ -77,6 +169,7 @@ export function bindTextPropertiesPanelControls(panel) {
|
|
|
77
169
|
if (panel.markdownToggle) {
|
|
78
170
|
panel.markdownToggle.addEventListener('change', (event) => {
|
|
79
171
|
panel._changeMarkdown(event.target.checked);
|
|
172
|
+
updateLinkButtonState(panel, event.target.checked);
|
|
80
173
|
});
|
|
81
174
|
}
|
|
82
175
|
|
|
@@ -110,10 +203,20 @@ export function unbindTextPropertiesPanelControls(panel) {
|
|
|
110
203
|
panel._onColorDocumentClick = null;
|
|
111
204
|
}
|
|
112
205
|
|
|
206
|
+
if (panel._onHighlightDocumentClick) {
|
|
207
|
+
document.removeEventListener('click', panel._onHighlightDocumentClick);
|
|
208
|
+
panel._onHighlightDocumentClick = null;
|
|
209
|
+
}
|
|
210
|
+
|
|
113
211
|
if (panel._onBgDocumentClick) {
|
|
114
212
|
document.removeEventListener('click', panel._onBgDocumentClick);
|
|
115
213
|
panel._onBgDocumentClick = null;
|
|
116
214
|
}
|
|
117
215
|
|
|
216
|
+
if (panel._onFontDocumentClick) {
|
|
217
|
+
document.removeEventListener('click', panel._onFontDocumentClick);
|
|
218
|
+
panel._onFontDocumentClick = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
118
221
|
panel._bindingsAttached = false;
|
|
119
222
|
}
|
|
@@ -97,6 +97,7 @@ export function getControlValuesFromProperties(properties) {
|
|
|
97
97
|
fontFamily: properties.fontFamily || 'Roboto, Arial, sans-serif',
|
|
98
98
|
fontSize: String(properties.fontSize || 18),
|
|
99
99
|
color: properties.color || '#000000',
|
|
100
|
+
highlightColor: properties.highlightColor !== undefined ? properties.highlightColor : 'transparent',
|
|
100
101
|
backgroundColor: properties.backgroundColor !== undefined ? properties.backgroundColor : 'transparent',
|
|
101
102
|
markdown: properties.markdown === true,
|
|
102
103
|
bold: properties.bold === true,
|
|
@@ -114,6 +115,7 @@ export function getFallbackControlValues() {
|
|
|
114
115
|
fontFamily: 'Arial, sans-serif',
|
|
115
116
|
fontSize: '18',
|
|
116
117
|
color: '#000000',
|
|
118
|
+
highlightColor: 'transparent',
|
|
117
119
|
backgroundColor: 'transparent',
|
|
118
120
|
markdown: false,
|
|
119
121
|
bold: false,
|
|
@@ -150,6 +152,12 @@ export function buildBackgroundColorUpdate(backgroundColor) {
|
|
|
150
152
|
};
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
export function buildHighlightColorUpdate(highlightColor) {
|
|
156
|
+
return {
|
|
157
|
+
properties: { highlightColor },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
export function buildMarkdownUpdate(markdown) {
|
|
154
162
|
return {
|
|
155
163
|
properties: { markdown },
|
|
@@ -182,6 +190,21 @@ export function applyTextAppearanceToDom(objectId, properties) {
|
|
|
182
190
|
htmlElement.style.backgroundColor = properties.backgroundColor;
|
|
183
191
|
}
|
|
184
192
|
}
|
|
193
|
+
if (properties.highlightColor !== undefined) {
|
|
194
|
+
// Устанавливаем CSS-переменную --highlight-color на элементе текста,
|
|
195
|
+
// чтобы она могла использоваться для подкраски выделенного фрагмента
|
|
196
|
+
if (properties.highlightColor === 'transparent') {
|
|
197
|
+
htmlElement.style.removeProperty('--highlight-color');
|
|
198
|
+
// Не сбрасываем backgroundColor, так как он может быть установлен отдельно
|
|
199
|
+
} else {
|
|
200
|
+
htmlElement.style.setProperty('--highlight-color', properties.highlightColor);
|
|
201
|
+
// Для обычного текста без Quill мы можем просто установить backgroundColor
|
|
202
|
+
// если нет выделения
|
|
203
|
+
if (!htmlElement.querySelector('span[style*="background-color"]')) {
|
|
204
|
+
htmlElement.style.backgroundColor = properties.highlightColor;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
185
208
|
if (properties.bold !== undefined) {
|
|
186
209
|
htmlElement.style.fontWeight = properties.bold ? 'bold' : '';
|
|
187
210
|
}
|