@growgroup/visual-editor 0.7.2 → 0.8.1
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/README.md +24 -1
- package/dist/editor.css +1 -1
- package/package.json +1 -1
- package/src/editor/components/EditorCanvas.tsx +2 -2
- package/src/editor/components/EditorFooter.tsx +4 -1
- package/src/editor/components/EditorPropertyPanel.tsx +6 -9
- package/src/editor/components/ppt/PptChrome.tsx +20 -6
- package/src/editor/components/property-panel/BorderSection.tsx +3 -1
- package/src/editor/hooks/useLinkNavigation.ts +2 -2
- package/src/editor/types.ts +5 -1
- package/src/editor/utils/border-sides.ts +112 -0
- package/src/editor/utils/dom-utils.ts +67 -5
- package/src/editor/utils/link-navigation.ts +61 -29
- package/src/editor/utils/style-utils.ts +10 -4
- package/src/editor/utils/tailwind-mappings.ts +2 -7
- package/src/editor/utils/tailwind-utils.ts +105 -0
- package/src/version.ts +1 -1
|
@@ -46,6 +46,7 @@ import { moveSlide, deleteSlide, duplicateSlide, updateSlideMeta, insertSlide }
|
|
|
46
46
|
import { startCleanup } from '../../../components/SaveNote';
|
|
47
47
|
import { unresolvedCount } from './PptComments';
|
|
48
48
|
import { enterCropMode, type CropSession } from '../../utils/crop-mode';
|
|
49
|
+
import { borderWidthStyles, summarizeBorder } from '../../utils/border-sides';
|
|
49
50
|
import { useGoogleFonts } from '../../hooks/useGoogleFonts';
|
|
50
51
|
import { DeckSlideRender } from '../../../components/DeckSlideRender';
|
|
51
52
|
const PptDesignProposals = lazy(() => import('./PptDesignProposals').then((m) => ({ default: m.PptDesignProposals })));
|
|
@@ -270,7 +271,11 @@ function Dropdown({
|
|
|
270
271
|
const PAINT_PROPS = new Set([
|
|
271
272
|
'backgroundColor', 'background', 'backgroundImage', 'clipPath', 'borderRadius',
|
|
272
273
|
'border', 'borderColor', 'borderWidth', 'borderStyle',
|
|
274
|
+
// 辺ごとの線も中身の図形へ(片側だけの線の読み書き。上の辺だけだと、枠の右・下・左を枠の外側から読んでいた)
|
|
273
275
|
'borderTopWidth', 'borderTopStyle', 'borderTopColor',
|
|
276
|
+
'borderRightWidth', 'borderRightStyle', 'borderRightColor',
|
|
277
|
+
'borderBottomWidth', 'borderBottomStyle', 'borderBottomColor',
|
|
278
|
+
'borderLeftWidth', 'borderLeftStyle', 'borderLeftColor',
|
|
274
279
|
]);
|
|
275
280
|
|
|
276
281
|
/** リボンが読む computed style の名前 → 選んだ文字の見た目(InlineTextSummary)の項目 */
|
|
@@ -1264,13 +1269,22 @@ export function PptRibbon({
|
|
|
1264
1269
|
|
|
1265
1270
|
/** 枠線の色/太さを変えるとき、枠線が無ければ実線を立てる(PowerPointの挙動) */
|
|
1266
1271
|
const ensureBorder = useCallback((styles: Record<string, string>) => {
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
+
// 片側だけの線(border-l-2 など)は上の辺が 0 なので、上だけ見ると「線が無い」と判定して
|
|
1273
|
+
// 四辺に 2px を足していた。辺ごとに見て、線が見えていれば足さず、太さはその辺だけに当てる
|
|
1274
|
+
const first = targets()[0];
|
|
1275
|
+
const border = summarizeBorder({ getPropertyValue: readComputed }, first ? paintTarget(first) : null);
|
|
1276
|
+
let out: Record<string, string> = { ...styles };
|
|
1277
|
+
if ('borderWidth' in out && border.sides) {
|
|
1278
|
+
const { borderWidth, ...rest } = out;
|
|
1279
|
+
out = { ...rest, ...borderWidthStyles(borderWidth, border.sides) };
|
|
1280
|
+
}
|
|
1281
|
+
// 線が無ければ 2px を立てる(片側だけの線を 0 にしていたら、その辺に)
|
|
1282
|
+
if (!('borderWidth' in styles) && (!Number.isFinite(border.width) || border.width === 0)) {
|
|
1283
|
+
Object.assign(out, borderWidthStyles('2px', border.sides));
|
|
1284
|
+
}
|
|
1285
|
+
if (!('borderStyle' in out) && (border.style === 'none' || border.style === '')) out.borderStyle = 'solid';
|
|
1272
1286
|
apply(out);
|
|
1273
|
-
}, [readComputed, apply]);
|
|
1287
|
+
}, [readComputed, apply, targets]);
|
|
1274
1288
|
|
|
1275
1289
|
const borderRadiusPx = useMemo(() => {
|
|
1276
1290
|
const v = parseFloat(readComputed('border-radius'));
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../../../components/ui/collapsible';
|
|
17
17
|
import { Circle, ChevronRight } from 'lucide-react';
|
|
18
18
|
import { rgbToHex } from '../../utils/style-utils';
|
|
19
|
+
import { borderWidthStyles } from '../../utils/border-sides';
|
|
19
20
|
import { BORDER_STYLES } from '../../constants';
|
|
20
21
|
import type { SelectedElementInfo } from '../../types';
|
|
21
22
|
|
|
@@ -52,7 +53,8 @@ export function BorderSection({
|
|
|
52
53
|
value={Math.round(selectedElement.borderWidth)}
|
|
53
54
|
onChange={(e) =>
|
|
54
55
|
onStyleChange({
|
|
55
|
-
|
|
56
|
+
// 片側だけの線は、その辺の線幅だけを変える
|
|
57
|
+
...borderWidthStyles(`${e.target.value}px`, selectedElement.borderSides),
|
|
56
58
|
borderStyle:
|
|
57
59
|
selectedElement.borderStyle === 'none'
|
|
58
60
|
? 'solid'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Alt(⌥)+ クリックでリンク先へ移る(utils/link-navigation.ts)の、エディタ側の行き先。
|
|
3
3
|
*
|
|
4
4
|
* ページの切替は、ページ一覧で切り替えるときと同じ経路を通す
|
|
5
5
|
* (未保存の変更は FrontendVisualEditor.handleContentChange が保存してから移る):
|
|
@@ -122,7 +122,7 @@ export function useLinkNavigation() {
|
|
|
122
122
|
[revealAnchor],
|
|
123
123
|
);
|
|
124
124
|
|
|
125
|
-
/** 紙面に
|
|
125
|
+
/** 紙面に Alt(⌥)+ クリックの移動を付ける。戻り値は後始末 */
|
|
126
126
|
const setupLinkNavigation = useCallback(
|
|
127
127
|
(iframeDoc: Document) => {
|
|
128
128
|
const detach = attachLinkNavigation(iframeDoc, {
|
package/src/editor/types.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* (スライド、ページ、コンポーネント等で共用)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { BorderSide } from './utils/border-sides';
|
|
7
|
+
|
|
6
8
|
// 再エクスポート(既存の型)
|
|
7
9
|
export type {
|
|
8
10
|
EditorTool,
|
|
@@ -184,7 +186,9 @@ export interface SelectedElementInfo {
|
|
|
184
186
|
rawFontSize?: string; // font-size
|
|
185
187
|
rawLineHeight?: string; // line-height
|
|
186
188
|
rawLetterSpacing?: string; // letter-spacing
|
|
187
|
-
rawBorderWidth?: string; // border-width
|
|
189
|
+
rawBorderWidth?: string; // border-width(片側だけの線ならその辺の値)
|
|
190
|
+
/** 線が見えている辺が 1〜3 辺のときだけ、その辺(utils/border-sides.ts)。線幅の変更をその辺だけに当てる */
|
|
191
|
+
borderSides?: BorderSide[];
|
|
188
192
|
rawGap?: string; // gap (flexbox/grid)
|
|
189
193
|
rawLeft?: string; // left (position)
|
|
190
194
|
rawTop?: string; // top (position)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 片側だけの線(`border-l-2` など)を、右パネル・リボンの「線」の欄で扱う。
|
|
3
|
+
*
|
|
4
|
+
* 欄は 1 本の線幅・スタイル・色しか持たない。以前は computed の `border-width` を
|
|
5
|
+
* parseFloat していたので、左だけ 2px の線は「0px」、スタイルは「none none none solid」と読めず、
|
|
6
|
+
* 線幅を変えると `border-[3px]` で四辺の枠になっていた。
|
|
7
|
+
* 線が見えている辺が 1〜3 辺なら、その辺の値を出し、線幅の変更もその辺だけに当てる。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { isBorderWidthClass, isZeroBorderWidthClass, type SideLetter } from './tailwind-utils';
|
|
11
|
+
|
|
12
|
+
export type BorderSide = 'top' | 'right' | 'bottom' | 'left';
|
|
13
|
+
|
|
14
|
+
export const BORDER_SIDES: readonly BorderSide[] = ['top', 'right', 'bottom', 'left'];
|
|
15
|
+
|
|
16
|
+
const SIDE_WIDTH_PROPERTY: Record<BorderSide, string> = {
|
|
17
|
+
top: 'borderTopWidth',
|
|
18
|
+
right: 'borderRightWidth',
|
|
19
|
+
bottom: 'borderBottomWidth',
|
|
20
|
+
left: 'borderLeftWidth',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface BorderSummary {
|
|
24
|
+
width: number;
|
|
25
|
+
style: string;
|
|
26
|
+
color: string;
|
|
27
|
+
/**
|
|
28
|
+
* 線が見えている辺が 1〜3 辺のときだけ、その辺(上・右・下・左の順)。四辺とも・線なしは undefined。
|
|
29
|
+
* 線が見えていなくても、要素自身が 1〜3 辺だけに線幅を指定していればその辺(declaredBorderSides)
|
|
30
|
+
*/
|
|
31
|
+
sides?: BorderSide[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 線が見えていないとき(片側だけの線を線幅 0 にした・スタイルを「なし」にした)の辺を、要素自身の指定から読む。
|
|
36
|
+
* computed では辺が分からず、次に線幅を入れると四辺の枠になっていた(実エディタで確認)。
|
|
37
|
+
* インラインの辺ごとの線幅(border-left-width)と、辺の太さのクラス(border-l-0 / border-x-2)を見る。
|
|
38
|
+
* 四辺まとめての指定(インラインの border-width・border / border-2 クラス)があれば四辺とみなす
|
|
39
|
+
*/
|
|
40
|
+
export function declaredBorderSides(element: Element): BorderSide[] | undefined {
|
|
41
|
+
const inline = (element as HTMLElement).style;
|
|
42
|
+
const inlineSides = inline ? BORDER_SIDES.filter((side) => inline.getPropertyValue(`border-${side}-width`) !== '') : [];
|
|
43
|
+
if (inlineSides.length === BORDER_SIDES.length) return undefined;
|
|
44
|
+
if (inline?.getPropertyValue('border-width')) return undefined;
|
|
45
|
+
const classes = (element.getAttribute('class') || '').split(/\s+/).filter(Boolean);
|
|
46
|
+
// 四辺まとめての太さ(border / border-2。border-0 は除く)があれば、辺ごとの指定があっても四辺とみなす
|
|
47
|
+
if (classes.some((cls) => isBorderWidthClass(cls, '') && !isZeroBorderWidthClass(cls))) return undefined;
|
|
48
|
+
// 太さ 0 のクラス(border-b-0 など)は「線を消す指定」なので、辺を名乗るものとして数えない。
|
|
49
|
+
// 欄で線幅を 0 にしたときは、同時に書くインラインの辺ごとの線幅(上)で辺が分かる
|
|
50
|
+
const has = (letter: SideLetter) =>
|
|
51
|
+
classes.some((cls) => isBorderWidthClass(cls, letter) && !isZeroBorderWidthClass(cls));
|
|
52
|
+
const classSides: BorderSide[] = [];
|
|
53
|
+
// s / e は書字方向の始め・終わり、bs / be はブロック方向。LTR の横書きを既定として当てる
|
|
54
|
+
if (has('t') || has('y') || has('bs')) classSides.push('top');
|
|
55
|
+
if (has('r') || has('x') || has('e')) classSides.push('right');
|
|
56
|
+
if (has('b') || has('y') || has('be')) classSides.push('bottom');
|
|
57
|
+
if (has('l') || has('x') || has('s')) classSides.push('left');
|
|
58
|
+
// その辺を 0 にするクラス(border-x-2 border-l-0 の左)は、辺から外す
|
|
59
|
+
const zero = (letter: SideLetter) =>
|
|
60
|
+
classes.some((cls) => isBorderWidthClass(cls, letter) && isZeroBorderWidthClass(cls));
|
|
61
|
+
const zeroSides = new Set<BorderSide>();
|
|
62
|
+
if (zero('t') || zero('y') || zero('bs')) zeroSides.add('top');
|
|
63
|
+
if (zero('r') || zero('x') || zero('e')) zeroSides.add('right');
|
|
64
|
+
if (zero('b') || zero('y') || zero('be')) zeroSides.add('bottom');
|
|
65
|
+
if (zero('l') || zero('x') || zero('s')) zeroSides.add('left');
|
|
66
|
+
const sides = BORDER_SIDES.filter(
|
|
67
|
+
(side) => inlineSides.includes(side) || (classSides.includes(side) && !zeroSides.has(side)),
|
|
68
|
+
);
|
|
69
|
+
return sides.length > 0 && sides.length < BORDER_SIDES.length ? sides : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* computed style(`getPropertyValue` を持つもの)から、「線」の欄に出す値。
|
|
74
|
+
* element を渡すと、線が見えていないときに要素自身の指定から辺を読む(declaredBorderSides)
|
|
75
|
+
*/
|
|
76
|
+
export function summarizeBorder(
|
|
77
|
+
style: Pick<CSSStyleDeclaration, 'getPropertyValue'>,
|
|
78
|
+
element?: Element | null,
|
|
79
|
+
): BorderSummary {
|
|
80
|
+
const perSide = BORDER_SIDES.map((side) => ({
|
|
81
|
+
side,
|
|
82
|
+
width: parseFloat(style.getPropertyValue(`border-${side}-width`)) || 0,
|
|
83
|
+
style: style.getPropertyValue(`border-${side}-style`),
|
|
84
|
+
color: style.getPropertyValue(`border-${side}-color`),
|
|
85
|
+
}));
|
|
86
|
+
const shown = perSide.filter((s) => s.width > 0 && s.style !== 'none' && s.style !== 'hidden');
|
|
87
|
+
if (shown.length === 0 || shown.length === BORDER_SIDES.length) {
|
|
88
|
+
// 四辺とも・線なしは今までどおり(省略形の値)
|
|
89
|
+
const sides = shown.length === 0 && element ? declaredBorderSides(element) : undefined;
|
|
90
|
+
return {
|
|
91
|
+
width: parseFloat(style.getPropertyValue('border-width')) || 0,
|
|
92
|
+
style: sides ? style.getPropertyValue(`border-${sides[0]}-style`) : style.getPropertyValue('border-style'),
|
|
93
|
+
color: sides ? style.getPropertyValue(`border-${sides[0]}-color`) : style.getPropertyValue('border-color'),
|
|
94
|
+
...(sides ? { sides } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const first = shown[0];
|
|
98
|
+
return { width: first.width, style: first.style, color: first.color, sides: shown.map((s) => s.side) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 線幅の変更。片側だけの線なら、その辺の線幅(borderLeftWidth など)にする */
|
|
102
|
+
export function borderWidthStyles(value: string, sides?: readonly BorderSide[]): Record<string, string> {
|
|
103
|
+
if (!sides || sides.length === 0 || sides.length === BORDER_SIDES.length) return { borderWidth: value };
|
|
104
|
+
const out: Record<string, string> = {};
|
|
105
|
+
for (const side of sides) out[SIDE_WIDTH_PROPERTY[side]] = value;
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** その辺の線幅の CSS プロパティ名(kebab)。インラインの生値を読むときに使う */
|
|
110
|
+
export function borderSideWidthCss(side: BorderSide): string {
|
|
111
|
+
return `border-${side}-width`;
|
|
112
|
+
}
|
|
@@ -676,6 +676,11 @@ export function convertToAbsolutePositioning(iframeDoc: Document): number {
|
|
|
676
676
|
|
|
677
677
|
// 1. まず全要素の現在の位置・サイズをキャプチャ(インライン要素は除外)
|
|
678
678
|
const captures: ElementCapture[] = [];
|
|
679
|
+
// 変換しないインライン要素の、変換前の位置(自己検証で使う)。
|
|
680
|
+
// 変換しなかった要素は流れの中に残るので、ブロックの兄弟が絶対配置で流れから抜けると
|
|
681
|
+
// その分だけ上へ詰まる。例: <td><div>見出し</div><span class="mt-[10px] inline-block">ラベル</span></td>
|
|
682
|
+
// で見出しだけが抜け、ラベルが見出しに重なる
|
|
683
|
+
const skippedInline: { element: HTMLElement; rect: DOMRect }[] = [];
|
|
679
684
|
|
|
680
685
|
editableElements.forEach((el) => {
|
|
681
686
|
const element = el as HTMLElement;
|
|
@@ -694,6 +699,7 @@ export function convertToAbsolutePositioning(iframeDoc: Document): number {
|
|
|
694
699
|
|
|
695
700
|
// インライン要素はスキップ(ただし flex/grid アイテムは除く)
|
|
696
701
|
if (!isFlexOrGridItem && isInlineElement(element, computedStyle)) {
|
|
702
|
+
skippedInline.push({ element, rect: element.getBoundingClientRect() });
|
|
697
703
|
return;
|
|
698
704
|
}
|
|
699
705
|
|
|
@@ -725,6 +731,47 @@ export function convertToAbsolutePositioning(iframeDoc: Document): number {
|
|
|
725
731
|
});
|
|
726
732
|
});
|
|
727
733
|
|
|
734
|
+
// 1.5 編集対象でない、流れの中のブロックの兄弟も一緒に倒す。
|
|
735
|
+
// 部品(data-part)の中では data-slot だけが編集対象なので、スロットの間にある罫線(h-px の div)などは
|
|
736
|
+
// 流れに残る。兄弟が絶対配置で抜けると、その分だけ上へ詰まる(ヘッダーの罫線が 116px 上へ動いた実測)。
|
|
737
|
+
// 自己検証は編集対象しか見ていなかったので、崩れたまま確定していた。
|
|
738
|
+
// 位置を固定するだけで編集対象にはしない(data-editable は付けない)
|
|
739
|
+
const captured = new Set<HTMLElement>(captures.map((c) => c.element));
|
|
740
|
+
const parentsOfCaptured = new Set<HTMLElement>();
|
|
741
|
+
captures.forEach(({ parent }) => {
|
|
742
|
+
if (parent && parent !== iframeDoc.body && parent !== artboard) parentsOfCaptured.add(parent);
|
|
743
|
+
});
|
|
744
|
+
parentsOfCaptured.forEach((parent) => {
|
|
745
|
+
Array.from(parent.children).forEach((child) => {
|
|
746
|
+
const element = child as HTMLElement;
|
|
747
|
+
if (captured.has(element) || skippedInline.some((s) => s.element === element)) return;
|
|
748
|
+
if (element.classList.contains('selection-box') || element.classList.contains('gg-comment-layer')) return;
|
|
749
|
+
if (isNonEditableTag(element)) return;
|
|
750
|
+
const computedStyle = iframeDoc.defaultView!.getComputedStyle(element);
|
|
751
|
+
// 流れの外(absolute / fixed)・非表示は兄弟に影響されない
|
|
752
|
+
if (computedStyle.position === 'absolute' || computedStyle.position === 'fixed') return;
|
|
753
|
+
if (computedStyle.display === 'none') return;
|
|
754
|
+
const parentDisplay = iframeDoc.defaultView!.getComputedStyle(parent).display;
|
|
755
|
+
const isFlexOrGridItem = /(^|\s)(inline-)?(flex|grid)($|\s)/.test(parentDisplay);
|
|
756
|
+
if (!isFlexOrGridItem && isInlineElement(element, computedStyle)) {
|
|
757
|
+
skippedInline.push({ element, rect: element.getBoundingClientRect() });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
captured.add(element);
|
|
761
|
+
captures.push({
|
|
762
|
+
element,
|
|
763
|
+
rect: element.getBoundingClientRect(),
|
|
764
|
+
parent,
|
|
765
|
+
parentRect: parent.getBoundingClientRect(),
|
|
766
|
+
computedStyle,
|
|
767
|
+
parentComputedStyle: iframeDoc.defaultView!.getComputedStyle(parent),
|
|
768
|
+
originalTransform: computedStyle.transform,
|
|
769
|
+
scale,
|
|
770
|
+
prevStyle: element.getAttribute('style'),
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
728
775
|
// 2. 親要素を処理(position: relative設定)
|
|
729
776
|
//
|
|
730
777
|
// [採寸より前にやる] offsetLeft/offsetTop は offsetParent 基準の値なので、
|
|
@@ -762,13 +809,18 @@ export function convertToAbsolutePositioning(iframeDoc: Document): number {
|
|
|
762
809
|
if (element.offsetParent !== parent) return;
|
|
763
810
|
// 寸法は offsetWidth(整数へ丸める)ではなく、rect の小数値を**切り上げ**て使う。
|
|
764
811
|
// 例: 必要幅1227.4pxの文字列を1227pxで固定すると最後の1文字だけが折り返す
|
|
765
|
-
// (284pxの「テキスト」で実害)。位置は1px未満の誤差が折り返しを生まないので offset
|
|
812
|
+
// (284pxの「テキスト」で実害)。位置は1px未満の誤差が折り返しを生まないので offset で良い。
|
|
813
|
+
// ただし rect は縮小した紙面の値で、ブラウザは 1/64px に丸めて返す(56px × 0.55 = 30.8 → 30.8125)。
|
|
814
|
+
// 倍率で割り戻すと 56.02 のように少しだけ大きくなり、そのまま切り上げると全部の要素が 1px 膨らむ
|
|
815
|
+
// (h-px の罫線が 2px、56px の帯が 57px になった実測)。丸めの分だけ引いてから切り上げる
|
|
766
816
|
const s = capScale || 1;
|
|
817
|
+
const quantum = 1 / 64 / s + 0.005;
|
|
818
|
+
const ceilSize = (v: number) => Math.max(0, Math.ceil(v - quantum));
|
|
767
819
|
offsets.set(element, {
|
|
768
820
|
left: element.offsetLeft,
|
|
769
821
|
top: element.offsetTop,
|
|
770
|
-
width:
|
|
771
|
-
height:
|
|
822
|
+
width: ceilSize(rect.width / s),
|
|
823
|
+
height: ceilSize(rect.height / s),
|
|
772
824
|
});
|
|
773
825
|
});
|
|
774
826
|
|
|
@@ -900,11 +952,21 @@ export function convertToAbsolutePositioning(iframeDoc: Document): number {
|
|
|
900
952
|
const overY = element.scrollHeight - element.clientHeight > 2;
|
|
901
953
|
return (overX || overY) && element.getAttribute('data-gg-pre-overflow') !== 'true';
|
|
902
954
|
});
|
|
903
|
-
|
|
955
|
+
// 変換しなかったインライン要素も、流れの中で動いていないかを見る。
|
|
956
|
+
// 変換した要素だけを比べていたため、ブロックの兄弟が抜けて詰まったラベルを見逃し、
|
|
957
|
+
// 等倍で採寸が正確なキャンバス表示でだけ崩れた姿が確定していた
|
|
958
|
+
// (1 枚表示は縮小による端数で別の要素がずれ、たまたま巻き戻っていた)
|
|
959
|
+
const movedInline = skippedInline.filter(({ element, rect }) => {
|
|
960
|
+
const now = element.getBoundingClientRect();
|
|
961
|
+
return Math.abs(now.left - rect.left) > 1.5 || Math.abs(now.top - rect.top) > 1.5;
|
|
962
|
+
});
|
|
963
|
+
if (moved.length > 0 || movedInline.length > 0) {
|
|
904
964
|
console.warn(
|
|
905
965
|
'[convertToAbsolutePositioning] 変換で',
|
|
906
966
|
moved.length,
|
|
907
|
-
'
|
|
967
|
+
'要素がずれた・変換しないインライン要素が',
|
|
968
|
+
movedInline.length,
|
|
969
|
+
'個動いたため巻き戻します(採寸タイミング、または流れに残る要素が詰まった可能性)',
|
|
908
970
|
);
|
|
909
971
|
captures.forEach(({ element, prevStyle }) => {
|
|
910
972
|
if (prevStyle == null) element.removeAttribute('style');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Alt(Mac は ⌥ Option)+ クリックで、紙面の中のリンク(`<a href>`)の先へ移る。
|
|
3
3
|
*
|
|
4
4
|
* - 同じサイトのページ(contentList[].href と照らし合わせる) … そのページを編集中にする
|
|
5
5
|
* - 同じページのアンカー(`#id`) … その要素が見える位置へ紙面を移す
|
|
@@ -7,19 +7,27 @@
|
|
|
7
7
|
* - `mailto:` / `tel:` / `javascript:` など … 何もしない
|
|
8
8
|
* - ページ一覧に無い同じオリジンの URL … 短く知らせる
|
|
9
9
|
*
|
|
10
|
-
* [
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* ⌘
|
|
14
|
-
*
|
|
10
|
+
* [⌘ / Ctrl ではなく Alt にした理由]
|
|
11
|
+
* 0.7.0 では ⌘ / Ctrl + クリックで移っていたが、⌘ / Ctrl + クリックはレイヤーパネルでは
|
|
12
|
+
* 「選択に足す・外す」(EditorLayerPanel)、紙面では「最深要素を選ぶ」で、複数選択のつもりの
|
|
13
|
+
* ⌘ クリックがリンクの上だとページ移動になった。紙面のクリックで Alt が空いている:
|
|
14
|
+
* Alt の既存の意味はホバーの距離メジャー(useAltMeasure、mousemove だけ)・中心からのリサイズ
|
|
15
|
+
* (ハンドルのドラッグ)・ズームツールの縮小(zoom-tool の間。下で止める)で、クリックには無い。
|
|
16
|
+
*
|
|
17
|
+
* [紙面のクリックとの両立]
|
|
18
|
+
* useElementSelection の mousedown は Alt を見ない(Alt + クリックは素のクリックと同じ選択)。
|
|
19
|
+
* Alt + ドラッグは移動、Alt + ダブルクリックは素のダブルクリック(潜る・テキスト編集)。
|
|
20
|
+
* 押した瞬間にはどれになるか決められないので、ここは mousedown で候補を覚えるだけにして
|
|
21
|
+
* 選択には手を出さず、
|
|
15
22
|
* - 動かさずに離した(ドラッグではない)
|
|
16
|
-
* -
|
|
23
|
+
* - ⌘ / Ctrl / Shift を押していない(⌘ + Alt + クリックは最深要素を選ぶだけで移らない)
|
|
17
24
|
* - 2 回目の mousedown(ダブルクリック)が LINK_NAV_DELAY_MS 以内に来なかった
|
|
18
25
|
* - 押した点がテキスト編集中の要素の中ではない
|
|
19
|
-
*
|
|
26
|
+
* ときだけ移る。
|
|
20
27
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
28
|
+
* Alt + クリックのリンクの既定の動作は、ブラウザによってはダウンロード(Chromium で実測)。
|
|
29
|
+
* 紙面のリンクの click は useElementSelection の handleClick が preventDefault するが、
|
|
30
|
+
* トリミング中は止めないので、Alt + クリックはここ(capture)でも必ず止める。
|
|
23
31
|
*/
|
|
24
32
|
import { MARQUEE_DRAG_THRESHOLD } from '../constants';
|
|
25
33
|
|
|
@@ -175,17 +183,29 @@ export function clearPendingAnchor(id?: string): void {
|
|
|
175
183
|
|
|
176
184
|
/**
|
|
177
185
|
* 1 回目のクリックから移るまでの待ち。この間に 2 回目の mousedown が来たら
|
|
178
|
-
* ダブルクリック(
|
|
186
|
+
* ダブルクリック(潜る・テキスト編集)として移動を取り消す
|
|
179
187
|
*/
|
|
180
188
|
export const LINK_NAV_DELAY_MS = 350;
|
|
181
189
|
|
|
182
|
-
/**
|
|
190
|
+
/** Alt を押してリンクの上にいる間、紙面の <html> に付ける(カーソルを指にする) */
|
|
183
191
|
export const LINK_NAV_ATTR = 'data-gg-link-nav';
|
|
184
192
|
|
|
185
193
|
const OVERLAY_SELECTOR =
|
|
186
194
|
'.selection-box,.marquee-selection-box,[data-editor-overlay],#gg-smart-guides,#gg-measure-layer';
|
|
187
|
-
/**
|
|
188
|
-
|
|
195
|
+
/**
|
|
196
|
+
* 描画・テキスト・コメント・手のひら・トリミング・ズームの間は、クリックに別の意味がある
|
|
197
|
+
* (ズームツールの Alt + クリックは縮小)
|
|
198
|
+
*/
|
|
199
|
+
const BLOCKING_BODY_CLASSES = [
|
|
200
|
+
'draw-mode',
|
|
201
|
+
'text-mode',
|
|
202
|
+
'comment-mode',
|
|
203
|
+
'move-mode',
|
|
204
|
+
'pan-mode',
|
|
205
|
+
'panning',
|
|
206
|
+
'gg-cropping',
|
|
207
|
+
'zoom-tool',
|
|
208
|
+
];
|
|
189
209
|
|
|
190
210
|
const isMac = () => {
|
|
191
211
|
if (typeof navigator === 'undefined') return false;
|
|
@@ -226,7 +246,7 @@ export interface LinkNavigationOptions {
|
|
|
226
246
|
}
|
|
227
247
|
|
|
228
248
|
/**
|
|
229
|
-
* 紙面(iframe の文書)に
|
|
249
|
+
* 紙面(iframe の文書)に Alt + クリックの移動を付ける。戻り値は後始末
|
|
230
250
|
*/
|
|
231
251
|
export function attachLinkNavigation(iframeDoc: Document, options: LinkNavigationOptions): () => void {
|
|
232
252
|
const win = iframeDoc.defaultView;
|
|
@@ -234,9 +254,9 @@ export function attachLinkNavigation(iframeDoc: Document, options: LinkNavigatio
|
|
|
234
254
|
const hostDoc = frameEl?.ownerDocument ?? null;
|
|
235
255
|
const hostWin = hostDoc?.defaultView ?? null;
|
|
236
256
|
const mac = isMac();
|
|
237
|
-
const mod = mac ? '
|
|
238
|
-
|
|
239
|
-
const
|
|
257
|
+
const mod = mac ? '⌥ ' : 'Alt + ';
|
|
258
|
+
/** Alt だけ。⌘ / Ctrl / Shift と一緒なら選択の操作(最深要素・追加)なので移らない */
|
|
259
|
+
const navKey = (e: MouseEvent | KeyboardEvent) => e.altKey && !e.metaKey && !e.ctrlKey && !e.shiftKey;
|
|
240
260
|
|
|
241
261
|
let pointer: { x: number; y: number } | null = null;
|
|
242
262
|
let modifier = false;
|
|
@@ -330,7 +350,7 @@ export function attachLinkNavigation(iframeDoc: Document, options: LinkNavigatio
|
|
|
330
350
|
if (hint) hint.hidden = true;
|
|
331
351
|
};
|
|
332
352
|
|
|
333
|
-
/**
|
|
353
|
+
/** Alt を押してリンクの上にいれば、指のカーソルと行き先の案内を出す */
|
|
334
354
|
const update = (buttons = 0) => {
|
|
335
355
|
if (!modifier || !pointer || buttons !== 0 || blocked()) {
|
|
336
356
|
disarm();
|
|
@@ -378,16 +398,26 @@ export function attachLinkNavigation(iframeDoc: Document, options: LinkNavigatio
|
|
|
378
398
|
};
|
|
379
399
|
const onKey = (e: KeyboardEvent) => {
|
|
380
400
|
if (e.key === 'Escape') cancel();
|
|
381
|
-
if (
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
401
|
+
if (e.key === 'Alt') {
|
|
402
|
+
// Alt の keydown で altKey が立たない実装があっても、押したことを優先する
|
|
403
|
+
modifier = e.type === 'keydown' ? !(e.metaKey || e.ctrlKey || e.shiftKey) : navKey(e);
|
|
404
|
+
update();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (e.key === 'Meta' || e.key === 'Control' || e.key === 'Shift') {
|
|
408
|
+
// Alt を押したまま ⌘ / Ctrl / Shift を足す(移らなくなる)・離す(移れるようになる)
|
|
409
|
+
const next = navKey(e);
|
|
410
|
+
if (next !== modifier) {
|
|
411
|
+
modifier = next;
|
|
412
|
+
update();
|
|
386
413
|
}
|
|
387
414
|
return;
|
|
388
415
|
}
|
|
389
|
-
|
|
390
|
-
|
|
416
|
+
// Alt を離したことに気づけなかった(別のウィンドウで離した等)ときの戻し
|
|
417
|
+
if (modifier && !navKey(e)) {
|
|
418
|
+
modifier = false;
|
|
419
|
+
disarm();
|
|
420
|
+
}
|
|
391
421
|
};
|
|
392
422
|
const onBlur = () => {
|
|
393
423
|
modifier = false;
|
|
@@ -397,7 +427,7 @@ export function attachLinkNavigation(iframeDoc: Document, options: LinkNavigatio
|
|
|
397
427
|
const onMouseDown = (e: MouseEvent) => {
|
|
398
428
|
// 2 回目の mousedown(ダブルクリック)・別のクリックが来たら、前の移動は取り消す
|
|
399
429
|
cancel();
|
|
400
|
-
if (e.button !== 0 || !navKey(e)
|
|
430
|
+
if (e.button !== 0 || !navKey(e)) return;
|
|
401
431
|
if (e.detail > 1 || blocked()) return;
|
|
402
432
|
// 選択枠のハンドル(リサイズ・回転)を押したのはハンドルの操作。下にリンクがあっても移らない
|
|
403
433
|
if ((e.target as Element | null)?.closest?.('[data-handle]')) return;
|
|
@@ -408,14 +438,16 @@ export function attachLinkNavigation(iframeDoc: Document, options: LinkNavigatio
|
|
|
408
438
|
const moved = (e: MouseEvent, p: { x: number; y: number }) =>
|
|
409
439
|
Math.abs(e.clientX - p.x) > MARQUEE_DRAG_THRESHOLD || Math.abs(e.clientY - p.y) > MARQUEE_DRAG_THRESHOLD;
|
|
410
440
|
const onMouseUp = (e: MouseEvent) => {
|
|
411
|
-
//
|
|
441
|
+
// Alt + ドラッグ(移動)だった
|
|
412
442
|
if (pending && moved(e, pending)) pending = null;
|
|
413
443
|
};
|
|
414
444
|
const onClick = (e: MouseEvent) => {
|
|
445
|
+
// 移らないとき(ズームツール・トリミング中・⌘ と一緒など)も、リンクのダウンロードはさせない
|
|
446
|
+
if (e.altKey && (e.target as Element | null)?.closest?.('a[href]')) e.preventDefault();
|
|
415
447
|
const p = pending;
|
|
416
448
|
pending = null;
|
|
417
449
|
if (!p || e.button !== 0 || e.detail > 1) return;
|
|
418
|
-
if (!navKey(e) ||
|
|
450
|
+
if (!navKey(e) || moved(e, p)) return;
|
|
419
451
|
timer = setTimeout(() => {
|
|
420
452
|
timer = null;
|
|
421
453
|
// 待っている間にテキスト編集に入った・紙面が差し替わった・モードが変わった
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { SelectedElementInfo, ImageFillMode } from '../types';
|
|
6
6
|
import { getArtboardScale } from './dom-utils';
|
|
7
7
|
import { extractValueFromTailwindClass } from './tailwind-utils';
|
|
8
|
+
import { summarizeBorder, borderSideWidthCss } from './border-sides';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* RGB/RGBA色をHEXに変換
|
|
@@ -525,7 +526,11 @@ export function extractElementInfo(
|
|
|
525
526
|
const rawFontSize = extractRawStyleValue(element.style.fontSize, 'fontSize');
|
|
526
527
|
const rawLineHeight = extractRawStyleValue(element.style.lineHeight, 'lineHeight');
|
|
527
528
|
const rawLetterSpacing = extractRawStyleValue(element.style.letterSpacing, 'letterSpacing');
|
|
528
|
-
|
|
529
|
+
// 片側だけの線(border-l-2 など)は、見えている辺の値を出す(utils/border-sides.ts)
|
|
530
|
+
const border = summarizeBorder(style, element);
|
|
531
|
+
const rawBorderWidth = border.sides
|
|
532
|
+
? extractRawStyleValue(element.style.getPropertyValue(borderSideWidthCss(border.sides[0])), 'borderWidth')
|
|
533
|
+
: extractRawStyleValue(element.style.borderWidth, 'borderWidth');
|
|
529
534
|
const rawGap = extractRawStyleValue(element.style.gap, 'gap');
|
|
530
535
|
// Position values (left/top)
|
|
531
536
|
const rawLeft = extractRawStyleValue(element.style.left, 'left');
|
|
@@ -573,9 +578,10 @@ export function extractElementInfo(
|
|
|
573
578
|
imagePositionX: parseBackgroundPositionX(style.backgroundPosition),
|
|
574
579
|
imagePositionY: parseBackgroundPositionY(style.backgroundPosition),
|
|
575
580
|
imageScale: parseBackgroundScale(style.backgroundSize),
|
|
576
|
-
borderWidth:
|
|
577
|
-
borderColor:
|
|
578
|
-
borderStyle: style
|
|
581
|
+
borderWidth: border.width,
|
|
582
|
+
borderColor: border.color,
|
|
583
|
+
borderStyle: border.style,
|
|
584
|
+
borderSides: border.sides,
|
|
579
585
|
borderRadius: parseFloat(style.borderRadius) || 0,
|
|
580
586
|
borderRadiusTopLeft: parseFloat(style.borderTopLeftRadius) || 0,
|
|
581
587
|
borderRadiusTopRight: parseFloat(style.borderTopRightRadius) || 0,
|
|
@@ -94,13 +94,8 @@ export const TAILWIND_CONFLICT_GROUPS: Record<string, string[]> = {
|
|
|
94
94
|
borderTopRightRadius: ['rounded-tr-'],
|
|
95
95
|
borderBottomRightRadius: ['rounded-br-'],
|
|
96
96
|
borderBottomLeftRadius: ['rounded-bl-'],
|
|
97
|
-
borderWidth
|
|
98
|
-
|
|
99
|
-
borderRightWidth: ['border-r-'],
|
|
100
|
-
borderBottomWidth: ['border-b-'],
|
|
101
|
-
borderLeftWidth: ['border-l-'],
|
|
102
|
-
borderColor: ['border-'],
|
|
103
|
-
borderStyle: ['border-solid', 'border-dashed', 'border-dotted', 'border-none'],
|
|
97
|
+
// borderWidth / border*Width / borderColor / borderStyle は tailwind-utils の removeConflictingClasses が
|
|
98
|
+
// クラスの形で見分けて消す(接頭辞 border- では太さ・色・スタイルが区別できない)
|
|
104
99
|
|
|
105
100
|
// Typography
|
|
106
101
|
// 注意: fontSize と color は両方 text- プレフィックスを使用するが、
|