@growgroup/visual-editor 0.2.1 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/editor/parts.ts +38 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growgroup/visual-editor",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "HTMLをブラウザ上で直接編集するビジュアルエディタ。スライド(固定サイズ)とWebページ(可変高さ)の両方に使え、保存先はアダプタで差し替えられます。",
5
5
  "keywords": [
6
6
  "visual-editor",
@@ -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', li: 'item', a: 'link', button: 'button', img: 'image',
152
- figcaption: 'caption', blockquote: 'quote', dt: 'term', dd: 'desc',
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
- * span は文字だけを直接持つときだけ。svg の中は見ない。
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 candidates = Array.from(root.querySelectorAll('*'));
176
- for (const el of candidates) {
177
- if (el.closest('svg')) continue;
178
- if (el.hasAttribute(SLOT_ATTR)) continue;
179
- const ancestorSlot = el.parentElement?.closest(`[${SLOT_ATTR}]`);
180
- if (ancestorSlot && root.contains(ancestorSlot)) continue;
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;