Recovered lost changes that restructured and better organized Layout component
Some checks failed
armco-org/Layout/pipeline/head There was a failure building this commit
Some checks failed
armco-org/Layout/pipeline/head There was a failure building this commit
This commit is contained in:
14
index.html
Normal file
14
index.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ar-theme="th-light-1">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>React Redux App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/Test.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2566
package-lock.json
generated
2566
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -6,6 +6,7 @@
|
||||
"module": "build/es/index.js",
|
||||
"types": "build/types/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vite --config vite-dev.config.ts",
|
||||
"build": "./build-tools/build.sh",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
@@ -13,15 +14,16 @@
|
||||
"publish:local": "./publish-local.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@armco/icon": "^0.0.6",
|
||||
"@armco/shared-components": "^0.0.53",
|
||||
"@armco/types": "^0.0.11",
|
||||
"@armco/utils": "^0.0.18",
|
||||
"@armco/icon": "^0.0.13",
|
||||
"@armco/shared-components": "^0.0.60",
|
||||
"@armco/utils": "^0.0.31",
|
||||
"bootstrap": "^5.3.8",
|
||||
"react": ">16.8.0",
|
||||
"react-dom": ">16.8.0",
|
||||
"uuid": "^10.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@armco/types": "^0.0.22",
|
||||
"@testing-library/dom": "^9.2.0",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
@@ -30,7 +32,7 @@
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
@@ -40,12 +42,13 @@
|
||||
"prettier-config-nick": "^1.0.2",
|
||||
"sass": "^1.63.4",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.0.0",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-dts": "^4.2.1",
|
||||
"vite-plugin-externalize-deps": "^0.8.0",
|
||||
"vite-plugin-externalize-deps": "^0.10.0",
|
||||
"vite-plugin-lib-inject-css": "^2.1.1",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "^0.30.1"
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.8"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
119
src/BuilderLayout.component.scss
Executable file → Normal file
119
src/BuilderLayout.component.scss
Executable file → Normal file
@@ -1,55 +1,68 @@
|
||||
.ar-BuilderLayout {
|
||||
&.hide-controls > .ar-Layout__canvas > .ar-Layout__grid-tools {
|
||||
grid-template: "grid" auto / auto;
|
||||
}
|
||||
.ar-Layout__help-panel {
|
||||
transition: all 0.5s;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
|
||||
&.show {
|
||||
max-height: 2rem;
|
||||
opacity: 1;
|
||||
background-color: var(--ar-color-selected);
|
||||
}
|
||||
}
|
||||
|
||||
.ar-BuilderLayout.hide-controls {
|
||||
> .ar-Layout__canvas > .ar-Layout__grid-tools {
|
||||
grid-template: "grid" auto/auto;
|
||||
}
|
||||
.ar-Layout__help-panel {
|
||||
transition: all 0.5s;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
&.show {
|
||||
max-height: 2rem;
|
||||
opacity: 1;
|
||||
background-color: var(--ar-color-selected);
|
||||
}
|
||||
}
|
||||
.ar-Layout__grid-tools {
|
||||
grid-template: ". colMod" 1.5rem "rowMod grid" 1fr / 1.5rem auto;
|
||||
|
||||
.ar-Layout__row-tools {
|
||||
grid-area: rowMod;
|
||||
}
|
||||
.ar-Layout__col-tools {
|
||||
grid-area: colMod;
|
||||
}
|
||||
.ar-Layout__grid-tools__main, .ar-Layout__col-tools, .ar-Layout__row-tools {
|
||||
background-color: var(--ar-bg);
|
||||
}
|
||||
.ar-Layout__grid {
|
||||
grid-area: grid;
|
||||
}
|
||||
.row-controller-cell, .column-controller-cell {
|
||||
.delete-row, .delete-column {
|
||||
display: none;
|
||||
}
|
||||
.delete-row {
|
||||
bottom: 0.25rem;
|
||||
left: 0.2rem;
|
||||
}
|
||||
.delete-column {
|
||||
right: 0.25rem;
|
||||
top: 0.2rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--ar-bg-hover-4);
|
||||
.delete-row, .delete-column {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
background-color: #03a9f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
grid-template: ". colMod" 1.5rem "rowMod grid" 1fr/1.5rem auto;
|
||||
.ar-Layout__row-tools {
|
||||
grid-area: rowMod;
|
||||
}
|
||||
.ar-Layout__col-tools {
|
||||
grid-area: colMod;
|
||||
}
|
||||
.ar-Layout__grid-tools__main,
|
||||
.ar-Layout__col-tools,
|
||||
.ar-Layout__row-tools {
|
||||
background-color: var(--ar-bg);
|
||||
}
|
||||
.ar-Layout__grid {
|
||||
grid-area: grid;
|
||||
}
|
||||
.row-controller-cell {
|
||||
.delete-row {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--ar-bg-hover-4);
|
||||
.delete-row {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
background-color: #03a9f4;
|
||||
}
|
||||
}
|
||||
.column-controller-cell {
|
||||
.delete-column {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--ar-bg-hover-4);
|
||||
.delete-column {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
background-color: #03a9f4;
|
||||
}
|
||||
}
|
||||
.delete-row {
|
||||
bottom: 0.25rem;
|
||||
left: 0.2rem;
|
||||
}
|
||||
.delete-column {
|
||||
right: 0.25rem;
|
||||
top: 0.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
826
src/BuilderLayout.tsx
Executable file → Normal file
826
src/BuilderLayout.tsx
Executable file → Normal file
@@ -1,778 +1,68 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import {
|
||||
ArPopoverSlots,
|
||||
ArSlotViewMode,
|
||||
ArThemes,
|
||||
BuilderLayoutProps,
|
||||
GridToolbarSpecs,
|
||||
SlotDescriptor,
|
||||
SlotProps,
|
||||
} from "@armco/types"
|
||||
import Icon from "@armco/icon"
|
||||
import Tooltip from "@armco/shared-components/Tooltip"
|
||||
import { useTheme, useStateWithHistory } from "@armco/utils/hooks"
|
||||
import { calculateRowHeights, checkIfAdjacent, generateGridAreaAndSizes, generateGridTemplate, generateGridToolbarSpecs, generateSlotConfigs, insertDimension, mergeHandler, removeHandler, selectCellsInSelectedRowCol, splitHandler } from "@armco/utils/gridHelper"
|
||||
import { getDocumentElement, getWindowElement } from "@armco/utils/domHelper"
|
||||
import { FC } from "react"
|
||||
import { ArSlotViewMode } from "@armco/shared-components/enums"
|
||||
import { SlotDescriptor } from "@models"
|
||||
import { useLayoutContext } from "./Layout.context"
|
||||
import type { LayoutProps } from "./types"
|
||||
import BuilderLayoutContainer from "./BuilderLayoutContainer"
|
||||
import BuilderLayoutCanvas from "./BuilderLayoutCanvas"
|
||||
import Slot from "./Slot"
|
||||
import LayoutControlPanel from "./LayoutControlPanel"
|
||||
import "./BuilderLayout.component.scss"
|
||||
|
||||
const BuilderLayout = ({
|
||||
acceptTextOnClick,
|
||||
classes,
|
||||
colWidths: externalColWidths,
|
||||
demo,
|
||||
displayMode,
|
||||
hideBuildModePaddings,
|
||||
isChild,
|
||||
lastSelected: externalLastSelected,
|
||||
mode,
|
||||
slotDropHandler,
|
||||
onLayoutChange,
|
||||
onSlotSelect: onExternalSlotSelect,
|
||||
gridSpecs,
|
||||
gridTemplate: externalGridTemplate,
|
||||
rowHeights: externalRowHeights,
|
||||
showControls: externalShowControls,
|
||||
showPanels,
|
||||
slots: externalSlots,
|
||||
slotRenderer,
|
||||
}: BuilderLayoutProps) => {
|
||||
const layoutRef = useRef<HTMLDivElement>(null)
|
||||
const ctrlPanelRef = useRef<HTMLDivElement>(null)
|
||||
const helpPanelRef = useRef<HTMLDivElement>(null)
|
||||
const specUpdateAction = useRef<boolean>(true)
|
||||
const [panelsDisplayed, displayPanels] = useState<boolean>()
|
||||
const [rowHeights, setRowHeights] = useState<string | Array<string>>()
|
||||
const [colWidths, setColWidths] = useState<string | Array<string>>()
|
||||
const [showControls, setShowControls] = useState<boolean>()
|
||||
const [gridArea, setGridArea] = useState<Array<Array<string>>>()
|
||||
const [gridTemplate, setGridTemplate] = useState<string>()
|
||||
const [gridToolbarSpecs, setGridToolbarSpecs] = useState<GridToolbarSpecs>()
|
||||
const [slots, setSlots, undoSlots, redoSlots, canUndo, canRedo] =
|
||||
useStateWithHistory<Array<SlotDescriptor>>()
|
||||
const [prevSlotCount, setPrevSlotsCount] = useState<number>()
|
||||
const [mergeEnabled, enableMerge] = useState<boolean>()
|
||||
const [splitEnabled, enableSplit] = useState<boolean>()
|
||||
const [lastSelected, setLastSelected] = useState<SlotDescriptor>()
|
||||
const [selectionOnlyModeEnabled, enableSelectionOnlyMode] =
|
||||
useState<boolean>()
|
||||
const [minMaxSelections, setMinMaxSelections] = useState<{
|
||||
minRow: number
|
||||
maxRow: number
|
||||
minColumn: number
|
||||
maxColumn: number
|
||||
}>()
|
||||
const {theme} = useTheme()
|
||||
const cmdCtrl = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<Icon icon="ai.AiFillMacCommand" attributes={{size: "0.8rem"}} /> /
|
||||
<Icon icon="md.MdOutlineKeyboardControlKey" attributes={{size: "0.8rem"}} />
|
||||
</>
|
||||
),
|
||||
[],
|
||||
)
|
||||
const isGrid = !displayMode || displayMode === "grid"
|
||||
// Props specific to the BuilderLayout view extracted from runtime function 'k'
|
||||
export interface BuilderLayoutProps extends Pick<LayoutProps, "classes" | "displayMode" | "hideBuildModePaddings" | "isChild" | "mode" | "slotDropHandler" | "onSlotSelect" | "slotRenderer"> { }
|
||||
|
||||
useEffect(() => {
|
||||
const doc = getDocumentElement(demo)
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (showControls) {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
|
||||
event.preventDefault()
|
||||
undoSlots()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "y") {
|
||||
event.preventDefault()
|
||||
redoSlots()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "m") {
|
||||
if (mergeEnabled && slots && minMaxSelections) {
|
||||
mergeHandler(setSlots, slots, minMaxSelections)
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "h") {
|
||||
if (splitEnabled && slots) {
|
||||
splitHandler(setSlots, slots, "horizontal", "after")
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "v") {
|
||||
if (splitEnabled && slots) {
|
||||
splitHandler(setSlots, slots, "vertical", "after")
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "r") {
|
||||
if (splitEnabled && slots && gridToolbarSpecs) {
|
||||
insertDimension(
|
||||
"row",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
"after",
|
||||
)
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === "c") {
|
||||
if (splitEnabled && slots && gridToolbarSpecs) {
|
||||
insertDimension(
|
||||
"column",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
"after",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const BuilderLayout: FC<BuilderLayoutProps> = (props) => {
|
||||
const {
|
||||
classes,
|
||||
displayMode,
|
||||
hideBuildModePaddings,
|
||||
isChild,
|
||||
mode,
|
||||
slotDropHandler,
|
||||
onSlotSelect,
|
||||
slotRenderer,
|
||||
} = props
|
||||
const { layoutService, controlsEnabled } = useLayoutContext()
|
||||
|
||||
doc?.addEventListener("keydown", handleKeyDown)
|
||||
// From runtime: displayMode undefined or 'grid' => treat as grid layout
|
||||
const isGridDisplay = !displayMode || displayMode === "grid"
|
||||
const slots: SlotDescriptor[] | undefined = layoutService?.slots
|
||||
const gridToolbarSpecs = layoutService?.gridToolbarSpecs
|
||||
|
||||
return () => {
|
||||
doc?.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [
|
||||
demo,
|
||||
gridToolbarSpecs,
|
||||
mergeEnabled,
|
||||
minMaxSelections,
|
||||
redoSlots,
|
||||
showControls,
|
||||
slots,
|
||||
splitEnabled,
|
||||
undoSlots,
|
||||
])
|
||||
if (!slots || !gridToolbarSpecs) return null
|
||||
|
||||
useEffect(() => {
|
||||
setRowHeights(externalRowHeights)
|
||||
setColWidths(externalColWidths || "1fr")
|
||||
}, [externalRowHeights, externalColWidths])
|
||||
|
||||
useEffect(() => {
|
||||
displayPanels(showPanels)
|
||||
}, [showPanels])
|
||||
|
||||
useEffect(() => {
|
||||
setShowControls(externalShowControls)
|
||||
}, [externalShowControls])
|
||||
|
||||
useEffect(() => setLastSelected(externalLastSelected), [externalLastSelected])
|
||||
|
||||
useEffect(() => {
|
||||
if (externalSlots) {
|
||||
JSON.stringify(externalSlots) !== JSON.stringify(slots) &&
|
||||
setSlots(externalSlots)
|
||||
} else if (externalGridTemplate) {
|
||||
const { slotConfigs } =
|
||||
generateSlotConfigs(externalGridTemplate)
|
||||
setSlots(slotConfigs)
|
||||
} else if (gridSpecs) {
|
||||
let parsedGridSpecs = gridSpecs
|
||||
if (typeof gridSpecs === "string") {
|
||||
try {
|
||||
parsedGridSpecs = JSON.parse(gridSpecs)
|
||||
} catch {
|
||||
console.warn("Grid Specs passed as string but incorrect JSON format")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof parsedGridSpecs === "object" &&
|
||||
"rows" in parsedGridSpecs &&
|
||||
"columns" in parsedGridSpecs
|
||||
) {
|
||||
const slots: Array<SlotDescriptor> = []
|
||||
for (let i = 0; i < parsedGridSpecs.rows; i++) {
|
||||
for (let j = 0; j < parsedGridSpecs.columns; j++) {
|
||||
const slotId = uuid()
|
||||
const gridArea = `ga-${slotId}`
|
||||
slots.push({
|
||||
slot: slotId,
|
||||
row: i,
|
||||
column: j,
|
||||
rowSpan: 1,
|
||||
colSpan: 1,
|
||||
gridArea,
|
||||
})
|
||||
}
|
||||
}
|
||||
setSlots(slots)
|
||||
}
|
||||
}
|
||||
}, [gridSpecs, externalGridTemplate, externalSlots])
|
||||
|
||||
useEffect(() => {
|
||||
if (slots) {
|
||||
if (slots.length !== prevSlotCount) {
|
||||
const { gridArea, rowHeights } = generateGridAreaAndSizes(
|
||||
slots,
|
||||
externalRowHeights,
|
||||
)
|
||||
setGridArea(gridArea)
|
||||
setRowHeights(rowHeights)
|
||||
setPrevSlotsCount(slots.length)
|
||||
}
|
||||
const { areAdjacent, ...rest } = checkIfAdjacent(slots)
|
||||
setMinMaxSelections(rest)
|
||||
enableSplit(slots.filter((sc) => sc.isSelected).length >= 1)
|
||||
enableMerge(areAdjacent)
|
||||
}
|
||||
}, [slots, prevSlotCount, externalRowHeights])
|
||||
|
||||
useEffect(() => {
|
||||
if (gridArea) {
|
||||
const gridTemplate = generateGridTemplate(
|
||||
gridArea,
|
||||
rowHeights,
|
||||
colWidths,
|
||||
)
|
||||
setGridTemplate(gridTemplate)
|
||||
onLayoutChange && onLayoutChange(gridTemplate, slots)
|
||||
}
|
||||
}, [gridArea, rowHeights, colWidths])
|
||||
|
||||
useEffect(() => {
|
||||
if (gridArea && showControls && slots) {
|
||||
if (specUpdateAction.current) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const calculatedRowHeights = calculateRowHeights(
|
||||
slots,
|
||||
rowHeights,
|
||||
demo,
|
||||
)
|
||||
setGridToolbarSpecs(
|
||||
generateGridToolbarSpecs(
|
||||
gridArea,
|
||||
calculatedRowHeights,
|
||||
colWidths,
|
||||
),
|
||||
)
|
||||
})
|
||||
}, 10)
|
||||
}
|
||||
}
|
||||
}, [slots, gridArea, showControls, rowHeights, colWidths])
|
||||
|
||||
useEffect(() => {
|
||||
if (panelsDisplayed) {
|
||||
const thisRect = layoutRef.current?.getBoundingClientRect()
|
||||
if (thisRect) {
|
||||
if (helpPanelRef.current) {
|
||||
helpPanelRef.current.style.top = thisRect.bottom + "px"
|
||||
helpPanelRef.current.style.left = thisRect.left + "px"
|
||||
helpPanelRef.current.style.width = thisRect.width + "px"
|
||||
}
|
||||
if (ctrlPanelRef.current) {
|
||||
const winObj = getWindowElement(demo)
|
||||
ctrlPanelRef.current.style.bottom =
|
||||
(winObj ? winObj.innerHeight - thisRect.top : 0) + "px"
|
||||
ctrlPanelRef.current.style.left = thisRect.left + "px"
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
const ctrlPanelRect = ctrlPanelRef.current?.getBoundingClientRect()
|
||||
if (thisRect && ctrlPanelRef.current && helpPanelRef.current) {
|
||||
if (ctrlPanelRect && ctrlPanelRect.width > thisRect.width) {
|
||||
ctrlPanelRef.current.style.width = thisRect.width + "px"
|
||||
ctrlPanelRef.current.style.overflow = "auto"
|
||||
}
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}, [panelsDisplayed, showControls, demo])
|
||||
|
||||
const onSlotSelect = (slotConfig: SlotDescriptor) => {
|
||||
const slotsClone = [...(slots || [])]
|
||||
specUpdateAction.current = true
|
||||
const matchedSlotConfig = slots?.find((sc) => sc.slot === slotConfig.slot)
|
||||
matchedSlotConfig &&
|
||||
(matchedSlotConfig.isSelected = !matchedSlotConfig.isSelected)
|
||||
setLastSelected(matchedSlotConfig)
|
||||
// Add Text in empty slot
|
||||
if (acceptTextOnClick && matchedSlotConfig && !matchedSlotConfig?.content) {
|
||||
if (!matchedSlotConfig.props) {
|
||||
matchedSlotConfig.props = {}
|
||||
}
|
||||
// matchedSlotConfig.props.classes = "p-2"
|
||||
matchedSlotConfig.content = {
|
||||
name: "Text",
|
||||
description:
|
||||
"Foundational text component, that accepts classes and style attributes to create varying types of other text components",
|
||||
source: "stuffle",
|
||||
componentName: "Text",
|
||||
props: {
|
||||
classes: "w-100",
|
||||
id: matchedSlotConfig.slot,
|
||||
style: { minHeight: "1.5rem", lineHeight: "1.5rem" },
|
||||
isEditable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
setSlots(slotsClone, true)
|
||||
onExternalSlotSelect && onExternalSlotSelect(slotConfig)
|
||||
}
|
||||
|
||||
const onRowSelect = (
|
||||
slotConfig: SlotDescriptor,
|
||||
// When cross on a row tool cell is clicked, this flag will be true
|
||||
selectForDelete?: boolean,
|
||||
) => {
|
||||
specUpdateAction.current = false
|
||||
if (gridToolbarSpecs) {
|
||||
const rowSlots = gridToolbarSpecs.rowTools.slots
|
||||
const selectedRow = rowSlots.find((slot) => slot.slot === slotConfig.slot)
|
||||
if (selectedRow) {
|
||||
if (selectForDelete !== undefined)
|
||||
selectedRow.isSelectedForInlineDelete = selectForDelete
|
||||
else selectedRow.isSelected = !selectedRow.isSelected
|
||||
}
|
||||
setGridToolbarSpecs({ ...gridToolbarSpecs })
|
||||
slots &&
|
||||
setTimeout(
|
||||
() =>
|
||||
setSlots(
|
||||
selectCellsInSelectedRowCol(
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
selectForDelete,
|
||||
),
|
||||
true,
|
||||
),
|
||||
10,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onColumnSelect = (
|
||||
slotConfig: SlotDescriptor,
|
||||
selectForDelete?: boolean,
|
||||
) => {
|
||||
specUpdateAction.current = false
|
||||
if (gridToolbarSpecs) {
|
||||
const colSlots = gridToolbarSpecs.colTools.slots
|
||||
const selectedColumn = colSlots.find(
|
||||
(slot) => slot.slot === slotConfig.slot,
|
||||
)
|
||||
if (selectedColumn) {
|
||||
if (selectForDelete !== undefined)
|
||||
selectedColumn.isSelectedForInlineDelete = selectForDelete
|
||||
else selectedColumn.isSelected = !selectedColumn.isSelected
|
||||
}
|
||||
setGridToolbarSpecs({ ...gridToolbarSpecs })
|
||||
slots &&
|
||||
setSlots(
|
||||
selectCellsInSelectedRowCol(
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
selectForDelete,
|
||||
),
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onRowDelete = (isInlineDelete?: boolean) => {
|
||||
specUpdateAction.current = false
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
setSlots(
|
||||
removeHandler(
|
||||
slots,
|
||||
gridToolbarSpecs,
|
||||
"row",
|
||||
isInlineDelete,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const onColumnDelete = (isInlineDelete?: boolean) => {
|
||||
specUpdateAction.current = false
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
setSlots(
|
||||
removeHandler(
|
||||
slots,
|
||||
gridToolbarSpecs,
|
||||
"column",
|
||||
isInlineDelete,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const rowToolSlots = gridToolbarSpecs?.rowTools.slots
|
||||
const colToolSlots = gridToolbarSpecs?.colTools.slots
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-BuilderLayout mh-100 w-100 d-flex flex-column overflow-auto${
|
||||
classes ? " " + classes : ""
|
||||
}${showControls ? "" : " hide-controls"}`}
|
||||
ref={layoutRef}
|
||||
onMouseOver={(e) => {
|
||||
e.stopPropagation()
|
||||
!isChild && isGrid && displayPanels(true)
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.stopPropagation()
|
||||
!isChild && isGrid && displayPanels(false)
|
||||
}}
|
||||
>
|
||||
{!isChild && isGrid && (
|
||||
<LayoutControlPanel
|
||||
classes="position-fixed"
|
||||
demo={demo}
|
||||
ref={ctrlPanelRef}
|
||||
setShowControls={setShowControls}
|
||||
showControls={showControls}
|
||||
slots={slots}
|
||||
show={panelsDisplayed}
|
||||
splitEnabled={splitEnabled}
|
||||
mergeEnabled={mergeEnabled}
|
||||
undoEnabled={canUndo}
|
||||
redoEnabled={canRedo}
|
||||
selectionOnlyModeEnabled={selectionOnlyModeEnabled}
|
||||
rowDeleteEnabled={
|
||||
rowToolSlots && rowToolSlots.findIndex((s) => s.isSelected) > -1
|
||||
}
|
||||
columnDeleteEnabled={
|
||||
colToolSlots && colToolSlots.findIndex((s) => s.isSelected) > -1
|
||||
}
|
||||
horizontalSplitHandler={() =>
|
||||
slots &&
|
||||
splitHandler(setSlots, slots, "horizontal", "after")
|
||||
}
|
||||
verticalSplitHandler={() =>
|
||||
slots &&
|
||||
splitHandler(setSlots, slots, "vertical", "after")
|
||||
}
|
||||
mergeHandler={() => {
|
||||
slots &&
|
||||
minMaxSelections &&
|
||||
mergeHandler(setSlots, slots, minMaxSelections)
|
||||
enableMerge(false)
|
||||
}}
|
||||
rowInsertHandler={(placement: "before" | "after") =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
insertDimension(
|
||||
"row",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
columnInsertHandler={(placement: "before" | "after") =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
insertDimension(
|
||||
"column",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
rowDeleteHandler={() => onRowDelete()}
|
||||
columnDeleteHandler={() => onColumnDelete()}
|
||||
undoHandler={undoSlots}
|
||||
redoHandler={redoSlots}
|
||||
enableSelectionOnlyMode={enableSelectionOnlyMode}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`ar-Layout__canvas flex-1${
|
||||
isChild && !hideBuildModePaddings ? " p-1" : ""
|
||||
}`}
|
||||
>
|
||||
{slots && (
|
||||
<div className="ar-Layout__grid-tools d-grid h-100">
|
||||
{showControls && !isChild && isGrid && (
|
||||
<>
|
||||
<div className="ar-Layout__grid-tools__main border" />
|
||||
<div
|
||||
className="ar-Layout__row-tools d-grid"
|
||||
style={{
|
||||
gridTemplate: gridToolbarSpecs?.rowTools.gridTemplate,
|
||||
}}
|
||||
>
|
||||
{gridToolbarSpecs?.rowTools.slots.map(
|
||||
(slotConfig, index, arr) => (
|
||||
<span
|
||||
className={`row-controller-cell position-relative border${
|
||||
slotConfig.isSelected ? " selected" : ""
|
||||
}`}
|
||||
style={{ gridArea: slotConfig.gridArea }}
|
||||
>
|
||||
<Slot
|
||||
demo={demo}
|
||||
mode={ArSlotViewMode.ROWCONTROLLER}
|
||||
config={{
|
||||
...slotConfig,
|
||||
props: {
|
||||
...(slotConfig.props || {}),
|
||||
classes: "h-100",
|
||||
},
|
||||
}}
|
||||
key={"controller-" + slotConfig.slot}
|
||||
onClick={(slotConfig: SlotDescriptor) => {
|
||||
onRowSelect(slotConfig)
|
||||
onExternalSlotSelect &&
|
||||
onExternalSlotSelect(slotConfig, "row")
|
||||
}}
|
||||
controlsEnabled={showControls}
|
||||
selectionOnlyModeEnabled={selectionOnlyModeEnabled}
|
||||
isLast={index === arr.length - 1}
|
||||
rowDeleteHandler={() => onRowDelete(true)}
|
||||
rowInsertHandler={(placement: "before" | "after") =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
insertDimension(
|
||||
"row",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
rowSelectHandler={(isEntering: boolean) =>
|
||||
onRowSelect(slotConfig, isEntering)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="ar-Layout__col-tools d-grid"
|
||||
style={{
|
||||
gridTemplate: gridToolbarSpecs?.colTools.gridTemplate,
|
||||
}}
|
||||
>
|
||||
{gridToolbarSpecs?.colTools.slots.map(
|
||||
(slotConfig, index, arr) => (
|
||||
<span
|
||||
className={`column-controller-cell position-relative border${
|
||||
slotConfig.isSelected ? " selected" : ""
|
||||
}`}
|
||||
style={{ gridArea: slotConfig.gridArea }}
|
||||
>
|
||||
<Slot
|
||||
demo={demo}
|
||||
mode={ArSlotViewMode.COLCONTROLLER}
|
||||
config={{
|
||||
...slotConfig,
|
||||
props: {
|
||||
...(slotConfig.props || {}),
|
||||
classes: "h-100",
|
||||
},
|
||||
}}
|
||||
key={"controller-" + slotConfig.slot}
|
||||
onClick={(slotConfig: SlotDescriptor) => {
|
||||
onColumnSelect(slotConfig)
|
||||
onExternalSlotSelect &&
|
||||
onExternalSlotSelect(slotConfig, "column")
|
||||
}}
|
||||
controlsEnabled={showControls}
|
||||
selectionOnlyModeEnabled={selectionOnlyModeEnabled}
|
||||
isLast={index === arr.length - 1}
|
||||
columnDeleteHandler={() => onColumnDelete(true)}
|
||||
columnInsertHandler={(
|
||||
placement: "before" | "after",
|
||||
) =>
|
||||
slots &&
|
||||
gridToolbarSpecs &&
|
||||
insertDimension(
|
||||
"column",
|
||||
gridToolbarSpecs,
|
||||
slots,
|
||||
setSlots,
|
||||
placement,
|
||||
)
|
||||
}
|
||||
columnSelectHandler={(isEntering: boolean) =>
|
||||
onColumnSelect(slotConfig, isEntering)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className={`ar-Layout__grid border${isGrid ? " d-grid" : ""}${
|
||||
displayMode ? " d-" + displayMode : ""
|
||||
}`}
|
||||
style={gridTemplate ? { gridTemplate } : {}}
|
||||
>
|
||||
{slots.map((sConfig) => {
|
||||
const props: SlotProps = {
|
||||
demo,
|
||||
config: sConfig,
|
||||
containerDisplayMode: displayMode,
|
||||
controlsEnabled: showControls,
|
||||
hideBuildModePaddings,
|
||||
lastSelected,
|
||||
layoutMode: mode,
|
||||
mode: ArSlotViewMode.BUILD,
|
||||
onClick: onSlotSelect,
|
||||
selectionOnlyModeEnabled,
|
||||
horizontalSplitHandler: (placement: "before" | "after") =>
|
||||
slots &&
|
||||
splitHandler(
|
||||
setSlots,
|
||||
slots,
|
||||
"horizontal",
|
||||
placement,
|
||||
sConfig,
|
||||
),
|
||||
verticalSplitHandler: (placement: "before" | "after") =>
|
||||
slots &&
|
||||
splitHandler(
|
||||
setSlots,
|
||||
slots,
|
||||
"vertical",
|
||||
placement,
|
||||
sConfig,
|
||||
),
|
||||
mergeHandler: () => {
|
||||
slots &&
|
||||
minMaxSelections &&
|
||||
mergeEnabled &&
|
||||
mergeHandler(
|
||||
setSlots,
|
||||
slots,
|
||||
minMaxSelections,
|
||||
sConfig,
|
||||
)
|
||||
enableMerge(false)
|
||||
},
|
||||
dropHandler: slotDropHandler,
|
||||
onSlotSelect: onExternalSlotSelect,
|
||||
}
|
||||
return slotRenderer ? (
|
||||
slotRenderer(props)
|
||||
) : (
|
||||
<Slot key={sConfig.slot} {...props} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isChild && isGrid && (
|
||||
<div
|
||||
className={`ar-Layout__help-panel position-fixed overflow-hidden d-flex px-2 justify-content-between py-1${
|
||||
panelsDisplayed ? " show" : ""
|
||||
}`}
|
||||
ref={helpPanelRef}
|
||||
>
|
||||
<span
|
||||
className="small fw-bold"
|
||||
style={{ color: theme === ArThemes.DARK1 ? "black" : "white" }}
|
||||
>
|
||||
Undo: Ctrl/Cmd + z, Redo: Ctrl/Cmd + y
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip demo={demo}>
|
||||
<Icon
|
||||
icon="fa.FaKeyboard"
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
attributes={{
|
||||
colors: {
|
||||
fillColor: "white"
|
||||
},
|
||||
classes: "me-3"
|
||||
}}
|
||||
/>
|
||||
<span slot={ArPopoverSlots.POPOVER} className="z-2">
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + h</div>
|
||||
<div className="col-7 fw-bold">Split Horizontally</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + v</div>
|
||||
<div className="col-7 fw-bold">Split Vertically</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + m</div>
|
||||
<div className="col-7 fw-bold">Merge Rectangle</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + r</div>
|
||||
<div className="col-7 fw-bold">Insert Row</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + c</div>
|
||||
<div className="col-7 fw-bold">Insert Column</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + d</div>
|
||||
<div className="col-7 fw-bold">Delete</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + z</div>
|
||||
<div className="col-7 fw-bold">Undo</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-5 fw-bold">{cmdCtrl} + y</div>
|
||||
<div className="col-7 fw-bold">Redo</div>
|
||||
</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip demo={demo}>
|
||||
<Icon
|
||||
icon="md.MdInfoOutline"
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
attributes={{
|
||||
colors: {
|
||||
fillColor: theme === ArThemes.DARK1 ? "black" : "white"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="ar-Layout__help-panel__help-content small z-2"
|
||||
slot={ArPopoverSlots.POPOVER}
|
||||
style={{ whiteSpace: "normal" }}
|
||||
>
|
||||
<div>Hover on grid to reveal control panel.</div>
|
||||
<div>Click a cell to select it.</div>
|
||||
<div>You may select multiple cells by clicking on them.</div>
|
||||
<div>
|
||||
Click a row header to select all cells in that row, and column
|
||||
header to select all cells in that column
|
||||
</div>
|
||||
<div>
|
||||
You may hover on any selected cells on specific sections
|
||||
within cell to split vertical or horizontal.
|
||||
</div>
|
||||
<div>
|
||||
For Horizontal: Hover on top center or bottom center region
|
||||
and click
|
||||
</div>
|
||||
<div>
|
||||
For Vertical: Hover on left center or right center region and
|
||||
click
|
||||
</div>
|
||||
<div>
|
||||
You may merge cells too, at least 2 cells should be selected
|
||||
to merge and the selected cells shouls form a rectangle.
|
||||
</div>
|
||||
<div>
|
||||
To delete a row or column, hover on it's corresponding row or
|
||||
column header to reveal delete button and click on it.
|
||||
</div>
|
||||
<div>
|
||||
Undo/Redo layout changes is supported. Use Undo/Redo buttons
|
||||
in the control panel on top of grid, or use Ctrl/Cmd + z to
|
||||
undo, Ctrl/Cmd + y to redo
|
||||
</div>
|
||||
<div>Edit Mode should be enabled to use any shortcuts</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<BuilderLayoutContainer
|
||||
classes={classes || ""}
|
||||
panelsDisplayable={!isChild && isGridDisplay}
|
||||
>
|
||||
<BuilderLayoutCanvas isChild={!!isChild} isGrid={isGridDisplay}>
|
||||
<div
|
||||
className={`ar-Layout__grid border${isGridDisplay ? " d-grid" : ""}${displayMode ? " d-" + displayMode : ""}`}
|
||||
style={layoutService.gridTemplate ? { gridTemplate: layoutService.gridTemplate } : {}}
|
||||
>
|
||||
{slots?.map((slotDesc) => {
|
||||
const slotProps = {
|
||||
slotDescriptor: slotDesc,
|
||||
containerDisplayMode: displayMode,
|
||||
controlsEnabled,
|
||||
hideBuildModePaddings,
|
||||
layoutMode: mode,
|
||||
mode: ArSlotViewMode.BUILD as const,
|
||||
dropHandler: slotDropHandler,
|
||||
onSlotSelect,
|
||||
slotRenderer,
|
||||
}
|
||||
return slotRenderer ? (
|
||||
slotRenderer(slotProps)
|
||||
) : (
|
||||
<Slot {...slotProps} key={slotDesc.slot} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</BuilderLayoutCanvas>
|
||||
</BuilderLayoutContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuilderLayout
|
||||
|
||||
82
src/BuilderLayoutCanvas.tsx
Normal file
82
src/BuilderLayoutCanvas.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { FC } from "react"
|
||||
import { Fragment } from "react"
|
||||
import { ArSlotViewMode } from "@armco/shared-components/enums"
|
||||
import type { BuilderLayoutCanvasProps, ISlotDescriptor } from "./types"
|
||||
import { useLayoutContext } from "./Layout.context"
|
||||
import Slot from "./Slot"
|
||||
|
||||
const BuilderLayoutCanvas: FC<BuilderLayoutCanvasProps> = ({ children, isChild, isGrid }) => {
|
||||
const { eventsService, layoutService, controlsEnabled } = useLayoutContext()
|
||||
const slots: ISlotDescriptor[] | undefined = layoutService?.slots
|
||||
const gridToolbarSpecs = layoutService?.gridToolbarSpecs
|
||||
const onSlotSelectCB = eventsService?.onSlotSelectCB
|
||||
|
||||
return (
|
||||
<div className="ar-Layout__canvas flex-1">
|
||||
{slots && (
|
||||
<div className="ar-Layout__grid-tools d-grid h-100">
|
||||
{controlsEnabled && !isChild && isGrid && (
|
||||
<Fragment>
|
||||
<div className="ar-Layout__grid-tools__main border" />
|
||||
<div
|
||||
className="ar-Layout__row-tools d-grid"
|
||||
style={{ gridTemplate: gridToolbarSpecs?.rowTools?.gridTemplate }}
|
||||
>
|
||||
{gridToolbarSpecs?.rowTools?.slots?.map((r: any, h: number, d: any[]) => (
|
||||
<span
|
||||
key={r.slot}
|
||||
className={`row-controller-cell position-relative border${r.isSelected ? " selected" : ""}`}
|
||||
style={{ gridArea: r.gridArea }}
|
||||
>
|
||||
<Slot
|
||||
mode={ArSlotViewMode.ROWCONTROLLER}
|
||||
slotDescriptor={r.setClasses("h-100")}
|
||||
onClick={(e: any) => {
|
||||
eventsService.selectRow(e)
|
||||
onSlotSelectCB && onSlotSelectCB(e, "row")
|
||||
}}
|
||||
controlsEnabled={controlsEnabled}
|
||||
isLast={h === d.length - 1}
|
||||
rowDeleteHandler={() => eventsService.deleteRow(true)}
|
||||
rowInsertHandler={(e: any) => eventsService.addRow(e)}
|
||||
rowSelectHandler={(e: any) => eventsService.selectRow(r, e)}
|
||||
>{`controller-${r.slot}`}</Slot>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="ar-Layout__col-tools d-grid"
|
||||
style={{ gridTemplate: gridToolbarSpecs?.colTools?.gridTemplate }}
|
||||
>
|
||||
{gridToolbarSpecs?.colTools?.slots?.map((r: any, h: number, d: any[]) => (
|
||||
<span
|
||||
key={r.slot}
|
||||
className={`column-controller-cell position-relative border${r.isSelected ? " selected" : ""}`}
|
||||
style={{ gridArea: r.gridArea }}
|
||||
>
|
||||
<Slot
|
||||
mode={ArSlotViewMode.COLCONTROLLER}
|
||||
slotDescriptor={r.setClasses("h-100")}
|
||||
onClick={(e: any) => {
|
||||
eventsService.selectColumn(e)
|
||||
onSlotSelectCB && onSlotSelectCB(e, "column")
|
||||
}}
|
||||
controlsEnabled={controlsEnabled}
|
||||
isLast={h === d.length - 1}
|
||||
columnDeleteHandler={() => eventsService.deleteColumn(true)}
|
||||
columnInsertHandler={(e: any) => eventsService.addColumn(e)}
|
||||
columnSelectHandler={(e: any) => eventsService.selectColumn(r, e)}
|
||||
>{`controller-${r.slot}`}</Slot>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuilderLayoutCanvas
|
||||
68
src/BuilderLayoutContainer.tsx
Normal file
68
src/BuilderLayoutContainer.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { FC, useRef, useEffect } from "react"
|
||||
import { BuilderLayoutContainerProps } from "./types"
|
||||
import LayoutHelp from "./LayoutHelp"
|
||||
import LayoutControlPanel from "./LayoutControlPanel"
|
||||
import { useLayoutContext } from "./Layout.context"
|
||||
|
||||
const BuilderLayoutContainer: FC<BuilderLayoutContainerProps> = ({ children, classes, panelsDisplayable }) => {
|
||||
const helpRef = useRef<HTMLDivElement>(null)
|
||||
const controlPanelRef = useRef<HTMLDivElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { layoutService, panelsDisplayed, displayPanels, controlsEnabled } = useLayoutContext()
|
||||
const rowSlots = layoutService?.gridToolbarSpecs?.rowTools?.slots || []
|
||||
const colSlots = layoutService?.gridToolbarSpecs?.colTools?.slots || []
|
||||
|
||||
useEffect(() => {
|
||||
const updatePanelPositions = () => {
|
||||
const containerRect = containerRef.current?.getBoundingClientRect()
|
||||
if (containerRect) {
|
||||
if (controlPanelRef.current) {
|
||||
controlPanelRef.current.style.bottom = containerRect.bottom + "px"
|
||||
controlPanelRef.current.style.left = containerRect.left + "px"
|
||||
controlPanelRef.current.style.width = containerRect.width + "px"
|
||||
}
|
||||
if (helpRef.current) {
|
||||
helpRef.current.style.top = window.innerHeight - containerRect.top + "px"
|
||||
helpRef.current.style.left = containerRect.left + "px"
|
||||
}
|
||||
setTimeout(() => {
|
||||
const helpRect = helpRef.current?.getBoundingClientRect()
|
||||
if (containerRect && helpRef.current && controlPanelRef.current && helpRect && helpRect.width > containerRect.width) {
|
||||
helpRef.current.style.width = containerRect.width + "px"
|
||||
helpRef.current.style.overflow = "auto"
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
const resizeObserver = new window.ResizeObserver(() => {
|
||||
if (panelsDisplayed) updatePanelPositions()
|
||||
})
|
||||
if (containerRef.current) resizeObserver.observe(containerRef.current)
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [panelsDisplayed, controlsEnabled])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-BuilderLayout mh-100 w-100 d-flex flex-column overflow-auto${classes ? " " + classes : ""}${controlsEnabled ? "" : " hide-controls"}`}
|
||||
ref={containerRef}
|
||||
onMouseOver={e => { e.stopPropagation(); panelsDisplayable && displayPanels(true) }}
|
||||
onMouseLeave={e => { e.stopPropagation(); panelsDisplayable && displayPanels(false) }}
|
||||
>
|
||||
{panelsDisplayable && (
|
||||
<LayoutControlPanel
|
||||
classes="position-fixed"
|
||||
ref={controlPanelRef}
|
||||
show={panelsDisplayed}
|
||||
rowDeleteEnabled={rowSlots.findIndex((e: any) => e.isSelected) > -1}
|
||||
columnDeleteEnabled={colSlots.findIndex((e: any) => e.isSelected) > -1}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{panelsDisplayable && (
|
||||
<LayoutHelp ref={helpRef} panelsDisplayed={panelsDisplayed} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuilderLayoutContainer
|
||||
@@ -1,54 +0,0 @@
|
||||
.ar-Slot {
|
||||
&.build {
|
||||
&:hover {
|
||||
border: 1px dashed #86cff1 !important;
|
||||
}
|
||||
&.selected {
|
||||
border: 1px solid #86cff1 !important;
|
||||
}
|
||||
&.last-selected {
|
||||
outline: 4px solid #2e9cfd !important;
|
||||
}
|
||||
&.selected-for-delete {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
.ar-Slot__overlay-btn {
|
||||
width: calc(100% / 3);
|
||||
&.slot:hover {
|
||||
background-color: rgba(85, 189, 237, 0.661);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ar-SlotTools {
|
||||
display: none;
|
||||
}
|
||||
&:hover .ar-SlotTools {
|
||||
display: flex;
|
||||
}
|
||||
&.row-controller {
|
||||
.ar-Slot__overlay-btn {
|
||||
height: calc((100% - 16px) / 3);
|
||||
&.slot:hover {
|
||||
background-color: rgba(85, 189, 237, 0.661);
|
||||
}
|
||||
&.delete {
|
||||
height: 1rem;
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.col-controller {
|
||||
.ar-Slot__overlay-btn {
|
||||
width: calc((100% - 16px) / 3);
|
||||
&.slot:hover {
|
||||
background-color: rgb(3, 169, 244, 0.5);
|
||||
}
|
||||
&.delete {
|
||||
width: 1rem;
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
156
src/BuilderSlot.tsx
Executable file → Normal file
156
src/BuilderSlot.tsx
Executable file → Normal file
@@ -1,98 +1,82 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { ArDndItemTypes, ArSlotViewMode, BuilderSlotProps } from "@armco/types"
|
||||
import { CSSProperties, useEffect, useRef, useState } from "react"
|
||||
import { ArSlotViewMode, ArDndItemTypes } from "@armco/shared-components/enums"
|
||||
import Draggable from "@armco/shared-components/Draggable"
|
||||
import Droppable from "@armco/shared-components/Droppable"
|
||||
import SlotTools from "./SlotTools"
|
||||
import "./BuilderSlot.component.scss"
|
||||
import { BuilderSlotProps } from "./types"
|
||||
import { useLayoutContext } from "./Layout.context"
|
||||
|
||||
const BuilderSlot = (props: BuilderSlotProps): JSX.Element => {
|
||||
const {
|
||||
acceptDropTypes,
|
||||
containerDisplayMode,
|
||||
content,
|
||||
dropHandler,
|
||||
lastSelected,
|
||||
onClick,
|
||||
selectionOnlyModeEnabled,
|
||||
style,
|
||||
...rest
|
||||
} = props
|
||||
const [selected, setSelected] = useState<boolean>()
|
||||
const slotRef = useRef<HTMLDivElement>(null)
|
||||
const { mode, config } = props
|
||||
const { slot, props: slotProps } = config
|
||||
const { classes, style: slotStyles, ...slotRest } = slotProps || {}
|
||||
const isToolSlot =
|
||||
mode === ArSlotViewMode.ROWCONTROLLER ||
|
||||
mode === ArSlotViewMode.COLCONTROLLER
|
||||
const {
|
||||
acceptDropTypes,
|
||||
containerDisplayMode,
|
||||
content,
|
||||
dropHandler,
|
||||
activeSlot,
|
||||
onClick,
|
||||
...rest
|
||||
} = props
|
||||
const { controlsEnabled, selectionOnlyModeEnabled, eventsService } = useLayoutContext()
|
||||
const [selected, setSelected] = useState<boolean>()
|
||||
const { mode = ArSlotViewMode.BUILD, slotDescriptor } = props
|
||||
const slotRef = useRef<HTMLDivElement>(null)
|
||||
const { slot, props: slotProps } = slotDescriptor
|
||||
const { classes, style: slotStyles, ...slotRest } = slotProps || {}
|
||||
const isToolSlot =
|
||||
mode === ArSlotViewMode.ROWCONTROLLER ||
|
||||
mode === ArSlotViewMode.COLCONTROLLER
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(config.isSelected)
|
||||
}, [config.isSelected])
|
||||
useEffect(() => {
|
||||
setSelected(!!slotDescriptor.isSelected)
|
||||
}, [slotDescriptor.isSelected])
|
||||
|
||||
const finalSlotStyles = {
|
||||
gridArea: config.gridArea,
|
||||
...slotStyles,
|
||||
...(style || {}),
|
||||
}
|
||||
const finalSlotStyles: CSSProperties = {
|
||||
gridArea: slotDescriptor.gridArea,
|
||||
...slotStyles,
|
||||
}
|
||||
if (containerDisplayMode && containerDisplayMode !== "grid" && !content) {
|
||||
finalSlotStyles.minWidth = "5rem"
|
||||
finalSlotStyles.minHeight = "5rem"
|
||||
}
|
||||
|
||||
if (containerDisplayMode && containerDisplayMode !== "grid" && !content) {
|
||||
finalSlotStyles.minWidth = "5rem"
|
||||
finalSlotStyles.minHeight = "5rem"
|
||||
}
|
||||
const slotRender = (
|
||||
<div
|
||||
className={`ar-Slot position-relative${classes ? " " + classes : ""}${mode === "build" ? " obscure-border" : ""}${selected ? " selected" : ""}${activeSlot && activeSlot.slot === slot ? " last-selected" : ""} ${mode}${slotDescriptor.isSelectedForInlineDelete ? " selected-for-delete" : ""}`}
|
||||
id={slot}
|
||||
key={slot}
|
||||
style={finalSlotStyles}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelected(!selected)
|
||||
onClick && onClick(slotDescriptor)
|
||||
}}
|
||||
ref={slotRef}
|
||||
{...slotRest}
|
||||
>
|
||||
{content}
|
||||
{controlsEnabled && !selectionOnlyModeEnabled && (
|
||||
<SlotTools {...rest} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const slotRender = (
|
||||
<div
|
||||
className={`ar-Slot position-relative${classes ? " " + classes : ""}${
|
||||
mode === "build" ? " obscure-border" : ""
|
||||
}${selected ? " selected" : ""} ${
|
||||
lastSelected && lastSelected.slot === slot ? " last-selected" : ""
|
||||
} ${mode}${
|
||||
config.isSelectedForInlineDelete ? " selected-for-delete" : ""
|
||||
}`}
|
||||
id={slot}
|
||||
key={slot}
|
||||
style={finalSlotStyles}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setSelected(!selected)
|
||||
onClick && onClick(config)
|
||||
}}
|
||||
ref={slotRef}
|
||||
{...slotRest}
|
||||
>
|
||||
{content}
|
||||
{props.controlsEnabled && !selectionOnlyModeEnabled && (
|
||||
<SlotTools {...rest} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return isToolSlot ? (
|
||||
slotRender
|
||||
) : (
|
||||
<Draggable
|
||||
canDrag={!!config.content && props.mode === ArSlotViewMode.BUILD}
|
||||
demo={props.demo}
|
||||
itemData={config}
|
||||
itemType={ArDndItemTypes.COMPONENTSLOT}
|
||||
>
|
||||
<Droppable
|
||||
acceptTypes={[
|
||||
ArDndItemTypes.COMPONENTSLOT,
|
||||
ArDndItemTypes.TREELISTITEM,
|
||||
ArDndItemTypes.ICON,
|
||||
...(acceptDropTypes || []),
|
||||
]}
|
||||
dropHandler={(sourceData) =>
|
||||
dropHandler && dropHandler(sourceData, config)
|
||||
}
|
||||
hideHoverEffect
|
||||
>
|
||||
{slotRender}
|
||||
</Droppable>
|
||||
</Draggable>
|
||||
)
|
||||
return isToolSlot ? (
|
||||
slotRender
|
||||
) : (
|
||||
<Draggable
|
||||
canDrag={!!slotDescriptor.content && mode === ArSlotViewMode.BUILD}
|
||||
itemData={slotDescriptor}
|
||||
itemType={ArDndItemTypes.COMPONENTSLOT}
|
||||
>
|
||||
<Droppable
|
||||
acceptTypes={[ArDndItemTypes.COMPONENTSLOT, ArDndItemTypes.TREELISTITEM, ArDndItemTypes.ICON, ...(acceptDropTypes || [])]}
|
||||
dropHandler={(sourceData) => dropHandler && dropHandler(sourceData, slotDescriptor)}
|
||||
hideHoverEffect
|
||||
>
|
||||
{slotRender}
|
||||
</Droppable>
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
export default BuilderSlot
|
||||
|
||||
128
src/Layout.context.tsx
Normal file
128
src/Layout.context.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { createContext, useContext, useState, useEffect, FC, ReactElement } from "react"
|
||||
import type { LayoutContextType, LayoutProviderProps } from "./types"
|
||||
import type LayoutService from "./services/Layout.service"
|
||||
import type EventsService from "./services/Events.service"
|
||||
import type HistoryService from "./services/History.service"
|
||||
import type { SlotDescriptor } from "./models"
|
||||
import type { FunctionType } from "@armco/types"
|
||||
import LayoutError, { LayoutErrors } from "./LayoutError"
|
||||
|
||||
const LayoutContext = createContext<LayoutContextType | null>(null)
|
||||
|
||||
const exampleUsage = `
|
||||
Example usage:
|
||||
<LayoutProvider
|
||||
layoutService={layoutService}
|
||||
eventsService={eventsService!}
|
||||
historyService={HistoryService!}
|
||||
showPanels={showPanels}
|
||||
showControls={showControls}
|
||||
>
|
||||
<App />
|
||||
</LayoutProvider>
|
||||
`
|
||||
|
||||
export const useLayoutContext = (): LayoutContextType => {
|
||||
const ctx = useContext(LayoutContext)
|
||||
if (!ctx)
|
||||
throw new LayoutError(
|
||||
"useLayoutContext must be used within a LayoutProvider.",
|
||||
LayoutErrors.MISSING_LAYOUT_CONTEXT,
|
||||
exampleUsage,
|
||||
)
|
||||
return ctx
|
||||
}
|
||||
|
||||
export const useLayoutService = (): LayoutService => {
|
||||
const ctx = useLayoutContext()
|
||||
if (!ctx)
|
||||
throw new LayoutError(
|
||||
"useLayoutService must be used within a LayoutProvider.",
|
||||
LayoutErrors.MISSING_LAYOUT_CONTEXT,
|
||||
exampleUsage,
|
||||
)
|
||||
return ctx.layoutService
|
||||
}
|
||||
|
||||
export const useEventsService = (): EventsService => {
|
||||
const ctx = useLayoutContext()
|
||||
if (!ctx)
|
||||
throw new LayoutError(
|
||||
"useEventsService must be used within a LayoutProvider.",
|
||||
LayoutErrors.MISSING_LAYOUT_CONTEXT,
|
||||
exampleUsage,
|
||||
)
|
||||
return ctx.eventsService
|
||||
}
|
||||
|
||||
export const useHistoryService = (): HistoryService<Array<SlotDescriptor>> => {
|
||||
const ctx = useLayoutContext()
|
||||
if (!ctx)
|
||||
throw new LayoutError(
|
||||
"useHistoryService must be used within a LayoutProvider.",
|
||||
LayoutErrors.MISSING_LAYOUT_CONTEXT,
|
||||
exampleUsage,
|
||||
)
|
||||
return ctx.historyService
|
||||
}
|
||||
|
||||
export const useShowControls = (): { controlsEnabled: boolean; enableControls: FunctionType } => {
|
||||
const ctx = useLayoutContext()
|
||||
if (!ctx)
|
||||
throw new LayoutError(
|
||||
"useShowControls must be used within a LayoutProvider.",
|
||||
LayoutErrors.MISSING_LAYOUT_CONTEXT,
|
||||
exampleUsage,
|
||||
)
|
||||
return { controlsEnabled: ctx.controlsEnabled, enableControls: ctx.enableControls as unknown as FunctionType }
|
||||
}
|
||||
|
||||
export const useSelectionOnlyMode = (): { selectionOnlyModeEnabled: boolean; enableSelectionOnlyMode: FunctionType } => {
|
||||
const ctx = useLayoutContext()
|
||||
if (!ctx)
|
||||
throw new LayoutError(
|
||||
"useSelectionOnlyMode must be used within a LayoutProvider.",
|
||||
LayoutErrors.MISSING_LAYOUT_CONTEXT,
|
||||
exampleUsage,
|
||||
)
|
||||
return {
|
||||
selectionOnlyModeEnabled: ctx.selectionOnlyModeEnabled,
|
||||
enableSelectionOnlyMode: ctx.enableSelectionOnlyMode as unknown as FunctionType,
|
||||
}
|
||||
}
|
||||
|
||||
const LayoutProvider: FC<LayoutProviderProps> = ({
|
||||
children,
|
||||
layoutService,
|
||||
eventsService,
|
||||
historyService,
|
||||
showPanels,
|
||||
showControls,
|
||||
}): ReactElement => {
|
||||
const [controlsEnabled, setControlsEnabled] = useState(!!showControls)
|
||||
const [selectionOnlyModeEnabled, setSelectionOnlyModeEnabled] = useState(false)
|
||||
const [panelsDisplayed, setPanelsDisplayed] = useState(!!showPanels)
|
||||
|
||||
useEffect(() => setPanelsDisplayed(!!showPanels), [showPanels])
|
||||
useEffect(() => setControlsEnabled(!!showControls), [showControls])
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider
|
||||
value={{
|
||||
layoutService,
|
||||
eventsService,
|
||||
historyService,
|
||||
controlsEnabled,
|
||||
enableControls: setControlsEnabled as unknown as FunctionType,
|
||||
selectionOnlyModeEnabled,
|
||||
enableSelectionOnlyMode: setSelectionOnlyModeEnabled as unknown as FunctionType,
|
||||
panelsDisplayed,
|
||||
displayPanels: setPanelsDisplayed as unknown as FunctionType,
|
||||
} as unknown as LayoutContextType}
|
||||
>
|
||||
{children}
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutProvider
|
||||
111
src/Layout.tsx
Executable file → Normal file
111
src/Layout.tsx
Executable file → Normal file
@@ -1,67 +1,64 @@
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ArAlertType, LayoutProps, SlotDescriptor } from "@armco/types"
|
||||
import { FC, useEffect, useState } from "react"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { ArAlertType } from "@armco/utils"
|
||||
import { get as httpGet } from "@armco/utils/network"
|
||||
import { useNotification } from "@armco/utils/hooks"
|
||||
import LayoutProvider from "./Layout.context"
|
||||
import useLayoutInit from "./hooks/useLayoutInit"
|
||||
import SlotDescriptor from "./models/SlotDescriptor"
|
||||
import BuilderLayout from "./BuilderLayout"
|
||||
import ReleaseLayout from "./ReleaseLayout"
|
||||
import { get } from "@armco/utils/network"
|
||||
import { useNotification } from "@armco/utils/hooks"
|
||||
import type { LayoutProps } from "./types"
|
||||
|
||||
const dummyGridSpecs = { rows: 5, columns: 5 }
|
||||
const Layout: FC<LayoutProps> = (props) => {
|
||||
const { classes, displayMode, mode, showPanels, showControls, slotRenderer, url } = props
|
||||
const { notify } = useNotification()
|
||||
const [, setTick] = useState(false)
|
||||
const rerender = () => setTick((v) => !v)
|
||||
|
||||
const Layout = (props: LayoutProps) => {
|
||||
const { mode, demo, gridSpecs, url } = props
|
||||
const propsClone = { ...props }
|
||||
propsClone.gridSpecs = demo ? gridSpecs || dummyGridSpecs : gridSpecs
|
||||
const { notify } = useNotification()
|
||||
const [localSlots, setLocalSlots] = useState<
|
||||
Array<SlotDescriptor> | undefined
|
||||
>()
|
||||
const [localGridTemplate, setLocalGridTemplate] = useState<
|
||||
string | undefined
|
||||
>()
|
||||
propsClone.slots = localSlots
|
||||
propsClone.gridTemplate = localGridTemplate
|
||||
const { layoutService, eventsService, historyService } = useLayoutInit({ ...props, rerender }, mode)
|
||||
|
||||
useEffect(() => {
|
||||
JSON.stringify(props.slots) !== JSON.stringify(localSlots) &&
|
||||
setLocalSlots(props.slots)
|
||||
}, [props.slots])
|
||||
useEffect(() => {
|
||||
if (layoutService && !(layoutService.slots || layoutService.gridTemplate) && url) {
|
||||
httpGet(url)
|
||||
.then((res: any) => {
|
||||
const body = Array.isArray(res.body) ? res.body[0] : res.body
|
||||
if (body?.descriptor) {
|
||||
if (body.descriptor.content)
|
||||
layoutService.slots = body.descriptor.content.map((v: any) => SlotDescriptor.create(v))
|
||||
if (body.descriptor.layout) layoutService.gridTemplate = body.descriptor.layout
|
||||
}
|
||||
rerender()
|
||||
})
|
||||
.catch(() =>
|
||||
notify({ message: "Failed to fetch descriptor for provided URL", uid: uuidv4(), type: ArAlertType.ERROR }),
|
||||
)
|
||||
}
|
||||
}, [url, notify, layoutService])
|
||||
|
||||
useEffect(() => {
|
||||
setLocalGridTemplate(props.gridTemplate)
|
||||
}, [props.gridTemplate])
|
||||
if ((mode ?? "build") === "build") {
|
||||
return (
|
||||
<LayoutProvider
|
||||
layoutService={layoutService}
|
||||
eventsService={eventsService!}
|
||||
historyService={historyService!}
|
||||
showPanels={showPanels}
|
||||
showControls={showControls}
|
||||
>
|
||||
<BuilderLayout {...props} />
|
||||
</LayoutProvider>
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
!(localSlots || localGridTemplate) &&
|
||||
url &&
|
||||
get(url)
|
||||
.then((res) => {
|
||||
const component = Array.isArray(res.body) ? res.body[0] : res.body
|
||||
component?.descriptor?.content &&
|
||||
setLocalSlots(component.descriptor.content as Array<SlotDescriptor>)
|
||||
component?.descriptor?.gridTemplate &&
|
||||
setLocalGridTemplate(component.descriptor.layout as string)
|
||||
})
|
||||
.catch((e) =>
|
||||
notify({
|
||||
message: "Failed to fetch descriptor for provided URL",
|
||||
uid: uuid(),
|
||||
type: ArAlertType.ERROR,
|
||||
}),
|
||||
)
|
||||
}, [url])
|
||||
|
||||
return mode === "build" || (!mode && demo) ? (
|
||||
<BuilderLayout {...propsClone} />
|
||||
) : (
|
||||
<ReleaseLayout
|
||||
classes={propsClone.classes}
|
||||
displayMode={propsClone.displayMode}
|
||||
slots={localSlots}
|
||||
gridTemplate={localGridTemplate}
|
||||
slotRenderer={propsClone.slotRenderer}
|
||||
/>
|
||||
)
|
||||
return (
|
||||
<ReleaseLayout
|
||||
classes={classes}
|
||||
displayMode={displayMode}
|
||||
slots={layoutService.slots}
|
||||
gridTemplate={layoutService.gridTemplate}
|
||||
slotRenderer={slotRenderer}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
||||
26
src/LayoutControlPanel.component.scss
Executable file → Normal file
26
src/LayoutControlPanel.component.scss
Executable file → Normal file
@@ -1,18 +1,14 @@
|
||||
.ar-LayoutControlPanel {
|
||||
transition: all 0.5s;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
|
||||
&.show {
|
||||
max-height: 2.25rem;
|
||||
max-width: calc(2.25rem - 2px);
|
||||
opacity: 1;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background-color: var(--ar-color-selected);
|
||||
white-space: nowrap;
|
||||
|
||||
&.show-all-controls {
|
||||
max-width: 30rem;
|
||||
}
|
||||
}
|
||||
&.show {
|
||||
max-height: 2.25rem;
|
||||
max-width: 30rem;
|
||||
opacity: 1;
|
||||
border-radius: 6px 6px 0 0;
|
||||
background-color: var(--ar-color-selected);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
388
src/LayoutControlPanel.tsx
Executable file → Normal file
388
src/LayoutControlPanel.tsx
Executable file → Normal file
@@ -1,269 +1,135 @@
|
||||
import { Ref, forwardRef } from "react"
|
||||
import {
|
||||
ArPopoverSlots,
|
||||
ArThemes,
|
||||
FunctionType,
|
||||
LayoutControlPanelProps,
|
||||
} from "@armco/types"
|
||||
import Icon from "@armco/icon"
|
||||
import Tooltip from "@armco/shared-components/Tooltip"
|
||||
import { forwardRef } from "react"
|
||||
import { useTheme } from "@armco/utils/hooks"
|
||||
import { ArThemes } from "@armco/utils"
|
||||
import { ArPopoverSlots } from "@armco/shared-components/enums"
|
||||
import { Tooltip } from "@armco/shared-components"
|
||||
import Icon from "@armco/icon"
|
||||
import { useLayoutContext, useSelectionOnlyMode, useShowControls } from "./Layout.context"
|
||||
import type { LayoutControlPanelProps } from "./types"
|
||||
import "./LayoutControlPanel.component.scss"
|
||||
|
||||
const getControl = (
|
||||
theme: string,
|
||||
icon: string,
|
||||
handler: FunctionType,
|
||||
tooltip: string,
|
||||
demo?: boolean,
|
||||
color?: string,
|
||||
isDisabled?: boolean,
|
||||
toggleColor?: string,
|
||||
showControls?: boolean,
|
||||
const actionButton = (
|
||||
theme: ArThemes,
|
||||
icon: string,
|
||||
onClick: () => void,
|
||||
tooltip: string,
|
||||
fillColor?: string,
|
||||
disabled?: boolean,
|
||||
backgroundColor?: string,
|
||||
fillPath?: boolean,
|
||||
) => {
|
||||
const iconColor =
|
||||
theme === ArThemes.DARK1
|
||||
? isDisabled
|
||||
? "#7a7aa7"
|
||||
: color || "black"
|
||||
: isDisabled
|
||||
? "#cfcfcf"
|
||||
: color || "white"
|
||||
return (
|
||||
<Tooltip
|
||||
classes={showControls === undefined || showControls ? "" : " w-0"}
|
||||
demo={demo}
|
||||
>
|
||||
<span
|
||||
className={`p-1 border-radius-l2 d-inline-block${
|
||||
!isDisabled ? " cursor-pointer hover-border hover-bg dark2" : ""
|
||||
}`}
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
onClick={isDisabled ? () => {} : handler}
|
||||
style={{ backgroundColor: toggleColor }}
|
||||
>
|
||||
<Icon icon={icon} attributes={{colors: {fillColor: iconColor}}} />
|
||||
</span>
|
||||
<span slot={ArPopoverSlots.POPOVER} style={{ whiteSpace: "normal" }}>
|
||||
<div>{tooltip}</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
const color = theme === ArThemes.DARK1 ? (disabled ? "#7a7aa7" : fillColor || "black") : disabled ? "#cfcfcf" : fillColor || "white"
|
||||
return (
|
||||
<Tooltip>
|
||||
<span
|
||||
className={`p-1 border-radius-l2 d-inline-block${disabled ? "" : " cursor-pointer hover-border hover-bg dark2"}`}
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
onClick={() => !disabled && onClick()}
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
<Icon
|
||||
icon={icon}
|
||||
attributes={{ colors: { fillColor: color, strokeColor: color } }}
|
||||
{...(fillPath ? ({ fillPath: true } as any) : {})}
|
||||
/>
|
||||
</span>
|
||||
<span slot={ArPopoverSlots.POPOVER} style={{ whiteSpace: "normal" }}>
|
||||
<div>{tooltip}</div>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
const LayoutControlPanel = forwardRef(
|
||||
(props: LayoutControlPanelProps, ref: Ref<HTMLDivElement>): JSX.Element => {
|
||||
const {
|
||||
classes,
|
||||
demo,
|
||||
showControls,
|
||||
setShowControls,
|
||||
slots,
|
||||
splitEnabled,
|
||||
mergeEnabled,
|
||||
rowDeleteEnabled,
|
||||
columnDeleteEnabled,
|
||||
undoEnabled,
|
||||
redoEnabled,
|
||||
selectionOnlyModeEnabled,
|
||||
show,
|
||||
enableSelectionOnlyMode,
|
||||
horizontalSplitHandler,
|
||||
verticalSplitHandler,
|
||||
mergeHandler,
|
||||
rowInsertHandler,
|
||||
columnInsertHandler,
|
||||
rowDeleteHandler,
|
||||
columnDeleteHandler,
|
||||
undoHandler,
|
||||
redoHandler,
|
||||
} = props
|
||||
const { theme } = useTheme()
|
||||
|
||||
const controls = [
|
||||
[
|
||||
showControls ? "md.MdEditOff" : "md.MdEdit",
|
||||
() => setShowControls(!showControls),
|
||||
"Edit Layout",
|
||||
"#ffa500",
|
||||
],
|
||||
[
|
||||
"ri.RiSplitCellsVertical",
|
||||
horizontalSplitHandler,
|
||||
splitEnabled ? "Split Horizontal" : "Select a cell to split",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!splitEnabled,
|
||||
],
|
||||
[
|
||||
"ri.RiSplitCellsHorizontal",
|
||||
verticalSplitHandler,
|
||||
splitEnabled ? "Split Vertical" : "Select a cell to split",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!splitEnabled,
|
||||
],
|
||||
[
|
||||
"fa.FaCompressArrowsAlt",
|
||||
mergeHandler,
|
||||
mergeEnabled
|
||||
? "Merge Selection"
|
||||
: "Selected at least two adjacent cells to merge",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!mergeEnabled,
|
||||
],
|
||||
[
|
||||
"ri.RiInsertRowTop",
|
||||
() => rowInsertHandler("before"),
|
||||
"Insert Row Before",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiInsertRowBottom",
|
||||
() => rowInsertHandler("after"),
|
||||
"Insert Row After",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiInsertColumnLeft",
|
||||
() => columnInsertHandler("before"),
|
||||
"Insert Column Before",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiInsertColumnRight",
|
||||
() => columnInsertHandler("after"),
|
||||
"Insert Column After",
|
||||
theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff",
|
||||
],
|
||||
[
|
||||
"ri.RiDeleteRow",
|
||||
rowDeleteHandler,
|
||||
rowDeleteEnabled ? "Delete Row" : "Select a row to delete",
|
||||
"#ff6666",
|
||||
!rowDeleteEnabled,
|
||||
],
|
||||
[
|
||||
"ri.RiDeleteColumn",
|
||||
columnDeleteHandler,
|
||||
columnDeleteEnabled ? "Delete Column" : "Select a column to delete",
|
||||
"#ff6666",
|
||||
!columnDeleteEnabled,
|
||||
],
|
||||
[
|
||||
"fc.FcUndo",
|
||||
undoHandler,
|
||||
undoEnabled ? "Undo Last" : "Nothing to undo",
|
||||
"",
|
||||
!undoEnabled,
|
||||
],
|
||||
[
|
||||
"fc.FcRedo",
|
||||
redoHandler,
|
||||
redoEnabled ? "Redo Last Undone" : "Nothing to redo",
|
||||
"",
|
||||
!redoEnabled,
|
||||
],
|
||||
[
|
||||
"gr.GrSelect",
|
||||
() => enableSelectionOnlyMode(!selectionOnlyModeEnabled),
|
||||
!selectionOnlyModeEnabled
|
||||
? "Enable Selection Only Mode"
|
||||
: "Disable Selection Only Mode",
|
||||
selectionOnlyModeEnabled ? "" : "#17dc81",
|
||||
false,
|
||||
selectionOnlyModeEnabled ? "#17dc81" : "",
|
||||
],
|
||||
]
|
||||
return (
|
||||
<div
|
||||
className={`ar-LayoutControlPanel p-1${show ? " show" : ""}${
|
||||
classes ? " " + classes : ""
|
||||
}${showControls ? " show-all-controls" : ""}`}
|
||||
style={{ overflow: "hidden" }}
|
||||
ref={ref}
|
||||
key={"layout-control-panel"}
|
||||
>
|
||||
{getControl(
|
||||
theme,
|
||||
controls[0][0] as string,
|
||||
controls[0][1] as FunctionType,
|
||||
controls[0][2] as string,
|
||||
demo,
|
||||
controls[0][3] as string,
|
||||
)}
|
||||
{showControls && (
|
||||
<>
|
||||
{slots && (
|
||||
<>
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(1, 4)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(4, 10)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(10, 12)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
<span className="border-right mx-2" />
|
||||
{controls
|
||||
.slice(12)
|
||||
.map((control) =>
|
||||
getControl(
|
||||
theme,
|
||||
control[0] as string,
|
||||
control[1] as FunctionType,
|
||||
control[2] as string,
|
||||
demo,
|
||||
control[3] as string,
|
||||
control[4] as boolean,
|
||||
control[5] as string,
|
||||
showControls,
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
const LayoutControlPanel = forwardRef<HTMLDivElement, LayoutControlPanelProps>((props, ref) => {
|
||||
const { classes, rowDeleteEnabled, columnDeleteEnabled, show } = props
|
||||
const { theme } = useTheme()
|
||||
const { eventsService, historyService, layoutService } = useLayoutContext()
|
||||
const { selectionOnlyModeEnabled, enableSelectionOnlyMode } = useSelectionOnlyMode()
|
||||
const { controlsEnabled, enableControls } = useShowControls()
|
||||
|
||||
const splitEnabled = layoutService.splitEnabled
|
||||
const mergeEnabled = layoutService.mergeEnabled
|
||||
|
||||
const actions: any[] = [
|
||||
[
|
||||
controlsEnabled ? "md.MdEditOff" : "md.MdEdit",
|
||||
() => enableControls(!controlsEnabled),
|
||||
"Edit Layout",
|
||||
"#ffa500",
|
||||
],
|
||||
[
|
||||
"ri.RiSplitCellsVertical",
|
||||
eventsService.splitHorizontalAfter,
|
||||
splitEnabled ? "Split Horizontal" : "Select a cell to split",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!splitEnabled,
|
||||
],
|
||||
[
|
||||
"ri.RiSplitCellsHorizontal",
|
||||
eventsService.splitVerticalAfter,
|
||||
splitEnabled ? "Split Vertical" : "Select a cell to split",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!splitEnabled,
|
||||
],
|
||||
[
|
||||
"fa.FaCompressArrowsAlt",
|
||||
() => {
|
||||
eventsService.mergeCells()
|
||||
layoutService.mergeEnabled = false
|
||||
},
|
||||
mergeEnabled ? "Merge Selection" : "Selected at least two adjacent cells to merge",
|
||||
theme === ArThemes.DARK1 ? "green" : "#17dc81",
|
||||
!mergeEnabled,
|
||||
],
|
||||
["ri.RiInsertRowTop", eventsService.addRowBefore, "Insert Row Before", theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff"],
|
||||
["ri.RiInsertRowBottom", eventsService.addRowAfter, "Insert Row After", theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff"],
|
||||
["ri.RiInsertColumnLeft", eventsService.addColumnBefore, "Insert Column Before", theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff"],
|
||||
["ri.RiInsertColumnRight", eventsService.addColumnAfter, "Insert Column After", theme === ArThemes.DARK1 ? "#0299e7" : "#0299ff"],
|
||||
["ri.RiDeleteRow", eventsService.deleteRow, rowDeleteEnabled ? "Delete Row" : "Select a row to delete", "#ff6666", !rowDeleteEnabled],
|
||||
[
|
||||
"ri.RiDeleteColumn",
|
||||
eventsService.deleteColumn,
|
||||
columnDeleteEnabled ? "Delete Column" : "Select a column to delete",
|
||||
"#ff6666",
|
||||
!columnDeleteEnabled,
|
||||
],
|
||||
["fc.FcUndo", eventsService.undo, historyService.canUndo ? "Undo Last" : "Nothing to undo", "", !historyService.canUndo],
|
||||
["fc.FcRedo", eventsService.redo, historyService.canRedo ? "Redo Last Undone" : "Nothing to redo", "", !historyService.canRedo],
|
||||
[
|
||||
"gr.GrSelect",
|
||||
() => enableSelectionOnlyMode(!selectionOnlyModeEnabled),
|
||||
selectionOnlyModeEnabled ? "Disable Selection Only Mode" : "Enable Selection Only Mode",
|
||||
selectionOnlyModeEnabled ? "" : "#17dc81",
|
||||
false,
|
||||
selectionOnlyModeEnabled ? "#17dc81" : "",
|
||||
true,
|
||||
],
|
||||
]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-LayoutControlPanel p-1${show ? " show" : ""}${classes ? " " + classes : ""}`}
|
||||
style={{ overflow: "hidden" }}
|
||||
ref={ref}
|
||||
key={"layout-control-panel"}
|
||||
>
|
||||
{actionButton(theme, actions[0][0], actions[0][1], actions[0][2], actions[0][3])}
|
||||
{layoutService.slots && (
|
||||
<>
|
||||
<span className="border-right mx-2" />
|
||||
{actions.slice(1, 4).map((e, idx) =>
|
||||
actionButton(theme, e[0], e[1], e[2], e[3], e[4], e[5])
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<span className="border-right mx-2" />
|
||||
{actions.slice(4, 10).map((e, idx) => actionButton(theme, e[0], e[1], e[2], e[3], e[4], e[5]))}
|
||||
<span className="border-right mx-2" />
|
||||
{actions.slice(10, 12).map((e, idx) => actionButton(theme, e[0], e[1], e[2], e[3], e[4], e[5]))}
|
||||
<span className="border-right mx-2" />
|
||||
{actions.slice(12).map((e, idx) => actionButton(theme, e[0], e[1], e[2], e[3], e[4], e[5], e[6]))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default LayoutControlPanel
|
||||
|
||||
21
src/LayoutError.tsx
Normal file
21
src/LayoutError.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
// LayoutError aligned with built artifacts (non-Error class, simple payload fields)
|
||||
export enum LayoutErrors {
|
||||
MISSING_SLOTS = "MISSING_SLOTS",
|
||||
MALFORMED_GRID_SPECS = "MALFORMED_GRID_SPECS",
|
||||
MISSING_LAYOUT_CONTEXT = "MISSING_LAYOUT_CONTEXT",
|
||||
MISSING_SLOT_DESCRIPTOR = "MISSING_SLOT_DESCRIPTOR",
|
||||
MISSING_SLOT_DESCRIPTOR_OBJ = "MISSING_SLOT_DESCRIPTOR_OBJ",
|
||||
MISSING_SLOT_ID = "MISSING_SLOT_ID",
|
||||
}
|
||||
|
||||
export default class LayoutError {
|
||||
error: string
|
||||
code?: LayoutErrors
|
||||
description?: string
|
||||
|
||||
constructor(error: string, code?: LayoutErrors, description?: string) {
|
||||
this.error = error
|
||||
this.code = code
|
||||
this.description = description
|
||||
}
|
||||
}
|
||||
98
src/LayoutHelp.tsx
Normal file
98
src/LayoutHelp.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { forwardRef, useMemo } from "react"
|
||||
import { ArThemes } from "@armco/utils"
|
||||
import { useTheme } from "@armco/utils/hooks"
|
||||
import { ArPopoverSlots } from "@armco/shared-components/enums"
|
||||
import { Tooltip } from "@armco/shared-components"
|
||||
import Icon from "@armco/icon"
|
||||
|
||||
type LayoutHelpProps = {
|
||||
panelsDisplayed?: boolean
|
||||
}
|
||||
|
||||
const keyBindings = [
|
||||
{ key: "h", action: "Split Horizontally" },
|
||||
{ key: "v", action: "Split Vertically" },
|
||||
{ key: "m", action: "Merge Rectangle" },
|
||||
{ key: "r", action: "Insert Row" },
|
||||
{ key: "c", action: "Insert Column" },
|
||||
{ key: "d", action: "Delete" },
|
||||
{ key: "z", action: "Undo" },
|
||||
{ key: "y", action: "Redo" },
|
||||
]
|
||||
|
||||
const helpPoints = [
|
||||
"Hover on grid to reveal control panel.",
|
||||
"Click a cell to select it.",
|
||||
"You may select multiple cells by clicking on them.",
|
||||
"Click a row header to select all cells in that row, and column header to select all cells in that column",
|
||||
"You may hover on any selected cells on specific sections within cell to split vertical or horizontal.",
|
||||
"For Horizontal: Hover on top center or bottom center region and click",
|
||||
"For Vertical: Hover on left center or right center region and click",
|
||||
"You may merge cells too, at least 2 cells should be selected to merge and the selected cells should form a rectangle.",
|
||||
"To delete a row or column, hover on its corresponding row or column header to reveal delete button and click on it.",
|
||||
"Undo/Redo layout changes is supported. Use Undo/Redo buttons in the control panel on top of grid, or use Ctrl/Cmd + z to undo, Ctrl/Cmd + y to redo",
|
||||
"Edit Mode should be enabled to use any shortcuts",
|
||||
]
|
||||
|
||||
const LayoutHelp = forwardRef<HTMLDivElement, LayoutHelpProps>(
|
||||
({ panelsDisplayed }, ref) => {
|
||||
const { theme } = useTheme()
|
||||
const cmdIcon = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<Icon icon="ai.AiFillMacCommand" attributes={{ size: "0.8rem" }} />{" "}
|
||||
<Icon icon="md.MdOutlineKeyboardControlKey" attributes={{ size: "0.8rem" }} />
|
||||
</>
|
||||
),
|
||||
[],
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={`ar-Layout__help-panel position-fixed overflow-hidden d-flex px-2 justify-content-between py-1${panelsDisplayed ? " show" : ""
|
||||
}`}
|
||||
ref={ref}
|
||||
>
|
||||
<span className="small fw-bold" style={{ color: theme === ArThemes.DARK1 ? "black" : "white" }}>
|
||||
Undo: Ctrl/Cmd + z, Redo: Ctrl/Cmd + y
|
||||
</span>
|
||||
<div>
|
||||
<Tooltip>
|
||||
<Icon
|
||||
icon="fa.FaKeyboard"
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
attributes={{ colors: { fillColor: "white" }, classes: "me-3" }}
|
||||
/>
|
||||
<span slot={ArPopoverSlots.POPOVER} className="z-2">
|
||||
{keyBindings.map((kb) => (
|
||||
<div className="row" key={kb.key}>
|
||||
<div className="col-5 fw-bold">
|
||||
{cmdIcon} + {kb.key}
|
||||
</div>
|
||||
<div className="col-7 fw-bold">{kb.action}</div>
|
||||
</div>
|
||||
))}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<Icon
|
||||
icon="md.MdInfoOutline"
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
attributes={{ colors: { fillColor: theme === ArThemes.DARK1 ? "black" : "white" } }}
|
||||
/>
|
||||
<span
|
||||
className="ar-Layout__help-panel__help-content small z-2"
|
||||
slot={ArPopoverSlots.POPOVER}
|
||||
style={{ whiteSpace: "normal" }}
|
||||
>
|
||||
{helpPoints.map((h, idx) => (
|
||||
<div key={idx}>{h}</div>
|
||||
))}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default LayoutHelp
|
||||
8
src/README.md
Normal file
8
src/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Recovered skeletons for Layout package
|
||||
|
||||
This directory mirrors the structure of build/es to guide reconstruction of lost sources.
|
||||
|
||||
Conventions:
|
||||
- .ts for services and utilities
|
||||
- .tsx for React components
|
||||
- Each file contains a TODO header describing intended module purpose inferred from build outputs.
|
||||
57
src/ReleaseLayout.tsx
Executable file → Normal file
57
src/ReleaseLayout.tsx
Executable file → Normal file
@@ -1,29 +1,38 @@
|
||||
import { ArSlotViewMode, ReleaseLayoutProps, SlotProps } from "@armco/types"
|
||||
import React from "react"
|
||||
import { ArSlotViewMode } from "@armco/shared-components/enums"
|
||||
import type { LayoutProps } from "./types"
|
||||
import Slot from "./Slot"
|
||||
import { SlotDescriptor } from "@models"
|
||||
|
||||
const ReleaseLayout = (props: ReleaseLayoutProps): JSX.Element => {
|
||||
const { classes, displayMode, gridTemplate, slots, slotRenderer } = props
|
||||
return (
|
||||
<div
|
||||
className={`ar-ReleaseLayout__grid h-100 ${
|
||||
displayMode ? "d-" + displayMode : "d-grid"
|
||||
}${classes ? " " + classes : ""}`}
|
||||
style={gridTemplate ? { gridTemplate } : {}}
|
||||
>
|
||||
{slots?.map((sConfig) => {
|
||||
const props: SlotProps = {
|
||||
config: sConfig,
|
||||
containerDisplayMode: displayMode,
|
||||
mode: ArSlotViewMode.RELEASE,
|
||||
}
|
||||
return slotRenderer ? (
|
||||
slotRenderer(props)
|
||||
) : (
|
||||
<Slot key={sConfig.slot} {...props} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
export interface ReleaseLayoutProps extends Pick<LayoutProps, "classes" | "displayMode" | "gridTemplate" | "slots" | "slotRenderer"> { }
|
||||
|
||||
// Release layout simply renders provided slots with a grid template (or displayMode variant) without builder controls.
|
||||
const ReleaseLayout: React.FC<ReleaseLayoutProps> = ({
|
||||
classes,
|
||||
displayMode,
|
||||
gridTemplate,
|
||||
slots,
|
||||
slotRenderer,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`ar-ReleaseLayout__grid h-100 ${displayMode ? "d-" + displayMode : "d-grid"}${classes ? " " + classes : ""}`}
|
||||
style={gridTemplate ? { gridTemplate } : {}}
|
||||
>
|
||||
{slots?.map((slotDesc: SlotDescriptor) => {
|
||||
const slotProps = {
|
||||
slotDescriptor: slotDesc,
|
||||
containerDisplayMode: displayMode,
|
||||
mode: ArSlotViewMode.RELEASE as const,
|
||||
}
|
||||
return slotRenderer ? (
|
||||
slotRenderer(slotProps)
|
||||
) : (
|
||||
<Slot {...slotProps} key={slotDesc.slot} />
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReleaseLayout
|
||||
|
||||
39
src/ReleaseSlot.tsx
Executable file → Normal file
39
src/ReleaseSlot.tsx
Executable file → Normal file
@@ -1,24 +1,23 @@
|
||||
import { ReleaseSlotProps } from "@armco/types"
|
||||
import type { ReleaseSlotProps } from "./types"
|
||||
import React from "react"
|
||||
|
||||
const ReleaseSlot = (props: ReleaseSlotProps): JSX.Element => {
|
||||
const { config, content } = props
|
||||
const { slot, props: slotProps } = config
|
||||
const {
|
||||
classes: slotClasses,
|
||||
style: slotStyles,
|
||||
...slotRest
|
||||
} = slotProps || {}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-Slot${slotClasses ? " " + slotClasses : ""}`}
|
||||
key={slot}
|
||||
style={{ gridArea: config.gridArea, ...slotStyles }}
|
||||
{...slotRest}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
const ReleaseSlot: React.FC<ReleaseSlotProps> = (props) => {
|
||||
const { slotDescriptor, content } = props
|
||||
const { props: slotProps } = slotDescriptor as any
|
||||
const { classes, style, ...rest } = (slotProps || {}) as {
|
||||
classes?: string
|
||||
style?: React.CSSProperties
|
||||
[key: string]: any
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`ar-Slot${classes ? " " + classes : ""}`}
|
||||
style={{ gridArea: slotDescriptor.gridArea, ...(style || {}) }}
|
||||
{...rest}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReleaseSlot
|
||||
|
||||
112
src/Resizable.component.scss
Executable file → Normal file
112
src/Resizable.component.scss
Executable file → Normal file
@@ -1,54 +1,60 @@
|
||||
.ar-Resizable {
|
||||
.handle {
|
||||
&.n, &.s {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1rem;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
&.e, &.w {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 1rem;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
&.ne, &.nw, &.se, &.sw {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
&.n {
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
&.s {
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
&.e {
|
||||
left: calc(100% - 0.5rem);
|
||||
}
|
||||
&.w {
|
||||
right: calc(100% - 0.5rem);
|
||||
}
|
||||
&.ne {
|
||||
left: calc(100% - 0.5rem);
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
&.nw {
|
||||
right: calc(100% - 0.5rem);
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
&.se {
|
||||
left: calc(100% - 0.5rem);
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
&.sw {
|
||||
right: calc(100% - 0.5rem);
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
&.n, &.s, &.e, &.w, &.ne, &.nw, &.se, &.sw {
|
||||
&:hover {
|
||||
background-color: var(--ar-bg-hover-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
.ar-Resizable .handle.n,
|
||||
.ar-Resizable .handle.s {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 1rem;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
.ar-Resizable .handle.e,
|
||||
.ar-Resizable .handle.w {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 1rem;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
.ar-Resizable .handle.ne,
|
||||
.ar-Resizable .handle.nw,
|
||||
.ar-Resizable .handle.se,
|
||||
.ar-Resizable .handle.sw {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
.ar-Resizable .handle.n {
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.s {
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.e {
|
||||
left: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.w {
|
||||
right: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.ne {
|
||||
left: calc(100% - 0.5rem);
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.nw {
|
||||
right: calc(100% - 0.5rem);
|
||||
bottom: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.se {
|
||||
left: calc(100% - 0.5rem);
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.sw {
|
||||
right: calc(100% - 0.5rem);
|
||||
top: calc(100% - 0.5rem);
|
||||
}
|
||||
.ar-Resizable .handle.n:hover,
|
||||
.ar-Resizable .handle.s:hover,
|
||||
.ar-Resizable .handle.e:hover,
|
||||
.ar-Resizable .handle.w:hover,
|
||||
.ar-Resizable .handle.ne:hover,
|
||||
.ar-Resizable .handle.nw:hover,
|
||||
.ar-Resizable .handle.se:hover,
|
||||
.ar-Resizable .handle.sw:hover {
|
||||
background-color: var(--ar-bg-hover-4);
|
||||
}
|
||||
|
||||
150
src/Resizable.tsx
Executable file → Normal file
150
src/Resizable.tsx
Executable file → Normal file
@@ -1,112 +1,66 @@
|
||||
import {
|
||||
ReactElement,
|
||||
Suspense,
|
||||
cloneElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { ArDirections, ResizableProps } from "@armco/types"
|
||||
import React, { Suspense, cloneElement, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import Slot from "./Slot"
|
||||
import type { ResizableProps } from "./types"
|
||||
import "./Resizable.component.scss"
|
||||
|
||||
const components: {
|
||||
[key: string]: (props: any) => JSX.Element
|
||||
} = {
|
||||
Slot,
|
||||
// Add more components as needed
|
||||
}
|
||||
const Resizable: React.FC<ResizableProps> = (props) => {
|
||||
const { reClasses, directions, display, componentName, children, style, ...rest } = props
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const lastYRef = useRef<number>(0)
|
||||
useRef<number>(0) // mimic extra ref from build (unused)
|
||||
const childRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const Resizable = (props: ResizableProps): JSX.Element => {
|
||||
const {
|
||||
reClasses,
|
||||
directions,
|
||||
display,
|
||||
componentName,
|
||||
children,
|
||||
style,
|
||||
...rest
|
||||
} = props
|
||||
const [resizing, setResizing] = useState(false)
|
||||
const mouseDownY = useRef<number>(0)
|
||||
const mouseDownX = useRef<number>(0)
|
||||
const childRef = useRef<HTMLElement>(null)
|
||||
const registry = useMemo(() => ({
|
||||
Slot,
|
||||
// Add more components as needed
|
||||
}), [])
|
||||
|
||||
const mouseMoveHandler = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (resizing && childRef.current) {
|
||||
const childRect = childRef.current.getBoundingClientRect()
|
||||
// const initialPosition = childRect.left + childRect.width
|
||||
// const dragOffset = e.clientX - initialPosition
|
||||
const onMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (dragging && childRef.current) {
|
||||
const rect = childRef.current.getBoundingClientRect()
|
||||
const _ratio = (e.clientX - rect.left) / rect.width
|
||||
// Placeholder for resize effect, original build does not apply it yet
|
||||
}
|
||||
}, [dragging])
|
||||
|
||||
// const newWidth = childRect.width + dragOffset
|
||||
const newWidth = e.clientX - childRect.left
|
||||
const newFrValue = newWidth / childRect.width
|
||||
const onMouseUp = useCallback(() => setDragging(false), [])
|
||||
|
||||
// setFrValue(newFrValue)
|
||||
}
|
||||
},
|
||||
[resizing, childRef],
|
||||
)
|
||||
const mouseUpHandler = useCallback(() => setResizing(false), [])
|
||||
const mouseDownHandler = useCallback(
|
||||
(
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
direction: ArDirections,
|
||||
) => {
|
||||
setResizing(true)
|
||||
mouseDownY.current = e.clientY
|
||||
// TODO: handle direction
|
||||
},
|
||||
[],
|
||||
)
|
||||
const onMouseDown = useCallback((e: React.MouseEvent, _dir: string) => {
|
||||
setDragging(true)
|
||||
lastYRef.current = e.clientY
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", mouseMoveHandler)
|
||||
document.addEventListener("mouseup", mouseUpHandler)
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler)
|
||||
document.removeEventListener("mouseup", mouseUpHandler)
|
||||
}
|
||||
}, [mouseMoveHandler, mouseUpHandler])
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousemove", onMouseMove)
|
||||
document.addEventListener("mouseup", onMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", onMouseMove)
|
||||
document.removeEventListener("mouseup", onMouseUp)
|
||||
}
|
||||
}, [onMouseMove, onMouseUp])
|
||||
|
||||
useEffect(() => {
|
||||
if (childRef.current) {
|
||||
const rect = childRef.current.getBoundingClientRect()
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (childRef.current) {
|
||||
childRef.current.getBoundingClientRect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const Component = (componentName && components[componentName]) as
|
||||
| React.ComponentType<any>
|
||||
| undefined
|
||||
const Dynamic = componentName && (registry as any)[componentName]
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-Resizable position-relative${
|
||||
reClasses ? " " + reClasses : ""
|
||||
}${display ? " " + display : ""}`}
|
||||
style={style}
|
||||
>
|
||||
{directions?.map((direction) => (
|
||||
<div
|
||||
key={direction}
|
||||
className={`handle position-absolute ${direction}`}
|
||||
onMouseDown={(e) => mouseDownHandler(e, direction)}
|
||||
/>
|
||||
))}
|
||||
{Component ? (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Component childRef={childRef} {...rest} />
|
||||
</Suspense>
|
||||
) : (
|
||||
children &&
|
||||
cloneElement(children as ReactElement, {
|
||||
childRef: childRef,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className={`ar-Resizable position-relative${reClasses ? " " + reClasses : ""}${display ? " " + display : ""}`} style={style}>
|
||||
{directions?.map((dir) => (
|
||||
<div className={`handle position-absolute ${dir}`} key={dir} onMouseDown={(e) => onMouseDown(e, dir as any)} />
|
||||
))}
|
||||
{Dynamic ? (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Dynamic childRef={childRef} {...rest} />
|
||||
</Suspense>
|
||||
) : (
|
||||
children && cloneElement(children as any, { childRef })
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Resizable
|
||||
|
||||
59
src/Slot.component.scss
Normal file
59
src/Slot.component.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
.ar-Droppable:not(.hide-hover-effect) {
|
||||
&:hover {
|
||||
border: 1px dashed #1677ff !important;
|
||||
}
|
||||
&.drag-over {
|
||||
border: 2px dashed #1677ff !important;
|
||||
}
|
||||
}
|
||||
.ar-Slot {
|
||||
.build {
|
||||
&:hover {
|
||||
border: 1px dashed #7d31f0 !important;
|
||||
.ar-SlotTools {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
&.selected {
|
||||
border: 1px solid #86cff1 !important;
|
||||
}
|
||||
&.active {
|
||||
outline: 4px solid #2e9cfd !important;
|
||||
}
|
||||
&.selected-for-delete {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
.ar-Slot__overlay-btn {
|
||||
width: 33.3333333333%;
|
||||
&.slot:hover {
|
||||
background-color: #55bdeda9;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ar-SlotTools {
|
||||
display: none;
|
||||
}
|
||||
.row-controller .ar-Slot__overlay-btn {
|
||||
height: calc((100% - 16px) / 3);
|
||||
&.delete {
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
.col-controller .ar-Slot__overlay-btn {
|
||||
width: calc((100% - 16px) / 3);
|
||||
&.delete {
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
.row-controller,
|
||||
.col-controller {
|
||||
.ar-Slot__overlay-btn {
|
||||
&.slot:hover {
|
||||
background-color: #55bdeda9;
|
||||
}
|
||||
&.delete {
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/Slot.tsx
Executable file → Normal file
178
src/Slot.tsx
Executable file → Normal file
@@ -1,98 +1,100 @@
|
||||
import { ReactNode, useEffect, useState } from "react"
|
||||
import {
|
||||
ArComponentResources,
|
||||
ArSlotViewMode,
|
||||
SlotDescriptor,
|
||||
SlotProps,
|
||||
} from "@armco/types"
|
||||
import {
|
||||
generateGridAreaAndSizes,
|
||||
generateGridTemplate,
|
||||
} from "@armco/utils/gridHelper"
|
||||
import { lazyImport } from "@armco/utils/helper"
|
||||
import Component_404 from "@armco/shared-components/Component_404"
|
||||
import BuilderSlot from "./BuilderSlot"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { ArSlotViewMode } from "@armco/shared-components/enums"
|
||||
import * as Components from "@armco/shared-components"
|
||||
import Layout from "./Layout"
|
||||
import BuilderSlot from "./BuilderSlot"
|
||||
import ReleaseSlot from "./ReleaseSlot"
|
||||
import SlotDescriptor from "./models/SlotDescriptor"
|
||||
import { useLayoutService } from "./Layout.context"
|
||||
import type { ISlotDescriptor, SlotProps } from "./types"
|
||||
import "./Slot.component.scss"
|
||||
|
||||
const Slot = (props: SlotProps): JSX.Element => {
|
||||
const { mode = ArSlotViewMode.PREVIEW, layoutMode = "preview" } = props
|
||||
const [content, setContent] = useState<ReactNode>()
|
||||
const config = props.config
|
||||
const Slot: React.FC<SlotProps> = (props) => {
|
||||
const { layoutMode = "preview", slotRenderer } = props
|
||||
const [content, setContent] = useState<any>()
|
||||
const slotDescriptor = props.slotDescriptor
|
||||
const layoutService = useLayoutService()
|
||||
const mode = layoutService.mode as any
|
||||
|
||||
useEffect(() => {
|
||||
const content = config.content as {
|
||||
componentName?: string
|
||||
source?: ArComponentResources
|
||||
descriptor?: { content: Array<SlotDescriptor> }
|
||||
props?: { [key: string]: any }
|
||||
}
|
||||
if (content) {
|
||||
if (content.componentName) {
|
||||
// const source = content.source || ArComponentResources.STUFFLE
|
||||
// TODO: In Stuffle use component's source property to specify sources like @armco/icon, @armco/Calendar etc. and use them here
|
||||
const Component = lazyImport(content.componentName, () => (
|
||||
<Component_404 />
|
||||
))
|
||||
// repo[content.componentName]
|
||||
const contentProps = content.props || {}
|
||||
if (content.componentName === "Layout") {
|
||||
contentProps.displayMode = config.props?.containerDisplayMode
|
||||
}
|
||||
setContent(
|
||||
<Component
|
||||
demo={mode === ArSlotViewMode.BUILD}
|
||||
activeId={props.lastSelected?.slot}
|
||||
{...{ ...(contentProps || {}) }}
|
||||
/>,
|
||||
)
|
||||
} else if (
|
||||
content.descriptor?.content &&
|
||||
Array.isArray(content.descriptor.content)
|
||||
) {
|
||||
const gaResponse =
|
||||
content?.descriptor?.content &&
|
||||
generateGridAreaAndSizes(content?.descriptor?.content, "auto")
|
||||
// Local registry for known local components usable by descriptor.componentName
|
||||
const localRegistry: Record<string, React.ComponentType<any>> = {
|
||||
Layout,
|
||||
}
|
||||
|
||||
const layout =
|
||||
gaResponse && generateGridTemplate(gaResponse.gridArea, "auto")
|
||||
const renderInlineSlot = (desc?: ISlotDescriptor) => {
|
||||
if (!(desc && (desc as any).slot)) {
|
||||
console.warn(
|
||||
"Missing slot ID on trying to create inline slot, Slot descriptor will not be processed:",
|
||||
desc,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
const nextProps =
|
||||
mode === ArSlotViewMode.BUILD
|
||||
? { ...props, slotDescriptor: desc }
|
||||
: { slotDescriptor: desc, containerDisplayMode: props.containerDisplayMode }
|
||||
return slotRenderer ? slotRenderer(nextProps) : (
|
||||
<Slot {...(nextProps as any)} key={(desc as any).slot} />
|
||||
)
|
||||
}
|
||||
|
||||
setContent(
|
||||
<Layout
|
||||
displayMode={config.props?.containerDisplayMode}
|
||||
slots={content?.descriptor?.content}
|
||||
gridTemplate={layout}
|
||||
hideBuildModePaddings={props.hideBuildModePaddings}
|
||||
mode={layoutMode}
|
||||
isChild={true}
|
||||
onSlotSelect={props.onSlotSelect}
|
||||
lastSelected={props.lastSelected}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setContent(null)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
config.content,
|
||||
layoutMode,
|
||||
mode,
|
||||
props.lastSelected,
|
||||
props.hideBuildModePaddings,
|
||||
])
|
||||
const resolveComponent = (data: any): React.ComponentType<any> | null => {
|
||||
if (data && data.componentName) {
|
||||
let C: any = (Components as any)[data.componentName]
|
||||
if (!C) C = localRegistry[data.componentName]
|
||||
return C || null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return mode !== ArSlotViewMode.PREVIEW && mode !== ArSlotViewMode.RELEASE ? (
|
||||
<BuilderSlot
|
||||
key={"builder-slot-" + props.config.slot}
|
||||
{...{ ...props, content }}
|
||||
/>
|
||||
) : (
|
||||
<ReleaseSlot
|
||||
key={"builder-slot-" + props.config.slot}
|
||||
{...{ ...props, content }}
|
||||
/>
|
||||
)
|
||||
const renderDescriptor = (data: any) => {
|
||||
const C = resolveComponent(data)
|
||||
if (C) {
|
||||
const passedProps: any = data.props || {}
|
||||
if (data.componentName === "Layout") {
|
||||
passedProps.displayMode = (slotDescriptor as any).containerDisplayMode
|
||||
}
|
||||
return (
|
||||
<C demo={mode === ArSlotViewMode.BUILD} activeId={layoutService.activeSlot?.slot} {...(passedProps || {})} />
|
||||
)
|
||||
} else if (data?.descriptor?.content && Array.isArray(data.descriptor.content)) {
|
||||
return (
|
||||
<Layout
|
||||
displayMode={(slotDescriptor as any).containerDisplayMode}
|
||||
slots={data?.descriptor?.content}
|
||||
hideBuildModePaddings={props.hideBuildModePaddings}
|
||||
mode={layoutMode}
|
||||
isChild={true}
|
||||
onSlotSelect={props.onSlotSelect}
|
||||
activeSlot={layoutService.activeSlot as any}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const c = (slotDescriptor as any).content
|
||||
if (!c) return
|
||||
if (Array.isArray(c)) {
|
||||
const nodes = c.map((i: any) => {
|
||||
i = SlotDescriptor.create(i)
|
||||
return renderInlineSlot(i)
|
||||
})
|
||||
setContent(c.length > 0 ? nodes : [])
|
||||
} else {
|
||||
const node = renderDescriptor(c)
|
||||
setContent(node)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [slotDescriptor.content, layoutMode, layoutService.mode, layoutService.activeSlot, props.hideBuildModePaddings])
|
||||
|
||||
// Choose builder or release slot based on service mode
|
||||
return mode !== ArSlotViewMode.PREVIEW && mode !== ArSlotViewMode.RELEASE ? (
|
||||
<BuilderSlot {...props} content={content} key={`builder-slot-${slotDescriptor.slot}`} />
|
||||
) : (
|
||||
<ReleaseSlot {...props} content={content} key={`release-slot-${slotDescriptor.slot}`} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Slot
|
||||
|
||||
258
src/SlotTools.tsx
Executable file → Normal file
258
src/SlotTools.tsx
Executable file → Normal file
@@ -1,171 +1,103 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
ArPopoverSlots,
|
||||
ArSlotViewMode,
|
||||
SlotControlProps,
|
||||
SlotToolsProps,
|
||||
} from "@armco/types"
|
||||
import Tooltip from "@armco/shared-components/Tooltip"
|
||||
import { FC, useEffect, useState } from "react"
|
||||
import { ArSlotViewMode, ArPopoverSlots } from "@armco/shared-components/enums"
|
||||
import { Tooltip } from "@armco/shared-components"
|
||||
import { useLayoutContext } from "./Layout.context"
|
||||
import type { SlotToolsProps } from "./types"
|
||||
|
||||
const staticSlotTools: Array<SlotControlProps> = [
|
||||
{ class: "ar-Slot__overlay-btn" },
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-up d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell Above",
|
||||
},
|
||||
{ class: "ar-Slot__overlay-btn" },
|
||||
{ class: "ar-Slot__overlay-btn cursor-pointer delete", isToolSlotOnly: true },
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-left d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell Before",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot merge d-flex cursor-pointer",
|
||||
tooltip: "Merge at this cell",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-right d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell After",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{ class: "ar-Slot__overlay-btn", isGridOnly: true },
|
||||
{
|
||||
class: "ar-Slot__overlay-btn slot split-down d-flex cursor-pointer",
|
||||
tooltip: "Insert Cell Below",
|
||||
isGridOnly: true,
|
||||
},
|
||||
{ class: "ar-Slot__overlay-btn", isGridOnly: true },
|
||||
const DEFAULT_CONTROLS = [
|
||||
{ class: "ar-Slot__overlay-btn" },
|
||||
{ class: "ar-Slot__overlay-btn slot split-up d-flex cursor-pointer", tooltip: "Insert Cell Above" },
|
||||
{ class: "ar-Slot__overlay-btn" },
|
||||
{ class: "ar-Slot__overlay-btn cursor-pointer delete", isToolSlotOnly: true },
|
||||
{ class: "ar-Slot__overlay-btn slot split-left d-flex cursor-pointer", tooltip: "Insert Cell Before", isGridOnly: true },
|
||||
{ class: "ar-Slot__overlay-btn slot merge d-flex cursor-pointer", tooltip: "Merge at this cell", isGridOnly: true },
|
||||
{ class: "ar-Slot__overlay-btn slot split-right d-flex cursor-pointer", tooltip: "Insert Cell After", isGridOnly: true },
|
||||
{ class: "ar-Slot__overlay-btn", isGridOnly: true },
|
||||
{ class: "ar-Slot__overlay-btn slot split-down d-flex cursor-pointer", tooltip: "Insert Cell Below", isGridOnly: true },
|
||||
{ class: "ar-Slot__overlay-btn", isGridOnly: true },
|
||||
]
|
||||
|
||||
const SlotTools = (props: SlotToolsProps): JSX.Element => {
|
||||
const {
|
||||
config,
|
||||
controlsEnabled,
|
||||
demo,
|
||||
mode = ArSlotViewMode.PREVIEW,
|
||||
horizontalSplitHandler,
|
||||
mergeHandler,
|
||||
rowInsertHandler,
|
||||
columnInsertHandler,
|
||||
rowDeleteHandler,
|
||||
columnDeleteHandler,
|
||||
verticalSplitHandler,
|
||||
rowSelectHandler,
|
||||
columnSelectHandler,
|
||||
} = props
|
||||
const [slotTools, setSlotTools] =
|
||||
useState<Array<SlotControlProps>>(staticSlotTools)
|
||||
const SlotTools: FC<SlotToolsProps> = (props) => {
|
||||
const { slotDescriptor, controlsEnabled, demo, mode = ArSlotViewMode.PREVIEW } = props
|
||||
const { eventsService } = useLayoutContext()
|
||||
const [controls, setControls] = useState<any[]>(DEFAULT_CONTROLS)
|
||||
|
||||
useEffect(() => {
|
||||
const slotToolsClone: Array<SlotControlProps> = JSON.parse(
|
||||
JSON.stringify(slotTools),
|
||||
)
|
||||
if (
|
||||
mode === ArSlotViewMode.ROWCONTROLLER ||
|
||||
mode === ArSlotViewMode.COLCONTROLLER
|
||||
) {
|
||||
const isRowController = mode === ArSlotViewMode.ROWCONTROLLER
|
||||
const handler = isRowController ? rowInsertHandler : columnInsertHandler
|
||||
slotToolsClone[0].onClick = () =>
|
||||
controlsEnabled && handler && handler("before")
|
||||
slotToolsClone[0].tooltip = isRowController
|
||||
? "Insert Row Above"
|
||||
: "Insert Column Before"
|
||||
slotToolsClone[0].class.indexOf("slot") === -1 &&
|
||||
(slotToolsClone[0].class += " slot cursor-pointer")
|
||||
slotToolsClone[1].tooltip = ""
|
||||
slotToolsClone[2].onClick = () =>
|
||||
controlsEnabled && handler && handler("after")
|
||||
slotToolsClone[2].tooltip = isRowController
|
||||
? "Insert Row Below"
|
||||
: "Insert Column After"
|
||||
slotToolsClone[2].class.indexOf("slot") === -1 &&
|
||||
(slotToolsClone[2].class += " slot cursor-pointer")
|
||||
slotToolsClone[3].tooltip = isRowController
|
||||
? "Delete Row"
|
||||
: "Delete Column"
|
||||
slotToolsClone[3].onClick = isRowController
|
||||
? rowDeleteHandler
|
||||
: columnDeleteHandler
|
||||
slotToolsClone[3].onMouseEnter = isRowController
|
||||
? () => rowSelectHandler && rowSelectHandler(true)
|
||||
: () => columnSelectHandler && columnSelectHandler(true)
|
||||
slotToolsClone[3].onMouseLeave = isRowController
|
||||
? () => rowSelectHandler && rowSelectHandler(false)
|
||||
: () => columnSelectHandler && columnSelectHandler(false)
|
||||
} else {
|
||||
slotToolsClone[1].onClick = () =>
|
||||
controlsEnabled &&
|
||||
horizontalSplitHandler &&
|
||||
horizontalSplitHandler("before")
|
||||
slotToolsClone[4].onClick = () =>
|
||||
controlsEnabled &&
|
||||
verticalSplitHandler &&
|
||||
verticalSplitHandler("before")
|
||||
slotToolsClone[5].onClick = () =>
|
||||
controlsEnabled && mergeHandler && mergeHandler()
|
||||
slotToolsClone[6].onClick = () =>
|
||||
controlsEnabled && verticalSplitHandler && verticalSplitHandler("after")
|
||||
slotToolsClone[8].onClick = () =>
|
||||
controlsEnabled &&
|
||||
horizontalSplitHandler &&
|
||||
horizontalSplitHandler("after")
|
||||
}
|
||||
setSlotTools([...slotToolsClone])
|
||||
}, [
|
||||
controlsEnabled,
|
||||
config,
|
||||
horizontalSplitHandler,
|
||||
verticalSplitHandler,
|
||||
mergeHandler,
|
||||
])
|
||||
const isRowController = mode === ArSlotViewMode.ROWCONTROLLER
|
||||
const deleteHandler = eventsService[isRowController ? "deleteRow" : "deleteColumn"].bind(eventsService)
|
||||
const addHandler = eventsService[isRowController ? "addRow" : "addColumn"].bind(eventsService)
|
||||
const selectHandler = eventsService[isRowController ? "selectRow" : "selectColumn"].bind(eventsService)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-SlotTools position-absolute h-100 w-100 flex-wrap top-0 start-0 ${
|
||||
mode === ArSlotViewMode.ROWCONTROLLER ? "flex-column" : ""
|
||||
}`}
|
||||
>
|
||||
{slotTools
|
||||
.map(
|
||||
(slotTool, index) =>
|
||||
(!slotTool.isGridOnly || mode === ArSlotViewMode.BUILD) &&
|
||||
(!slotTool.isToolSlotOnly ||
|
||||
[
|
||||
ArSlotViewMode.ROWCONTROLLER,
|
||||
ArSlotViewMode.COLCONTROLLER,
|
||||
].indexOf(mode) > -1) && (
|
||||
<span
|
||||
className={slotTool.class}
|
||||
key={"slotTool-" + index}
|
||||
onClick={(e) => {
|
||||
slotTool.tooltip && e.stopPropagation()
|
||||
slotTool.onClick && slotTool.onClick()
|
||||
}}
|
||||
onMouseEnter={slotTool.onMouseEnter}
|
||||
onMouseLeave={slotTool.onMouseLeave}
|
||||
>
|
||||
{slotTool.tooltip ? (
|
||||
<Tooltip classes="w-100" demo={demo}>
|
||||
<span
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
className="d-flex h-100 w-100"
|
||||
/>
|
||||
<span slot={ArPopoverSlots.POPOVER}>
|
||||
{slotTool.tooltip}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={`${slotTool.class}`} />
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
)
|
||||
.filter((s) => s)}
|
||||
</div>
|
||||
)
|
||||
useEffect(() => {
|
||||
// deep clone
|
||||
const o = JSON.parse(JSON.stringify(controls))
|
||||
if (mode === ArSlotViewMode.ROWCONTROLLER || mode === ArSlotViewMode.COLCONTROLLER) {
|
||||
o[0].onClick = () => controlsEnabled && addHandler("before", undefined as any)
|
||||
o[0].tooltip = isRowController ? "Insert Row Above" : "Insert Column Before"
|
||||
if (o[0].class.indexOf("slot") === -1) o[0].class += " slot cursor-pointer"
|
||||
o[1].tooltip = ""
|
||||
o[2].onClick = () => controlsEnabled && addHandler("after", undefined as any)
|
||||
o[2].tooltip = isRowController ? "Insert Row Below" : "Insert Column After"
|
||||
if (o[2].class.indexOf("slot") === -1) o[2].class += " slot cursor-pointer"
|
||||
o[3].tooltip = isRowController ? "Delete Row" : "Delete Column"
|
||||
o[3].onClick = () => deleteHandler(true)
|
||||
o[3].onMouseEnter = () => selectHandler(slotDescriptor, true)
|
||||
o[3].onMouseLeave = () => selectHandler(slotDescriptor, false)
|
||||
} else {
|
||||
o[1].onClick = () => {
|
||||
eventsService.clickedSlot = slotDescriptor
|
||||
controlsEnabled && eventsService.splitHorizontalBefore()
|
||||
}
|
||||
o[4].onClick = () => {
|
||||
eventsService.clickedSlot = slotDescriptor
|
||||
controlsEnabled && eventsService.splitVerticalBefore()
|
||||
}
|
||||
o[5].onClick = () => {
|
||||
eventsService.clickedSlot = slotDescriptor
|
||||
controlsEnabled && eventsService.mergeHandler()
|
||||
}
|
||||
o[6].onClick = () => {
|
||||
eventsService.clickedSlot = slotDescriptor
|
||||
controlsEnabled && eventsService.splitVerticalAfter()
|
||||
}
|
||||
o[8].onClick = () => {
|
||||
eventsService.clickedSlot = slotDescriptor
|
||||
controlsEnabled && eventsService.splitHorizontalAfter()
|
||||
}
|
||||
}
|
||||
setControls([...o])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [controlsEnabled, slotDescriptor])
|
||||
|
||||
return (
|
||||
<div className={`ar-SlotTools position-absolute h-100 w-100 flex-wrap top-0 start-0 ${isRowController ? "flex-column" : ""}`}>
|
||||
{controls
|
||||
.map((o, idx) => (
|
||||
(!o.isGridOnly || mode === ArSlotViewMode.BUILD) &&
|
||||
(!o.isToolSlotOnly || [ArSlotViewMode.ROWCONTROLLER, ArSlotViewMode.COLCONTROLLER].indexOf(mode) > -1) && (
|
||||
<span
|
||||
className={o.class}
|
||||
key={`slotTool-${idx}`}
|
||||
onClick={(ev) => {
|
||||
if (o.tooltip) ev.stopPropagation()
|
||||
o.onClick && o.onClick()
|
||||
}}
|
||||
onMouseEnter={o.onMouseEnter}
|
||||
onMouseLeave={o.onMouseLeave}
|
||||
>
|
||||
{o.tooltip ? (
|
||||
<Tooltip classes="w-100" demo={demo as any}>
|
||||
<span slot={ArPopoverSlots.ANCHOR} className="d-flex h-100 w-100" />
|
||||
<span slot={ArPopoverSlots.POPOVER}>{o.tooltip}</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
))
|
||||
.filter(Boolean)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SlotTools
|
||||
|
||||
14
src/Test.tsx
Normal file
14
src/Test.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { DndProvider } from "react-dnd"
|
||||
import { HTML5Backend } from "react-dnd-html5-backend"
|
||||
import { TouchBackend } from "react-dnd-touch-backend"
|
||||
import { isMobile as checkMobile } from "@armco/utils/helper"
|
||||
import Layout from "./Layout"
|
||||
import "./test.scss"
|
||||
|
||||
const isMobile = checkMobile()
|
||||
const backend = isMobile ? TouchBackend : HTML5Backend
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
|
||||
root.render(<div className="flex-center h-100 w-100 container"><DndProvider backend={backend}><Layout /></DndProvider></div>)
|
||||
60
src/hooks/useLayoutInit.ts
Normal file
60
src/hooks/useLayoutInit.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useRef, useEffect } from "react"
|
||||
import LayoutService from "@services/Layout.service"
|
||||
import EventsService from "@services/Events.service"
|
||||
import HistoryService from "@services/History.service"
|
||||
import { UseLayoutInitProps, LayoutMode } from "../types"
|
||||
import type SlotDescriptor from "@models/SlotDescriptor"
|
||||
|
||||
export default function useLayoutInit(
|
||||
initProps: UseLayoutInitProps,
|
||||
mode: LayoutMode = "build",
|
||||
) {
|
||||
const historyRef = useRef<HistoryService<SlotDescriptor[]>>()
|
||||
const layoutRef = useRef<LayoutService>()
|
||||
const eventsRef = useRef<EventsService>()
|
||||
|
||||
if (mode === "build" && !historyRef.current)
|
||||
historyRef.current = new HistoryService<SlotDescriptor[]>()
|
||||
if (!layoutRef.current) {
|
||||
layoutRef.current = new LayoutService(
|
||||
{
|
||||
colWidths: initProps.colWidths,
|
||||
gridSpecs: initProps.gridSpecs,
|
||||
gridTemplate: initProps.gridTemplate,
|
||||
rowHeights: initProps.rowHeights,
|
||||
slots: initProps.slots,
|
||||
acceptTextOnClick: initProps.acceptTextOnClick,
|
||||
activeSlot: initProps.activeSlot,
|
||||
mode,
|
||||
},
|
||||
initProps.rerender,
|
||||
historyRef.current,
|
||||
initProps.onLayoutChange,
|
||||
)
|
||||
}
|
||||
if (mode === "build" && !eventsRef.current && historyRef.current) {
|
||||
eventsRef.current = new (EventsService as any)(
|
||||
layoutRef.current,
|
||||
historyRef.current,
|
||||
initProps.onSlotSelect,
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
mode === "build" &&
|
||||
eventsRef.current &&
|
||||
eventsRef.current.handleEvent
|
||||
) {
|
||||
const handler = (e: KeyboardEvent) => eventsRef.current?.handleEvent(e)
|
||||
document.addEventListener("keydown", handler)
|
||||
return () => document.removeEventListener("keydown", handler)
|
||||
}
|
||||
}, [mode])
|
||||
|
||||
return {
|
||||
eventsService: eventsRef.current,
|
||||
layoutService: layoutRef.current,
|
||||
historyService: historyRef.current,
|
||||
}
|
||||
}
|
||||
14
src/index.ts
Executable file → Normal file
14
src/index.ts
Executable file → Normal file
@@ -1,7 +1,17 @@
|
||||
export { default } from "./Layout"
|
||||
export { default as Layout } from "./Layout"
|
||||
export { default as BuilderLayout } from "./BuilderLayout"
|
||||
export { default as ReleaseLayout } from "./ReleaseLayout"
|
||||
|
||||
export { default as Slot } from "./Slot"
|
||||
export { default as BuilderSlot } from "./BuilderSlot"
|
||||
export { default as ReleaseSlot } from "./ReleaseSlot"
|
||||
export { default as Resizable } from "./Resizable"
|
||||
export { default as SlotTools } from "./SlotTools"
|
||||
|
||||
export { default as Resizable } from "./Resizable"
|
||||
|
||||
export { default as ComponentInfo } from "./models/ComponentInfo"
|
||||
|
||||
export { default as useLayoutInit } from "./hooks/useLayoutInit"
|
||||
export { default as LayoutProvider } from "./Layout.context"
|
||||
|
||||
export * from "./types"
|
||||
|
||||
29
src/models/ComponentInfo.tsx
Normal file
29
src/models/ComponentInfo.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ArComponentResources } from "@armco/shared-components/enums"
|
||||
import { IComponentInfo, ISlotDescriptor } from "../types"
|
||||
|
||||
interface Descriptor {
|
||||
content: Array<ISlotDescriptor>
|
||||
layout: string
|
||||
}
|
||||
|
||||
class ComponentInfo implements IComponentInfo {
|
||||
[key: string]: any
|
||||
componentName: string
|
||||
source?: ArComponentResources
|
||||
descriptor?: Descriptor
|
||||
props?: { [key: string]: any }
|
||||
|
||||
constructor(
|
||||
componentName: string,
|
||||
source?: ArComponentResources,
|
||||
descriptor?: Descriptor,
|
||||
props?: { [key: string]: any }
|
||||
) {
|
||||
this.componentName = componentName
|
||||
this.source = source
|
||||
this.descriptor = descriptor
|
||||
this.props = props
|
||||
}
|
||||
}
|
||||
|
||||
export default ComponentInfo
|
||||
130
src/models/LayoutDescriptor.ts
Normal file
130
src/models/LayoutDescriptor.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { FunctionType } from "@armco/types"
|
||||
import { IGridSpecs, ISlotDescriptor, LayoutProps } from "../types"
|
||||
import SlotDescriptor from "./SlotDescriptor"
|
||||
|
||||
class LayoutDescriptor implements LayoutProps {
|
||||
acceptTextOnClick?: boolean
|
||||
colWidths?: string | Array<string>
|
||||
displayMode?:
|
||||
| "grid"
|
||||
| "inline"
|
||||
| "inline-flex"
|
||||
| "inline-grid"
|
||||
| "flex"
|
||||
| "inline-block"
|
||||
| "block"
|
||||
| "table"
|
||||
hideBuildModePaddings?: boolean
|
||||
gridSpecs?: string | IGridSpecs
|
||||
gridTemplate: string = ""
|
||||
isChild?: boolean
|
||||
activeSlot?: SlotDescriptor
|
||||
mode?: "build" | "preview" | "release"
|
||||
rowHeights?: string | Array<string>
|
||||
onLayoutChange?: FunctionType
|
||||
slotDropHandler?: FunctionType
|
||||
onSlotSelect?: FunctionType
|
||||
showControls?: boolean
|
||||
showPanels?: boolean
|
||||
slotRenderer?: FunctionType
|
||||
slots: Array<SlotDescriptor> = []
|
||||
url?: string
|
||||
|
||||
setAcceptTextOnClick(value: boolean): this {
|
||||
this.acceptTextOnClick = value
|
||||
return this
|
||||
}
|
||||
setColWidths(value: string | Array<string>): this {
|
||||
this.colWidths = value
|
||||
return this
|
||||
}
|
||||
setDisplayMode(
|
||||
value:
|
||||
| "grid"
|
||||
| "inline"
|
||||
| "inline-flex"
|
||||
| "inline-grid"
|
||||
| "flex"
|
||||
| "inline-block"
|
||||
| "block"
|
||||
| "table",
|
||||
): this {
|
||||
this.displayMode = value
|
||||
return this
|
||||
}
|
||||
setHideBuildModePaddings(value: boolean): this {
|
||||
this.hideBuildModePaddings = value
|
||||
return this
|
||||
}
|
||||
setGridSpecs(value: string | IGridSpecs): this {
|
||||
this.gridSpecs = value
|
||||
return this
|
||||
}
|
||||
setGridTemplate(value: string): this {
|
||||
this.gridTemplate = value
|
||||
return this
|
||||
}
|
||||
setIsChild(value: boolean): this {
|
||||
this.isChild = value
|
||||
return this
|
||||
}
|
||||
setActiveSlot(value: ISlotDescriptor): this {
|
||||
this.activeSlot = value ? SlotDescriptor.create(value) : undefined
|
||||
return this
|
||||
}
|
||||
setMode(value: "build" | "preview" | "release"): this {
|
||||
this.mode = value
|
||||
return this
|
||||
}
|
||||
setRowHeights(value: string | Array<string>): this {
|
||||
this.rowHeights = value
|
||||
return this
|
||||
}
|
||||
setOnLayoutChange(value: FunctionType): this {
|
||||
this.onLayoutChange = value
|
||||
return this
|
||||
}
|
||||
setSlotDropHandler(value: FunctionType): this {
|
||||
this.slotDropHandler = value
|
||||
return this
|
||||
}
|
||||
setOnSlotSelect(value: FunctionType): this {
|
||||
this.onSlotSelect = value
|
||||
return this
|
||||
}
|
||||
setShowControls(value: boolean): this {
|
||||
this.showControls = value
|
||||
return this
|
||||
}
|
||||
setShowPanels(value: boolean): this {
|
||||
this.showPanels = value
|
||||
return this
|
||||
}
|
||||
setSlotRenderer(value: FunctionType): this {
|
||||
this.slotRenderer = value
|
||||
return this
|
||||
}
|
||||
setSlots(value: Array<ISlotDescriptor>): this {
|
||||
this.slots = value
|
||||
? value.map((s) => s && SlotDescriptor.create(s)).filter((s) => s)
|
||||
: []
|
||||
return this
|
||||
}
|
||||
setUrl(value: string): this {
|
||||
this.url = value
|
||||
return this
|
||||
}
|
||||
static create(props: LayoutProps): LayoutDescriptor {
|
||||
const instance = new LayoutDescriptor()
|
||||
Object.keys(props).forEach((key) => {
|
||||
const value = (props as any)[key]
|
||||
const setter = `set${key.charAt(0).toUpperCase()}${key.slice(1)}`
|
||||
if (typeof (instance as any)[setter] === "function") {
|
||||
;(instance as any)[setter](value)
|
||||
}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
export default LayoutDescriptor
|
||||
134
src/models/SlotDescriptor.ts
Normal file
134
src/models/SlotDescriptor.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ISlotDescriptor, IComponentInfo, LayoutProps } from "../types"
|
||||
import LayoutError, { LayoutErrors } from "../LayoutError"
|
||||
|
||||
export interface SlotDescriptorProps extends ISlotDescriptor {}
|
||||
|
||||
export default class SlotDescriptor implements ISlotDescriptor {
|
||||
colSpan: number = 1
|
||||
column: number = -1
|
||||
gridArea: string = ""
|
||||
row: number = -1
|
||||
rowSpan: number = 1
|
||||
slot: string
|
||||
content?: Array<ISlotDescriptor> | IComponentInfo
|
||||
props?: {
|
||||
[key: string]: any
|
||||
containerDisplayMode?: LayoutProps["displayMode"]
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
splitFrom?: string
|
||||
isSelected?: boolean
|
||||
isSelectedForInlineDelete?: boolean
|
||||
|
||||
constructor(
|
||||
slot: string,
|
||||
content?: Array<ISlotDescriptor> | IComponentInfo,
|
||||
props?: {
|
||||
[key: string]: any
|
||||
containerDisplayMode?: LayoutProps["displayMode"]
|
||||
style?: React.CSSProperties
|
||||
},
|
||||
) {
|
||||
this.slot = slot
|
||||
this.content = content
|
||||
this.props = props
|
||||
}
|
||||
|
||||
setColSpan(colSpan: number): this {
|
||||
this.colSpan = colSpan
|
||||
return this
|
||||
}
|
||||
setColumn(column: number): this {
|
||||
this.column = column
|
||||
return this
|
||||
}
|
||||
setRow(row: number): this {
|
||||
this.row = row
|
||||
return this
|
||||
}
|
||||
setRowSpan(rowSpan: number): this {
|
||||
this.rowSpan = rowSpan
|
||||
return this
|
||||
}
|
||||
setGridArea(gridArea: string): this {
|
||||
this.gridArea = gridArea
|
||||
return this
|
||||
}
|
||||
setSplitFrom(splitFrom?: string): this {
|
||||
this.splitFrom = splitFrom ?? this.splitFrom
|
||||
return this
|
||||
}
|
||||
setIsSelected(isSelected?: boolean): this {
|
||||
this.isSelected = isSelected ?? this.isSelected
|
||||
return this
|
||||
}
|
||||
setIsSelectedForInlineDelete(isSelectedForInlineDelete?: boolean): this {
|
||||
this.isSelectedForInlineDelete =
|
||||
isSelectedForInlineDelete ?? this.isSelectedForInlineDelete
|
||||
return this
|
||||
}
|
||||
setProps(props?: {
|
||||
[key: string]: any
|
||||
containerDisplayMode?: LayoutProps["displayMode"]
|
||||
style?: React.CSSProperties
|
||||
}): this {
|
||||
this.props = props
|
||||
return this
|
||||
}
|
||||
setContent(content?: Array<ISlotDescriptor> | IComponentInfo): this {
|
||||
this.content = content
|
||||
return this
|
||||
}
|
||||
setStyle(style: React.CSSProperties): this {
|
||||
if (this.props && this.props.style) {
|
||||
this.props.style = { ...this.props.style, ...style }
|
||||
} else {
|
||||
this.props = { ...(this.props || {}), style }
|
||||
}
|
||||
return this
|
||||
}
|
||||
setClasses(classes: string): this {
|
||||
if (this.props) {
|
||||
this.props.classes = classes
|
||||
} else {
|
||||
this.props = { classes }
|
||||
}
|
||||
return this
|
||||
}
|
||||
get containerDisplayMode(): LayoutProps["displayMode"] | undefined {
|
||||
return this.props?.containerDisplayMode
|
||||
}
|
||||
static cloneOne(slotDescriptor: SlotDescriptor): SlotDescriptor {
|
||||
return SlotDescriptor.create({ ...slotDescriptor })
|
||||
}
|
||||
static clone(slotDescriptors: Array<SlotDescriptor>): SlotDescriptor[] {
|
||||
return slotDescriptors.map((e) => SlotDescriptor.cloneOne(e))
|
||||
}
|
||||
static create(args: ISlotDescriptor): SlotDescriptor {
|
||||
if (!args)
|
||||
throw new LayoutError(
|
||||
"Missing SlotDescriptor object",
|
||||
LayoutErrors.MISSING_SLOT_DESCRIPTOR_OBJ,
|
||||
"SlotDescriptor.create: Failed to create SlotDescriptor as args is missing",
|
||||
)
|
||||
if (!args.slot)
|
||||
throw new LayoutError(
|
||||
"Missing Slot ID",
|
||||
LayoutErrors.MISSING_SLOT_ID,
|
||||
"SlotDescriptor.create: Failed to create SlotDescriptor as args.slot is missing",
|
||||
)
|
||||
if (args instanceof SlotDescriptor) return args
|
||||
const e = new SlotDescriptor(args.slot, args.content, args.props)
|
||||
return e
|
||||
.setColSpan(args.colSpan ?? e.colSpan)
|
||||
.setColumn(args.column ?? e.column)
|
||||
.setRow(args.row ?? e.row)
|
||||
.setRowSpan(args.rowSpan ?? e.rowSpan)
|
||||
.setGridArea(args.gridArea ?? e.gridArea)
|
||||
.setSplitFrom(args.splitFrom ?? e.splitFrom)
|
||||
.setIsSelected(args.isSelected ?? e.isSelected)
|
||||
.setIsSelectedForInlineDelete(
|
||||
args.isSelectedForInlineDelete ?? e.isSelectedForInlineDelete,
|
||||
)
|
||||
}
|
||||
}
|
||||
3
src/models/index.ts
Normal file
3
src/models/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ComponentInfo } from "./ComponentInfo"
|
||||
export { default as LayoutDescriptor } from "./LayoutDescriptor"
|
||||
export { default as SlotDescriptor } from "./SlotDescriptor"
|
||||
71
src/react-app-env.d.ts
vendored
71
src/react-app-env.d.ts
vendored
@@ -1,71 +0,0 @@
|
||||
/// <reference types="node" />
|
||||
/// <reference types="react" />
|
||||
/// <reference types="react-dom" />
|
||||
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
readonly NODE_ENV: "development" | "production" | "test"
|
||||
readonly PUBLIC_URL: string
|
||||
}
|
||||
}
|
||||
|
||||
declare module "*.avif" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.bmp" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.gif" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.jpg" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.jpeg" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.webp" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
import * as React from "react"
|
||||
|
||||
export const ReactComponent: React.FunctionComponent<React.SVGProps<
|
||||
SVGSVGElement
|
||||
> & { title?: string }>
|
||||
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.module.css" {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module "*.module.scss" {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
|
||||
declare module "*.module.sass" {
|
||||
const classes: { readonly [key: string]: string }
|
||||
export default classes
|
||||
}
|
||||
796
src/services/Events.service.ts
Normal file
796
src/services/Events.service.ts
Normal file
@@ -0,0 +1,796 @@
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { FunctionType } from "@armco/types"
|
||||
import { ArComponentResources } from "@armco/shared-components/enums"
|
||||
import { SlotDescriptor } from "../models"
|
||||
import LayoutService from "./Layout.service"
|
||||
import HistoryService from "./History.service"
|
||||
|
||||
/**
|
||||
* EventsService wires user interactions (keyboard and toolbar actions)
|
||||
* to Layout state mutations (split/merge/insert/remove/select and undo/redo).
|
||||
*
|
||||
* Stage 1: This is a strongly-typed scaffold based on the generated .d.ts.
|
||||
* Stage 2 will fill in implementations aligned with build/es/Events.service.js.
|
||||
*/
|
||||
class EventsService {
|
||||
layoutService: LayoutService
|
||||
historyService: HistoryService<Array<SlotDescriptor>>
|
||||
onSlotSelectCB?: FunctionType
|
||||
clickedSlot: SlotDescriptor | null = null
|
||||
|
||||
constructor(
|
||||
layoutService: LayoutService,
|
||||
historyService: HistoryService<Array<SlotDescriptor>>,
|
||||
onSlotSelectCB?: FunctionType,
|
||||
) {
|
||||
this.layoutService = layoutService
|
||||
this.historyService = historyService
|
||||
this.onSlotSelectCB = onSlotSelectCB
|
||||
|
||||
// Bind methods to preserve `this` when used as callbacks
|
||||
this.handleEvent = this.handleEvent.bind(this)
|
||||
this.call = this.call.bind(this)
|
||||
this.undo = this.undo.bind(this)
|
||||
this.redo = this.redo.bind(this)
|
||||
this.splitCell = this.splitCell.bind(this)
|
||||
this.mergeCells = this.mergeCells.bind(this)
|
||||
this.onSlotSelect = this.onSlotSelect.bind(this)
|
||||
this.onDimSelect = this.onDimSelect.bind(this)
|
||||
this.addRowBefore = this.addRowBefore.bind(this)
|
||||
this.addRowAfter = this.addRowAfter.bind(this)
|
||||
this.deleteRowInline = this.deleteRowInline.bind(this)
|
||||
this.deleteRowNormal = this.deleteRowNormal.bind(this)
|
||||
this.addColumnBefore = this.addColumnBefore.bind(this)
|
||||
this.addColumnAfter = this.addColumnAfter.bind(this)
|
||||
this.deleteColumnInline = this.deleteColumnInline.bind(this)
|
||||
this.deleteColumnNormal = this.deleteColumnNormal.bind(this)
|
||||
this.splitHorizontalBefore = this.splitHorizontalBefore.bind(this)
|
||||
this.splitHorizontalAfter = this.splitHorizontalAfter.bind(this)
|
||||
this.splitVerticalBefore = this.splitVerticalBefore.bind(this)
|
||||
this.splitVerticalAfter = this.splitVerticalAfter.bind(this)
|
||||
this.splitHorizontal = this.splitHorizontal.bind(this)
|
||||
this.splitVertical = this.splitVertical.bind(this)
|
||||
this.splitHandler = this.splitHandler.bind(this)
|
||||
this.mergeHandler = this.mergeHandler.bind(this)
|
||||
this.addRow = this.addRow.bind(this)
|
||||
this.deleteRow = this.deleteRow.bind(this)
|
||||
this.addColumn = this.addColumn.bind(this)
|
||||
this.deleteColumn = this.deleteColumn.bind(this)
|
||||
this.selectSlot = this.selectSlot.bind(this)
|
||||
this.selectRow = this.selectRow.bind(this)
|
||||
this.selectColumn = this.selectColumn.bind(this)
|
||||
this.insertDimension = this.insertDimension.bind(this)
|
||||
this.removeHandler = this.removeHandler.bind(this)
|
||||
this.findIntersectingSlots = this.findIntersectingSlots.bind(this)
|
||||
this.generateInsertionIndexes = this.generateInsertionIndexes.bind(this)
|
||||
this.selectCellsInSelectedRowCol =
|
||||
this.selectCellsInSelectedRowCol.bind(this)
|
||||
}
|
||||
|
||||
// ======================= Event Handler Wrappers =======================
|
||||
/**
|
||||
* Registers keyboard events for undo, redo, merge, split, and add row/column actions.
|
||||
* This function listens for specific key combinations (Ctrl/Cmd + Z/Y/M/H/V/R/C)
|
||||
* and triggers the corresponding actions in the layout.
|
||||
* @param event
|
||||
*/
|
||||
handleEvent(event: KeyboardEvent): void {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (event.key === "z") {
|
||||
event.preventDefault()
|
||||
this.undo()
|
||||
} else if (event.key === "y") {
|
||||
event.preventDefault()
|
||||
this.redo()
|
||||
} else if (event.key === "m") {
|
||||
this.mergeHandler()
|
||||
} else if (this.layoutService.splitEnabled) {
|
||||
if (event.key === "h") this.splitHorizontal()
|
||||
else if (event.key === "v") this.splitVertical()
|
||||
else if (event.key === "r") this.addRow()
|
||||
else if (event.key === "c") this.addColumn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a generic event handler with optional pre and post hooks.
|
||||
* This generic event handler that calls one of the other event handlers in this class
|
||||
*
|
||||
* @param eventHandler
|
||||
* @param preHook
|
||||
* @param postHook
|
||||
* @param args
|
||||
* @returns
|
||||
*/
|
||||
call(
|
||||
func: keyof EventsService,
|
||||
preHook?: FunctionType,
|
||||
postHook?: FunctionType,
|
||||
...args: any[]
|
||||
): any {
|
||||
if (preHook) preHook()
|
||||
// @ts-expect-error - index access
|
||||
const result = this[func](...args)
|
||||
if (postHook) postHook()
|
||||
return result
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
const slots = this.historyService.undo()
|
||||
this.layoutService.updateSlots(slots || [], true)
|
||||
}
|
||||
|
||||
redo(): void {
|
||||
const slots = this.historyService.redo()
|
||||
this.layoutService.updateSlots(slots || [], true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new row before the specified selection.
|
||||
* @param selection - An optional selection index.
|
||||
*/
|
||||
addRowBefore(selection?: number): SlotDescriptor[] | undefined {
|
||||
return this.addRow("before", selection)
|
||||
}
|
||||
/**
|
||||
* Adds a new row after the specified selection.
|
||||
* @param selection - An optional selection index.
|
||||
*/
|
||||
addRowAfter(selection?: number): SlotDescriptor[] | undefined {
|
||||
return this.addRow("after", selection)
|
||||
}
|
||||
/**
|
||||
* Deletes a row inline.
|
||||
* @returns The updated slots array or null.
|
||||
*/
|
||||
deleteRowInline(): Array<SlotDescriptor> | null {
|
||||
return this.deleteRow(true)
|
||||
}
|
||||
/**
|
||||
* Deletes a row normally.
|
||||
* @returns The updated slots array or null.
|
||||
*/
|
||||
deleteRowNormal(): Array<SlotDescriptor> | null {
|
||||
return this.deleteRow(false)
|
||||
}
|
||||
/**
|
||||
* Adds a new column before the specified selection.
|
||||
* @param selection - An optional selection index.
|
||||
*/
|
||||
addColumnBefore(selection?: number): SlotDescriptor[] | undefined {
|
||||
return this.addColumn("before", selection)
|
||||
}
|
||||
/**
|
||||
* Adds a new column after the specified selection.
|
||||
* @param selection - An optional selection index.
|
||||
*/
|
||||
addColumnAfter(selection?: number): SlotDescriptor[] | undefined {
|
||||
return this.addColumn("after", selection)
|
||||
}
|
||||
/**
|
||||
* Deletes a column inline.
|
||||
* @returns The updated slots array or null.
|
||||
*/
|
||||
deleteColumnInline(): Array<SlotDescriptor> | null {
|
||||
return this.deleteColumn(true)
|
||||
}
|
||||
/**
|
||||
* Deletes a column normally.
|
||||
* @returns The updated slots array or null.
|
||||
*/
|
||||
deleteColumnNormal(): Array<SlotDescriptor> | null {
|
||||
return this.deleteColumn(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new row to the layout.
|
||||
* @param placement - The placement of the new row ("before" or "after").
|
||||
* @param selection - An optional selection index.
|
||||
*/
|
||||
addRow(
|
||||
placement: "before" | "after" = "after",
|
||||
selection?: number,
|
||||
): SlotDescriptor[] | undefined {
|
||||
return (
|
||||
this.layoutService.gridToolbarSpecs &&
|
||||
this.insertDimension("row", placement, selection)
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Deletes a row from the layout.
|
||||
* @param isInlineDelete - A boolean to indicate whether it is an inline delete.
|
||||
* @returns The updated slots array or null.
|
||||
*/
|
||||
deleteRow(isInlineDelete?: boolean): Array<SlotDescriptor> | null {
|
||||
return (
|
||||
(this.layoutService.gridToolbarSpecs &&
|
||||
this.removeHandler("row", isInlineDelete)) ||
|
||||
null
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Adds a new column to the layout.
|
||||
* @param placement - The placement of the new column ("before" or "after").
|
||||
* @param selection - An optional selection index.
|
||||
*/
|
||||
addColumn(
|
||||
placement: "before" | "after" = "after",
|
||||
selection?: number,
|
||||
): SlotDescriptor[] | undefined {
|
||||
return (
|
||||
this.layoutService.gridToolbarSpecs &&
|
||||
this.insertDimension("column", placement, selection)
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Deletes a column from the layout.
|
||||
* @param isInlineDelete - A boolean to indicate whether it is an inline delete.
|
||||
* @returns The updated slots array or null.
|
||||
*/
|
||||
deleteColumn(isInlineDelete?: boolean): Array<SlotDescriptor> | null {
|
||||
return (
|
||||
(this.layoutService.gridToolbarSpecs &&
|
||||
this.removeHandler("column", isInlineDelete)) ||
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a cell based on orientation and placement.
|
||||
* @param orientation - The orientation of the split ("horizontal" or "vertical").
|
||||
* @param placement - The placement of the split ("before" or "after").
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
splitCell(
|
||||
orientation: "horizontal" | "vertical",
|
||||
placement: "before" | "after",
|
||||
): SlotDescriptor[] {
|
||||
return this.splitHandler(orientation, placement)
|
||||
}
|
||||
/**
|
||||
* Merges selected cells into a primary cell configuration.
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
mergeCells(): SlotDescriptor[] | undefined {
|
||||
return this.mergeHandler()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles slot selection.
|
||||
* @param slotDescriptor - The slot descriptor to select.
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
selectSlot(slotDescriptor: SlotDescriptor): SlotDescriptor[] {
|
||||
return this.onSlotSelect(slotDescriptor)
|
||||
}
|
||||
/**
|
||||
* Handles row selection.
|
||||
* @param slotDescriptor - The slot descriptor to select.
|
||||
* @param selectForDelete - A boolean to indicate whether to select for delete.
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
selectRow(
|
||||
slotDescriptor: SlotDescriptor,
|
||||
selectForDelete?: boolean,
|
||||
): SlotDescriptor[] | undefined {
|
||||
return this.onDimSelect(slotDescriptor, "row", selectForDelete)
|
||||
}
|
||||
/**
|
||||
* Handles column selection.
|
||||
* @param slotDescriptor - The slot descriptor to select.
|
||||
* @param selectForDelete - A boolean to indicate whether to select for delete.
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
selectColumn(
|
||||
slotDescriptor: SlotDescriptor,
|
||||
selectForDelete?: boolean,
|
||||
): SlotDescriptor[] | undefined {
|
||||
return this.onDimSelect(slotDescriptor, "column", selectForDelete)
|
||||
}
|
||||
|
||||
/** Splits a slot horizontally and places new slot before the one being split */
|
||||
splitHorizontalBefore(): SlotDescriptor[] {
|
||||
return this.splitHandler("horizontal", "before")
|
||||
}
|
||||
/** Splits a slot horizontally and places new slot after the one being split */
|
||||
splitHorizontalAfter(): SlotDescriptor[] {
|
||||
return this.splitHandler("horizontal", "after")
|
||||
}
|
||||
/** Splits a slot vertically and places new slot before the one being split */
|
||||
splitVerticalBefore(): SlotDescriptor[] {
|
||||
return this.splitHandler("vertical", "before")
|
||||
}
|
||||
/** Splits a slot vertically and places new slot after the one being split */
|
||||
splitVerticalAfter(): SlotDescriptor[] {
|
||||
return this.splitHandler("vertical", "after")
|
||||
}
|
||||
/** Split slot horizontally */
|
||||
splitHorizontal(placement: "before" | "after" = "after"): SlotDescriptor[] {
|
||||
return this.splitHandler("horizontal", placement)
|
||||
}
|
||||
/** Split slot vertically */
|
||||
splitVertical(placement: "before" | "after" = "after"): SlotDescriptor[] {
|
||||
return this.splitHandler("vertical", placement)
|
||||
}
|
||||
|
||||
// ================= Event Primaries/Juices =================
|
||||
/**
|
||||
* Inserts a new row or column dimension into the grid.
|
||||
* @param type - The type of dimension ("row" or "column").
|
||||
* @param placement - The placement of the new dimension ("before" or "after").
|
||||
* @param selection - An optional selection index.
|
||||
* @returns An updated slots array.
|
||||
*/
|
||||
insertDimension(
|
||||
type: "row" | "column",
|
||||
placement: "before" | "after",
|
||||
selection?: number,
|
||||
): SlotDescriptor[] {
|
||||
let slots: SlotDescriptor[] = [...this.layoutService.slots]
|
||||
const isRow = type === "row"
|
||||
const gts = this.layoutService.gridToolbarSpecs!
|
||||
const toolSlots = gts[isRow ? "colTools" : "rowTools"].slots
|
||||
let created: SlotDescriptor[] = []
|
||||
if (toolSlots.length > 0) {
|
||||
const insertionIndexes = this.generateInsertionIndexes(
|
||||
isRow,
|
||||
placement,
|
||||
selection,
|
||||
)
|
||||
insertionIndexes.sort()
|
||||
const spanExtensions: Record<
|
||||
string,
|
||||
{ d: SlotDescriptor; count: number }
|
||||
> = {}
|
||||
insertionIndexes.forEach((idx, loopIdx) => {
|
||||
const selectedToolSlot =
|
||||
gts[isRow ? "rowTools" : "colTools"].slots[
|
||||
idx - loopIdx - (placement === "after" ? 1 : 0)
|
||||
]
|
||||
const intersectingSlots = this.findIntersectingSlots(
|
||||
selectedToolSlot,
|
||||
isRow,
|
||||
)
|
||||
const newOnes = toolSlots
|
||||
.map((toolSlot) => {
|
||||
const intersecting = intersectingSlots.find((i) =>
|
||||
this.isIntersecting(toolSlot, i, !isRow),
|
||||
)
|
||||
if (intersecting) {
|
||||
if (
|
||||
this.getCreateOrExtend(
|
||||
intersecting,
|
||||
selectedToolSlot,
|
||||
placement,
|
||||
isRow,
|
||||
)
|
||||
) {
|
||||
const newId = `gsa-${uuid()}`
|
||||
return SlotDescriptor.create({
|
||||
slot: newId,
|
||||
row: isRow ? idx : toolSlot.row,
|
||||
column: isRow ? toolSlot.column : idx,
|
||||
rowSpan: isRow ? 1 : toolSlot.rowSpan,
|
||||
colSpan: isRow ? toolSlot.colSpan : 1,
|
||||
gridArea: newId,
|
||||
})
|
||||
} else {
|
||||
if (spanExtensions[intersecting.slot])
|
||||
spanExtensions[intersecting.slot].count += 1
|
||||
else
|
||||
spanExtensions[intersecting.slot] = {
|
||||
d: intersecting,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((s): s is SlotDescriptor => !!s)
|
||||
created = created.concat(newOnes)
|
||||
})
|
||||
insertionIndexes.forEach((idx) => {
|
||||
const dim = isRow ? "row" : "column"
|
||||
slots.filter((s) => s[dim] >= idx).forEach((s) => s[dim]++)
|
||||
})
|
||||
Object.values(spanExtensions).forEach((ext) => {
|
||||
if (isRow) ext.d.rowSpan += ext.count
|
||||
else ext.d.colSpan += ext.count
|
||||
})
|
||||
} else {
|
||||
const id = `gsa-${crypto.randomUUID()}`
|
||||
created.push(
|
||||
SlotDescriptor.create({
|
||||
slot: id,
|
||||
row: 0,
|
||||
column: 0,
|
||||
rowSpan: 1,
|
||||
colSpan: 1,
|
||||
gridArea: id,
|
||||
}),
|
||||
)
|
||||
}
|
||||
slots = slots.concat(created)
|
||||
this.layoutService.updateSlots(slots)
|
||||
return slots
|
||||
}
|
||||
/**
|
||||
* Removes slots based on type and grid toolbar specifications.
|
||||
* @param type - The type of removal ("row" or "column").
|
||||
* @param isInlineDelete - A boolean to indicate whether it is an inline delete.
|
||||
* @returns The updated slots array or null.
|
||||
*/
|
||||
removeHandler(
|
||||
type: "row" | "column",
|
||||
isInlineDelete?: boolean,
|
||||
): Array<SlotDescriptor> | null {
|
||||
const slots: SlotDescriptor[] = [...this.layoutService.slots]
|
||||
const isRow = type === "row"
|
||||
const gts = this.layoutService.gridToolbarSpecs!
|
||||
const toolSlots = isRow ? gts.rowTools.slots : gts.colTools.slots
|
||||
toolSlots
|
||||
.filter((s) =>
|
||||
isInlineDelete ? s.isSelectedForInlineDelete : s.isSelected,
|
||||
)
|
||||
.sort((a, b) => (isRow ? a.row - b.row : a.column - b.column))
|
||||
.forEach((toolSlot) => {
|
||||
slots
|
||||
.filter((slot) =>
|
||||
isRow
|
||||
? toolSlot.row >= slot.row &&
|
||||
toolSlot.row < slot.row + slot.rowSpan
|
||||
: toolSlot.column >= slot.column &&
|
||||
toolSlot.column < slot.column + slot.colSpan,
|
||||
)
|
||||
.forEach((slot) => {
|
||||
if (
|
||||
isRow
|
||||
? slot.rowSpan <= toolSlot.rowSpan
|
||||
: slot.colSpan <= toolSlot.colSpan
|
||||
) {
|
||||
const idx = slots.indexOf(slot)
|
||||
if (idx !== -1) slots.splice(idx, 1)
|
||||
} else {
|
||||
if (isRow) slot.rowSpan -= 1
|
||||
else slot.colSpan -= 1
|
||||
}
|
||||
})
|
||||
let affected = slots.filter((slot) =>
|
||||
isRow ? slot.row > toolSlot.row : slot.column > toolSlot.column,
|
||||
)
|
||||
affected.forEach((slot) =>
|
||||
isRow ? (slot.row -= 1) : (slot.column -= 1),
|
||||
)
|
||||
affected = toolSlots.filter((slot) =>
|
||||
isRow ? slot.row > toolSlot.row : slot.column > toolSlot.column,
|
||||
)
|
||||
affected.forEach((slot) =>
|
||||
isRow ? (slot.row -= 1) : (slot.column -= 1),
|
||||
)
|
||||
toolSlots.splice(toolSlots.indexOf(toolSlot), 1)
|
||||
})
|
||||
this.layoutService.updateSlots(slots)
|
||||
return slots
|
||||
}
|
||||
/**
|
||||
* Merges selected slots into a primary slot configuration.
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
mergeHandler(): SlotDescriptor[] | undefined {
|
||||
if (this.layoutService.mergeEnabled) {
|
||||
let clicked = this.clickedSlot
|
||||
this.clickedSlot = null
|
||||
const slots = [...this.layoutService.slots]
|
||||
const selected = slots.filter((s) => s.isSelected)
|
||||
const minMax = this.layoutService.minMaxSelections
|
||||
selected.sort((a, b) =>
|
||||
a.row !== b.row ? a.row - b.row : a.column - b.column,
|
||||
)
|
||||
const first = selected[0]
|
||||
let primary = clicked
|
||||
? slots.find((s) => s.slot === clicked?.slot)
|
||||
: first
|
||||
if (!primary) return
|
||||
primary.row = first.row
|
||||
primary.column = first.column
|
||||
primary.rowSpan = minMax.maxRow - minMax.minRow + 1
|
||||
primary.colSpan = minMax.maxColumn - minMax.minColumn + 1
|
||||
selected.forEach((sel) => {
|
||||
const idx = slots.findIndex((s) => s.slot === sel.slot)
|
||||
const found = slots[idx]
|
||||
if (found && found.slot !== primary?.slot) slots.splice(idx, 1)
|
||||
})
|
||||
this.layoutService.mergeEnabled = false
|
||||
this.layoutService.updateSlots(slots)
|
||||
return slots
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Splits a slot based on orientation and placement.
|
||||
* @param orientation - The orientation of the split ("horizontal" or "vertical").
|
||||
* @param placement - The placement of the split ("before" or "after").
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
splitHandler(
|
||||
orientation: "horizontal" | "vertical",
|
||||
placement: "before" | "after",
|
||||
): SlotDescriptor[] {
|
||||
const slots = [...this.layoutService.slots]
|
||||
let targets: SlotDescriptor[] = []
|
||||
if (this.clickedSlot) {
|
||||
const found = slots.find((s) => s.slot === this.clickedSlot?.slot)
|
||||
if (found) targets.push(found)
|
||||
this.clickedSlot = null
|
||||
} else {
|
||||
targets = slots.filter((s) => s.isSelected)
|
||||
}
|
||||
targets.forEach((slot) => {
|
||||
const { row, slot: slotId, column, colSpan, rowSpan } = slot
|
||||
const isHorizontal = orientation === "horizontal"
|
||||
const isAfter = placement === "after"
|
||||
const newId = `gsa-${crypto.randomUUID()}`
|
||||
const spanProp = isHorizontal ? "rowSpan" : "colSpan"
|
||||
const dimProp = isHorizontal ? "row" : "column"
|
||||
let spanVal: number = slot[spanProp]
|
||||
if (spanVal > 1) {
|
||||
spanVal = slot[spanProp] - 1
|
||||
slot[spanProp] = spanVal
|
||||
}
|
||||
const newSlot = SlotDescriptor.create({
|
||||
slot: newId,
|
||||
row: isHorizontal && isAfter ? spanVal + row : row,
|
||||
column: isHorizontal ? column : isAfter ? spanVal + column : column,
|
||||
rowSpan: isHorizontal ? 1 : rowSpan,
|
||||
colSpan: isHorizontal ? colSpan : 1,
|
||||
gridArea: newId,
|
||||
splitFrom: slotId,
|
||||
})
|
||||
if (isHorizontal ? rowSpan === 1 : colSpan === 1) {
|
||||
slots.forEach((s) => {
|
||||
const {
|
||||
row: r,
|
||||
column: c,
|
||||
slot: sId,
|
||||
colSpan: sColSpan,
|
||||
rowSpan: sRowSpan,
|
||||
} = s
|
||||
if (isHorizontal) {
|
||||
if (row >= r && row < r + sRowSpan && sId !== slotId) s.rowSpan += 1
|
||||
else if (r > row) s.row += 1
|
||||
} else {
|
||||
if (column >= c && column < c + sColSpan && sId !== slotId)
|
||||
s.colSpan += 1
|
||||
else if (c > column) s.column += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!isAfter) slot[dimProp] += 1
|
||||
slots.push(newSlot)
|
||||
})
|
||||
this.layoutService.updateSlots(slots)
|
||||
return slots
|
||||
}
|
||||
/**
|
||||
* Handles slot selection.
|
||||
* @param slotDescriptor - The slot descriptor to select.
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
onSlotSelect(slotDescriptor: SlotDescriptor): SlotDescriptor[] {
|
||||
const slots = [...this.layoutService.slots]
|
||||
if (slotDescriptor) slotDescriptor.isSelected = !slotDescriptor.isSelected
|
||||
if (
|
||||
this.layoutService.acceptTextOnClick &&
|
||||
slotDescriptor &&
|
||||
!slotDescriptor?.content
|
||||
) {
|
||||
slotDescriptor.props ||= {}
|
||||
slotDescriptor.content = {
|
||||
name: "SamEditor",
|
||||
description: "Rich text editor",
|
||||
source: ArComponentResources.STUFFLE ?? "stuffle",
|
||||
componentName: "SamEditor",
|
||||
props: {
|
||||
pmOptions: { menuBar: false },
|
||||
classes: "w-100",
|
||||
id: slotDescriptor.slot,
|
||||
style: { minHeight: "1.5rem", lineHeight: "1.5rem" },
|
||||
isEditable: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
this.layoutService.activeSlot = slotDescriptor
|
||||
if (this.onSlotSelectCB) this.onSlotSelectCB(slotDescriptor)
|
||||
this.layoutService.updateSlots(slots, true)
|
||||
return slots
|
||||
}
|
||||
/**
|
||||
* Handles dimension selection.
|
||||
* @param slotDescriptor - The selected dimension descriptor.
|
||||
* @param type - "row" | "column".
|
||||
* @param selectForDelete - Whether to select for inline delete.
|
||||
* @returns The updated slots array.
|
||||
*/
|
||||
onDimSelect(
|
||||
slotDescriptor: SlotDescriptor,
|
||||
type: "row" | "column",
|
||||
selectForDelete?: boolean,
|
||||
): SlotDescriptor[] | undefined {
|
||||
const gts = this.layoutService.gridToolbarSpecs
|
||||
if (!gts) return
|
||||
const toolSlot = gts[type === "row" ? "rowTools" : "colTools"].slots.find(
|
||||
(s) => s.slot === slotDescriptor.slot,
|
||||
)
|
||||
if (toolSlot) {
|
||||
if (selectForDelete !== undefined)
|
||||
toolSlot.isSelectedForInlineDelete = selectForDelete
|
||||
else toolSlot.isSelected = !toolSlot.isSelected
|
||||
}
|
||||
const updated = this.selectCellsInSelectedRowCol(selectForDelete)
|
||||
this.layoutService.updateSlots(updated, true)
|
||||
return updated
|
||||
}
|
||||
|
||||
// ======================= Event Helpers =======================
|
||||
/**
|
||||
* Finds intersecting slots based on a tool slot and dimension.
|
||||
* @param toolSlot - The tool slot descriptor.
|
||||
* @param isRow - True if row, false if column.
|
||||
* @returns Intersecting slot descriptors.
|
||||
*/
|
||||
findIntersectingSlots(
|
||||
toolSlot: SlotDescriptor,
|
||||
isRow: boolean,
|
||||
): SlotDescriptor[] {
|
||||
return this.layoutService.slots.filter((slot) =>
|
||||
this.isIntersecting(toolSlot, slot, isRow),
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Generates insertion indexes for a new dimension.
|
||||
* @param isRow - Whether dimension is row.
|
||||
* @param placement - "before" | "after".
|
||||
* @param selection - Optional selected index.
|
||||
* @returns An array of insertion indexes.
|
||||
*/
|
||||
generateInsertionIndexes(
|
||||
isRow: boolean,
|
||||
placement: "before" | "after",
|
||||
selection?: number,
|
||||
): number[] {
|
||||
let indexes: number[] = []
|
||||
if (selection !== undefined)
|
||||
indexes.push(placement === "after" ? selection + 1 : selection)
|
||||
else
|
||||
indexes = this.generateInsertionIndexesForDimension(
|
||||
isRow ? "rowTools" : "colTools",
|
||||
isRow ? "row" : "column",
|
||||
placement,
|
||||
isRow,
|
||||
)
|
||||
indexes.sort()
|
||||
indexes = indexes.map((v, idx) => v + idx)
|
||||
return indexes
|
||||
}
|
||||
/**
|
||||
* Generates insertion indexes for a specific dimension.
|
||||
* @param tools - "rowTools" | "colTools".
|
||||
* @param dimension - "row" | "column".
|
||||
* @param placement - "before" | "after".
|
||||
* @param isRow - Whether dimension is row.
|
||||
*/
|
||||
generateInsertionIndexesForDimension(
|
||||
tools: "rowTools" | "colTools",
|
||||
dimension: "row" | "column",
|
||||
placement: "before" | "after",
|
||||
isRow: boolean,
|
||||
): number[] {
|
||||
let indexes: number[] = []
|
||||
const gts = this.layoutService.gridToolbarSpecs!
|
||||
const against = gts[isRow ? "rowTools" : "colTools"].slots
|
||||
const selected = gts[tools].slots
|
||||
.filter((s) => s.isSelected)
|
||||
.map((s) => s[dimension] + (placement === "before" ? 0 : 1))
|
||||
if (selected.length > 0) indexes = indexes.concat(selected)
|
||||
else if (placement === "before") indexes.push(0)
|
||||
else indexes.push(Math.max(...against.map((s) => s[dimension])) + 1)
|
||||
return indexes
|
||||
}
|
||||
/**
|
||||
* Checks if a tool slot intersects with a given slot.
|
||||
* @param toolSlot - The tool slot descriptor.
|
||||
* @param slot - The slot descriptor to check.
|
||||
* @param isRow - Whether the check is for rows.
|
||||
* @returns True if intersects, otherwise false.
|
||||
*/
|
||||
isIntersecting(
|
||||
toolSlot: SlotDescriptor,
|
||||
slot: SlotDescriptor,
|
||||
isRow: boolean,
|
||||
): boolean {
|
||||
const dim = isRow ? "row" : "column"
|
||||
const span = isRow ? "rowSpan" : "colSpan"
|
||||
const l = slot[dim]
|
||||
const c = slot[span]
|
||||
const r = toolSlot[dim]
|
||||
const a = toolSlot[span]
|
||||
return l <= r && l + c >= r + a
|
||||
}
|
||||
/**
|
||||
* Determines if a slot should be created or extended based on placement.
|
||||
* @param slot - The slot descriptor.
|
||||
* @param selectedToolSlot - The selected tool slot descriptor.
|
||||
* @param placement - "before" | "after".
|
||||
* @param isRow - Whether rows are targeted.
|
||||
*/
|
||||
getCreateOrExtend(
|
||||
slot: SlotDescriptor,
|
||||
selectedToolSlot: SlotDescriptor,
|
||||
placement: "before" | "after",
|
||||
isRow: boolean,
|
||||
): boolean {
|
||||
const dim = isRow ? "row" : "column"
|
||||
const span = isRow ? "rowSpan" : "colSpan"
|
||||
if (placement === "before") return slot[dim] === selectedToolSlot[dim]
|
||||
return (
|
||||
slot[dim] + slot[span] === selectedToolSlot[dim] + selectedToolSlot[span]
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Selects cells in the selected row or column.
|
||||
* @param selectForDelete - Whether to select for delete.
|
||||
* @returns Updated slots with selection flags.
|
||||
*/
|
||||
selectCellsInSelectedRowCol(selectForDelete?: boolean): SlotDescriptor[] {
|
||||
const rowSlots = this.layoutService.gridToolbarSpecs?.rowTools.slots
|
||||
const colSlots = this.layoutService.gridToolbarSpecs?.colTools.slots
|
||||
const slots = [...this.layoutService.slots]
|
||||
slots.forEach((slot) => {
|
||||
slot[
|
||||
selectForDelete !== undefined
|
||||
? "isSelectedForInlineDelete"
|
||||
: "isSelected"
|
||||
] = this.isSlotSelected(
|
||||
slot,
|
||||
rowSlots || [],
|
||||
colSlots || [],
|
||||
selectForDelete,
|
||||
)
|
||||
})
|
||||
return slots
|
||||
}
|
||||
/**
|
||||
* Checks if a slot is selected based on row and column slots.
|
||||
* @param slot - The slot descriptor to check.
|
||||
* @param rowSlots - Row tool slots.
|
||||
* @param colSlots - Column tool slots.
|
||||
* @param selectForDelete - Inline delete mode flag.
|
||||
*/
|
||||
isSlotSelected(
|
||||
slot: SlotDescriptor,
|
||||
rowSlots: Array<SlotDescriptor>,
|
||||
colSlots: Array<SlotDescriptor>,
|
||||
selectForDelete?: boolean,
|
||||
): boolean {
|
||||
const { row, rowSpan, column, colSpan } = slot
|
||||
const rowMatch = rowSlots
|
||||
.filter((r) => r.row < row + rowSpan && r.row + r.rowSpan > row)
|
||||
.every((r) =>
|
||||
selectForDelete !== undefined
|
||||
? r.isSelectedForInlineDelete
|
||||
: r.isSelected,
|
||||
)
|
||||
const colMatch = colSlots
|
||||
.filter(
|
||||
(c) => c.column < column + colSpan && c.column + c.colSpan > column,
|
||||
)
|
||||
.every((c) =>
|
||||
selectForDelete !== undefined
|
||||
? c.isSelectedForInlineDelete
|
||||
: c.isSelected,
|
||||
)
|
||||
return rowMatch || colMatch
|
||||
}
|
||||
}
|
||||
|
||||
export default EventsService
|
||||
72
src/services/History.service.ts
Normal file
72
src/services/History.service.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* HistoryService manages undo/redo for layout state.
|
||||
*/
|
||||
class HistoryService<T = any> {
|
||||
past: T[] = []
|
||||
present: T | null = null
|
||||
future: T[] = []
|
||||
|
||||
constructor(initialState?: T) {
|
||||
if (initialState !== undefined) {
|
||||
this.present = initialState
|
||||
}
|
||||
}
|
||||
|
||||
get get(): T | null {
|
||||
return this.present
|
||||
}
|
||||
|
||||
get canUndo(): boolean {
|
||||
return this.past.length > 0
|
||||
}
|
||||
|
||||
get canRedo(): boolean {
|
||||
return this.future.length > 0
|
||||
}
|
||||
/**
|
||||
* Reverts to the previous state in the history.
|
||||
* @returns The previous state or null if there are no past states.
|
||||
*/
|
||||
undo(): T | null {
|
||||
if (!this.canUndo) return this.present
|
||||
const pastCopy = [...this.past]
|
||||
const previous = pastCopy.pop()
|
||||
this.future = [this.present as T, ...this.future]
|
||||
this.past = pastCopy
|
||||
this.present = previous ?? null
|
||||
return this.present
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts to the next state in the future.
|
||||
* @returns The next state or null if there are no future states.
|
||||
*/
|
||||
redo(): T | null {
|
||||
if (!this.canRedo) return this.present
|
||||
const futureCopy = [...this.future]
|
||||
const next = futureCopy.shift()
|
||||
this.past = [...this.past, this.present as T]
|
||||
this.future = futureCopy
|
||||
this.present = next ?? null
|
||||
return this.present
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the new state and optionally skips history recording.
|
||||
* @param newState The new state to set.
|
||||
* @param skipHistory If true, skips recording the current state in history.
|
||||
* @returns The new state.
|
||||
*/
|
||||
set(newState: T, skipHistory = false): T {
|
||||
if (!skipHistory) {
|
||||
this.past = this.present ? [...this.past, this.present] : this.past
|
||||
}
|
||||
this.present = newState
|
||||
if (!skipHistory) {
|
||||
this.future = []
|
||||
}
|
||||
return this.present
|
||||
}
|
||||
}
|
||||
|
||||
export default HistoryService
|
||||
593
src/services/Layout.service.ts
Normal file
593
src/services/Layout.service.ts
Normal file
@@ -0,0 +1,593 @@
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { FunctionType } from "@armco/types"
|
||||
import { LayoutDescriptor, SlotDescriptor } from "@models"
|
||||
import LayoutError, { LayoutErrors } from "../LayoutError"
|
||||
import HistoryService from "./History.service"
|
||||
import {
|
||||
IGridSpecs,
|
||||
IGridToolbarSpecs,
|
||||
IMinMaxSelections,
|
||||
LayoutMode,
|
||||
} from "../types"
|
||||
|
||||
const DEFAULT_GRID_SPECS = { rows: 1, columns: 1 }
|
||||
|
||||
class LayoutService {
|
||||
private readonly layoutDescriptor: LayoutDescriptor
|
||||
gridTemplate: string = ""
|
||||
slots: SlotDescriptor[] = []
|
||||
gridSpecs?: string | IGridSpecs
|
||||
gridArea?: Array<Array<string>>
|
||||
rowHeights?: Array<string> | string
|
||||
colWidths?: Array<string> | string
|
||||
gridToolbarSpecs?: IGridToolbarSpecs
|
||||
acceptTextOnClick?: boolean
|
||||
minMaxSelections: IMinMaxSelections = {
|
||||
minRow: -Infinity,
|
||||
maxRow: Infinity,
|
||||
minColumn: -Infinity,
|
||||
maxColumn: Infinity,
|
||||
}
|
||||
mergeEnabled: boolean = false
|
||||
splitEnabled: boolean = false
|
||||
mode: LayoutMode = "build"
|
||||
activeSlot?: SlotDescriptor
|
||||
historyService?: HistoryService<Array<SlotDescriptor>>
|
||||
onLayoutChange?: FunctionType
|
||||
rerender: FunctionType
|
||||
|
||||
constructor(
|
||||
descriptor: any,
|
||||
rerender: FunctionType,
|
||||
historyService?: HistoryService<Array<SlotDescriptor>>,
|
||||
onLayoutChange?: FunctionType,
|
||||
) {
|
||||
this.layoutDescriptor = LayoutDescriptor.create(descriptor)
|
||||
this.extract = this.extract.bind(this)
|
||||
this.rerender = rerender
|
||||
this.historyService = historyService
|
||||
this.onLayoutChange = onLayoutChange
|
||||
this.initLayout = this.initLayout.bind(this)
|
||||
this.updateSlots = this.updateSlots.bind(this)
|
||||
this.updateGTS = this.updateGTS.bind(this)
|
||||
this.SGAtoGTS = this.SGAtoGTS.bind(this)
|
||||
this.StoGA = this.StoGA.bind(this)
|
||||
this.GStoS = this.GStoS.bind(this)
|
||||
this.GAtoGT = this.GAtoGT.bind(this)
|
||||
this.GTtoS = this.GTtoS.bind(this)
|
||||
this.checkIfAdjacent = this.checkIfAdjacent.bind(this)
|
||||
this.calculateRowHeights = this.calculateRowHeights.bind(this)
|
||||
this.extract()
|
||||
this.initLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts properties from the layout descriptor and initializes the layout.
|
||||
* This method is called in the constructor and should not be called again.
|
||||
* It initializes grid template, slots, grid area, row heights, column widths,
|
||||
* and calls initLayout to set up the layout.
|
||||
*/
|
||||
extract(): void {
|
||||
this.gridTemplate = this.layoutDescriptor.gridTemplate
|
||||
this.slots = this.layoutDescriptor.slots || []
|
||||
this.gridSpecs = this.layoutDescriptor.gridSpecs || DEFAULT_GRID_SPECS
|
||||
this.rowHeights = this.layoutDescriptor.rowHeights
|
||||
this.colWidths = this.layoutDescriptor.colWidths
|
||||
this.acceptTextOnClick = this.layoutDescriptor.acceptTextOnClick
|
||||
this.activeSlot = this.layoutDescriptor.activeSlot
|
||||
this.mode = this.layoutDescriptor.mode || "build"
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes layout by extracting grid template, slots, grid area, row heights, and column widths.
|
||||
* If slots are provided, gridArea and gridTemplate is generated using optional rowHeights and colWidths
|
||||
* If gridTemplate is provided, slots are generated using it
|
||||
* If gridSpecs is provided, slots are generated using it
|
||||
* Final outcome: Given optional slots, optional gridTemplate, optional gridSpecs, optional rowHeights, optional colWidths;
|
||||
* generated gridTemplate, generated gridArea, generated slots, generated rowHeights, generated colWidths
|
||||
*
|
||||
* This function is mainly for a clean layout that doesn't have any content in it except basic row and column specifications,
|
||||
* though it still needs to be called for generating the internal gridArea property which in turn is used for generating gridTemplate
|
||||
* and gridToolbarSpecs.
|
||||
*
|
||||
* If all of slots, gridTemplate and gridSpecs are provided, slots take precedence followed by gridTemplate and then gridSpecs
|
||||
* If rowHeights and colWidths are provided alongwith gridTemplate, rowHeights and colWidths take precedence
|
||||
*/
|
||||
private initLayout(): void {
|
||||
if (!this.slots || this.slots.length === 0) {
|
||||
if (this.gridTemplate) {
|
||||
const parsedTemplate = this.GTtoS()
|
||||
this.slots = parsedTemplate.slots
|
||||
this.rowHeights = this.rowHeights || parsedTemplate.rowHeights
|
||||
this.colWidths = this.colWidths || parsedTemplate.colWidths
|
||||
} else if (this.gridSpecs) {
|
||||
this.slots = this.GStoS()
|
||||
} else {
|
||||
this.slots = []
|
||||
}
|
||||
}
|
||||
const gridAreaRowHeights = this.StoGA()
|
||||
this.gridArea = gridAreaRowHeights.gridArea
|
||||
this.rowHeights = gridAreaRowHeights.rowHeights
|
||||
if (!this.gridTemplate) {
|
||||
this.gridTemplate = this.GAtoGT(
|
||||
this.gridArea,
|
||||
this.rowHeights,
|
||||
this.colWidths,
|
||||
)
|
||||
}
|
||||
this.historyService?.set(this.slots)
|
||||
this.rerender()
|
||||
if (this.mode === "build") this.updateGTS()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the slots and optionally skips history.
|
||||
* This function and its callers are all supposed to be used only in build mode.
|
||||
* Code internal to Layout ensures the same.
|
||||
*
|
||||
* @param slots - An array of slot descriptors.
|
||||
* @param skipHistory - A boolean to indicate whether to skip history.
|
||||
* @param enforceGTSUpdate - A boolean to enforce grid toolbar specs update.
|
||||
*/
|
||||
updateSlots(
|
||||
slots: SlotDescriptor[],
|
||||
skipHistory?: boolean,
|
||||
enforceGTSUpdate?: boolean,
|
||||
): void {
|
||||
const lengthChanged = this.slots.length !== slots.length
|
||||
if (this.slots !== slots) {
|
||||
this.slots = slots as any[]
|
||||
if (lengthChanged) {
|
||||
this.StoGA()
|
||||
this.resetRowToolbarHeights()
|
||||
// Lifecycle guarantee: StoGA() sets gridArea & rowHeights before GAtoGT.
|
||||
// colWidths may be undefined for layouts initialized via gridSpecs/slots; GAtoGT internally defaults missing columns to 1fr.
|
||||
this.gridTemplate = this.GAtoGT(
|
||||
this.gridArea!,
|
||||
this.rowHeights!,
|
||||
this.colWidths,
|
||||
)
|
||||
}
|
||||
this.mergeEnabled = this.checkIfAdjacent()
|
||||
this.splitEnabled = slots.filter((slot) => slot.isSelected).length >= 1
|
||||
this.historyService?.set(SlotDescriptor.clone(slots), !!skipHistory)
|
||||
this.rerender()
|
||||
if (lengthChanged || enforceGTSUpdate) this.updateGTS()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapping Grid toolbar specs generator around timeout and animation frame requester
|
||||
* since GTAtoGTS performs DOM operations to calculate row heights, and
|
||||
* should be called after the slots have been updated and component has re-rendered.
|
||||
*/
|
||||
updateGTS(): void {
|
||||
setTimeout(
|
||||
() =>
|
||||
requestAnimationFrame(() => {
|
||||
this.SGAtoGTS()
|
||||
this.rerender()
|
||||
}),
|
||||
10,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert grid area, slots and row heights to grid toolbar specs
|
||||
* Grid toolbars (row and column slots) get messed up when size of cells in their corresponding rows/columns change
|
||||
* due to content, this function is to calculate the row heights and column widths, based on common height of row and column widths
|
||||
* after content has been added
|
||||
* This function relies on grid area generated and saved in Layout service, and doesn't accept
|
||||
* explicit passed grid area for the sake of simplicity. It generates row tools grid area and column tools grid area
|
||||
* and uses those to generate their corresponding grid templates.
|
||||
*
|
||||
* !IMPORTANT: This function performs a DOM operation to calculate row heights, so it should be called after the slots have been updated
|
||||
* and component has re-rendered.
|
||||
*
|
||||
* ***Calling this function directly may result in incorrect row heights.***
|
||||
*
|
||||
* @param gridArea Grid Area of row tools
|
||||
* @returns grid toolbar specs
|
||||
* @sets grid toolbar specs
|
||||
*/
|
||||
private SGAtoGTS(): IGridToolbarSpecs {
|
||||
const gridArea = this.gridArea!
|
||||
const rowHeights = this.calculateRowHeights()
|
||||
const rowToolGridAreas = gridArea.map((row: any[]) => [
|
||||
row.every((cell) => cell === row[0])
|
||||
? row[0].replace("gsa-", "gtra-")
|
||||
: "gtra-" + uuidv4(),
|
||||
])
|
||||
const colToolGridAreas = gridArea[0].map((cell: any, colIdx: number) =>
|
||||
gridArea.every((row: any[]) => row[colIdx] === gridArea[0][colIdx])
|
||||
? gridArea[0][colIdx].replace("gsa-", "gtca-")
|
||||
: "gtca-" + uuidv4(),
|
||||
)
|
||||
const rowToolSlots = LayoutService.countOccurrences(
|
||||
rowToolGridAreas.map((row: any[]) => row[0]),
|
||||
).map((occurrence: { slotId: string; span: number; location: number }) =>
|
||||
SlotDescriptor.create({
|
||||
slot: occurrence.slotId,
|
||||
gridArea: occurrence.slotId,
|
||||
row: occurrence.location,
|
||||
column: 0,
|
||||
rowSpan: occurrence.span,
|
||||
colSpan: 1,
|
||||
props: { style: { gridArea: occurrence.slotId } },
|
||||
}),
|
||||
)
|
||||
const colToolSlots = LayoutService.countOccurrences(colToolGridAreas).map(
|
||||
(occurrence) =>
|
||||
SlotDescriptor.create({
|
||||
slot: occurrence.slotId,
|
||||
gridArea: occurrence.slotId,
|
||||
row: 0,
|
||||
column: occurrence.location,
|
||||
rowSpan: 1,
|
||||
colSpan: occurrence.span,
|
||||
props: { style: { gridArea: occurrence.slotId } },
|
||||
}),
|
||||
)
|
||||
this.gridToolbarSpecs = {
|
||||
rowTools: {
|
||||
slots: rowToolSlots,
|
||||
gridTemplate: this.GAtoGT(
|
||||
rowToolGridAreas,
|
||||
rowHeights || "minmax(auto, 1fr)",
|
||||
this.colWidths || "1fr",
|
||||
),
|
||||
},
|
||||
colTools: {
|
||||
slots: colToolSlots,
|
||||
gridTemplate: this.GAtoGT([colToolGridAreas], "1fr", "1fr"),
|
||||
},
|
||||
}
|
||||
return this.gridToolbarSpecs
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts slots and row heights to grid area and processed row heights
|
||||
* @param currentRowHeights
|
||||
* @returns
|
||||
*/
|
||||
StoGA(): { gridArea: string[][]; rowHeights: string[] } {
|
||||
const initialRowHeights = this.layoutDescriptor.rowHeights
|
||||
let gridArea: any[] = []
|
||||
const processedRowHeights: any[] = []
|
||||
this.slots.forEach((slot: any) => {
|
||||
let rowEnd = slot.row + slot.rowSpan,
|
||||
colEnd = slot.column + slot.colSpan
|
||||
for (let rowIdx = slot.row; rowIdx < rowEnd; rowIdx++) {
|
||||
const rowHeight = Array.isArray(initialRowHeights)
|
||||
? initialRowHeights[rowIdx]
|
||||
: initialRowHeights
|
||||
slot.content
|
||||
? (processedRowHeights[rowIdx] = rowHeight || "auto")
|
||||
: (processedRowHeights[rowIdx] = "minmax(2rem, auto)")
|
||||
gridArea[rowIdx] || (gridArea[rowIdx] = [])
|
||||
for (let colIdx = slot.column; colIdx < colEnd; colIdx++)
|
||||
gridArea[rowIdx][colIdx] = slot.gridArea
|
||||
}
|
||||
})
|
||||
this.gridArea = gridArea
|
||||
this.rowHeights = processedRowHeights
|
||||
return { gridArea, rowHeights: processedRowHeights }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts grid specs to slot descriptors.
|
||||
* @returns Array of SlotDescriptor
|
||||
*/
|
||||
GStoS(): SlotDescriptor[] {
|
||||
if (typeof this.gridSpecs === "string") {
|
||||
try {
|
||||
this.gridSpecs = JSON.parse(this.gridSpecs)
|
||||
} catch {
|
||||
console.error(
|
||||
"[COMPONENT:LAYOUT] Grid Specs passed as JSON string but incorrect non-parseable format",
|
||||
)
|
||||
throw new LayoutError(
|
||||
"Invalid grid specs string",
|
||||
LayoutErrors.MALFORMED_GRID_SPECS,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof this.gridSpecs === "object" &&
|
||||
"rows" in this.gridSpecs &&
|
||||
"columns" in this.gridSpecs
|
||||
) {
|
||||
const slotDescriptors: any[] = []
|
||||
for (let rowIdx = 0; rowIdx < this.gridSpecs.rows; rowIdx++) {
|
||||
for (let colIdx = 0; colIdx < this.gridSpecs.columns; colIdx++) {
|
||||
const slotId = `gsa-${uuidv4()}`
|
||||
slotDescriptors.push(
|
||||
SlotDescriptor.create({
|
||||
slot: slotId,
|
||||
row: rowIdx,
|
||||
column: colIdx,
|
||||
rowSpan: 1,
|
||||
colSpan: 1,
|
||||
gridArea: slotId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return slotDescriptors
|
||||
}
|
||||
console.error(
|
||||
"[COMPONENT:LAYOUT] Grid Specs are malformed, should be an object with rows and columns",
|
||||
)
|
||||
throw new LayoutError(
|
||||
"Invalid grid specs",
|
||||
LayoutErrors.MALFORMED_GRID_SPECS,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts grid area, row heights and column widths to grid template
|
||||
* Row Heights at this point are the final processed row heights, and calculated row heights in case of
|
||||
* grid templates for toolbars
|
||||
* Calling function should pass gridArea always, not rely on this.gridArea since gridArea is different for main grid
|
||||
* and for toolbar grids, passing explicitly distinguishes between the two
|
||||
*
|
||||
* @param gridArea
|
||||
* @param rowHeight
|
||||
* @param colWidth
|
||||
* @returns grid template string
|
||||
*/
|
||||
private GAtoGT(
|
||||
gridArea: string[][],
|
||||
rowHeights?: string[] | string,
|
||||
colWidths?: string[] | string,
|
||||
): string {
|
||||
let gridTemplate = gridArea
|
||||
.map(
|
||||
(row, rowIdx) =>
|
||||
`"${row.join(" ")}" ${
|
||||
Array.isArray(rowHeights)
|
||||
? rowHeights[rowIdx] || "auto"
|
||||
: rowHeights || "minmax(3rem, 1fr)"
|
||||
}`,
|
||||
)
|
||||
.join(" ")
|
||||
const colTemplate =
|
||||
" / " +
|
||||
(Array.isArray(colWidths)
|
||||
? colWidths.join(" ")
|
||||
: Array.from(
|
||||
{ length: gridArea[0].length },
|
||||
() => colWidths || "1fr",
|
||||
).join(" "))
|
||||
gridTemplate += colTemplate
|
||||
const trimmedTemplate = gridTemplate.trim()
|
||||
if (this.onLayoutChange) this.onLayoutChange(this.gridTemplate, this.slots)
|
||||
else
|
||||
console.warn(
|
||||
"Missing callback onLayoutChange, you may want to pass it as a prop to Layout component in order to capture the updated layout and use it.",
|
||||
)
|
||||
return trimmedTemplate
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse slots, row heights and column widths from grid template.
|
||||
*
|
||||
* This grid template parser is currently best effort basis and may not support some edge cases, it does
|
||||
* support calc(), auto, fr, px, rem, %, minmax(), repeat() and other CSS grid properties, but has not been
|
||||
* tested extensively against all possible grid templates.
|
||||
*
|
||||
* Example grid template:
|
||||
* "ga-1 ga-1 ga-2" 1fr
|
||||
* "ga-3 ga-4 ga-5" auto
|
||||
* "ga-6 ga-7 ga-7" 1fr
|
||||
* / 1fr 1fr 1fr
|
||||
*
|
||||
* @param gridTemplate
|
||||
* @returns
|
||||
*/
|
||||
GTtoS(): {
|
||||
slots: SlotDescriptor[]
|
||||
rowHeights: string[]
|
||||
colWidths: string[]
|
||||
} {
|
||||
const gridTemplateParts = this.gridTemplate
|
||||
.split('"')
|
||||
.map((part: string) => part.trim())
|
||||
.filter((part: string) => part)
|
||||
let colWidths: any[] = []
|
||||
const rowHeightsMap: any = {}
|
||||
gridTemplateParts.forEach((part: string, idx: number) => {
|
||||
if (part.startsWith("ga-")) rowHeightsMap[part] = "auto"
|
||||
else if (part.includes("calc(")) {
|
||||
const closeIdx = part.lastIndexOf(")"),
|
||||
heightValue = part.substring(0, closeIdx + 1)
|
||||
rowHeightsMap[gridTemplateParts[idx - 1]] = heightValue
|
||||
const afterCalc = part.substring(closeIdx + 1).trim()
|
||||
if (afterCalc.includes("/"))
|
||||
colWidths = afterCalc
|
||||
.split("/")[1]
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((width: string) => width.trim())
|
||||
} else {
|
||||
rowHeightsMap[gridTemplateParts[idx - 1]] = part
|
||||
}
|
||||
})
|
||||
const rowHeights = Object.values(rowHeightsMap) as string[]
|
||||
return {
|
||||
slots: LayoutService.generateSlots(Object.keys(rowHeightsMap)),
|
||||
rowHeights,
|
||||
colWidths: colWidths || [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates slot descriptors from grid template rows.
|
||||
* Rows is an Array of strings extracted from grid template after left after parsing out
|
||||
* row heights and column widths
|
||||
* Example:
|
||||
* "ga-1 ga-1 ga-2" 1fr
|
||||
* "ga-3 ga-4 ga-5" auto
|
||||
* "ga-6 ga-7 ga-7" 1fr
|
||||
* @param rows
|
||||
* @returns Array of SlotDescriptor
|
||||
*/
|
||||
static generateSlots(rows: string[]): SlotDescriptor[] {
|
||||
const slotDescriptors: any[] = []
|
||||
const seenSlots = new Set<string>()
|
||||
rows.forEach((rowString, rowIdx) => {
|
||||
const slotNames = rowString.split(" ")
|
||||
slotNames.forEach((slotName, colIdx) => {
|
||||
const trimmedSlot = slotName.trim()
|
||||
if (!seenSlots.has(trimmedSlot)) {
|
||||
seenSlots.add(trimmedSlot)
|
||||
const rowSpan = rows.filter((row) => row.includes(trimmedSlot)).length
|
||||
const colSpan = slotNames.filter(
|
||||
(name) => name === trimmedSlot,
|
||||
).length
|
||||
slotDescriptors.push(
|
||||
SlotDescriptor.create({
|
||||
slot: trimmedSlot,
|
||||
gridArea: trimmedSlot,
|
||||
row: rowIdx,
|
||||
column: colIdx,
|
||||
rowSpan,
|
||||
colSpan,
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
return slotDescriptors
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if selected slots are adjacent.
|
||||
* @returns true if adjacent, false otherwise
|
||||
*/
|
||||
checkIfAdjacent(): boolean {
|
||||
const selectedSlots = this.slots.filter((slot: any) => slot.isSelected)
|
||||
let minRow = Infinity,
|
||||
maxRow = -Infinity,
|
||||
minCol = Infinity,
|
||||
maxCol = -Infinity,
|
||||
totalSpan = 0
|
||||
selectedSlots?.forEach((slot: any) => {
|
||||
if (slot.row !== undefined && slot.column !== undefined) {
|
||||
minRow = Math.min(minRow, slot.row)
|
||||
maxRow = Math.max(maxRow, slot.row + (slot.rowSpan - 1))
|
||||
minCol = Math.min(minCol, slot.column)
|
||||
maxCol = Math.max(maxCol, slot.column + (slot.colSpan - 1))
|
||||
totalSpan += slot.rowSpan * slot.colSpan
|
||||
}
|
||||
})
|
||||
const area = (maxRow - minRow + 1) * (maxCol - minCol + 1)
|
||||
this.minMaxSelections = {
|
||||
minRow,
|
||||
maxRow,
|
||||
minColumn: minCol,
|
||||
maxColumn: maxCol,
|
||||
}
|
||||
return selectedSlots && selectedSlots.length > 1 && area === totalSpan
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the occurrences of each unique string in the given array and returns an array of objects
|
||||
* representing each unique string, its count (span), and its first occurrence index (location).
|
||||
*
|
||||
* @param array - The array of strings to count occurrences in.
|
||||
* @returns An array of objects, each containing:
|
||||
* - `slotId`: The unique string from the input array.
|
||||
* - `span`: The number of times the string appears in the input array.
|
||||
* - `location`: The index of the first occurrence of the string in the input array.
|
||||
*/
|
||||
static countOccurrences(
|
||||
array: string[],
|
||||
): { slotId: string; span: number; location: number }[] {
|
||||
const occurrences: { slotId: string; span: number; location: number }[] = []
|
||||
for (let idx = 0; idx < array.length; idx++) {
|
||||
const slotId = array[idx]
|
||||
const found = occurrences.find((entry) => entry.slotId === slotId)
|
||||
if (found === undefined)
|
||||
occurrences.push({ slotId, span: 1, location: idx })
|
||||
else found.span++
|
||||
}
|
||||
return occurrences
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the heights of the grid template rows of the row tools container to their default values: "minmax(2rem, auto)".
|
||||
* This function is used to ensure that the row toolbar heights are consistent and do not interfere with the layout of the grid.
|
||||
* This function is called when the layout is updated (layoutService.updateSlots) and ensures that the row toolbar heights
|
||||
* do not influence heights of the main layout grid cells.
|
||||
*/
|
||||
resetRowToolbarHeights(): void {
|
||||
if (!this.gridArea) {
|
||||
console.warn(
|
||||
"Grid area is not defined. Cannot reset row toolbar heights.",
|
||||
)
|
||||
return
|
||||
}
|
||||
const defaultRowHeights = Array.from(
|
||||
{ length: this.gridArea.length },
|
||||
() => "minmax(2rem, auto)",
|
||||
)
|
||||
if (this.gridToolbarSpecs && this.gridToolbarSpecs.rowTools) {
|
||||
this.gridToolbarSpecs.rowTools.gridTemplate = this.GAtoGT(
|
||||
this.gridArea.map((row: any[]) => [
|
||||
row.every((cell: any) => cell === row[0])
|
||||
? row[0].replace("gsa-", "gtra-")
|
||||
: "gtra-" + uuidv4(),
|
||||
]),
|
||||
defaultRowHeights,
|
||||
"1fr",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates row heights based on slots and optional row heights.
|
||||
* @param slots - An array of slot descriptors.
|
||||
* @param rowHeights - An optional array or string of row heights.
|
||||
* @param demo - A boolean to indicate whether it is a demo.
|
||||
* @returns An array of calculated row heights.
|
||||
*/
|
||||
calculateRowHeights(): string[] {
|
||||
const slots = this.slots
|
||||
const existing = this.rowHeights
|
||||
const maxRowIdx = Math.max(
|
||||
...slots.map((slot: any) => slot.row + slot.rowSpan),
|
||||
)
|
||||
const baseArray: string[] = Array.isArray(existing)
|
||||
? [...existing]
|
||||
: Array.from(
|
||||
{ length: maxRowIdx },
|
||||
() => existing || "minmax(2rem, auto)",
|
||||
)
|
||||
const calculatedRowHeights: string[] = []
|
||||
for (let rowIdx = 0; rowIdx < maxRowIdx; rowIdx++) {
|
||||
const current = baseArray[rowIdx]
|
||||
if (current && current !== "" && current.indexOf("auto") === -1) {
|
||||
calculatedRowHeights[rowIdx] = current
|
||||
continue
|
||||
}
|
||||
const slotHeights = slots
|
||||
.filter(
|
||||
(slot: any) => slot.row <= rowIdx && rowIdx < slot.row + slot.rowSpan,
|
||||
)
|
||||
.map((slot: any) => {
|
||||
const element = document.getElementById(slot.slot)
|
||||
return element
|
||||
? Math.floor(element.getBoundingClientRect().height / slot.rowSpan)
|
||||
: 0
|
||||
})
|
||||
const maxHeight = Math.max(...slotHeights)
|
||||
calculatedRowHeights[rowIdx] =
|
||||
maxHeight === 0 ? "minmax(2rem, auto)" : `${maxHeight + 2}px`
|
||||
}
|
||||
return calculatedRowHeights
|
||||
}
|
||||
}
|
||||
|
||||
export default LayoutService
|
||||
3
src/services/index.ts
Normal file
3
src/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as LayoutService } from "./Layout.service"
|
||||
export { default as EventsService } from "./Events.service"
|
||||
export { default as HistoryService } from "./History.service"
|
||||
2
src/test.scss
Normal file
2
src/test.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
@use "bootstrap/scss/bootstrap.scss";
|
||||
@use "@armco/shared-components/es/assets/Avatar.css";
|
||||
222
src/types.ts
Normal file
222
src/types.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
// Recovered subset of Layout types (trimmed for initial compilation). Expand as needed.
|
||||
|
||||
import type { ReactNode, RefObject } from "react"
|
||||
import { BaseProps, FunctionType } from "@armco/types"
|
||||
import type {
|
||||
ArComponentResources,
|
||||
ArDirections,
|
||||
ArDisplayTypes,
|
||||
ArSlotViewMode,
|
||||
} from "@armco/shared-components/enums"
|
||||
import { EventsService, HistoryService, LayoutService } from "@services"
|
||||
import { SlotDescriptor } from "@models"
|
||||
|
||||
export interface ISlotDescriptor {
|
||||
colSpan: number
|
||||
column: number
|
||||
content?: Array<ISlotDescriptor> | IComponentInfo
|
||||
isSelected?: boolean
|
||||
isSelectedForInlineDelete?: boolean
|
||||
gridArea: string
|
||||
props?: {
|
||||
containerDisplayMode?: LayoutProps["displayMode"]
|
||||
style?: React.CSSProperties
|
||||
[key: string]: any
|
||||
}
|
||||
row: number
|
||||
rowSpan: number
|
||||
slot: string
|
||||
splitFrom?: string
|
||||
}
|
||||
|
||||
export interface IGridSpecs {
|
||||
rows: number
|
||||
columns: number
|
||||
}
|
||||
|
||||
export interface IGridToolbarSpecs {
|
||||
rowTools: {
|
||||
slots: Array<SlotDescriptor>
|
||||
gridTemplate: string
|
||||
}
|
||||
colTools: {
|
||||
slots: Array<SlotDescriptor>
|
||||
gridTemplate: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface IComponentInfo {
|
||||
componentName: string
|
||||
source?: ArComponentResources
|
||||
descriptor?: {
|
||||
content: Array<ISlotDescriptor>
|
||||
layout: string
|
||||
}
|
||||
props?: {
|
||||
[key: string]: any
|
||||
}
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface IMinMaxSelections {
|
||||
minRow: number
|
||||
maxRow: number
|
||||
minColumn: number
|
||||
maxColumn: number
|
||||
}
|
||||
|
||||
export type LayoutMode = "build" | "preview" | "release"
|
||||
|
||||
export interface ReleaseSlotProps extends BaseProps {
|
||||
slotDescriptor: SlotDescriptor
|
||||
containerDisplayMode?: SlotProps["containerDisplayMode"]
|
||||
content?: ReactNode
|
||||
}
|
||||
|
||||
export interface BuilderLayoutCanvasProps {
|
||||
children?: ReactNode
|
||||
isChild?: boolean
|
||||
isGrid?: boolean
|
||||
}
|
||||
|
||||
export interface BuilderLayoutContainerProps {
|
||||
children?: ReactNode
|
||||
classes?: string
|
||||
panelsDisplayable?: boolean
|
||||
}
|
||||
export interface BuilderSlotProps extends SlotProps {}
|
||||
|
||||
export interface SlotToolsProps extends BaseProps {
|
||||
slotDescriptor: SlotDescriptor
|
||||
mode?: ArSlotViewMode
|
||||
controlsEnabled?: boolean
|
||||
horizontalSplitHandler?: FunctionType
|
||||
verticalSplitHandler?: FunctionType
|
||||
mergeHandler?: FunctionType
|
||||
rowInsertHandler?: FunctionType
|
||||
columnInsertHandler?: FunctionType
|
||||
rowDeleteHandler?: FunctionType
|
||||
columnDeleteHandler?: FunctionType
|
||||
rowSelectHandler?: FunctionType
|
||||
columnSelectHandler?: FunctionType
|
||||
}
|
||||
|
||||
export interface ReleaseLayoutProps extends BaseProps {
|
||||
displayMode?: LayoutProps["displayMode"]
|
||||
gridTemplate?: string
|
||||
slots?: Array<SlotDescriptor>
|
||||
slotRenderer?: FunctionType
|
||||
mode?: LayoutProps["mode"]
|
||||
}
|
||||
|
||||
export type BuilderLayoutProps = LayoutProps
|
||||
|
||||
export interface SlotControlProps {
|
||||
class: string
|
||||
tooltip?: string
|
||||
isGridOnly?: boolean
|
||||
isToolSlotOnly?: boolean
|
||||
onClick?: FunctionType
|
||||
onMouseEnter?: FunctionType
|
||||
onMouseLeave?: FunctionType
|
||||
}
|
||||
|
||||
export interface LayoutControlPanelProps extends BaseProps {
|
||||
columnDeleteEnabled?: boolean
|
||||
redoEnabled?: boolean
|
||||
rowDeleteEnabled?: boolean
|
||||
show?: boolean
|
||||
slots?: Array<SlotDescriptor>
|
||||
undoEnabled?: boolean
|
||||
}
|
||||
|
||||
export interface ResizableProps extends BaseProps {
|
||||
componentName?: string
|
||||
children?: ReactNode
|
||||
display?: ArDisplayTypes
|
||||
directions?: Array<ArDirections>
|
||||
reClasses?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface LayoutProps extends BaseProps {
|
||||
acceptTextOnClick?: boolean
|
||||
colWidths?: string | Array<string>
|
||||
displayMode?:
|
||||
| "grid"
|
||||
| "inline"
|
||||
| "inline-flex"
|
||||
| "inline-grid"
|
||||
| "flex"
|
||||
| "inline-block"
|
||||
| "block"
|
||||
| "table"
|
||||
hideBuildModePaddings?: boolean
|
||||
gridSpecs?: string | IGridSpecs
|
||||
gridTemplate?: string
|
||||
isChild?: boolean
|
||||
activeSlot?: SlotDescriptor
|
||||
readonly mode?: LayoutMode
|
||||
rowHeights?: string | Array<string>
|
||||
onLayoutChange?: FunctionType
|
||||
slotDropHandler?: FunctionType
|
||||
onSlotSelect?: FunctionType
|
||||
showControls?: boolean
|
||||
showPanels?: boolean
|
||||
slotRenderer?: FunctionType
|
||||
slots?: Array<SlotDescriptor>
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface SlotProps extends Omit<BaseProps, "style"> {
|
||||
childRef?: RefObject<HTMLDivElement>
|
||||
slotDescriptor: SlotDescriptor
|
||||
containerDisplayMode?: LayoutProps["displayMode"]
|
||||
hideBuildModePaddings?: boolean
|
||||
isFirst?: boolean
|
||||
isLast?: boolean
|
||||
activeSlot?: SlotDescriptor
|
||||
mode?: ArSlotViewMode
|
||||
controlsEnabled?: boolean
|
||||
acceptDropTypes?: Array<string>
|
||||
layoutMode?: LayoutProps["mode"]
|
||||
content?: ReactNode
|
||||
columnDeleteHandler?: FunctionType
|
||||
columnInsertHandler?: FunctionType
|
||||
columnSelectHandler?: FunctionType
|
||||
dropHandler?: FunctionType
|
||||
horizontalSplitHandler?: FunctionType
|
||||
mergeHandler?: FunctionType
|
||||
onClick?: FunctionType
|
||||
onSlotSelect?: FunctionType
|
||||
rowDeleteHandler?: FunctionType
|
||||
rowInsertHandler?: FunctionType
|
||||
rowSelectHandler?: FunctionType
|
||||
verticalSplitHandler?: FunctionType
|
||||
slotRenderer?: FunctionType
|
||||
}
|
||||
|
||||
export type UseLayoutInitProps = LayoutProps & {
|
||||
rerender: FunctionType
|
||||
}
|
||||
|
||||
export interface LayoutContextType {
|
||||
layoutService: LayoutService
|
||||
eventsService: EventsService
|
||||
historyService: HistoryService<Array<SlotDescriptor>>
|
||||
controlsEnabled: boolean
|
||||
enableControls: FunctionType
|
||||
selectionOnlyModeEnabled: boolean
|
||||
enableSelectionOnlyMode: FunctionType
|
||||
panelsDisplayed: boolean
|
||||
displayPanels: FunctionType
|
||||
}
|
||||
|
||||
export interface LayoutProviderProps {
|
||||
children: ReactNode
|
||||
layoutService: LayoutService
|
||||
eventsService: EventsService
|
||||
historyService: HistoryService<Array<SlotDescriptor>>
|
||||
showPanels?: boolean
|
||||
showControls?: boolean
|
||||
}
|
||||
0
src/utils.ts
Normal file
0
src/utils.ts
Normal file
@@ -6,6 +6,7 @@
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
@@ -19,7 +20,14 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@models": ["src/models/index.ts"],
|
||||
"@models/*": ["src/models/*"],
|
||||
"@hooks/*": ["src/hooks/*"],
|
||||
"@services": ["src/services/index.ts"],
|
||||
"@services/*": ["src/services/*"],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
21
vite-dev.config.ts
Normal file
21
vite-dev.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from "vitest/config"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
server: {
|
||||
open: true,
|
||||
},
|
||||
build: {
|
||||
outDir: "build",
|
||||
sourcemap: true,
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "src/setupTests",
|
||||
mockReset: true,
|
||||
},
|
||||
})
|
||||
@@ -5,12 +5,14 @@ import react from "@vitejs/plugin-react"
|
||||
import dts from "vite-plugin-dts"
|
||||
import { libInjectCss } from "vite-plugin-lib-inject-css"
|
||||
import { externalizeDeps } from "vite-plugin-externalize-deps"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
libInjectCss(),
|
||||
tsconfigPaths(),
|
||||
dts({ outDir: "build/types" }),
|
||||
externalizeDeps(),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user