Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface IElementModel
childMapperPreviousSibling?: Nullable<Ref<IElementModel>>
childMapperPropKey?: Nullable<string>
children: Array<IElementModel>
closestConcreteParent: Nullable<Ref<IElementModel>>
// the closest container node that element belongs to
closestContainerNode: IComponentModel | IPageModel
// closestPage: Nullable<Ref<IPageModel>>
Expand All @@ -60,6 +61,8 @@ export interface IElementModel
firstChild?: Nullable<Ref<IElementModel>>
hooks: Array<IHook>
id: string
// elements with corresponding dom elements are concrete
isConcreteElement: boolean
isRoot: boolean
isTextContentEditable: boolean
label: string
Expand All @@ -84,6 +87,11 @@ export interface IElementModel
// renderComponentType: Nullable<Ref<IComponent>>
renderingMetadata: Nullable<RenderingMetadata>
slug: string
/**
* The smallest subtree that can visually represent the current element.
* it may include the element itself or descendant elements.
*/
smallestConcreteRepresentativeSubtree: Array<IElementModel>
/**
* to render a component we create a duplicate for each element
* keeps track of source element in case this is a duplicate
Expand Down
1 change: 1 addition & 0 deletions libs/frontend/abstract/types/src/geometry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './rectangle.interface'
10 changes: 10 additions & 0 deletions libs/frontend/abstract/types/src/geometry/rectangle.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Rectangle {
bottom: number
height: number
left: number
right: number
top: number
width: number
x: number
y: number
}
1 change: 1 addition & 0 deletions libs/frontend/abstract/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './crud'
export * from './explorer-pane-type.constant'
export * from './form/form'
export * from './form/modal'
export * from './geometry'
export * from './keys'
export * from './layout'
export * from './model'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,138 +1,97 @@
import CheckOutlined from '@ant-design/icons/CheckOutlined'
import DeleteOutlined from '@ant-design/icons/DeleteOutlined'
import DragOutlined from '@ant-design/icons/DragOutlined'
import EditOutlined from '@ant-design/icons/EditOutlined'
import type { BuilderDragData } from '@codelab/frontend/abstract/application'
import {
BuilderDndAction,
isRuntimeComponentRef,
isRuntimeElementRef,
} from '@codelab/frontend/abstract/application'
import { elementRef } from '@codelab/frontend/abstract/domain'
import { MakeChildrenDraggable } from '@codelab/frontend/application/dnd'
import { elementRef, isElementRef } from '@codelab/frontend/abstract/domain'
import { useStore } from '@codelab/frontend/application/shared/store'
import { ClickOverlay } from '@codelab/frontend/presentation/view'
import {
ElementOverlay,
MarginPaddingOverlay,
} from '@codelab/frontend/presentation/view'
import { isServer } from '@codelab/shared/utils'
import { observer } from 'mobx-react-lite'
import React from 'react'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import { queryRenderedElementById } from '../../utils'

const StyledOverlayContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
max-height: 20px;
justify-content: space-between;
& > *:not(:last-child) {
margin-right: 0.3rem;
}
`

const StyledSpan = styled.p`
height: 20px;
min-width: 50px;
margin: 2px;
overflow: hidden;
white-space: nowrap;
`

const StyledOverlayButtonGroup = styled.div`
display: flex;
flex-direction: row;
`
import { useVirtualBoundingRect } from './hooks/use-virtual-bounding-rect'

export const BuilderClickOverlay = observer<{
renderContainerRef: React.MutableRefObject<HTMLElement | null>
}>(({ renderContainerRef }) => {
const { builderService, elementService, runtimeElementService } = useStore()
const selectedNode = builderService.selectedNode
const { builderService, componentService, elementService } = useStore()
const runtimeSelectedNode = builderService.selectedNode

if (isServer || !selectedNode || !isRuntimeElementRef(selectedNode)) {
return null
}
const selectedNode = !runtimeSelectedNode
? null
: isRuntimeElementRef(runtimeSelectedNode)
? runtimeSelectedNode.current.element
: isRuntimeComponentRef(runtimeSelectedNode)
? runtimeSelectedNode.current.component
: null

const element = selectedNode.current.element.current
const domElement = queryRenderedElementById(element.id)
const renderContainer = renderContainerRef.current

if (!domElement || !renderContainerRef.current) {
const boundingRect = useVirtualBoundingRect({
activeNode: selectedNode,
activeRuntimeNode: runtimeSelectedNode,
renderContainer,
})

if (
isServer ||
!selectedNode ||
!isElementRef(selectedNode) ||
!renderContainer ||
!boundingRect
) {
return null
}

const content = (
<StyledOverlayContainer>
<StyledOverlayButtonGroup>
<div
className="flex size-7 cursor-pointer items-center justify-center align-middle"
onClick={(event) => {
event.stopPropagation()
elementService.deleteModal.open(elementRef(selectedNode.id))
}}
>
<div
className="flex size-5 items-center justify-center rounded-full align-middle"
style={{ backgroundColor: '#375583', color: 'red' }}
>
<DeleteOutlined />
</div>
</div>
<MakeChildrenDraggable<BuilderDragData>
data={{
action: BuilderDndAction.MoveElement,
}}
id={selectedNode.id}
>
<div className="flex size-7 items-center justify-center align-middle">
<div
className="flex size-5 items-center justify-center rounded-full align-middle"
style={{ backgroundColor: '#375583', color: 'white' }}
>
<DragOutlined color="white" />
</div>
</div>
</MakeChildrenDraggable>
<div
className="flex size-7 cursor-pointer items-center justify-center align-middle"
onClick={(event) => {
event.stopPropagation()
element.setIsTextContentEditable(!element.isTextContentEditable)
}}
>
<div
aria-label="Toggle Content Editing"
className="flex size-5 items-center justify-center rounded-full align-middle"
style={{ backgroundColor: '#375583', color: 'white' }}
>
{element.isTextContentEditable ? (
<CheckOutlined />
) : (
<EditOutlined />
)}
</div>
</div>
</StyledOverlayButtonGroup>
<StyledSpan>{element.name}</StyledSpan>
</StyledOverlayContainer>
)
const htmlElement = queryRenderedElementById(selectedNode.id)
const parentElement = selectedNode.current.closestConcreteParent?.current

const parentHtmlElement =
parentElement && queryRenderedElementById(parentElement.id)

return createPortal(
<ClickOverlay
content={content}
dependencies={[
selectedNode.current.style.guiCss(
runtimeElementService.currentStylePseudoClass,
),
selectedNode.current.style.customCss,
element.tailwindClassNames,
element.props.values,
element.nextSibling?.id,
element.parentElement?.id,
element.isTextContentEditable,
]}
element={domElement}
renderContainer={renderContainerRef.current}
/>,
renderContainerRef.current,
<>
<ElementOverlay
parentContainer={parentHtmlElement}
rootContainer={renderContainer}
targetBoundingRect={boundingRect}
toolbar={{
draggable: {
id: selectedNode.current.id,
},
editText: {
isTextEditable: selectedNode.current.isTextContentEditable,
toggle: (event) => {
event.stopPropagation()
selectedNode.current.setIsTextContentEditable(
!selectedNode.current.isTextContentEditable,
)
},
},
onDelete: (event) => {
event.stopPropagation()

if (isElementRef(selectedNode)) {
elementService.deleteModal.open(elementRef(selectedNode.id))
} else {
componentService.deleteModal.open(selectedNode)
}
},
title: selectedNode.current.name,
}}
/>
{htmlElement && (
<MarginPaddingOverlay
element={htmlElement}
renderContainer={renderContainer}
/>
)}
</>,
renderContainer,
)
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,86 @@
import { isRuntimeElementRef } from '@codelab/frontend/abstract/application'
import {
isRuntimeComponentRef,
isRuntimeElementRef,
} from '@codelab/frontend/abstract/application'
import { isElementRef } from '@codelab/frontend/abstract/domain'
import { useStore } from '@codelab/frontend/application/shared/store'
import {
HoverOverlay,
ElementOverlay,
MarginPaddingOverlay,
} from '@codelab/frontend/presentation/view'
import { isServer } from '@codelab/shared/utils'
import { observer } from 'mobx-react-lite'
import React from 'react'
import { createPortal } from 'react-dom'
import { queryRenderedElementById } from '../../utils'
import { useVirtualBoundingRect } from './hooks/use-virtual-bounding-rect'

export const BuilderHoverOverlay = observer<{
renderContainerRef: React.MutableRefObject<HTMLElement | null>
}>(({ renderContainerRef }) => {
const { builderService } = useStore()
const hoveredNode = builderService.hoveredNode
const selectedNode = builderService.selectedNode
const runtimeHoveredNode = builderService.hoveredNode
const runtimeSelectedNode = builderService.selectedNode

if (isServer || !hoveredNode || !isRuntimeElementRef(hoveredNode)) {
return null
}
const hoveredNode = !runtimeHoveredNode
? null
: isRuntimeElementRef(runtimeHoveredNode)
? runtimeHoveredNode.current.element
: isRuntimeComponentRef(runtimeHoveredNode)
? runtimeHoveredNode.current.component
: null

const selectedNode = !runtimeSelectedNode
? null
: isRuntimeElementRef(runtimeSelectedNode)
? runtimeSelectedNode.current.element
: isRuntimeComponentRef(runtimeSelectedNode)
? runtimeSelectedNode.current.component
: null

const renderContainer = renderContainerRef.current

const element = queryRenderedElementById(hoveredNode.current.element.id)
const boundingRect = useVirtualBoundingRect({
activeNode: hoveredNode,
activeRuntimeNode: runtimeHoveredNode,
renderContainer,
})

if (
!element ||
!renderContainerRef.current ||
hoveredNode.id === selectedNode?.id
isServer ||
!hoveredNode ||
!isElementRef(hoveredNode) ||
!renderContainer ||
hoveredNode.current.id === selectedNode?.current.id ||
!boundingRect
) {
return null
}

const htmlElement = queryRenderedElementById(hoveredNode.id)
const parentElement = hoveredNode.current.closestConcreteParent?.current

const parentHtmlElement =
parentElement && queryRenderedElementById(parentElement.id)

return createPortal(
<>
{hoveredNode.id !== selectedNode?.id && (
<HoverOverlay
element={element}
renderContainer={renderContainerRef.current}
<ElementOverlay
parentContainer={parentHtmlElement}
rootContainer={renderContainer}
targetBoundingRect={boundingRect}
toolbar={{
title: hoveredNode.current.name,
}}
/>
{htmlElement && (
<MarginPaddingOverlay
element={htmlElement}
renderContainer={renderContainer}
/>
)}
<MarginPaddingOverlay
element={element}
renderContainer={renderContainerRef.current}
/>
</>,
renderContainerRef.current,
renderContainer,
)
})

Expand Down
Loading