mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
fix(web): expose hidden edit targets in layers (#2067)
* fix(web): expose hidden edit targets in layers Co-authored-by: multica-agent <github@multica.ai> * fix(web): respect visibility overrides in edit layers Co-authored-by: multica-agent <github@multica.ai> * fix(web): keep hidden layout targets editable Co-authored-by: multica-agent <github@multica.ai> * fix(web): address hidden layer review followups Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
parent
1b9caf50a0
commit
94421f9676
26 changed files with 491 additions and 105 deletions
|
|
@ -22,10 +22,12 @@ export function emptyManualEditDraft(source = ''): ManualEditDraft {
|
|||
}
|
||||
|
||||
export function ManualEditPanel({
|
||||
targets,
|
||||
selectedTarget,
|
||||
draft,
|
||||
error,
|
||||
canUndo,
|
||||
onSelectTarget,
|
||||
onDraftChange,
|
||||
onStyleChange,
|
||||
onInvalidStyle,
|
||||
|
|
@ -114,107 +116,132 @@ export function ManualEditPanel({
|
|||
};
|
||||
|
||||
return (
|
||||
<aside className="manual-edit-right">
|
||||
<section className="manual-edit-modal cc-panel">
|
||||
<div className="manual-edit-tabs" role="tablist" aria-label="Manual edit tabs">
|
||||
{(targetForInspector ? ELEMENT_TABS : PAGE_TABS).map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === item.id}
|
||||
className={tab === item.id ? 'selected' : ''}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<>
|
||||
<section className="manual-edit-layers">
|
||||
<div className="manual-edit-panel-head">
|
||||
<h3>{t('manualEdit.layers')}</h3>
|
||||
<span>{targets.length}</span>
|
||||
</div>
|
||||
{targetForInspector ? (
|
||||
<>
|
||||
{tab === 'content' ? (
|
||||
<ContentEditor
|
||||
target={targetForInspector}
|
||||
draft={draft}
|
||||
busy={busy}
|
||||
onDraftChange={onDraftChange}
|
||||
onApply={applyContent}
|
||||
/>
|
||||
) : null}
|
||||
{tab === 'style' ? (
|
||||
<StyleInspector
|
||||
styles={draft.styles}
|
||||
layoutEnabled={targetForInspector.isLayoutContainer}
|
||||
onClearSelection={onClearSelection}
|
||||
onChange={changeTargetStyle}
|
||||
/>
|
||||
) : null}
|
||||
{tab === 'attributes' ? (
|
||||
<div className="manual-edit-tab-body">
|
||||
<label className="manual-edit-field">
|
||||
<span>Attributes JSON</span>
|
||||
<textarea
|
||||
className="manual-edit-code"
|
||||
value={draft.attributesText}
|
||||
onChange={(event) => onDraftChange({ ...draft, attributesText: event.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" className="btn btn-primary" disabled={busy} onClick={applyAttributes}>Apply Attributes</button>
|
||||
</div>
|
||||
) : null}
|
||||
{tab === 'html' ? (
|
||||
<div className="manual-edit-tab-body">
|
||||
<label className="manual-edit-field">
|
||||
<span>Selected element HTML</span>
|
||||
<textarea
|
||||
className="manual-edit-code tall"
|
||||
value={draft.outerHtml}
|
||||
onChange={(event) => onDraftChange({ ...draft, outerHtml: event.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={busy}
|
||||
onClick={() => onApplyPatch({ id: targetForInspector.id, kind: 'set-outer-html', html: draft.outerHtml }, `HTML: ${targetForInspector.label}`)}
|
||||
>
|
||||
Apply HTML
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{tab === 'source' ? (
|
||||
<div className="manual-edit-layer-list">
|
||||
{targets.length > 0 ? targets.map((target) => (
|
||||
<button
|
||||
key={target.id}
|
||||
type="button"
|
||||
className={`manual-edit-layer-row${selectedTarget?.id === target.id ? ' selected' : ''}`}
|
||||
onClick={() => onSelectTarget(target)}
|
||||
>
|
||||
<strong>{target.label}</strong>
|
||||
<span>
|
||||
{target.tagName}
|
||||
{target.isHidden ? ` - ${t('manualEdit.hiddenBadge')}` : ''}
|
||||
</span>
|
||||
</button>
|
||||
)) : (
|
||||
<p className="manual-edit-empty">{t('manualEdit.noEditableLayers')}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<aside className="manual-edit-right">
|
||||
<section className="manual-edit-modal cc-panel">
|
||||
<div className="manual-edit-tabs" role="tablist" aria-label="Manual edit tabs">
|
||||
{(targetForInspector ? ELEMENT_TABS : PAGE_TABS).map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === item.id}
|
||||
className={tab === item.id ? 'selected' : ''}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{targetForInspector ? (
|
||||
<>
|
||||
{tab === 'content' ? (
|
||||
<ContentEditor
|
||||
target={targetForInspector}
|
||||
draft={draft}
|
||||
busy={busy}
|
||||
onDraftChange={onDraftChange}
|
||||
onApply={applyContent}
|
||||
/>
|
||||
) : null}
|
||||
{tab === 'style' ? (
|
||||
<StyleInspector
|
||||
styles={draft.styles}
|
||||
layoutEnabled={targetForInspector.isLayoutContainer}
|
||||
onClearSelection={onClearSelection}
|
||||
onChange={changeTargetStyle}
|
||||
/>
|
||||
) : null}
|
||||
{tab === 'attributes' ? (
|
||||
<div className="manual-edit-tab-body">
|
||||
<label className="manual-edit-field">
|
||||
<span>Attributes JSON</span>
|
||||
<textarea
|
||||
className="manual-edit-code"
|
||||
value={draft.attributesText}
|
||||
onChange={(event) => onDraftChange({ ...draft, attributesText: event.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<button type="button" className="btn btn-primary" disabled={busy} onClick={applyAttributes}>Apply Attributes</button>
|
||||
</div>
|
||||
) : null}
|
||||
{tab === 'html' ? (
|
||||
<div className="manual-edit-tab-body">
|
||||
<label className="manual-edit-field">
|
||||
<span>Selected element HTML</span>
|
||||
<textarea
|
||||
className="manual-edit-code tall"
|
||||
value={draft.outerHtml}
|
||||
onChange={(event) => onDraftChange({ ...draft, outerHtml: event.currentTarget.value })}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={busy}
|
||||
onClick={() => onApplyPatch({ id: targetForInspector.id, kind: 'set-outer-html', html: draft.outerHtml }, `HTML: ${targetForInspector.label}`)}
|
||||
>
|
||||
Apply HTML
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{tab === 'source' ? (
|
||||
<SourceEditor
|
||||
draft={draft}
|
||||
busy={busy}
|
||||
onDraftChange={onDraftChange}
|
||||
onApply={() => onApplyPatch({ kind: 'set-full-source', source: draft.fullSource }, 'Full source')}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : !targetForInspector ? (
|
||||
tab === 'source' ? (
|
||||
<SourceEditor
|
||||
draft={draft}
|
||||
busy={busy}
|
||||
onDraftChange={onDraftChange}
|
||||
onApply={() => onApplyPatch({ kind: 'set-full-source', source: draft.fullSource }, 'Full source')}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
) : !targetForInspector ? (
|
||||
tab === 'source' ? (
|
||||
<SourceEditor
|
||||
draft={draft}
|
||||
busy={busy}
|
||||
onDraftChange={onDraftChange}
|
||||
onApply={() => onApplyPatch({ kind: 'set-full-source', source: draft.fullSource }, 'Full source')}
|
||||
/>
|
||||
) : (
|
||||
<PageInspector
|
||||
enabled={pageStylesEnabled}
|
||||
onStyleChange={(styles) => {
|
||||
const normalized = normalizeManualEditStyles(styles, { layoutEnabled: true });
|
||||
if (!normalized.ok) {
|
||||
onError(normalized.error);
|
||||
onInvalidStyle?.('__body__', Object.keys(styles) as Array<keyof ManualEditStyles>);
|
||||
return;
|
||||
}
|
||||
onError('');
|
||||
onStyleChange?.('__body__', normalized.styles, 'Page styles');
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
) : (
|
||||
<PageInspector
|
||||
enabled={pageStylesEnabled}
|
||||
onStyleChange={(styles) => {
|
||||
const normalized = normalizeManualEditStyles(styles, { layoutEnabled: true });
|
||||
if (!normalized.ok) {
|
||||
onError(normalized.error);
|
||||
onInvalidStyle?.('__body__', Object.keys(styles) as Array<keyof ManualEditStyles>);
|
||||
return;
|
||||
}
|
||||
onError('');
|
||||
onStyleChange?.('__body__', normalized.styles, 'Page styles');
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{targetForInspector?.kind === 'image' && onPickImage ? (
|
||||
<div className="cc-section">
|
||||
|
|
@ -299,8 +326,9 @@ export function ManualEditPanel({
|
|||
) : null}
|
||||
|
||||
{error ? <div className="manual-edit-error">{error}</div> : null}
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,12 +119,31 @@ export function buildManualEditBridge(enabled: boolean): string {
|
|||
}
|
||||
function isLayoutContainer(el){
|
||||
var display = window.getComputedStyle(el).display || '';
|
||||
return display.indexOf('flex') >= 0 || display.indexOf('grid') >= 0;
|
||||
if (display.indexOf('flex') >= 0 || display.indexOf('grid') >= 0) return true;
|
||||
return hasOwnDisplayHiddenState(el) && inferKind(el) === 'container';
|
||||
}
|
||||
function hasOwnDisplayHiddenState(el){
|
||||
var computed = window.getComputedStyle(el);
|
||||
return computed.display === 'none' || el.hasAttribute('hidden');
|
||||
}
|
||||
function hasHiddenAncestorDisplayState(el){
|
||||
var node = el;
|
||||
while (node && node !== document.documentElement) {
|
||||
if (hasOwnDisplayHiddenState(node)) return true;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isHiddenTarget(el, rect){
|
||||
var targetVisibility = window.getComputedStyle(el).visibility;
|
||||
if (targetVisibility === 'hidden' || targetVisibility === 'collapse') return true;
|
||||
return hasHiddenAncestorDisplayState(el);
|
||||
}
|
||||
function targetFrom(el, includeOuterHtml){
|
||||
var rect = el.getBoundingClientRect();
|
||||
var kind = inferKind(el);
|
||||
var id = stableId(el);
|
||||
var hidden = isHiddenTarget(el, rect);
|
||||
var fields = {};
|
||||
if (kind === 'link') {
|
||||
fields.text = (el.textContent || '').trim();
|
||||
|
|
@ -147,6 +166,7 @@ export function buildManualEditBridge(enabled: boolean): string {
|
|||
attributes: attrsFor(el),
|
||||
styles: stylesFor(el),
|
||||
isLayoutContainer: isLayoutContainer(el),
|
||||
isHidden: hidden,
|
||||
outerHtml: includeOuterHtml ? (el.outerHTML || '').replace(/\\sdata-od-runtime-id="[^"]*"/g, '').replace(/\\sdata-od-source-path="[^"]*"/g, '').replace(/\\sdata-od-edit-selected="[^"]*"/g, '') : ''
|
||||
};
|
||||
}
|
||||
|
|
@ -155,8 +175,8 @@ export function buildManualEditBridge(enabled: boolean): string {
|
|||
var targets = [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var rect = nodes[i].getBoundingClientRect();
|
||||
if (rect.width < 4 || rect.height < 4) continue;
|
||||
if (!isSourceMappable(nodes[i])) continue;
|
||||
if (!isHiddenTarget(nodes[i], rect) && (rect.width < 4 || rect.height < 4)) continue;
|
||||
targets.push(targetFrom(nodes[i], false));
|
||||
}
|
||||
return targets;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export interface ManualEditTarget {
|
|||
attributes: Record<string, string>;
|
||||
styles: ManualEditStyles;
|
||||
isLayoutContainer: boolean;
|
||||
isHidden?: boolean;
|
||||
outerHtml: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -976,9 +976,11 @@ export const ar: Dict = {
|
|||
'fileViewer.draw': 'رسم',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -864,9 +864,11 @@ export const de: Dict = {
|
|||
'fileViewer.draw': 'Zeichnen',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -1567,9 +1567,11 @@ export const en: Dict = {
|
|||
'fileViewer.draw': 'Draw',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -865,9 +865,11 @@ export const esES: Dict = {
|
|||
'fileViewer.draw': 'Dibujar',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -1000,9 +1000,11 @@ export const fa: Dict = {
|
|||
'fileViewer.draw': 'رسم',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -992,9 +992,11 @@ export const fr: Dict = {
|
|||
'fileViewer.draw': 'Dessiner',
|
||||
'manualEdit.layers': "Calques",
|
||||
'manualEdit.editableCount': "{count} modifiable(s)",
|
||||
'manualEdit.hiddenBadge': "Masqué",
|
||||
'manualEdit.title': "Éditeur manuel",
|
||||
'manualEdit.selectLayer': "Sélectionnez un calque",
|
||||
'manualEdit.empty': "Cliquez sur un élément dans l’aperçu ou choisissez un calque.",
|
||||
'manualEdit.noEditableLayers': "Aucun calque modifiable trouvé.",
|
||||
'manualEdit.noClass': "aucune classe",
|
||||
'manualEdit.tabsAria': "Onglets d’édition manuelle",
|
||||
'manualEdit.tabContent': "Contenu",
|
||||
|
|
|
|||
|
|
@ -976,9 +976,11 @@ export const hu: Dict = {
|
|||
'fileViewer.draw': 'Rajz',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -1093,9 +1093,11 @@ export const id: Dict = {
|
|||
|
||||
'manualEdit.layers': 'Lapisan',
|
||||
'manualEdit.editableCount': '{count} dapat diedit',
|
||||
'manualEdit.hiddenBadge': 'Tersembunyi',
|
||||
'manualEdit.title': 'Editor manual',
|
||||
'manualEdit.selectLayer': 'Pilih lapisan',
|
||||
'manualEdit.empty': 'Klik elemen di pratinjau atau pilih lapisan.',
|
||||
'manualEdit.noEditableLayers': 'Tidak ada lapisan yang dapat diedit.',
|
||||
'manualEdit.noClass': 'tanpa class',
|
||||
'manualEdit.tabsAria': 'Tab edit manual',
|
||||
'manualEdit.tabContent': 'Konten',
|
||||
|
|
|
|||
|
|
@ -891,9 +891,11 @@ export const it: Dict = {
|
|||
'fileViewer.draw': 'Disegna',
|
||||
'manualEdit.layers': 'Livelli',
|
||||
'manualEdit.editableCount': '{count} modificabile',
|
||||
'manualEdit.hiddenBadge': 'Nascosto',
|
||||
'manualEdit.title': 'Editor manuale',
|
||||
'manualEdit.selectLayer': 'Seleziona un livello',
|
||||
'manualEdit.empty': 'Clicca un elemento nell\'anteprima o scegli un livello.',
|
||||
'manualEdit.noEditableLayers': 'Nessun livello modificabile trovato.',
|
||||
'manualEdit.noClass': 'nessuna classe',
|
||||
'manualEdit.tabsAria': 'Schede di modifica manuale',
|
||||
'manualEdit.tabContent': 'Contenuto',
|
||||
|
|
|
|||
|
|
@ -863,9 +863,11 @@ export const ja: Dict = {
|
|||
'fileViewer.draw': '描画',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -976,9 +976,11 @@ export const ko: Dict = {
|
|||
'fileViewer.draw': '그리기',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -976,9 +976,11 @@ export const pl: Dict = {
|
|||
'fileViewer.draw': 'Rysuj',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -999,9 +999,11 @@ export const ptBR: Dict = {
|
|||
'fileViewer.draw': 'Desenhar',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -999,9 +999,11 @@ export const ru: Dict = {
|
|||
'fileViewer.draw': 'Рисовать',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -916,9 +916,11 @@ export const th: Dict = {
|
|||
'fileViewer.draw': 'วาดรูป',
|
||||
'manualEdit.layers': "เลเยอร์",
|
||||
'manualEdit.editableCount': "ใช้แก้ได้ {count} รูปแบบ",
|
||||
'manualEdit.hiddenBadge': "ซ่อน",
|
||||
'manualEdit.title': "กล่องควบคุม",
|
||||
'manualEdit.selectLayer': "เลือกเลเยอร์ขึ้นมา",
|
||||
'manualEdit.empty': "เลือกกล่องด้านบนเพื่อเปิดดู",
|
||||
'manualEdit.noEditableLayers': "ไม่พบเลเยอร์ที่แก้ไขได้",
|
||||
'manualEdit.noClass': "ไร้คลาสสไตล์",
|
||||
'manualEdit.tabsAria': "แท็บปรับโครง",
|
||||
'manualEdit.tabContent': "ตัวหนังสือ",
|
||||
|
|
|
|||
|
|
@ -963,9 +963,11 @@ export const tr: Dict = {
|
|||
'fileViewer.draw': 'Çiz',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -1018,9 +1018,11 @@ export const uk: Dict = {
|
|||
'fileViewer.draw': 'Малювати',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "Hidden",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "No editable layers found.",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -1556,9 +1556,11 @@ export const zhCN: Dict = {
|
|||
'fileViewer.draw': '绘制',
|
||||
'manualEdit.layers': '图层',
|
||||
'manualEdit.editableCount': '{count} 个可编辑元素',
|
||||
'manualEdit.hiddenBadge': '隐藏',
|
||||
'manualEdit.title': '手动编辑器',
|
||||
'manualEdit.selectLayer': '选择图层',
|
||||
'manualEdit.empty': '在预览中点击元素,或选择一个图层。',
|
||||
'manualEdit.noEditableLayers': '未找到可编辑图层。',
|
||||
'manualEdit.noClass': '无类名',
|
||||
'manualEdit.tabsAria': '手动编辑选项卡',
|
||||
'manualEdit.tabContent': '内容',
|
||||
|
|
|
|||
|
|
@ -1167,9 +1167,11 @@ export const zhTW: Dict = {
|
|||
'fileViewer.draw': '繪製',
|
||||
'manualEdit.layers': "Layers",
|
||||
'manualEdit.editableCount': "{count} editable",
|
||||
'manualEdit.hiddenBadge': "隱藏",
|
||||
'manualEdit.title': "Manual editor",
|
||||
'manualEdit.selectLayer': "Select a layer",
|
||||
'manualEdit.empty': "Click an element in the preview or choose a layer.",
|
||||
'manualEdit.noEditableLayers': "未找到可編輯圖層。",
|
||||
'manualEdit.noClass': "no class",
|
||||
'manualEdit.tabsAria': "Manual edit tabs",
|
||||
'manualEdit.tabContent': "Content",
|
||||
|
|
|
|||
|
|
@ -1882,9 +1882,11 @@ export interface Dict {
|
|||
'fileViewer.draw': string;
|
||||
'manualEdit.layers': string;
|
||||
'manualEdit.editableCount': string;
|
||||
'manualEdit.hiddenBadge': string;
|
||||
'manualEdit.title': string;
|
||||
'manualEdit.selectLayer': string;
|
||||
'manualEdit.empty': string;
|
||||
'manualEdit.noEditableLayers': string;
|
||||
'manualEdit.noClass': string;
|
||||
'manualEdit.tabsAria': string;
|
||||
'manualEdit.tabContent': string;
|
||||
|
|
|
|||
|
|
@ -22053,16 +22053,17 @@ body.desktop-pet-shell .pet-task-item {
|
|||
/* Manual edit mode */
|
||||
.manual-edit-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(420px, 1fr) 280px;
|
||||
grid-template-columns: 240px minmax(420px, 1fr) 280px;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 10px;
|
||||
background: var(--bg);
|
||||
}
|
||||
.manual-edit-workspace > .manual-edit-canvas { grid-column: 1; grid-row: 1; }
|
||||
.manual-edit-workspace > .manual-edit-canvas { grid-column: 2; grid-row: 1; }
|
||||
.manual-edit-workspace > .manual-edit-layers { grid-column: 1; grid-row: 1; }
|
||||
.manual-edit-workspace > .manual-edit-right {
|
||||
grid-column: 2;
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { act } from 'react';
|
|||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { Simulate } from 'react-dom/test-utils';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import { ManualEditPanel, emptyManualEditDraft, manualEditPatchSummary, normalizeManualEditStyles, type ManualEditDraft } from '../../src/components/ManualEditPanel';
|
||||
import { emptyManualEditStyles, type ManualEditPatch, type ManualEditStyles, type ManualEditTarget } from '../../src/edit-mode/types';
|
||||
|
||||
|
|
@ -119,6 +120,55 @@ describe('ManualEditPanel', () => {
|
|||
expect(onClearSelection).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('lists hidden targets so they can be selected outside the canvas', () => {
|
||||
const onSelectTarget = vi.fn();
|
||||
const hiddenTarget: ManualEditTarget = {
|
||||
...target,
|
||||
id: 'authors',
|
||||
label: 'Authors',
|
||||
tagName: 'section',
|
||||
kind: 'container',
|
||||
rect: { x: 0, y: 0, width: 0, height: 0 },
|
||||
isHidden: true,
|
||||
};
|
||||
renderPanel({ targets: [target, hiddenTarget], selectedTarget: null, onSelectTarget });
|
||||
|
||||
const hiddenRow = Array.from(host.querySelectorAll('.manual-edit-layer-row'))
|
||||
.find((row) => row.textContent?.includes('Authors')) as HTMLButtonElement | undefined;
|
||||
if (!hiddenRow) throw new Error('Hidden target row not found');
|
||||
expect(hiddenRow.textContent).toContain('Hidden');
|
||||
|
||||
act(() => {
|
||||
hiddenRow.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onSelectTarget).toHaveBeenCalledWith(hiddenTarget);
|
||||
});
|
||||
|
||||
it('renders layer panel labels from the active locale', () => {
|
||||
const hiddenTarget: ManualEditTarget = {
|
||||
...target,
|
||||
id: 'authors',
|
||||
label: 'Authors',
|
||||
tagName: 'section',
|
||||
kind: 'container',
|
||||
isHidden: true,
|
||||
};
|
||||
renderPanel({ targets: [hiddenTarget], selectedTarget: null, locale: 'fr' });
|
||||
|
||||
expect(host.textContent).toContain('Calques');
|
||||
expect(host.textContent).toContain('Masqué');
|
||||
expect(host.textContent).not.toContain('Layers');
|
||||
expect(host.textContent).not.toContain('Hidden');
|
||||
});
|
||||
|
||||
it('renders the empty layers message from the active locale', () => {
|
||||
renderPanel({ targets: [], selectedTarget: null, locale: 'fr' });
|
||||
|
||||
expect(host.textContent).toContain('Aucun calque modifiable trouvé.');
|
||||
expect(host.textContent).not.toContain('No editable layers found.');
|
||||
});
|
||||
|
||||
it('normalizes font stacks and writes a usable font-family value', () => {
|
||||
const onDraftChange = vi.fn();
|
||||
const onStyleChange = vi.fn();
|
||||
|
|
@ -464,6 +514,48 @@ describe('ManualEditPanel', () => {
|
|||
expect(onStyleChange).toHaveBeenCalledWith('hero-title', { flexDirection: 'column' }, 'Style: Hero Title');
|
||||
});
|
||||
|
||||
it('keeps layout controls enabled for hidden layout containers', () => {
|
||||
const onStyleChange = vi.fn();
|
||||
const hiddenLayoutTarget: ManualEditTarget = {
|
||||
...target,
|
||||
id: 'hidden-section',
|
||||
label: 'Hidden Section',
|
||||
tagName: 'section',
|
||||
kind: 'container',
|
||||
rect: { x: 0, y: 0, width: 0, height: 0 },
|
||||
isHidden: true,
|
||||
isLayoutContainer: true,
|
||||
};
|
||||
renderPanel({
|
||||
onStyleChange,
|
||||
targets: [hiddenLayoutTarget],
|
||||
selectedTarget: hiddenLayoutTarget,
|
||||
styles: {
|
||||
...emptyManualEditStyles(),
|
||||
gap: '12px',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
});
|
||||
|
||||
const layoutSection = sectionByTitle('LAYOUT');
|
||||
expect(layoutSection.classList.contains('cc-section-inactive')).toBe(false);
|
||||
expect(layoutSection.textContent).not.toContain('Select a container or group to edit layout.');
|
||||
const gapIncrease = layoutSection.querySelector('button[aria-label="Gap increase"]') as HTMLButtonElement | null;
|
||||
const directionSelect = layoutSection.querySelector('select') as HTMLSelectElement | null;
|
||||
if (!gapIncrease || !directionSelect) throw new Error('Layout controls not found');
|
||||
expect(gapIncrease.disabled).toBe(false);
|
||||
expect(directionSelect.disabled).toBe(false);
|
||||
|
||||
act(() => {
|
||||
gapIncrease.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));
|
||||
directionSelect.value = 'column';
|
||||
directionSelect.dispatchEvent(new dom.window.Event('change', { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(onStyleChange).toHaveBeenCalledWith('hidden-section', { gap: '13px' }, 'Style: Hidden Section');
|
||||
expect(onStyleChange).toHaveBeenCalledWith('hidden-section', { flexDirection: 'column' }, 'Style: Hidden Section');
|
||||
});
|
||||
|
||||
it('summarizes full-source history entries without rendering the full file', () => {
|
||||
const source = '<html><body>' + 'x'.repeat(10_000) + '</body></html>';
|
||||
|
||||
|
|
@ -501,12 +593,15 @@ describe('ManualEditPanel', () => {
|
|||
onStyleChange = vi.fn<OnStyleChange>(),
|
||||
onInvalidStyle = vi.fn<OnInvalidStyle>(),
|
||||
onClearSelection = vi.fn<OnClearSelection>(),
|
||||
onSelectTarget = vi.fn<(target: ManualEditTarget) => void>(),
|
||||
attributesText = '{}',
|
||||
targets = [target],
|
||||
selectedTarget = target,
|
||||
styles = emptyManualEditStyles(),
|
||||
pageStylesEnabled = true,
|
||||
outerHtml = target.outerHtml,
|
||||
fullSource = '<html></html>',
|
||||
locale,
|
||||
}: {
|
||||
onDraftChange?: OnDraftChange;
|
||||
onApplyPatch?: OnApplyPatch;
|
||||
|
|
@ -514,12 +609,15 @@ describe('ManualEditPanel', () => {
|
|||
onStyleChange?: OnStyleChange;
|
||||
onInvalidStyle?: OnInvalidStyle;
|
||||
onClearSelection?: OnClearSelection;
|
||||
onSelectTarget?: (target: ManualEditTarget) => void;
|
||||
attributesText?: string;
|
||||
targets?: ManualEditTarget[];
|
||||
selectedTarget?: ManualEditTarget | null;
|
||||
styles?: ReturnType<typeof emptyManualEditStyles>;
|
||||
pageStylesEnabled?: boolean;
|
||||
outerHtml?: string;
|
||||
fullSource?: string;
|
||||
locale?: 'en' | 'fr';
|
||||
} = {}) {
|
||||
const draft = {
|
||||
...emptyManualEditDraft(fullSource),
|
||||
|
|
@ -529,9 +627,9 @@ describe('ManualEditPanel', () => {
|
|||
outerHtml,
|
||||
};
|
||||
act(() => {
|
||||
root.render(
|
||||
const panel = (
|
||||
<ManualEditPanel
|
||||
targets={[target]}
|
||||
targets={targets}
|
||||
selectedTarget={selectedTarget}
|
||||
draft={draft}
|
||||
history={[]}
|
||||
|
|
@ -539,7 +637,7 @@ describe('ManualEditPanel', () => {
|
|||
canUndo={false}
|
||||
canRedo={false}
|
||||
pageStylesEnabled={pageStylesEnabled}
|
||||
onSelectTarget={vi.fn<(target: ManualEditTarget) => void>()}
|
||||
onSelectTarget={onSelectTarget}
|
||||
onDraftChange={onDraftChange}
|
||||
onStyleChange={onStyleChange}
|
||||
onInvalidStyle={onInvalidStyle}
|
||||
|
|
@ -549,7 +647,10 @@ describe('ManualEditPanel', () => {
|
|||
onCancelDraft={vi.fn<() => void>()}
|
||||
onUndo={vi.fn<() => void>()}
|
||||
onRedo={vi.fn<() => void>()}
|
||||
/>,
|
||||
/>
|
||||
);
|
||||
root.render(
|
||||
locale ? <I18nProvider initial={locale}>{panel}</I18nProvider> : panel,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,201 @@ describe('manual edit bridge target normalization', () => {
|
|||
expect(isMeaningfulManualEditElement(script, { width: 80, height: 24 })).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps source-mappable display:none targets available for the layers panel', async () => {
|
||||
const posts: Array<{ type?: string; targets?: Array<{ id: string; isHidden?: boolean }> }> = [];
|
||||
const dom = new JSDOM(
|
||||
`<main>
|
||||
<h1 data-od-source-path="path-0-0">Visible title</h1>
|
||||
<section data-od-source-path="path-0-1" style="display:none">
|
||||
<p data-od-source-path="path-0-1-0">Hidden author notes</p>
|
||||
</section>
|
||||
</main>${buildManualEditBridge(true)}`,
|
||||
{ runScripts: 'dangerously', url: 'http://localhost' },
|
||||
);
|
||||
const visible = dom.window.document.querySelector('h1')!;
|
||||
const hiddenSection = dom.window.document.querySelector('section')!;
|
||||
const hiddenParagraph = dom.window.document.querySelector('p')!;
|
||||
visible.getBoundingClientRect = () => ({
|
||||
x: 0, y: 0, width: 160, height: 32,
|
||||
top: 0, right: 160, bottom: 32, left: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
hiddenSection.getBoundingClientRect = () => ({
|
||||
x: 0, y: 0, width: 0, height: 0,
|
||||
top: 0, right: 0, bottom: 0, left: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
hiddenParagraph.getBoundingClientRect = hiddenSection.getBoundingClientRect;
|
||||
dom.window.parent.postMessage = ((message: unknown) => {
|
||||
posts.push(message as { type?: string; targets?: Array<{ id: string; isHidden?: boolean }> });
|
||||
}) as typeof dom.window.parent.postMessage;
|
||||
|
||||
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
||||
data: { type: 'od-edit-mode', enabled: true },
|
||||
}));
|
||||
await new Promise((resolve) => dom.window.setTimeout(resolve, 0));
|
||||
|
||||
const targetsMessage = posts.find((message) => message.type === 'od-edit-targets');
|
||||
expect(targetsMessage?.targets?.map((target) => target.id)).toEqual([
|
||||
'path-0-0',
|
||||
'path-0-1',
|
||||
'path-0-1-0',
|
||||
]);
|
||||
expect(targetsMessage?.targets?.find((target) => target.id === 'path-0-1')?.isHidden).toBe(true);
|
||||
expect(targetsMessage?.targets?.find((target) => target.id === 'path-0-1-0')?.isHidden).toBe(true);
|
||||
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
it('treats hidden containers as layout editable targets', async () => {
|
||||
const posts: Array<{ type?: string; targets?: Array<{ id: string; isHidden?: boolean; isLayoutContainer?: boolean }> }> = [];
|
||||
const dom = new JSDOM(
|
||||
`<main>
|
||||
<section data-od-source-path="path-0-0" style="display:none">
|
||||
<p data-od-source-path="path-0-0-0">Hidden layout copy</p>
|
||||
</section>
|
||||
</main>${buildManualEditBridge(true)}`,
|
||||
{ runScripts: 'dangerously', url: 'http://localhost' },
|
||||
);
|
||||
const section = dom.window.document.querySelector('section')!;
|
||||
const paragraph = dom.window.document.querySelector('p')!;
|
||||
section.getBoundingClientRect = () => ({
|
||||
x: 0, y: 0, width: 0, height: 0,
|
||||
top: 0, right: 0, bottom: 0, left: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
paragraph.getBoundingClientRect = section.getBoundingClientRect;
|
||||
dom.window.parent.postMessage = ((message: unknown) => {
|
||||
posts.push(message as { type?: string; targets?: Array<{ id: string; isHidden?: boolean; isLayoutContainer?: boolean }> });
|
||||
}) as typeof dom.window.parent.postMessage;
|
||||
|
||||
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
||||
data: { type: 'od-edit-mode', enabled: true },
|
||||
}));
|
||||
await new Promise((resolve) => dom.window.setTimeout(resolve, 0));
|
||||
|
||||
const targetsMessage = posts.find((message) => message.type === 'od-edit-targets');
|
||||
const hiddenSection = targetsMessage?.targets?.find((target) => target.id === 'path-0-0');
|
||||
const hiddenParagraph = targetsMessage?.targets?.find((target) => target.id === 'path-0-0-0');
|
||||
expect(hiddenSection?.isHidden).toBe(true);
|
||||
expect(hiddenSection?.isLayoutContainer).toBe(true);
|
||||
expect(hiddenParagraph?.isLayoutContainer).toBe(false);
|
||||
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
it('does not treat visibility-hidden block containers as layout editable targets', async () => {
|
||||
const posts: Array<{ type?: string; targets?: Array<{ id: string; isHidden?: boolean; isLayoutContainer?: boolean }> }> = [];
|
||||
const dom = new JSDOM(
|
||||
`<main>
|
||||
<section data-od-source-path="path-0-0" style="visibility:hidden">
|
||||
<p data-od-source-path="path-0-0-0">Hidden block copy</p>
|
||||
</section>
|
||||
</main>${buildManualEditBridge(true)}`,
|
||||
{ runScripts: 'dangerously', url: 'http://localhost' },
|
||||
);
|
||||
const section = dom.window.document.querySelector('section')!;
|
||||
const paragraph = dom.window.document.querySelector('p')!;
|
||||
section.getBoundingClientRect = () => ({
|
||||
x: 0, y: 0, width: 160, height: 32,
|
||||
top: 0, right: 160, bottom: 32, left: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
paragraph.getBoundingClientRect = () => ({
|
||||
x: 8, y: 8, width: 140, height: 20,
|
||||
top: 8, right: 148, bottom: 28, left: 8,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
dom.window.parent.postMessage = ((message: unknown) => {
|
||||
posts.push(message as { type?: string; targets?: Array<{ id: string; isHidden?: boolean; isLayoutContainer?: boolean }> });
|
||||
}) as typeof dom.window.parent.postMessage;
|
||||
|
||||
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
||||
data: { type: 'od-edit-mode', enabled: true },
|
||||
}));
|
||||
await new Promise((resolve) => dom.window.setTimeout(resolve, 0));
|
||||
|
||||
const targetsMessage = posts.find((message) => message.type === 'od-edit-targets');
|
||||
const hiddenSection = targetsMessage?.targets?.find((target) => target.id === 'path-0-0');
|
||||
expect(hiddenSection?.isHidden).toBe(true);
|
||||
expect(hiddenSection?.isLayoutContainer).toBe(false);
|
||||
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
it('does not treat block containers hidden only by an ancestor as layout editable targets', async () => {
|
||||
const posts: Array<{ type?: string; targets?: Array<{ id: string; isHidden?: boolean; isLayoutContainer?: boolean }> }> = [];
|
||||
const dom = new JSDOM(
|
||||
`<main>
|
||||
<div data-od-source-path="path-0-0" style="display:none">
|
||||
<section data-od-source-path="path-0-0-0">Nested hidden section</section>
|
||||
</div>
|
||||
</main>${buildManualEditBridge(true)}`,
|
||||
{ runScripts: 'dangerously', url: 'http://localhost' },
|
||||
);
|
||||
const wrapper = dom.window.document.querySelector('div')!;
|
||||
const section = dom.window.document.querySelector('section')!;
|
||||
wrapper.getBoundingClientRect = () => ({
|
||||
x: 0, y: 0, width: 0, height: 0,
|
||||
top: 0, right: 0, bottom: 0, left: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
section.getBoundingClientRect = wrapper.getBoundingClientRect;
|
||||
dom.window.parent.postMessage = ((message: unknown) => {
|
||||
posts.push(message as { type?: string; targets?: Array<{ id: string; isHidden?: boolean; isLayoutContainer?: boolean }> });
|
||||
}) as typeof dom.window.parent.postMessage;
|
||||
|
||||
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
||||
data: { type: 'od-edit-mode', enabled: true },
|
||||
}));
|
||||
await new Promise((resolve) => dom.window.setTimeout(resolve, 0));
|
||||
|
||||
const targetsMessage = posts.find((message) => message.type === 'od-edit-targets');
|
||||
const hiddenSection = targetsMessage?.targets?.find((target) => target.id === 'path-0-0-0');
|
||||
expect(hiddenSection?.isHidden).toBe(true);
|
||||
expect(hiddenSection?.isLayoutContainer).toBe(false);
|
||||
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
it('does not mark visibility:visible descendants as hidden', async () => {
|
||||
const posts: Array<{ type?: string; targets?: Array<{ id: string; isHidden?: boolean }> }> = [];
|
||||
const dom = new JSDOM(
|
||||
`<main>
|
||||
<section data-od-source-path="path-0-0" style="visibility:hidden">
|
||||
<p data-od-source-path="path-0-0-0" style="visibility:visible">Visible child copy</p>
|
||||
</section>
|
||||
</main>${buildManualEditBridge(true)}`,
|
||||
{ runScripts: 'dangerously', url: 'http://localhost' },
|
||||
);
|
||||
const section = dom.window.document.querySelector('section')!;
|
||||
const visibleChild = dom.window.document.querySelector('p')!;
|
||||
section.getBoundingClientRect = () => ({
|
||||
x: 0, y: 0, width: 160, height: 32,
|
||||
top: 0, right: 160, bottom: 32, left: 0,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
visibleChild.getBoundingClientRect = () => ({
|
||||
x: 8, y: 8, width: 140, height: 20,
|
||||
top: 8, right: 148, bottom: 28, left: 8,
|
||||
toJSON: () => ({}),
|
||||
} as DOMRect);
|
||||
dom.window.parent.postMessage = ((message: unknown) => {
|
||||
posts.push(message as { type?: string; targets?: Array<{ id: string; isHidden?: boolean }> });
|
||||
}) as typeof dom.window.parent.postMessage;
|
||||
|
||||
dom.window.dispatchEvent(new dom.window.MessageEvent('message', {
|
||||
data: { type: 'od-edit-mode', enabled: true },
|
||||
}));
|
||||
await new Promise((resolve) => dom.window.setTimeout(resolve, 0));
|
||||
|
||||
const targetsMessage = posts.find((message) => message.type === 'od-edit-targets');
|
||||
expect(targetsMessage?.targets?.find((target) => target.id === 'path-0-0')?.isHidden).toBe(true);
|
||||
expect(targetsMessage?.targets?.find((target) => target.id === 'path-0-0-0')?.isHidden).toBe(false);
|
||||
|
||||
dom.window.close();
|
||||
});
|
||||
|
||||
it('does not expose path targets unless they carry a source path marker', () => {
|
||||
const dom = new JSDOM('<main><h1>Runtime title</h1><p data-od-source-path="path-0-1">Source text</p></main>');
|
||||
const runtimeTitle = dom.window.document.querySelector('h1')!;
|
||||
|
|
|
|||
Loading…
Reference in a new issue