Reorganized modules
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"packages": [
|
||||
"src/packages/*"
|
||||
"packages/*"
|
||||
],
|
||||
"version": "independent"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"src/packages/*"
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
64
packages/ui/src/ColorSelectorRadio.tsx
Normal file
64
packages/ui/src/ColorSelectorRadio.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { ArPopoverSlots } from "@armco/types"
|
||||
|
||||
const textColors: Array<string> = [
|
||||
"#172B4D",
|
||||
"#0747A6",
|
||||
"#008DA6",
|
||||
"#006644",
|
||||
"#FF991F",
|
||||
"#BF2600",
|
||||
"#403294",
|
||||
"#97A0AF",
|
||||
"#4C9AFF",
|
||||
"#00B8D9",
|
||||
"#36B37E",
|
||||
"#FFC400",
|
||||
"#FF5630",
|
||||
"#6554C0",
|
||||
"#FFFFFF",
|
||||
"#B3D4FF",
|
||||
"#B3F5FF",
|
||||
"#ABF5D1",
|
||||
"#FFF0B3",
|
||||
"#FFBDAD",
|
||||
"#EAE6FF",
|
||||
]
|
||||
const bgColors: Array<string> = [
|
||||
"#FFFFFF",
|
||||
"#DCDFE4",
|
||||
"#C6EDFB",
|
||||
"#D3F1A7",
|
||||
"#FEDEC8",
|
||||
"#FDD0EC",
|
||||
"#DFD8FD",
|
||||
]
|
||||
|
||||
const ColorSelectorRadio = ({
|
||||
slot,
|
||||
variant,
|
||||
}: {
|
||||
variant: "text" | "background"
|
||||
slot: ArPopoverSlots
|
||||
}) => {
|
||||
const colors = variant === "text" ? textColors : bgColors
|
||||
return (
|
||||
<div
|
||||
className="ar-ColorSelectorRadio d-grid p-1"
|
||||
style={{ gridTemplateColumns: "repeat(7, 1.5rem)" }}
|
||||
>
|
||||
{colors.map((color) => (
|
||||
<span
|
||||
className="ar-ColorSelectorRadio__button m-1"
|
||||
style={{ padding: "1px", height: "1rem", width: "1rem" }}
|
||||
>
|
||||
<span
|
||||
className="d-inline-block border cursor-pointer w-100 h-100"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ColorSelectorRadio
|
||||
385
packages/ui/src/FlexContent.tsx
Normal file
385
packages/ui/src/FlexContent.tsx
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Document component, that wraps and manages Text components
|
||||
* - on enter add a new Text component
|
||||
*
|
||||
*/
|
||||
import {
|
||||
FormEvent,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import {
|
||||
ArPopoverPositions,
|
||||
ArPopoverSlots,
|
||||
ChunkInfo,
|
||||
CompInfo,
|
||||
FlexContentProps,
|
||||
FlexContentType,
|
||||
LibRepoType,
|
||||
SelectionInfo,
|
||||
TextInfo,
|
||||
TextProps,
|
||||
} from "@armco/types"
|
||||
import {
|
||||
applyTool, // Formatting
|
||||
DomHelper,
|
||||
PopoverV2,
|
||||
selectionHandler, // Selection
|
||||
splitChunkAtCursor, // Formatting
|
||||
TextFormatter,
|
||||
} from "../.."
|
||||
import * as atoms from "../../atoms"
|
||||
import * as molecules from "../../molecules"
|
||||
import "./index.scss"
|
||||
|
||||
const demoDummyState: {
|
||||
entireSelectionFormatted?: boolean
|
||||
contentRegistry: FlexContentType
|
||||
} = {
|
||||
contentRegistry: {},
|
||||
}
|
||||
|
||||
Array.from({ length: 4 }, (i: number) => {
|
||||
const id = uuid()
|
||||
demoDummyState.contentRegistry[id] = {
|
||||
id,
|
||||
name: "Text",
|
||||
text: `lorem ipsum${i}`,
|
||||
order: i,
|
||||
chunks: {},
|
||||
length: -1,
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const MemoWrapper = memo(
|
||||
(props: { componentName: string; props: TextProps; repo: LibRepoType }) => {
|
||||
const { componentName, props: componentProps, repo } = props
|
||||
const Component = repo[componentName]
|
||||
return Component ? (
|
||||
<Component key={componentProps.descriptor.id} {...componentProps} />
|
||||
) : null
|
||||
},
|
||||
(prevProps, props) => {
|
||||
return (
|
||||
JSON.stringify(prevProps.props.descriptor.chunks) !==
|
||||
JSON.stringify(props.props.descriptor.chunks)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const initId = uuid()
|
||||
const initText = {
|
||||
[initId]: {
|
||||
id: initId,
|
||||
name: "Text",
|
||||
text: "",
|
||||
order: 0,
|
||||
chunks: {},
|
||||
length: -1,
|
||||
},
|
||||
}
|
||||
const DEFAULT_PREVENTERS = ["Enter", "Backspace", "Delete"]
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
||||
const isAndroid = /Android/.test(navigator.userAgent)
|
||||
|
||||
const FlexContent = (props: FlexContentProps): JSX.Element => {
|
||||
const {
|
||||
allowFormatting,
|
||||
isEditable = true,
|
||||
hasFloatingFormatter,
|
||||
content = initText,
|
||||
demo,
|
||||
onCommit,
|
||||
theme,
|
||||
} = props
|
||||
const [contentState, updateContentState] = useState<{
|
||||
entireSelectionFormatted?: boolean
|
||||
contentRegistry: FlexContentType
|
||||
}>()
|
||||
const [formatterDisplayed, displayFormatter] = useState<boolean>(false)
|
||||
const activeChunkRef = useRef<ChunkInfo>()
|
||||
const flexContentRef = useRef<HTMLDivElement>(null)
|
||||
const selectionInfoRef = useRef<SelectionInfo | null>()
|
||||
const keyRef = useRef<string>()
|
||||
const repo: LibRepoType = useMemo(() => ({ ...atoms, ...molecules }), [])
|
||||
const winObj = useMemo(() => DomHelper.getWindowElement(demo), [demo])
|
||||
const docObj = useMemo(() => DomHelper.getDocumentElement(demo), [demo])
|
||||
const memoizedSelectionHandler = useCallback(
|
||||
() =>
|
||||
selectionHandler(
|
||||
activeChunkRef,
|
||||
docObj,
|
||||
winObj,
|
||||
selectionInfoRef,
|
||||
contentState,
|
||||
),
|
||||
[contentState, docObj, winObj],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "b") {
|
||||
event.preventDefault()
|
||||
selectionInfoRef.current &&
|
||||
contentState &&
|
||||
applyTool(
|
||||
"bold",
|
||||
true,
|
||||
selectionInfoRef.current,
|
||||
contentState,
|
||||
(state) => {
|
||||
updateContentState(state)
|
||||
selectionInfoRef.current = null
|
||||
},
|
||||
)
|
||||
// undoSlots()
|
||||
}
|
||||
}
|
||||
|
||||
docObj?.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
return () => {
|
||||
docObj?.removeEventListener("keydown", handleKeyDown)
|
||||
}
|
||||
}, [contentState, docObj])
|
||||
|
||||
useEffect(() => {
|
||||
const useContent = content
|
||||
? { contentRegistry: content }
|
||||
: demo
|
||||
? demoDummyState
|
||||
: { contentRegistry: content || {} }
|
||||
useContent?.contentRegistry &&
|
||||
Object.values(useContent.contentRegistry).forEach(
|
||||
(obj: TextInfo | CompInfo) => {
|
||||
obj.length = (obj.text as string)?.length
|
||||
if (Object.keys(obj.chunks).length === 0) {
|
||||
const id = uuid()
|
||||
const end = obj.text ? (obj.text as string).length - 1 : 0
|
||||
obj.chunks[id] = {
|
||||
id,
|
||||
start: 0,
|
||||
end,
|
||||
text: obj.text.substring(0, end),
|
||||
formats: {},
|
||||
parent: obj.id,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
updateContentState(useContent)
|
||||
}, [content, demo])
|
||||
|
||||
useEffect(() => {
|
||||
const flexContentDom = flexContentRef.current
|
||||
const displayFormatterHandler = () =>
|
||||
winObj?.getSelection()?.toString().length && displayFormatter(true)
|
||||
// flexContentDom?.addEventListener("focus", memoizedSelectionHandler)
|
||||
docObj?.addEventListener("selectionchange", memoizedSelectionHandler)
|
||||
flexContentDom?.addEventListener("mouseup", displayFormatterHandler)
|
||||
return () => {
|
||||
// flexContentDom?.removeEventListener("focus", memoizedSelectionHandler)
|
||||
docObj?.removeEventListener("selectionchange", memoizedSelectionHandler)
|
||||
flexContentDom?.removeEventListener("mouseup", displayFormatterHandler)
|
||||
}
|
||||
}, [winObj, docObj, memoizedSelectionHandler, flexContentRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (flexContentRef.current) {
|
||||
setTimeout(() => flexContentRef.current?.focus(), 10)
|
||||
}
|
||||
}, [flexContentRef])
|
||||
|
||||
const textConfigs =
|
||||
contentState &&
|
||||
Object.entries(contentState.contentRegistry as FlexContentType)
|
||||
textConfigs?.sort((t1, t2) => t1[1].order - t2[1].order)
|
||||
|
||||
const handleInput = (e: FormEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
const input = (e as unknown as InputEvent).data
|
||||
const selection = winObj?.getSelection()
|
||||
if (selection) {
|
||||
const range = selection.getRangeAt(0)
|
||||
const anchorText =
|
||||
(selection.anchorNode as HTMLElement).textContent + (input || "")
|
||||
const cursorPosition = selection.anchorOffset
|
||||
const startContainer = range.startContainer
|
||||
if (flexContentRef.current) {
|
||||
const childNodes = Array.from(flexContentRef.current.childNodes)
|
||||
childNodes.forEach((node) => {
|
||||
node.nodeType === Node.TEXT_NODE && node.remove()
|
||||
})
|
||||
}
|
||||
|
||||
if (contentState && activeChunkRef.current) {
|
||||
const content = contentState.contentRegistry
|
||||
const textConfig = content[activeChunkRef.current.parent]
|
||||
textConfig.text = input // TODO: Fix this value, might only capture chunk's text
|
||||
textConfig.cursorPosition = cursorPosition
|
||||
if (
|
||||
startContainer?.parentNode &&
|
||||
!(startContainer.parentNode as HTMLElement).classList.contains(
|
||||
"ar-FlexContent",
|
||||
)
|
||||
) {
|
||||
textConfig.startContainer = startContainer
|
||||
}
|
||||
const activeText =
|
||||
contentState.contentRegistry[activeChunkRef.current.parent]
|
||||
const activeChunk = activeText?.chunks[activeChunkRef.current.id]
|
||||
activeChunk.text = anchorText
|
||||
activeChunk.end = activeChunk.start + activeChunk.text.length
|
||||
updateContentState({ ...contentState })
|
||||
}
|
||||
onCommit && onCommit()
|
||||
keyRef.current = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const contentRenders = (
|
||||
<div
|
||||
ref={flexContentRef}
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
className="ar-FlexContent w-100"
|
||||
tabIndex={-1}
|
||||
onKeyDown={(e) => {
|
||||
const selection = winObj?.getSelection()
|
||||
if (selection && contentState) {
|
||||
const range = selection.getRangeAt(0)
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
splitChunkAtCursor(
|
||||
selection,
|
||||
contentState,
|
||||
activeChunkRef,
|
||||
updateContentState,
|
||||
)
|
||||
} else if (e.key === "Backspace") {
|
||||
if (range && range.startOffset > 0) {
|
||||
range.setStart(range.startContainer, range.startOffset - 1)
|
||||
range.deleteContents()
|
||||
}
|
||||
} else if (e.key === "Delete") {
|
||||
if (isMac || isIOS || isAndroid) {
|
||||
if (range.startOffset > 0) {
|
||||
range.setStart(range.startContainer, range.startOffset - 1)
|
||||
range.deleteContents()
|
||||
}
|
||||
} else {
|
||||
// On Windows, "Delete" key deletes character after the cursor
|
||||
if (
|
||||
range.startContainer.textContent &&
|
||||
range.startOffset < range.startContainer.textContent.length
|
||||
) {
|
||||
range.setEnd(range.startContainer, range.startOffset + 1)
|
||||
range.deleteContents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBeforeInput={handleInput}
|
||||
contentEditable={isEditable}
|
||||
suppressContentEditableWarning={true}
|
||||
>
|
||||
{textConfigs?.map((entry) => {
|
||||
const props: TextProps = {
|
||||
descriptor: entry[1] as TextInfo,
|
||||
allowFormatting,
|
||||
isEditable,
|
||||
demo,
|
||||
}
|
||||
return (
|
||||
<MemoWrapper
|
||||
key={`flex-content-item-${entry[0]}`}
|
||||
componentName={entry[1].name}
|
||||
repo={repo}
|
||||
props={props}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isEditable && allowFormatting && hasFloatingFormatter ? (
|
||||
<PopoverV2
|
||||
classes="align-self-start w-100 border-bottom"
|
||||
contentClasses="invert-bg"
|
||||
isOpen={formatterDisplayed}
|
||||
onClose={() => displayFormatter(false)}
|
||||
position={ArPopoverPositions.TOP}
|
||||
demo={demo}
|
||||
topOffset={16}
|
||||
popAtPointer
|
||||
hideMarker
|
||||
>
|
||||
{contentRenders}
|
||||
<TextFormatter
|
||||
activeFormats={activeChunkRef.current?.formats}
|
||||
demo={demo}
|
||||
slot={ArPopoverSlots.POPOVER}
|
||||
content={contentState}
|
||||
callback={(state) => {
|
||||
updateContentState(state)
|
||||
selectionInfoRef.current = null
|
||||
}}
|
||||
selectionInfo={selectionInfoRef.current}
|
||||
theme={theme}
|
||||
isFloating
|
||||
/>
|
||||
</PopoverV2>
|
||||
) : (
|
||||
contentRenders
|
||||
)}
|
||||
<hr />
|
||||
<div className="row">
|
||||
<div className="col-4">
|
||||
<h6>Content Registry</h6>
|
||||
<pre className="my-3">
|
||||
{JSON.stringify(
|
||||
contentState?.contentRegistry,
|
||||
(key, value) => {
|
||||
if (key.startsWith("__reactFiber")) {
|
||||
if (value.stateNode?.data) {
|
||||
return value.stateNode.data
|
||||
} else {
|
||||
return value.toString()
|
||||
}
|
||||
}
|
||||
if (key === "id" || key === "length") {
|
||||
return undefined
|
||||
}
|
||||
return value
|
||||
},
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<h6>Active Chunk (ref - no live update)</h6>
|
||||
<pre className="my-3">
|
||||
{JSON.stringify(activeChunkRef.current, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="col-4">
|
||||
<h6>Selection Info (ref - no live update)</h6>
|
||||
<pre className="my-3">
|
||||
{JSON.stringify(selectionInfoRef.current, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FlexContent
|
||||
39
packages/ui/src/TextFormatter.component.scss
Executable file
39
packages/ui/src/TextFormatter.component.scss
Executable file
@@ -0,0 +1,39 @@
|
||||
.ar-TextFormatter {
|
||||
max-height: 2.5rem;
|
||||
--color-tool-indicator-color: linear-gradient(to right, rgb(101, 84, 192) 25%, rgb(0, 184, 217) 25%, rgb(0, 184, 217) 50%, rgb(255, 153, 31) 50%, rgb(255, 153, 31) 75%, rgb(222, 53, 11) 75%);
|
||||
--color-tool-indicator-color-inactive: linear-gradient(to right, rgb(151, 160, 175) 25%, rgb(179, 186, 197) 25%, rgb(179, 186, 197) 50%, rgb(223, 225, 230) 50%, rgb(223, 225, 230) 75%, rgb(179, 186, 197) 75%);
|
||||
.ar-TextTool {
|
||||
.ar-TextTool__anchor {
|
||||
height: 2rem;
|
||||
min-width: 2rem;
|
||||
transition: background-color 0.3s;
|
||||
.ar-LoadableIcon {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
&.active:not(.enabled):hover {
|
||||
background-color: var(--ar-color-disabled-4-invert) !important;
|
||||
}
|
||||
&.enabled {
|
||||
background-color: #e9f2ff !important;
|
||||
}
|
||||
&.for-color .ar-TextTool__color-tool-indicator {
|
||||
background: var(--color-tool-indicator-color-inactive);
|
||||
width: 1rem;
|
||||
height: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&.for-color.active .ar-TextTool__color-tool-indicator {
|
||||
background: var(--color-tool-indicator-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.ar-ColorSelectorRadio__button {
|
||||
outline: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
outline: 1px solid #aeaeae;
|
||||
}
|
||||
}
|
||||
282
packages/ui/src/TextFormatter.tsx
Executable file
282
packages/ui/src/TextFormatter.tsx
Executable file
@@ -0,0 +1,282 @@
|
||||
import {
|
||||
ArThemes,
|
||||
TextFormatterTool,
|
||||
TextFormatterProps,
|
||||
TextFormat,
|
||||
} from "@armco/types"
|
||||
import { TextTool } from "../.."
|
||||
import "./TextFormatter.component.scss"
|
||||
|
||||
const editToolGroups: {
|
||||
[key: string]: {
|
||||
scope: 0 | 1 | 2
|
||||
popoverContentClasses?: string
|
||||
tools: Array<TextFormatterTool>
|
||||
}
|
||||
} = {
|
||||
fontStyle: {
|
||||
scope: 0,
|
||||
popoverContentClasses: "py-2 px-0",
|
||||
tools: [
|
||||
{
|
||||
name: "bold",
|
||||
label: "Bold",
|
||||
darkModeHoverColor: "#dadada",
|
||||
value: true,
|
||||
icon: "fa.FaBold",
|
||||
tooltip: "Bold",
|
||||
size: "0.9rem",
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
label: "Italic",
|
||||
darkModeHoverColor: "#dadada",
|
||||
value: true,
|
||||
icon: "fa.FaItalic",
|
||||
tooltip: "Oblique",
|
||||
size: "0.9rem",
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
label: "Underline",
|
||||
darkModeHoverColor: "#dadada",
|
||||
value: true,
|
||||
icon: "fa.FaUnderline",
|
||||
tooltip: "Underline",
|
||||
size: "0.9rem",
|
||||
},
|
||||
{
|
||||
name: "strikethrough",
|
||||
label: "Strikethrough",
|
||||
value: true,
|
||||
icon: "fa.FaStrikethrough",
|
||||
tooltip: "Strikethrough",
|
||||
size: "0.9rem",
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
label: "Code",
|
||||
value: true,
|
||||
icon: "fa.FaCode",
|
||||
tooltip: "Code",
|
||||
},
|
||||
{
|
||||
name: "scriptPlacement",
|
||||
label: "Subscript",
|
||||
value: "sub",
|
||||
icon: "fa.FaSubscript",
|
||||
tooltip: "Subscript",
|
||||
},
|
||||
{
|
||||
name: "scriptPlacement",
|
||||
label: "SuperScript",
|
||||
value: "sup",
|
||||
icon: "fa.FaSuperscript",
|
||||
tooltip: "Superscript",
|
||||
},
|
||||
],
|
||||
},
|
||||
size: {
|
||||
scope: 0,
|
||||
tools: [
|
||||
{
|
||||
name: "increase-size",
|
||||
icon: "ri.RiCharacterRecognitionFill",
|
||||
darkModeHoverColor: "#dadada",
|
||||
subIcon: "fa.FaLongArrowAltUp",
|
||||
size: "1.4rem",
|
||||
tooltip: "Bigger",
|
||||
},
|
||||
{
|
||||
name: "decrease-size",
|
||||
icon: "ri.RiCharacterRecognitionFill",
|
||||
darkModeHoverColor: "#dadada",
|
||||
subIcon: "fa.FaLongArrowAltDown",
|
||||
size: "0.9rem",
|
||||
tooltip: "Smaller",
|
||||
},
|
||||
],
|
||||
},
|
||||
color: {
|
||||
scope: 0,
|
||||
tools: [
|
||||
{
|
||||
name: "foreground",
|
||||
icon: "ri.RiCharacterRecognitionFill",
|
||||
darkModeHoverColor: "#dadada",
|
||||
subIcon: "md.MdKeyboardArrowDown",
|
||||
subIconSize: "1rem",
|
||||
tooltip: "Change Text Color",
|
||||
popover: "textColor",
|
||||
},
|
||||
{
|
||||
name: "background",
|
||||
icon: "md.MdEdit",
|
||||
darkModeHoverColor: "#dadada",
|
||||
subIcon: "md.MdKeyboardArrowDown",
|
||||
subIconSize: "1rem",
|
||||
tooltip: "Change Background Color",
|
||||
popover: "backgroundColor",
|
||||
},
|
||||
],
|
||||
},
|
||||
indentation: {
|
||||
scope: 1,
|
||||
popoverContentClasses: "py-2 px-0",
|
||||
tools: [
|
||||
{
|
||||
name: "left-align",
|
||||
label: "Align Left",
|
||||
icon: "fa.FaAlignLeft",
|
||||
darkModeHoverColor: "#dadada",
|
||||
tooltip: "Align Left",
|
||||
},
|
||||
{
|
||||
name: "center-align",
|
||||
label: "Align Center",
|
||||
icon: "fa.FaAlignCenter",
|
||||
darkModeHoverColor: "#dadada",
|
||||
tooltip: "Centre Text",
|
||||
},
|
||||
{
|
||||
name: "right-align",
|
||||
label: "Align Right",
|
||||
icon: "fa.FaAlignRight",
|
||||
darkModeHoverColor: "#dadada",
|
||||
tooltip: "Align Right",
|
||||
},
|
||||
{
|
||||
name: "increase-indent",
|
||||
label: "Increase Indent",
|
||||
icon: "im.ImIndentIncrease",
|
||||
tooltip: "Indent Right",
|
||||
},
|
||||
{
|
||||
name: "decrease-indent",
|
||||
label: "Decrease Indent",
|
||||
icon: "im.ImIndentDecrease",
|
||||
tooltip: "Indent Right",
|
||||
},
|
||||
],
|
||||
},
|
||||
clear: {
|
||||
scope: 0,
|
||||
tools: [
|
||||
{
|
||||
name: "clear",
|
||||
icon: "md.MdFormatClear",
|
||||
darkModeHoverColor: "#dadada",
|
||||
tooltip: "Clear Formatting",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const TextFormatter = (props: TextFormatterProps): JSX.Element => {
|
||||
const {
|
||||
activeFormats,
|
||||
content,
|
||||
callback,
|
||||
demo,
|
||||
isFloating,
|
||||
selectionInfo,
|
||||
theme,
|
||||
variant = 0,
|
||||
} = props
|
||||
|
||||
const otherTheme = theme === ArThemes.DARK1 ? ArThemes.LIGHT1 : ArThemes.DARK1
|
||||
|
||||
const toolRenders = (
|
||||
editTools: {
|
||||
scope: number
|
||||
popoverContentClasses?: string
|
||||
tools: Array<TextFormatterTool>
|
||||
},
|
||||
category: string,
|
||||
) => {
|
||||
const inScopeTools = editTools.tools
|
||||
.filter((tool) => !tool.scope || tool.scope <= variant)
|
||||
.map((t) => {
|
||||
t.onClick = () =>
|
||||
content &&
|
||||
selectionInfo &&
|
||||
applyTool(
|
||||
t.name as keyof TextFormat,
|
||||
t.value,
|
||||
selectionInfo,
|
||||
content,
|
||||
callback,
|
||||
)
|
||||
return t
|
||||
})
|
||||
let initialTools
|
||||
if (inScopeTools.length > 3) {
|
||||
initialTools = inScopeTools.slice(0, 2)
|
||||
initialTools.push({
|
||||
name: "more",
|
||||
icon: "md.MdMoreHoriz",
|
||||
darkModeHoverColor: "#dadada",
|
||||
tooltip: "View more",
|
||||
popover: "moreTools",
|
||||
popoverContentClasses: editTools.popoverContentClasses,
|
||||
})
|
||||
} else {
|
||||
initialTools = inScopeTools
|
||||
}
|
||||
|
||||
const initialRenders = initialTools.map((tool) => (
|
||||
<TextTool
|
||||
key={`text-tool-${tool.name}`}
|
||||
active={isFloating}
|
||||
bgClass={isFloating ? "invert-bg" : ""}
|
||||
category={category}
|
||||
demo={demo}
|
||||
formatter={() =>
|
||||
selectionInfo &&
|
||||
content &&
|
||||
tool.name !== "more" &&
|
||||
applyTool(
|
||||
tool.name as keyof TextFormat,
|
||||
tool.value,
|
||||
selectionInfo,
|
||||
content,
|
||||
callback,
|
||||
)
|
||||
}
|
||||
restTools={inScopeTools.length > 3 ? inScopeTools.slice(2) : []}
|
||||
tool={tool}
|
||||
theme={(isFloating ? theme : otherTheme) || ArThemes.LIGHT1}
|
||||
enabled={
|
||||
!!(activeFormats && activeFormats[tool.name as keyof TextFormat])
|
||||
}
|
||||
/>
|
||||
))
|
||||
return initialRenders
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`ar-TextFormatter py-1 invert-bg${
|
||||
isFloating ? " is-floating" : ""
|
||||
} ${isFloating ? "flex-center border-radius-l2" : "flex-v-center"}`}
|
||||
>
|
||||
{Object.entries(editToolGroups)
|
||||
.filter((entry) => entry[1].scope <= variant)
|
||||
.map(([category, editTools], index, arr) => {
|
||||
return (
|
||||
<div
|
||||
key={`tools-group-${index}`}
|
||||
className={`ar-TextFormatter__group flex-v-center${
|
||||
index < arr.length - 1 ? " border-right" : ""
|
||||
} px-1`}
|
||||
>
|
||||
{toolRenders(editTools, category)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
.filter((g) => g)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextFormatter
|
||||
131
packages/ui/src/TextTool.tsx
Normal file
131
packages/ui/src/TextTool.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
ArPopoverSlots,
|
||||
ArThemes,
|
||||
ListItemContent,
|
||||
TextToolProps,
|
||||
} from "@armco/types"
|
||||
import {
|
||||
ColorSelectorRadio,
|
||||
List,
|
||||
LoadableIcon,
|
||||
PopoverV2,
|
||||
Tooltip,
|
||||
} from "../.."
|
||||
|
||||
const popovers = (restTools?: Array<ListItemContent>, theme?: ArThemes) => ({
|
||||
textColor: (
|
||||
<ColorSelectorRadio variant="text" slot={ArPopoverSlots.POPOVER} />
|
||||
),
|
||||
backgroundColor: (
|
||||
<ColorSelectorRadio variant="background" slot={ArPopoverSlots.POPOVER} />
|
||||
),
|
||||
moreTools: (
|
||||
<List
|
||||
data={restTools || []}
|
||||
itemVariant="soft"
|
||||
itemClasses="px-3"
|
||||
slot={ArPopoverSlots.POPOVER}
|
||||
theme={theme}
|
||||
invertBg
|
||||
/>
|
||||
),
|
||||
})
|
||||
|
||||
const TextTool = ({
|
||||
active,
|
||||
bgClass,
|
||||
category,
|
||||
demo,
|
||||
enabled,
|
||||
formatter,
|
||||
restTools,
|
||||
tool,
|
||||
theme,
|
||||
}: TextToolProps) => {
|
||||
const {
|
||||
color,
|
||||
darkModeColor,
|
||||
name,
|
||||
icon,
|
||||
subIcon,
|
||||
tooltip,
|
||||
size,
|
||||
subIconSize,
|
||||
paddingClass,
|
||||
popover,
|
||||
popoverContentClasses,
|
||||
} = tool
|
||||
const [hovered, setHovered] = useState<boolean>()
|
||||
|
||||
const iconColor = active
|
||||
? enabled
|
||||
? "#0c66e4"
|
||||
: theme === ArThemes.DARK1
|
||||
? darkModeColor
|
||||
: color || "white"
|
||||
: "#929292"
|
||||
|
||||
const toolRenderWithTooltip = (
|
||||
<Tooltip demo={demo} slot={ArPopoverSlots.ANCHOR}>
|
||||
<span
|
||||
className={`ar-TextTool ${theme || ArThemes.LIGHT1}`}
|
||||
slot={ArPopoverSlots.ANCHOR}
|
||||
onClick={(e) => {
|
||||
// e.preventDefault()
|
||||
// e.stopPropagation()
|
||||
formatter && formatter()
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`ar-TextTool__anchor d-inline-flex border-radius-l2 flex-center ${
|
||||
paddingClass || "p-1"
|
||||
}${hovered ? " hovered" : ""}${
|
||||
category === "color" ? " for-color" : ""
|
||||
}${active ? " active cursor-pointer" : ""}${
|
||||
bgClass ? " " + bgClass : ""
|
||||
}${enabled ? " enabled" : ""}`}
|
||||
key={"text-formatter-tool-" + name}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<span>
|
||||
<LoadableIcon
|
||||
size={(size || "1rem") as string}
|
||||
icon={icon}
|
||||
color={iconColor}
|
||||
/>
|
||||
{category === "color" && (
|
||||
<div className="ar-TextTool__color-tool-indicator" />
|
||||
)}
|
||||
</span>
|
||||
{subIcon && (
|
||||
<LoadableIcon
|
||||
size={subIconSize || "0.8rem"}
|
||||
icon={subIcon}
|
||||
color={iconColor}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span slot={ArPopoverSlots.POPOVER}>{tooltip}</span>
|
||||
</Tooltip>
|
||||
)
|
||||
return popover && active ? (
|
||||
<PopoverV2
|
||||
demo={demo}
|
||||
contentClasses={popoverContentClasses}
|
||||
invertBg
|
||||
hideMarker
|
||||
preserveMarkerSpace
|
||||
closeOnSelfClick
|
||||
>
|
||||
{toolRenderWithTooltip}
|
||||
{popovers(restTools, theme)[popover as "textColor" | "backgroundColor"]}
|
||||
</PopoverV2>
|
||||
) : (
|
||||
toolRenderWithTooltip
|
||||
)
|
||||
}
|
||||
|
||||
export default TextTool
|
||||
8
packages/ui/src/index.scss
Normal file
8
packages/ui/src/index.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.ar-FlexContent {
|
||||
min-height: 2.5rem;
|
||||
line-height: 1.2rem;
|
||||
font-family: "Noto Sans Chakma", system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
28
packages/ui/tsconfig.json
Normal file
28
packages/ui/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "build",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"jsx": "react-jsx",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"target": "es5",
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["build", "plop-templates", "node_modules", "src/**/*.test.*", "src/**/*.spec.*", "src/stories", "scripts"]
|
||||
}
|
||||
Reference in New Issue
Block a user