@growgroup/visual-editor 0.2.0 → 0.2.2
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
CHANGED
|
@@ -1293,7 +1293,7 @@ export const EditorPropertyPanel = memo(function EditorPropertyPanel() {
|
|
|
1293
1293
|
<span className="truncate font-medium">部品: {selectedPartLabel}</span>
|
|
1294
1294
|
</div>
|
|
1295
1295
|
<p className="text-[10px] leading-snug text-gray-500">
|
|
1296
|
-
スロット(data-slot)
|
|
1296
|
+
スロット(data-slot)の中だけ編集できます。「この姿で部品を更新」で定義が変わり、他ページへの反映は利用側(savePart)が行います。
|
|
1297
1297
|
</p>
|
|
1298
1298
|
<div className="flex gap-1">
|
|
1299
1299
|
<Button
|
|
@@ -683,11 +683,11 @@ export function useComponentEditMode({
|
|
|
683
683
|
});
|
|
684
684
|
closeContextMenu();
|
|
685
685
|
toast.success('部品から切り離しました', {
|
|
686
|
-
description: 'この要素は普通の HTML
|
|
686
|
+
description: 'この要素は普通の HTML になり、部品の更新の対象から外れます',
|
|
687
687
|
});
|
|
688
688
|
}, [selectedElement?.id, getIframeDoc, notifyIframeChange, setSelectedElement, closeContextMenu]);
|
|
689
689
|
|
|
690
|
-
// インスタンスの今の姿で定義を更新する(版 +1)
|
|
690
|
+
// インスタンスの今の姿で定義を更新する(版 +1)。他ページへの反映は利用側(io.savePart の実装)が行う
|
|
691
691
|
const handleUpdatePart = useCallback(async () => {
|
|
692
692
|
if (!selectedElement?.id) return;
|
|
693
693
|
const doc = getIframeDoc();
|
|
@@ -698,9 +698,7 @@ export function useComponentEditMode({
|
|
|
698
698
|
if (!def) return;
|
|
699
699
|
notifyIframeChange(true);
|
|
700
700
|
closeContextMenu();
|
|
701
|
-
toast.success(`部品「${def.name ?? def.id}」を v${def.version}
|
|
702
|
-
description: '他のページのインスタンスへは parts:sync で反映します',
|
|
703
|
-
});
|
|
701
|
+
toast.success(`部品「${def.name ?? def.id}」を v${def.version} に更新しました`);
|
|
704
702
|
} catch (error) {
|
|
705
703
|
console.error('Failed to update part:', error);
|
|
706
704
|
toast.error('部品の更新に失敗しました');
|
package/src/editor/parts.ts
CHANGED
|
@@ -148,15 +148,21 @@ export function stripEditorAttrs(root: Element): void {
|
|
|
148
148
|
/** スロットを推定して付ける対象(文字や画像を持ちうる要素) */
|
|
149
149
|
const SLOT_TAGS: Record<string, string> = {
|
|
150
150
|
h1: 'heading', h2: 'heading', h3: 'heading', h4: 'heading', h5: 'heading', h6: 'heading',
|
|
151
|
-
p: 'body',
|
|
152
|
-
figcaption: 'caption', blockquote: 'quote',
|
|
153
|
-
td: 'cell', th: 'cell', label: 'label', span: 'text', time: 'time', small: 'note',
|
|
151
|
+
p: 'body', a: 'link', button: 'button', img: 'image',
|
|
152
|
+
figcaption: 'caption', blockquote: 'quote', label: 'label', span: 'text', time: 'time', small: 'note',
|
|
154
153
|
};
|
|
155
154
|
|
|
155
|
+
/** タグだけの骨格(「同じ形の兄弟」の判定用。文字と属性は見ない) */
|
|
156
|
+
const tagSkeleton = (el: Element): string =>
|
|
157
|
+
`${el.tagName.toLowerCase()}${el.children.length ? `(${Array.from(el.children).map(tagSkeleton).join(',')})` : ''}`;
|
|
158
|
+
|
|
156
159
|
/**
|
|
157
|
-
* スロットの推定。既に `data-slot`
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
+
* スロットの推定。既に `data-slot` があるものはそのまま。祖先が既にスロットなら付けない(入れ子のスロットを作らない)。
|
|
161
|
+
*
|
|
162
|
+
* 1. 繰り返し(ul/ol・dl・table、同じ形の子が 2 つ以上並ぶ器)は**器ごと 1 つのスロット**(list / items)。
|
|
163
|
+
* カードが 3 枚か 4 枚かはページの内容であって部品の構造ではないので、項目ごとにスロットを切らない
|
|
164
|
+
* (切ると、定義より多い項目が sync で捨てられる)。ルート自身が繰り返しの器ならルートがスロットになる
|
|
165
|
+
* 2. 残りはタグで(見出し→heading、段落→body、リンク→link …)。span は文字だけを直接持つときだけ。svg の中は見ない
|
|
160
166
|
* 戻り値は付けた数。
|
|
161
167
|
*/
|
|
162
168
|
export function inferSlots(root: Element): number {
|
|
@@ -172,12 +178,32 @@ export function inferSlots(root: Element): number {
|
|
|
172
178
|
const base = v.replace(/-\d+$/, '');
|
|
173
179
|
if (base) used.set(base, Math.max(used.get(base) ?? 0, 1));
|
|
174
180
|
}
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
const inSlot = (el: Element) => {
|
|
182
|
+
const a = el.parentElement?.closest(`[${SLOT_ATTR}]`);
|
|
183
|
+
return !!(a && root.contains(a));
|
|
184
|
+
};
|
|
185
|
+
const skip = (el: Element) => !!el.closest('svg') || el.hasAttribute(SLOT_ATTR) || inSlot(el);
|
|
186
|
+
const isRepeatContainer = (el: Element) => {
|
|
187
|
+
const kids = Array.from(el.children);
|
|
188
|
+
const tag = el.tagName.toLowerCase();
|
|
189
|
+
if ((tag === 'ul' || tag === 'ol') && kids.some((k) => k.tagName.toLowerCase() === 'li')) return 'list';
|
|
190
|
+
if (tag === 'dl' || tag === 'table') return 'list';
|
|
191
|
+
if (kids.length >= 2 && kids.every((k) => tagSkeleton(k) === tagSkeleton(kids[0]))) return 'items';
|
|
192
|
+
return null;
|
|
193
|
+
};
|
|
194
|
+
// 1. 繰り返しの器(ルート自身も含む。文書順なので外側が先に決まり、中の器は祖先がスロットになって飛ばされる)
|
|
195
|
+
for (const el of [root, ...Array.from(root.querySelectorAll('*'))]) {
|
|
196
|
+
if (el !== root && skip(el)) continue;
|
|
197
|
+
if (el === root && el.hasAttribute(SLOT_ATTR)) continue;
|
|
198
|
+
const kind = isRepeatContainer(el);
|
|
199
|
+
if (!kind) continue;
|
|
200
|
+
el.setAttribute(SLOT_ATTR, nameFor(kind));
|
|
201
|
+
count++;
|
|
202
|
+
}
|
|
203
|
+
if (root.hasAttribute(SLOT_ATTR)) return count;
|
|
204
|
+
// 2. 残りはタグで
|
|
205
|
+
for (const el of Array.from(root.querySelectorAll('*'))) {
|
|
206
|
+
if (skip(el)) continue;
|
|
181
207
|
const tag = el.tagName.toLowerCase();
|
|
182
208
|
const base = SLOT_TAGS[tag];
|
|
183
209
|
if (!base) continue;
|
package/src/io.ts
CHANGED
|
@@ -77,7 +77,7 @@ export type UploadResult = { url: string; width?: number; height?: number };
|
|
|
77
77
|
*
|
|
78
78
|
* ページに挿すときは**実体化**する: 定義を複製してルートに `data-part` `data-part-v` を付けた
|
|
79
79
|
* 完全な HTML がページに残る。エディタは `data-slot` の中だけを編集対象にし、
|
|
80
|
-
*
|
|
80
|
+
* 定義を変えたあとの他ページへの反映は利用側(savePart の実装)が行う。
|
|
81
81
|
*/
|
|
82
82
|
export type EditorPartDef = {
|
|
83
83
|
/** 識別子。`data-part` の値になる(ファイル名にも使うので英数字と - _ だけ) */
|