kingkont 0.7.87 → 0.7.89
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/renderer/settings.js +51 -7
- package/renderer/styles.css +22 -3
package/package.json
CHANGED
package/renderer/settings.js
CHANGED
|
@@ -123,6 +123,25 @@ function renderLabelNodeBody(node, body) {
|
|
|
123
123
|
ed.textContent = node.text || '';
|
|
124
124
|
applyLabelStyle(ed, node.textStyle);
|
|
125
125
|
|
|
126
|
+
// Принудительно держим класс is-empty синхронизированным с textContent.
|
|
127
|
+
// CSS-селектор :empty не годится — после стирания всего текста браузер
|
|
128
|
+
// часто оставляет <br> внутри contenteditable, и :empty перестаёт
|
|
129
|
+
// матчить → плейсхолдер пропадал, инлайн-блок схлопывался по ширине,
|
|
130
|
+
// юзер кликал мимо и терял фокус. Класс на основе textContent надёжнее.
|
|
131
|
+
const syncEmpty = () => {
|
|
132
|
+
const empty = !ed.textContent || ed.textContent.length === 0;
|
|
133
|
+
ed.classList.toggle('is-empty', empty);
|
|
134
|
+
// Если контент стал «пустым», но в DOM остался шум (br/пустые text-node) —
|
|
135
|
+
// чистим innerHTML, чтобы курсор спокойно стоял в начале без артефактов.
|
|
136
|
+
// НЕ делаем если уже пусто — иначе зацикливание input-события.
|
|
137
|
+
if (empty && ed.innerHTML !== '' && ed.isContentEditable) {
|
|
138
|
+
// Сохраняем фокус: clearing innerHTML на сфокусированном
|
|
139
|
+
// contenteditable не должен дёргать blur, но сохраним ranged selection.
|
|
140
|
+
ed.innerHTML = '';
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
syncEmpty();
|
|
144
|
+
|
|
126
145
|
const enterEditMode = () => {
|
|
127
146
|
if (ed.isContentEditable) return;
|
|
128
147
|
ed.contentEditable = 'plaintext-only';
|
|
@@ -141,6 +160,7 @@ function renderLabelNodeBody(node, body) {
|
|
|
141
160
|
// closest('[contenteditable]') в обработчиках вне (dblclick/contextmenu)
|
|
142
161
|
// не матчился — и ПКМ снова открывал наш node-menu.
|
|
143
162
|
ed.removeAttribute('contenteditable');
|
|
163
|
+
syncEmpty();
|
|
144
164
|
};
|
|
145
165
|
|
|
146
166
|
ed.addEventListener('dblclick', e => {
|
|
@@ -148,7 +168,11 @@ function renderLabelNodeBody(node, body) {
|
|
|
148
168
|
enterEditMode();
|
|
149
169
|
});
|
|
150
170
|
ed.addEventListener('blur', exitEditMode);
|
|
151
|
-
ed.addEventListener('input', () => {
|
|
171
|
+
ed.addEventListener('input', () => {
|
|
172
|
+
node.text = ed.textContent;
|
|
173
|
+
syncEmpty();
|
|
174
|
+
scheduleSave();
|
|
175
|
+
});
|
|
152
176
|
ed.addEventListener('keydown', e => {
|
|
153
177
|
if (e.key === 'Escape') { e.preventDefault(); ed.blur(); }
|
|
154
178
|
});
|
|
@@ -727,6 +751,12 @@ function attachDrag(el, node) {
|
|
|
727
751
|
// Edit-режим label — не таскаем (контекст редактирования текста).
|
|
728
752
|
if (e.currentTarget?.classList?.contains('label-text') && e.currentTarget.isContentEditable) return;
|
|
729
753
|
if (e.target.closest('.delete')) return;
|
|
754
|
+
// Источник mousedown'а: header или label-text. Для label-text НЕ
|
|
755
|
+
// вызываем preventDefault и canvas.appendChild на mousedown — они
|
|
756
|
+
// ломают браузерный счётчик кликов и dblclick на label не срабатывает.
|
|
757
|
+
// Z-promote и preventDefault выполнятся ниже, в onMove, когда drag
|
|
758
|
+
// реально начнётся (перешёл порог 4px).
|
|
759
|
+
const sourceIsLabelText = e.currentTarget?.classList?.contains('label-text');
|
|
730
760
|
// Multi-select c модификаторами — без drag
|
|
731
761
|
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
732
762
|
e.preventDefault();
|
|
@@ -741,16 +771,18 @@ function attachDrag(el, node) {
|
|
|
741
771
|
state.selectedNodeIds.add(node.id);
|
|
742
772
|
renderSelection();
|
|
743
773
|
}
|
|
744
|
-
e.preventDefault();
|
|
774
|
+
if (!sourceIsLabelText) e.preventDefault();
|
|
745
775
|
const startX = e.clientX, startY = e.clientY;
|
|
746
776
|
const origX = node.x, origY = node.y;
|
|
747
777
|
el.classList.add('selected');
|
|
748
|
-
canvas.appendChild(el);
|
|
749
778
|
const arr = state.currentBoard.metadata.nodes;
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
arr.
|
|
753
|
-
arr.
|
|
779
|
+
if (!sourceIsLabelText) {
|
|
780
|
+
canvas.appendChild(el);
|
|
781
|
+
const idx = arr.indexOf(node);
|
|
782
|
+
if (idx >= 0 && idx < arr.length - 1) {
|
|
783
|
+
arr.splice(idx, 1);
|
|
784
|
+
arr.push(node);
|
|
785
|
+
}
|
|
754
786
|
}
|
|
755
787
|
|
|
756
788
|
// Multi-drag: если выделено несколько нод и кликнули по одной из них —
|
|
@@ -780,6 +812,18 @@ function attachDrag(el, node) {
|
|
|
780
812
|
const onMove = ev => {
|
|
781
813
|
const dx = ev.clientX - startX, dy = ev.clientY - startY;
|
|
782
814
|
if (!dragInitialized && Math.hypot(dx, dy) < 4) return;
|
|
815
|
+
// Первый раз перешли порог — для label-text z-promote отложен сюда,
|
|
816
|
+
// чтобы не ломать dblclick. Также гасим случайно стартовавший
|
|
817
|
+
// text-selection в read-режиме.
|
|
818
|
+
if (!dragInitialized && sourceIsLabelText) {
|
|
819
|
+
canvas.appendChild(el);
|
|
820
|
+
const idx = arr.indexOf(node);
|
|
821
|
+
if (idx >= 0 && idx < arr.length - 1) {
|
|
822
|
+
arr.splice(idx, 1);
|
|
823
|
+
arr.push(node);
|
|
824
|
+
}
|
|
825
|
+
try { window.getSelection()?.removeAllRanges(); } catch {}
|
|
826
|
+
}
|
|
783
827
|
dragInitialized = true;
|
|
784
828
|
// Проверяем — курсор над таймлайном?
|
|
785
829
|
const tlTrackEl = document.elementsFromPoint(ev.clientX, ev.clientY)
|
package/renderer/styles.css
CHANGED
|
@@ -510,15 +510,23 @@
|
|
|
510
510
|
всё равно имеет высоту хотя бы одной строки выбранного шрифта. */
|
|
511
511
|
min-height: 1.5em;
|
|
512
512
|
}
|
|
513
|
-
|
|
513
|
+
/* Плейсхолдер показываем по классу is-empty (а не :empty pseudo) —
|
|
514
|
+
selector :empty ломается когда браузер оставляет <br> в contenteditable
|
|
515
|
+
после Cmd+A/Backspace. JS ставит/снимает класс по textContent. */
|
|
516
|
+
.node.label-node .label-text.is-empty::before {
|
|
514
517
|
content: attr(data-placeholder);
|
|
515
518
|
color: rgba(200,200,200,0.55);
|
|
516
519
|
}
|
|
520
|
+
/* Гарантированный размер пустой ноды — чтобы юзер не терял её визуально
|
|
521
|
+
(кликабельная область + плейсхолдер). */
|
|
522
|
+
.node.label-node .label-text.is-empty {
|
|
523
|
+
min-width: 240px;
|
|
524
|
+
}
|
|
517
525
|
/* Когда label пустая — рисуем подсказывающий dashed-контур, чтобы юзер
|
|
518
526
|
видел что нода существует (текст-плейсхолдер мог быть не замечен).
|
|
519
527
|
При наличии текста — никакой рамки, чисто подпись на холсте. */
|
|
520
|
-
.node.label-node:has(.label-text
|
|
521
|
-
outline: 1px dashed rgba(200,200,200,0.
|
|
528
|
+
.node.label-node:has(.label-text.is-empty) {
|
|
529
|
+
outline: 1px dashed rgba(200,200,200,0.35);
|
|
522
530
|
outline-offset: 1px;
|
|
523
531
|
border-radius: 3px;
|
|
524
532
|
}
|
|
@@ -526,6 +534,17 @@
|
|
|
526
534
|
природу» ноды, в read-mode сигнал что dblclick → редактировать. */
|
|
527
535
|
.node.label-node .label-text { cursor: text; }
|
|
528
536
|
.node.label-node:hover .label-text:not([contenteditable]) { cursor: pointer; }
|
|
537
|
+
/* В read-режиме блокируем text-selection — чтобы single-click не выделял
|
|
538
|
+
слово, и mousedown без preventDefault (для работы dblclick) не давал
|
|
539
|
+
браузеру стартовать selection-drag. В edit-режиме выделение разрешено. */
|
|
540
|
+
.node.label-node .label-text:not([contenteditable]) {
|
|
541
|
+
user-select: none;
|
|
542
|
+
-webkit-user-select: none;
|
|
543
|
+
}
|
|
544
|
+
.node.label-node .label-text[contenteditable] {
|
|
545
|
+
user-select: text;
|
|
546
|
+
-webkit-user-select: text;
|
|
547
|
+
}
|
|
529
548
|
/* === Label-шрифты ===
|
|
530
549
|
Все варианты — bundled woff2 (см. assets/fonts/), кириллица поддержана. */
|
|
531
550
|
.node.label-node .label-text[data-font="pencil"] {
|