Skip to content

Commit

Permalink
split editor and providers
Browse files Browse the repository at this point in the history
  • Loading branch information
LiveDuo committed Nov 5, 2023
1 parent fedfbb0 commit 403c115
Show file tree
Hide file tree
Showing 2 changed files with 383 additions and 377 deletions.
381 changes: 381 additions & 0 deletions lib/client/vanilla/editor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,381 @@
import React, { useEffect, useRef, useState } from 'react'

import './index.css'

import TrashIcon from '@heroicons/react/24/outline/TrashIcon'
import ArrowDownIcon from '@heroicons/react/24/outline/ArrowDownIcon'
import ArrowUpIcon from '@heroicons/react/24/outline/ArrowUpIcon'

import Squares2X2Icon from '@heroicons/react/24/outline/Squares2X2Icon'
import ArrowSmallUpIcon from '@heroicons/react/24/outline/ArrowSmallUpIcon'
import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon'

import Select from './select'

const standaloneServerPort = 12785

const themes = [
{ name: 'Hyper UI', folder: 'hyperui' },
{ name: 'Tailblocks', folder: 'tailblocks' },
{ name: 'Flowrift', folder: 'flowrift' },
{ name: 'Meraki UI', folder: 'meraki-light' },
{ name: 'Preline', folder: 'preline' },
{ name: 'Flowbite', folder: 'flowbite' },
]

function debounce(callback, timeout = 1000) {
let timeoutFn
return (...args) => {
const context = this
clearTimeout(timeoutFn)
timeoutFn = setTimeout(() => callback.apply(context, args), timeout)
}
}

const getBaseUrl = (standaloneServer) => {
return standaloneServer ? `http://localhost:${standaloneServerPort}` : ''
}

const getImageUrl = (standaloneServer, imageSrc) => {
const baseUrl = getBaseUrl(standaloneServer)
return `${baseUrl}/api/builder/handle?type=asset&path=${imageSrc}`
}

const Category = ({ themeIndex, category, components, standaloneServer }) => {
const [show, setShow] = useState(false)

return (
<div id={category.toLowerCase()}>
<div
onClick={() => setShow((i) => !i)}
className={`h-12 cursor-pointer bg-white border-b last:border-b-0 flex items-center px-2 ${
show ? 'shadow-sm' : ''
}`}
>
<div className="flex-1 flex items-center">
<Squares2X2Icon className="h-4 w-4 ml-2 mr-4" />{' '}
<h2 className="text-xs uppercase">{category}</h2>
</div>
<a style={{ transform: `rotate(${show ? 180 : 0}deg)` }}>
<ArrowSmallUpIcon className="h-4 w-4" />
</a>
</div>
{show && (
<div>
{components.map((c, i) => (
<img
key={i}
className="cursor-grab mb-2"
src={getImageUrl(
standaloneServer,
`/themes/${themes[themeIndex].name.replaceAll(' ', '')}/${c.folder}/preview.png`,
)}
draggable="true"
onDragStart={(e) => e.dataTransfer.setData('component', `${category}-${i}`)}
/>
))}
</div>
)}
</div>
)
}

