Skip to content
Closed
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 @@ -54,6 +54,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 @@ -65,6 +66,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 @@ -89,6 +92,10 @@ 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 @@ -4,6 +4,7 @@ export * from './explorer-pane-type'
export * from './form/form'
export * from './form/modal'
export * from './form-names'
export * from './geometry'
export * from './keys'
export * from './layout'
export * from './page'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,122 +1,77 @@
import DeleteOutlined from '@ant-design/icons/DeleteOutlined'
import DragOutlined from '@ant-design/icons/DragOutlined'
import type {
BuilderDragData,
IComponentApplicationService,
IElementService,
} from '@codelab/frontend/abstract/application'
import { BuilderDndAction } from '@codelab/frontend/abstract/application'
import type { IBuilderDomainService } from '@codelab/frontend/abstract/domain'
import { isElementRef } from '@codelab/frontend/abstract/domain'
import { MakeChildrenDraggable } from '@codelab/frontend/application/dnd'
import { ClickOverlay } from '@codelab/frontend/presentation/view'
import { ElementOverlay } 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<{
builderService: IBuilderDomainService
elementService: IElementService
componentService: IComponentApplicationService
renderContainerRef: React.MutableRefObject<HTMLElement | null>
}>(({ builderService, elementService, renderContainerRef }) => {
const selectedNode = builderService.selectedNode
}>(
({
builderService,
componentService,
elementService,
renderContainerRef,
}) => {
const selectedNode = builderService.selectedNode
const renderContainer = renderContainerRef.current

if (isServer || !selectedNode || !isElementRef(selectedNode)) {
return null
}
const boundingRect = useVirtualBoundingRect({
activeNode: selectedNode,
renderContainer,
})

const element = queryRenderedElementById(selectedNode.id)
if (
renderContainerRef.current === null ||
isServer ||
!selectedNode ||
!isElementRef(selectedNode) ||
!boundingRect
) {
return null
}

if (!element || !renderContainerRef.current) {
return null
}
const parentElement = selectedNode.current.closestConcreteParent?.current

const content = (
<StyledOverlayContainer>
<StyledOverlayButtonGroup>
<div
className="flex h-7 w-7 cursor-pointer items-center justify-center align-middle"
onClick={(event) => {
event.stopPropagation()
elementService.deleteModal.open(selectedNode)
}}
>
<div
className="flex h-5 w-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 h-7 w-7 items-center justify-center align-middle">
<div
className="flex h-5 w-5 items-center justify-center rounded-full align-middle"
style={{ backgroundColor: '#375583', color: 'white' }}
>
<DragOutlined color="white" />
</div>
</div>
</MakeChildrenDraggable>
</StyledOverlayButtonGroup>
<StyledSpan>{selectedNode.current.name}</StyledSpan>
</StyledOverlayContainer>
)
const parentHtmlElement =
parentElement && queryRenderedElementById(parentElement.id)

const { closestParentElement, nextSibling } = selectedNode.current
const breakpoint = builderService.selectedBuilderBreakpoint
const props = selectedNode.current.props.values
const parentId = closestParentElement?.id
const nextSiblingId = nextSibling?.id
const dependencies = [props, nextSiblingId, breakpoint, parentId]
return createPortal(
<ElementOverlay
autoScroll
parentContainer={parentHtmlElement}
rootContainer={renderContainerRef.current}
targetBoundingRect={boundingRect}
toolbar={{
draggable: {
id: selectedNode.current.id,
},
onDelete: (event) => {
event.stopPropagation()

return createPortal(
<ClickOverlay
content={content}
dependencies={[
selectedNode.current.style.guiCss,
selectedNode.current.style.customCss,
selectedNode.current.tailwindClassNames,
selectedNode.current.props.values,
selectedNode.current.nextSibling?.id,
selectedNode.current.parentElement?.id,
]}
element={element}
renderContainer={renderContainerRef.current}
/>,
renderContainerRef.current,
)
})
if (isElementRef(selectedNode)) {
elementService.deleteModal.open(selectedNode)
} else {
componentService.deleteModal.open(selectedNode)
}
},
title: selectedNode.current.name,
}}
/>,
renderContainerRef.current,
)
},
)

BuilderClickOverlay.displayName = 'BuilderClickOverlay'
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { IElementService } from '@codelab/frontend/abstract/application'
import type { IBuilderDomainService } from '@codelab/frontend/abstract/domain'
import { isElementRef } from '@codelab/frontend/abstract/domain'
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<{
builderService: IBuilderDomainService
Expand All @@ -18,35 +19,48 @@ export const BuilderHoverOverlay = observer<{
}>(({ builderService, renderContainerRef }) => {
const hoveredNode = builderService.hoveredNode
const selectedNode = builderService.selectedNode
const renderContainer = renderContainerRef.current

if (isServer || !hoveredNode || !isElementRef(hoveredNode)) {
return null
}

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

if (
!element ||
!renderContainerRef.current ||
hoveredNode.id === selectedNode?.id
isServer ||
!hoveredNode ||
!isElementRef(hoveredNode) ||
!renderContainer ||
hoveredNode.id === selectedNode?.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
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
type IPageNodeRef,
isElementRef,
} from '@codelab/frontend/abstract/domain'
import type { Rectangle } from '@codelab/frontend/abstract/types'
import { calculateEncapsulatingRect } from '@codelab/frontend/shared/utils'
import type { Nullable } from '@codelab/shared/abstract/types'
import { useEffect, useState } from 'react'
import useResizeObserver from 'use-resize-observer/polyfilled'
import { queryRenderedElementById } from '../../../utils'

interface useVirtualBoundingRectProps {
activeNode: Nullable<IPageNodeRef>
renderContainer: Nullable<HTMLElement>
}

/**
* Finds the bounding rect of any element regardless of it being a
* concrete element (possessing html counterpart) or not.
* @param param0
* @returns
*/
export const useVirtualBoundingRect = ({
activeNode,
renderContainer,
}: useVirtualBoundingRectProps) => {
const [boundingRect, setBoundingRect] = useState<DOMRect | null>(null)

const { height, width } = useResizeObserver({
ref: renderContainer,
})

const dependencies =
activeNode && isElementRef(activeNode)
? [
activeNode.current.style.guiCss,
activeNode.current.style.customCss,
activeNode.current.tailwindClassNames,
activeNode.current.props.values,
activeNode.current.nextSibling?.id,
activeNode.current.parentElement?.id,
]
: []

useEffect(() => {
if (!activeNode || !isElementRef(activeNode)) {
return
}

const representativeElementSubset =
activeNode.current.smallestConcreteRepresentativeSubtree.map((el) => {
return queryRenderedElementById(el.id)
})

const encapsulatingRect: Rectangle = calculateEncapsulatingRect(
representativeElementSubset.filter((el) =>
Boolean(el),
) as Array<HTMLElement>,
)

setBoundingRect(
new DOMRect(
encapsulatingRect.left,
encapsulatingRect.top,
encapsulatingRect.right - encapsulatingRect.left,
encapsulatingRect.bottom - encapsulatingRect.top,
),
)
}, [height, width, activeNode, ...dependencies])

return boundingRect
}
Loading