Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 2 additions & 60 deletions app/ycode/components/CanvasTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
*/

import React, { useEffect, useMemo, useCallback, forwardRef, useImperativeHandle, useRef } from 'react';
import { createRoot } from 'react-dom/client';
import { useEditor, EditorContent } from '@tiptap/react';
import { Mark, mergeAttributes } from '@tiptap/core';
import Document from '@tiptap/extension-document';
Expand All @@ -32,14 +31,11 @@ import { RichTextImage } from '@/lib/tiptap-extensions/rich-text-image';
import { getTextStyleClasses } from '@/lib/text-format-utils';
import type { Layer, TextStyle, CollectionField, Collection } from '@/types';
import type { FieldVariable } from '@/types';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import Icon from '@/components/ui/icon';
import {
parseValueToContent,
getVariableLabel,
} from '@/lib/cms-variables-utils';
import { DynamicVariable, getDynamicVariableLabel } from '@/lib/tiptap-extensions/dynamic-variable';
import { createDynamicVariableNodeView } from '@/lib/dynamic-variable-view';
import { RichTextComponent } from '@/lib/tiptap-extensions/rich-text-component';
import { useCanvasTextEditorStore } from '@/stores/useCanvasTextEditorStore';
import { RichTextLink } from '@/lib/tiptap-extensions/rich-text-link';
Expand Down Expand Up @@ -76,61 +72,7 @@ export interface CanvasTextEditorHandle {
* DynamicVariable with React node view for the canvas text editor.
* Extends the shared extension with canvas-specific Badge styling.
*/
const DynamicVariableWithNodeView = DynamicVariable.extend({
addNodeView() {
return ({ node, getPos, editor }) => {
const container = document.createElement('span');
container.className = 'inline-block';
container.contentEditable = 'false';

const variable = node.attrs.variable;
if (variable) {
container.setAttribute('data-variable', JSON.stringify(variable));
}

const label = getDynamicVariableLabel(node);

const handleDelete = () => {
const pos = getPos();
if (typeof pos === 'number') {
editor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run();
}
};

const root = createRoot(container);

const renderBadge = () => {
root.render(
<Badge variant="inline_variable_canvas">
<span>{label}</span>
{editor.isEditable && (
<Button
onClick={handleDelete}
className="size-4! p-0! -mr-1"
variant="inline_variable_canvas"
>
<Icon name="x" className="size-2" />
</Button>
)}
</Badge>
);
};

queueMicrotask(renderBadge);

const updateListener = () => renderBadge();
editor.on('update', updateListener);

return {
dom: container,
destroy: () => {
editor.off('update', updateListener);
setTimeout(() => root.unmount(), 0);
},
};
};
},
});
const DynamicVariableWithNodeView = createDynamicVariableNodeView('canvas');

/**
* All block/mark extensions use a ref so renderHTML always reads the latest textStyles.
Expand Down
11 changes: 2 additions & 9 deletions app/ycode/components/CenterCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { cn } from '@/lib/utils';
import { getCollectionVariable, canDeleteLayer, findLayerById, findParentCollectionLayer, canLayerHaveLink, updateLayerProps, removeRichTextSublayer } from '@/lib/layer-utils';
import { CANVAS_BORDER, CANVAS_PADDING } from '@/lib/canvas-utils';
import { buildFieldGroupsForLayer, flattenFieldGroups, filterFieldGroupsByType, SIMPLE_TEXT_FIELD_TYPES } from '@/lib/collection-field-utils';
import { buildFieldVariableData } from '@/lib/variable-format-utils';
import { getRichTextValue } from '@/lib/tiptap-utils';
import { DropContainerIndicator, DropLineIndicator } from '@/components/DropIndicators';
import { DragCaptureOverlay } from '@/components/DragCaptureOverlay';
Expand Down Expand Up @@ -2147,15 +2148,7 @@ const CenterCanvas = React.memo(function CenterCanvas({
const flatFields = flattenFieldGroups(fieldGroups);
const field = flatFields.find(f => f.id === fieldId);
addFieldVariable(
{
type: 'field',
data: {
field_id: fieldId,
relationships: relationshipPath,
source,
field_type: field?.type || null,
},
},
buildFieldVariableData(fieldId, relationshipPath, field?.type ?? null, source),
flatFields,
collectionFieldsFromStore
);
Expand Down
117 changes: 75 additions & 42 deletions app/ycode/components/ExpandableRichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* a RichTextEditorSheet for full-featured editing.
*/

import { useMemo, useState } from 'react';
import { useMemo, useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import Icon from '@/components/ui/icon';
import {
Expand All @@ -15,9 +15,11 @@ import {
} from '@/components/ui/dropdown-menu';
import RichTextEditor from './RichTextEditor';
import RichTextEditorSheet from './RichTextEditorSheet';
import VariableFormatSelector from './VariableFormatSelector';
import { CollectionFieldSelector, type FieldSourceType } from './CollectionFieldSelector';
import { hasLinkOrComponent, getSoleCmsFieldBinding } from '@/lib/tiptap-utils';
import { getVariableLabel } from '@/lib/cms-variables-utils';
import { isFormattableFieldType, buildFieldVariableData } from '@/lib/variable-format-utils';
import { flattenFieldGroups, filterFieldGroupsByType, RICH_TEXT_ONLY_FIELD_TYPES } from '@/lib/collection-field-utils';
import type { CollectionField, Collection, CollectionFieldType } from '@/types';
import type { FieldGroup } from '@/lib/collection-field-utils';
Expand Down Expand Up @@ -63,12 +65,41 @@ export default function ExpandableRichTextEditor({
const [cmsDropdownOpen, setCmsDropdownOpen] = useState(false);
const isComplex = useMemo(() => hasLinkOrComponent(value), [value]);

const richTextBinding = useMemo(() => {
const soleBinding = useMemo(() => {
if (!buttonOnly) return null;
const binding = getSoleCmsFieldBinding(value);
return binding?.field_type === 'rich_text' ? binding : null;
return getSoleCmsFieldBinding(value);
}, [buttonOnly, value]);

const richTextBinding = useMemo(() => {
return soleBinding?.field_type === 'rich_text' ? soleBinding : null;
}, [soleBinding]);

const formattableBinding = useMemo(() => {
return soleBinding && isFormattableFieldType(soleBinding.field_type) ? soleBinding : null;
}, [soleBinding]);

const handleFormatChange = useCallback((formatId: string) => {
if (!value?.content?.[0]?.content?.[0]?.attrs?.variable) return;
const variable = value.content[0].content[0].attrs.variable;
const updatedContent = {
type: 'doc',
content: [{
type: 'paragraph',
content: [{
type: 'dynamicVariable',
attrs: {
...value.content[0].content[0].attrs,
variable: {
...variable,
data: { ...variable.data, format: formatId },
},
},
}],
}],
};
onChange(updatedContent);
}, [value, onChange]);

const textFieldGroups = useMemo(
() => filterFieldGroupsByType(fieldGroups, allowedFieldTypes),
[fieldGroups, allowedFieldTypes],
Expand All @@ -82,16 +113,7 @@ export default function ExpandableRichTextEditor({

const handleFieldSelect = (fieldId: string, relationshipPath: string[], source?: FieldSourceType, layerId?: string) => {
const field = fields.find(f => f.id === fieldId);
const variableData = {
type: 'field' as const,
data: {
field_id: fieldId,
relationships: relationshipPath,
source,
field_type: field?.type || null,
collection_layer_id: layerId,
},
};
const variableData = buildFieldVariableData(fieldId, relationshipPath, field?.type ?? null, source, layerId);
const label = getVariableLabel(variableData, fields, allFields);

const newContent = {
Expand All @@ -108,36 +130,47 @@ export default function ExpandableRichTextEditor({
setCmsDropdownOpen(false);
};

if (richTextBinding && !sheetOpen) {
if ((richTextBinding || formattableBinding) && !sheetOpen) {
const activeBinding = richTextBinding || formattableBinding;
return (
<>
<Button
asChild
variant="data"
className="justify-between! cursor-pointer"
onClick={() => setSheetOpen(true)}
>
<div>
<span className="flex items-center gap-1.5 truncate">
<Icon name="database" className="size-3 opacity-60 shrink-0" />
<span className="truncate">{richTextBinding.label || 'CMS Field'}</span>
</span>
<Button
className="size-4! p-0! shrink-0"
variant="outline"
onClick={(e) => {
e.stopPropagation();
if (onClear) {
onClear();
} else {
onChange({ type: 'doc', content: [{ type: 'paragraph' }] });
}
}}
>
<Icon name="x" className="size-2" />
</Button>
</div>
</Button>
<div className="flex items-center gap-1">
<Button
asChild
variant="data"
className="justify-between! cursor-pointer flex-1"
onClick={() => setSheetOpen(true)}
>
<div>
<span className="flex items-center gap-1.5 truncate">
<Icon name="database" className="size-3 opacity-60 shrink-0" />
<span className="truncate">{activeBinding!.label || 'CMS Field'}</span>
</span>
<Button
className="size-4! p-0! shrink-0"
variant="outline"
onClick={(e) => {
e.stopPropagation();
if (onClear) {
onClear();
} else {
onChange({ type: 'doc', content: [{ type: 'paragraph' }] });
}
}}
>
<Icon name="x" className="size-2" />
</Button>
</div>
</Button>
{formattableBinding && (
<VariableFormatSelector
fieldType={formattableBinding.field_type}
currentFormat={formattableBinding.format}
onFormatChange={handleFormatChange}
variant="sidebar"
/>
)}
</div>
<RichTextEditorSheet
open={sheetOpen}
onOpenChange={(open) => {
Expand Down
71 changes: 4 additions & 67 deletions app/ycode/components/RichTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ import {
} from '@/components/ui/dropdown-menu';
import { CollectionFieldSelector, type FieldSourceType } from './CollectionFieldSelector';
import { flattenFieldGroups, filterFieldGroupsByType, RICH_TEXT_ONLY_FIELD_TYPES, type FieldGroup } from '@/lib/collection-field-utils';
import { DynamicVariable, getDynamicVariableLabel } from '@/lib/tiptap-extensions/dynamic-variable';
import { buildFieldVariableData } from '@/lib/variable-format-utils';
import { createDynamicVariableNodeView } from '@/lib/dynamic-variable-view';
import { RichTextComponent } from '@/lib/tiptap-extensions/rich-text-component';
import { RichTextLink, getLinkSettingsFromMark } from '@/lib/tiptap-extensions/rich-text-link';
import { RichTextImage } from '@/lib/tiptap-extensions/rich-text-image';
Expand Down Expand Up @@ -120,61 +121,7 @@ export type { FieldVariable } from '@/types';
* DynamicVariable with React node view for the sidebar rich-text editor.
* Extends the shared extension with a Badge-based node view.
*/
const DynamicVariableWithNodeView = DynamicVariable.extend({
addNodeView() {
return ({ node, getPos, editor }) => {
const container = document.createElement('span');
container.className = 'inline-block';
container.contentEditable = 'false';

const variable = node.attrs.variable;
if (variable) {
container.setAttribute('data-variable', JSON.stringify(variable));
}

const label = getDynamicVariableLabel(node);

const handleDelete = () => {
const pos = getPos();
if (typeof pos === 'number') {
editor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run();
}
};

const root = createRoot(container);

const renderBadge = () => {
root.render(
<Badge variant="secondary">
<span>{label}</span>
{editor.isEditable && (
<Button
onClick={handleDelete}
className="size-4! p-0! -mr-1"
variant="outline"
>
<Icon name="x" className="size-2" />
</Button>
)}
</Badge>
);
};

queueMicrotask(renderBadge);

const updateListener = () => renderBadge();
editor.on('update', updateListener);

return {
dom: container,
destroy: () => {
editor.off('update', updateListener);
setTimeout(() => root.unmount(), 0);
},
};
};
},
});
const DynamicVariableWithNodeView = createDynamicVariableNodeView('sidebar');

/**
* RichTextComponent with React node view for embedding components.
Expand Down Expand Up @@ -828,18 +775,8 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({

const handleFieldSelect = (fieldId: string, relationshipPath: string[], source?: FieldSourceType, layerId?: string) => {
const field = fields.find(f => f.id === fieldId);
addFieldVariableInternal({
type: 'field',
data: {
field_id: fieldId,
relationships: relationshipPath,
source,
field_type: field?.type || null,
collection_layer_id: layerId,
},
});
addFieldVariableInternal(buildFieldVariableData(fieldId, relationshipPath, field?.type ?? null, source, layerId));

// Close the dropdown after selection
setIsDropdownOpen(false);
};

Expand Down
Loading