function Editor({ standaloneServer = false }) {
const canvasRef = useRef(null)

const popoverRef = useRef(null)
const moveUpRef = useRef(null)
const moveDownRef = useRef(null)
const deleteRef = useRef(null)

const [isPreview, setIsPreview] = useState(false)
const [hoveredComponent, setHoveredComponent] = useState()
const [components, setComponents] = useState([])

const [selectOpen, setSelectOpen] = useState(false)

const [themeIndex, setThemeIndex] = useState(0)

const loadTheme = async (index) => {
const baseUrl = getBaseUrl(standaloneServer)
const url = `${baseUrl}/api/builder/handle?type=theme&name=${themes[index].folder}`
const _componentsList = await fetch(url).then((r) => r.json())

const _components = _componentsList.reduce((r, c) => {
const category = c.folder.replace(/[0-9]/g, '')
if (!r[category]) r[category] = []
r[category].push(c)
return r
}, {})

setComponents(_components)
}

const loadPage = async () => {
const baseUrl = getBaseUrl(standaloneServer)
const url2 = `${baseUrl}/api/builder/handle?type=data&path=${location.pathname}`
const data = await fetch(url2).then((r) => r.text())
canvasRef.current.innerHTML = data
}

const savePage = async () => {
console.log('dom changed')

const baseUrl = getBaseUrl(standaloneServer)
const url = `${baseUrl}/api/builder/handle?type=data&path=${location.pathname}`

await fetch(url, { method: 'post', body: canvasRef.current.innerHTML })
}

const onDomChange = () => {
canvasRef.current
const config = { attributes: true, childList: true, subtree: true }
const observer = new MutationObserver(
debounce(() => {
savePage()
}),
)
observer.observe(canvasRef.current, config)
return observer
}

useEffect(() => {
loadPage()
loadTheme(themeIndex)

const observer = onDomChange()

return () => observer.disconnect()
}, [])

const clearComponents = async () => {
canvasRef.current.innerHTML = ''
}

const onCanvasDrop = async (e) => {
e.preventDefault()

const [categoryId, componentId] = e.dataTransfer.getData('component').split('-')
const html = components[categoryId][componentId].source

const _components = getComponents()
if (_components.length === 0) {
canvasRef.current.innerHTML = html
} else if (isElementTopHalf(hoveredComponent, e)) {
hoveredComponent.insertAdjacentHTML('beforebegin', html)
} else if (!isElementTopHalf(hoveredComponent, e)) {
hoveredComponent.insertAdjacentHTML('afterend', html)
}

cleanCanvas()
}

const getElementPosition = (element) => {
const box = element.getBoundingClientRect()

const body = document.body
const documentElement = document.documentElement

const scrollTop = window.scrollY || documentElement.scrollTop || body.scrollTop
const scrollLeft = window.scrollX || documentElement.scrollLeft || body.scrollLeft

const clientTop = documentElement.clientTop || body.clientTop || 0
const clientLeft = documentElement.clientLeft || body.clientLeft || 0

return { top: box.top + scrollTop - clientTop, left: box.left + scrollLeft - clientLeft }
}

const onCanvasMouseOver = () => {
const components = getComponents()
components.forEach((c) => {
if (c.matches(':hover')) {
if (!c.isEqualNode(hoveredComponent)) {
setHoveredComponent(c)

const rect = getElementPosition(c)
popoverRef.current.style.top = `${rect.top}px`
popoverRef.current.style.left = `${rect.left}px`
}
}
})
}

const onCanvasMouseLeave = () => {
setHoveredComponent()
}

const isEventOnElement = (element, event) => {
if (!element) return
const rect = element.getBoundingClientRect()
const isX = rect.top < event.clientY && rect.bottom > event.clientY
const isY = rect.left < event.clientX && rect.right > event.clientX
return isX && isY
}

const onCanvasClick = (e) => {
if (isEventOnElement(deleteRef.current, e)) {
const clickEvent = new MouseEvent('click', { bubbles: true })
deleteRef.current.dispatchEvent(clickEvent)
} else if (isEventOnElement(moveUpRef.current, e)) {
const clickEvent = new MouseEvent('click', { bubbles: true })
moveUpRef.current.dispatchEvent(clickEvent)
} else if (isEventOnElement(moveDownRef.current, e)) {
const clickEvent = new MouseEvent('click', { bubbles: true })
moveDownRef.current.dispatchEvent(clickEvent)
}
}

const onComponentDelete = () => {
canvasRef.current.removeChild(hoveredComponent)
setHoveredComponent()
}

const onComponentMoveUp = () => {
canvasRef.current.insertBefore(hoveredComponent, hoveredComponent.previousElementSibling)
}

const onComponentMoveDown = () => {
canvasRef.current.insertBefore(hoveredComponent.nextElementSibling, hoveredComponent)
}

const isElementTopHalf = (element, event) => {
const rect = element.getBoundingClientRect()
return rect.top + (rect.bottom - rect.top) / 2 > event.clientY
}

const onCanvasDragOver = (e) => {
e.preventDefault()

const components = getComponents()
components.forEach((c) => {
if (isEventOnElement(c, e)) {
const isTopHalf = isElementTopHalf(c, e)
c.style[`border-${isTopHalf ? 'top' : 'bottom'}`] = '4px solid cornflowerblue'
c.style[`border-${!isTopHalf ? 'top' : 'bottom'}`] = ''

if (!c.isEqualNode(hoveredComponent)) {
setHoveredComponent(c)
}
}
})
}

const cleanCanvas = () => {
setHoveredComponent()

const components = getComponents()
components.forEach((c) => {
c.style['border-top'] = ''
c.style['border-bottom'] = ''
})
}

// NOTE this trigger more times than it should
const onCanvasDragLeave = () => {
cleanCanvas()
}

const getComponents = () => {
return Array.from(canvasRef.current?.children ?? []).filter((c) => c.nodeName !== 'SCRIPT')
}

return (
<div className="flex flex-row bg-white">
<div
ref={popoverRef}
className="absolute z-10 pointer-events-none bg-gray-500"
style={{ display: hoveredComponent ? 'block' : 'none' }}
>
<div className="flex flex-row p-1">
{getComponents().indexOf(hoveredComponent) < getComponents().length - 1 && (
<ArrowDownIcon
ref={moveDownRef}
onClick={onComponentMoveDown}
className="h-7 w-7 text-white p-1"
/>
)}
{getComponents().indexOf(hoveredComponent) > 0 && (
<ArrowUpIcon
ref={moveUpRef}
onClick={onComponentMoveUp}
className="h-7 w-7 text-white p-1"
/>
)}
<TrashIcon
id={'delete'}
ref={deleteRef}
onClick={onComponentDelete}
className="h-7 w-7 text-white p-1"
/>
</div>
</div>
{!isPreview && (
<div className="w-56 p-2" style={{ height: '100vh', overflowY: 'scroll', flexShrink: 0 }}>
{Object.keys(components).map((c, i) => (
<Category
key={i}
category={c}
themeIndex={themeIndex}
components={components[c]}
standaloneServer={standaloneServer}
/>
))}
</div>
)}
<div className="w-full" style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<div className="flex items-center m-2">
<TrashIcon className="h-6 w-6 mx-2 cursor-pointer" onClick={clearComponents} />

<div className="mr-auto ml-auto">
<div
className="flex rounded py-2 px-4 transition cursor-pointer items-center justify-center mr-auto ml-auto"
onClick={() => setSelectOpen(true)}
>
{themes[themeIndex].name}
<ChevronDownIcon className="h-4 w-4 ml-2" />
</div>
<Select
defaultValue={themes[themeIndex].name}
values={themes.map((c) => c.name)}
open={selectOpen}
setOpen={setSelectOpen}
onChange={(e) => {
const index = themes.findIndex((r) => r.name === e)
loadTheme(index)
setThemeIndex(index)
}}
/>
</div>

<button
className="bg-green-500 hover:bg-green-700 text-white px-4 py-2 ml-auto mr-6 rounded-md"
onClick={() => setIsPreview((s) => !s)}
>
{!isPreview ? 'Preview' : 'Editor'}
</button>
</div>
<div className="flex justify-center bg-gray-200" style={{ overflowY: 'scroll' }}>
<div
id="editor"
ref={canvasRef}
className="bg-white ease-animation"
onClick={onCanvasClick}
onMouseOver={onCanvasMouseOver}
onMouseLeave={onCanvasMouseLeave}
onDrop={onCanvasDrop}
onDragOver={onCanvasDragOver}
onDragLeave={onCanvasDragLeave}
style={{
flex: 1,
margin: isPreview ? '0px' : '20px',
maxWidth: isPreview ? '100%' : '868px',
minHeight: '1024px',
height: 'fit-content',
}}
></div>
</div>
</div>
</div>
)
}
export default Editor
Loading

0 comments on commit 403c115

Please sign in to comment.