feat(site-builder): array editors scroll to the item picked in Layers
Wires ArrayItemFieldsEditor and FeaturesEditor up to useLayerFocus() so clicking a virtual row in the Layers tree scrolls the matching item's card into view in the right-hand array editor. scrollIntoView is optional-chained on both the queried element and the method itself so a miss or an environment without it degrades silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useEditor } from '@craftjs/core';
|
import { useEditor } from '@craftjs/core';
|
||||||
import { CollapsibleSection, ArrayPropEditor, smallInputStyle } from './shared';
|
import { CollapsibleSection, ArrayPropEditor, smallInputStyle } from './shared';
|
||||||
|
import { useLayerFocus } from '../../left/LayerFocusContext';
|
||||||
|
|
||||||
/* ---------- Shared array-item field editor ----------
|
/* ---------- Shared array-item field editor ----------
|
||||||
Extracted from SectionTypePanel and GenericPropsEditor, which both had a
|
Extracted from SectionTypePanel and GenericPropsEditor, which both had a
|
||||||
@@ -18,7 +19,26 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
|
|||||||
const sampleItem = arrayItems[0] || {};
|
const sampleItem = arrayItems[0] || {};
|
||||||
const itemFields = typeof sampleItem === 'object' && sampleItem !== null ? Object.keys(sampleItem) : [];
|
const itemFields = typeof sampleItem === 'object' && sampleItem !== null ? Object.keys(sampleItem) : [];
|
||||||
|
|
||||||
|
// Layers panel -> array editor "scroll to this item" hookup. The outer
|
||||||
|
// <div ref={rootRef}> wraps ArrayPropEditor's rendered cards (the actual
|
||||||
|
// per-item background box lives in shared.tsx's ArrayPropEditor, which is
|
||||||
|
// also used by MediaStylePanel/FormStylePanel -- rather than touch that
|
||||||
|
// shared component for one consumer, the data-array-item tag below goes
|
||||||
|
// on the content renderItem returns, which is enough for scrollIntoView
|
||||||
|
// to bring the right card into the viewport.
|
||||||
|
const { focus } = useLayerFocus();
|
||||||
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!focus || focus.prop !== propKey) return;
|
||||||
|
const card = rootRef.current?.querySelector(`[data-array-item="${propKey}:${focus.index}"]`);
|
||||||
|
card?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
|
||||||
|
// `focus.nonce` is in the dep list so clicking the SAME row twice
|
||||||
|
// re-scrolls (the request object is otherwise identical).
|
||||||
|
}, [focus?.nonce, focus?.prop, focus?.index, propKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div ref={rootRef}>
|
||||||
<CollapsibleSection title={propKey.replace(/([A-Z])/g, ' $1').trim()}>
|
<CollapsibleSection title={propKey.replace(/([A-Z])/g, ' $1').trim()}>
|
||||||
<ArrayPropEditor
|
<ArrayPropEditor
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
@@ -29,6 +49,7 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
|
|||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
data-array-item={`${propKey}:${index}`}
|
||||||
value={String(item)}
|
value={String(item)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
actions.setProp(selectedId, (props: any) => {
|
actions.setProp(selectedId, (props: any) => {
|
||||||
@@ -42,7 +63,7 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<div data-array-item={`${propKey}:${index}`} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
{itemFields.map((field) => {
|
{itemFields.map((field) => {
|
||||||
const fieldVal = item[field];
|
const fieldVal = item[field];
|
||||||
if (typeof fieldVal === 'boolean') {
|
if (typeof fieldVal === 'boolean') {
|
||||||
@@ -123,5 +144,6 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useEditor } from '@craftjs/core';
|
import { useEditor } from '@craftjs/core';
|
||||||
import { labelStyle, inputStyle, sectionGap } from './shared';
|
import { labelStyle, inputStyle, sectionGap } from './shared';
|
||||||
import { AssetPicker } from '../../../ui/AssetPicker';
|
import { AssetPicker } from '../../../ui/AssetPicker';
|
||||||
|
import { useLayerFocus } from '../../left/LayerFocusContext';
|
||||||
|
|
||||||
interface Feature {
|
interface Feature {
|
||||||
title?: string; description?: string; icon?: string;
|
title?: string; description?: string; icon?: string;
|
||||||
@@ -27,10 +28,24 @@ export const FeaturesEditor: React.FC<{ selectedId: string; features: unknown }>
|
|||||||
mutate((arr) => [...arr, { title: 'New Feature', description: 'Describe this feature.', icon: '🔧', image: '', imageAlt: '', buttonText: '', buttonUrl: '' }]);
|
mutate((arr) => [...arr, { title: 'New Feature', description: 'Describe this feature.', icon: '🔧', image: '', imageAlt: '', buttonText: '', buttonUrl: '' }]);
|
||||||
const remove = (i: number) => mutate((arr) => { arr.splice(i, 1); return arr; });
|
const remove = (i: number) => mutate((arr) => { arr.splice(i, 1); return arr; });
|
||||||
|
|
||||||
|
// Layers panel -> array editor "scroll to this item" hookup. See
|
||||||
|
// ArrayItemFields.tsx for the generic-editor counterpart; this component
|
||||||
|
// keeps its own per-feature cards, so it tags/scrolls them directly.
|
||||||
|
const { focus } = useLayerFocus();
|
||||||
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!focus || focus.prop !== 'features') return;
|
||||||
|
const card = rootRef.current?.querySelector(`[data-array-item="features:${focus.index}"]`);
|
||||||
|
card?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
|
||||||
|
// `focus.nonce` is in the dep list so clicking the SAME row twice
|
||||||
|
// re-scrolls (the request object is otherwise identical).
|
||||||
|
}, [focus?.nonce, focus?.prop, focus?.index]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div ref={rootRef} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{list.map((feat, i) => (
|
{list.map((feat, i) => (
|
||||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<div key={i} data-array-item={`features:${i}`} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||||
<input type="text" value={feat.title || ''} onChange={(e) => update(i, 'title', e.target.value)} placeholder="Title" style={{ ...inputStyle, flex: 1 }} />
|
<input type="text" value={feat.title || ''} onChange={(e) => update(i, 'title', e.target.value)} placeholder="Title" style={{ ...inputStyle, flex: 1 }} />
|
||||||
<button onClick={() => remove(i)} title="Remove" style={{ padding: '2px 8px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', flex: 'none' }}>×</button>
|
<button onClick={() => remove(i)} title="Remove" style={{ padding: '2px 8px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', flex: 'none' }}>×</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user