diff --git a/editor/src/components/text-editor/text-editor.spec.browser2.tsx b/editor/src/components/text-editor/text-editor.spec.browser2.tsx index b1378b65fd2c..99da66b44416 100644 --- a/editor/src/components/text-editor/text-editor.spec.browser2.tsx +++ b/editor/src/components/text-editor/text-editor.spec.browser2.tsx @@ -420,6 +420,115 @@ describe('Use the text editor', () => { )`), ) }) + it('deletes existing span elements', async () => { + const editor = await renderTestEditorWithCode( + formatTestProjectCode(` + import * as React from 'react' + import { Storyboard } from 'utopia-api' + + export var storyboard = ( + + + Hello + + + ) + `), + 'await-first-dom-report', + ) + + await enterTextEditMode(editor, 'start', 'span') + + deleteTypedText() + + await closeTextEditor() + await editor.getDispatchFollowUpActionsFinished() + + expect(editor.getEditorState().editor.mode.type).toEqual('select') + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( + formatTestProjectCode(` + import * as React from 'react' + import { Storyboard } from 'utopia-api' + + export var storyboard = ( + + ) + `), + ) + }) + it('does not delete span elements with event handlers', async () => { + const editor = await renderTestEditorWithCode( + formatTestProjectCode(` + import * as React from 'react' + import { Storyboard } from 'utopia-api' + + export var storyboard = ( + + console.log('click')} + > + Hello + + + ) + `), + 'await-first-dom-report', + ) + + await enterTextEditMode(editor, 'start', 'span') + + deleteTypedText() + + await closeTextEditor() + await editor.getDispatchFollowUpActionsFinished() + + expect(editor.getEditorState().editor.mode.type).toEqual('select') + expect(getPrintedUiJsCode(editor.getEditorState())).toEqual( + formatTestProjectCode(` + import * as React from 'react' + import { Storyboard } from 'utopia-api' + + export var storyboard = ( + + console.log('click')} + /> + + ) + `), + ) + }) }) describe('collapses runs of text', () => { it('only when the elements involved are eligible', async () => { @@ -1797,23 +1906,26 @@ async function testModifierExpectingWayTooManySavesTheFirstTime( return { before, after } } -async function enterTextEditMode(editor: EditorRenderResult): Promise { +async function enterTextEditMode( + editor: EditorRenderResult, + where: 'start' | 'end' = 'end', + testId: string = 'div', +): Promise { const canvasControlsLayer = editor.renderedDOM.getByTestId(CanvasControlsContainerID) - const div = editor.renderedDOM.getByTestId('div') - const divBounds = div.getBoundingClientRect() - const divCorner = { - x: divBounds.x + 50, - y: divBounds.y + 40, + const element = editor.renderedDOM.getByTestId(testId) + const bounds = element.getBoundingClientRect() + const corner = { + x: bounds.x + (where === 'start' ? 1 : 50), + y: bounds.y + (where === 'start' ? 1 : 40), } FOR_TESTS_setNextGeneratedUid('text-span') await pressKey('t') await editor.getDispatchFollowUpActionsFinished() - await mouseClickAtPoint(canvasControlsLayer, divCorner) + await mouseClickAtPoint(canvasControlsLayer, corner) await editor.getDispatchFollowUpActionsFinished() } - function typeText(text: string) { document.execCommand('insertText', false, text) } @@ -1923,6 +2035,66 @@ function projectWithoutTextWithExtraStyle(extraStyleProps: { [prop: string]: str `) } +const projectWithTextSpan = formatTestProjectCode(` +import * as React from 'react' +import { Storyboard } from 'utopia-api' + + +export var storyboard = ( + + + Hello + + +) +`) + +function projectWithoutTextSpanWithExtraStyle(extraStyleProps: { [prop: string]: string }) { + const styleProps = { + backgroundColor: '#0091FFAA', + position: 'absolute', + left: 0, + top: 0, + width: 288, + height: 362, + ...extraStyleProps, + } + + return formatTestProjectCode(` + import * as React from 'react' + import { Storyboard } from 'utopia-api' + + + export var storyboard = ( + + + Hello + + + ) + `) +} + const emptyProject = formatTestProjectCode(`import * as React from 'react' import { Storyboard } from 'utopia-api' diff --git a/editor/src/components/text-editor/text-editor.tsx b/editor/src/components/text-editor/text-editor.tsx index 1c15a6935aaa..91139ea6e20c 100644 --- a/editor/src/components/text-editor/text-editor.tsx +++ b/editor/src/components/text-editor/text-editor.tsx @@ -31,7 +31,7 @@ import { EditorModes } from '../editor/editor-modes' import { useDispatch } from '../editor/store/dispatch-context' import { MainEditorStoreProvider } from '../editor/store/store-context-providers' import { Substores, useEditorState, useRefEditorState } from '../editor/store/store-hook' -import { printCSSNumber } from '../inspector/common/css-utils' +import { DOMEventHandlerNames, printCSSNumber } from '../inspector/common/css-utils' import { toggleTextBold, toggleTextItalic, @@ -43,6 +43,8 @@ import { mapArrayToDictionary } from '../../core/shared/array-utils' import { TextRelatedProperties } from '../../core/properties/css-properties' import { assertNever } from '../../core/shared/utils' import { notice } from '../common/notice' +import type { AllElementProps } from '../editor/store/editor-state' +import { toString } from '../../core/shared/element-path' export const TextEditorSpanId = 'text-editor' @@ -305,6 +307,7 @@ const TextEditor = React.memo((props: TextEditorProps) => { ) const metadataRef = useRefEditorState((store) => store.editor.jsxMetadata) + const allElementPropsRef = useRefEditorState((store) => store.editor.allElementProps) const savedContentRef = React.useRef(null) @@ -330,6 +333,7 @@ const TextEditor = React.memo((props: TextEditorProps) => { currentElement.focus() savedContentRef.current = currentElement.textContent + const initialText = currentElement.textContent const elementCanvasFrame = MetadataUtils.getFrameOrZeroRectInCanvasCoords( elementPath, @@ -339,6 +343,12 @@ const TextEditor = React.memo((props: TextEditorProps) => { currentElement.style.minWidth = '0.5px' } + const canDeleteWhenEmpty = canDeleteElementWhenEmpty( + metadataRef.current, + elementPath, + allElementPropsRef.current, + ) + return () => { const content = currentElement.textContent if (content != null) { @@ -349,10 +359,19 @@ const TextEditor = React.memo((props: TextEditorProps) => { savedContentRef.current = content requestAnimationFrame(() => dispatch([getSaveAction(elementPath, content, textProp)])) } + // remove dangling empty spans + if ( + content != null && + initialText !== content && + content.replace(/^\n/, '').length === 0 && + canDeleteWhenEmpty + ) { + requestAnimationFrame(() => dispatch([deleteView(elementPath)])) + } } } } - }, [dispatch, elementPath, elementState, metadataRef, textProp]) + }, [dispatch, elementPath, elementState, textProp, metadataRef, allElementPropsRef]) React.useLayoutEffect(() => { if (myElement.current == null) { @@ -593,3 +612,29 @@ function getSaveAction( ): EditorAction { return updateText(elementPath, escapeHTML(content, textProp), textProp) } + +function canDeleteElementWhenEmpty( + jsxMetadata: ElementInstanceMetadataMap, + path: ElementPath, + allElementProps: AllElementProps, +): boolean { + const element = MetadataUtils.findElementByElementPath(jsxMetadata, path) + if (element == null) { + return false + } + if (!MetadataUtils.isSpan(element)) { + return false + } + + const elementProps = allElementProps[toString(path)] + if (elementProps == null) { + return false + } + + // it must not have defined event handlers + if (Object.keys(elementProps).some((prop) => DOMEventHandlerNames.includes(prop as any))) { + return false + } + + return true +}