Backing up

This commit is contained in:
2025-09-16 00:31:37 +05:30
parent 2fe251dfc4
commit d6146bc820
68 changed files with 1574 additions and 31058 deletions

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" ar-theme="th-dark-1">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
@@ -8,6 +8,7 @@
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="root" class="vh-100"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

30377
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,21 +7,12 @@
"packages/*"
],
"scripts": {
"dev": "vite",
"start": "vite",
"build": "tsc && vite build",
"generate": "plop",
"atom": "plop atom",
"molecule": "plop molecule",
"component": "plop component",
"page": "plop page",
"preview": "vite preview",
"test": "vitest",
"format": "prettier --write .",
"lint": "eslint .",
"type-check": "tsc"
"dev": "vite --config vite-run.config.ts",
"start": "serve -s build",
"build": "./build-tools/build.sh",
"build:sm": "./build-tools/build.sh --dev"
},
"dependencies": {
"peerDependencies": {
"@reduxjs/toolkit": "^1.8.1",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
@@ -29,32 +20,7 @@
"react-dom": "^18.2.0",
"react-redux": "^8.0.1",
"react-router-dom": "^6.13.0",
"uuid": "^10.0.0"
},
"devDependencies": {
"@armco/types": "^0.0.6",
"@testing-library/dom": "^9.2.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.2.5",
"@types/moment": "^2.13.0",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/react-table": "^7.7.20",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.0.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-prettier": "^4.2.1",
"jsdom": "^21.1.0",
"plop": "^3.1.2",
"prettier": "^2.7.1",
"prettier-config-nick": "^1.0.2",
"sass": "^1.63.4",
"typescript": "^5.0.2",
"vite": "^4.0.0",
"vitest": "^0.30.1"
"uuid": "^9.0.0"
},
"eslintConfig": {
"extends": [

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -7,8 +7,8 @@
"test": "jest"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"jest": "^26.0.0"

View File

@@ -0,0 +1 @@
export * from "./utils";

View File

@@ -0,0 +1,21 @@
export function debounce(func, wait, immediate) {
if (immediate === void 0) { immediate = false; }
var timeout;
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var later = function () {
timeout = undefined;
if (!immediate) {
func.apply(void 0, args);
}
};
clearTimeout(timeout);
if (immediate && !timeout) {
func.apply(void 0, args);
}
timeout = setTimeout(later, wait || 1000);
};
}

View File

@@ -0,0 +1 @@
export * from "./helper";

3
packages/event-manager/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
build
handlers

View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Get the directory of the current script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
DEV_FLAG=""
# Parse arguments
for arg in "$@"
do
case $arg in
--dev)
DEV_FLAG="--dev"
shift # Remove --dev from processing
;;
esac
done
echo "[BUILD:SH] Dev flag is: $DEV_FLAG"
echo "[BUILD:SH] Removing build if exists"
rm -rf build
echo "[BUILD:SH] Checking TS Types"
npx tsc
echo "[BUILD:SH] Initiating build..."
# Conditionally use vite-dev.config.ts if --dev flag is present
if [ "$DEV_FLAG" == "--dev" ]; then
vite build --config vite-dev.config.ts
else
vite build
fi
echo "[BUILD:SH] Running post processor scripts..."
# Run Post processors: Update style imports in .js files, create component modules
node "$SCRIPT_DIR/post-processor.js" build/cjs $DEV_FLAG
node "$SCRIPT_DIR/post-processor.js" build/es $DEV_FLAG

View File

@@ -0,0 +1,31 @@
import { promises as fs } from "fs"
import { dirname, resolve } from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
async function generateModule(fileName, isDev) {
if (fileName === "handlers.js") {
const dir = fileName.slice(0, -3)
const name = `@sampadak/event-manager/${dir}`
const packageJsonContent = {
name,
main: `../${isDev ? "build/" : ""}cjs/${dir}.js`,
module: `../${isDev ? "build/" : ""}es/${dir}.js`,
types: `../${isDev ? "build/" : ""}types/${dir}.d.ts`,
}
const dirPath = resolve(__dirname, `../${isDev ? "" : "build/"}${dir}`)
try {
await fs.mkdir(dirPath, { recursive: true })
await fs.writeFile(
resolve(dirPath, "package.json"),
JSON.stringify(packageJsonContent, null, 2),
)
} catch (error) {
console.error(`Error processing directory ${dirPath}:`, error)
}
}
}
export default generateModule

View File

@@ -0,0 +1,24 @@
import { readdir } from "fs/promises"
import generateModule from "./generate-module.js"
async function postProcessor(dir, isDev) {
try {
const files = await readdir(dir)
await Promise.all(
files.map(async (file) => {
await generateModule(file, isDev)
}),
)
} catch (error) {
console.error(`Error processing directory ${dir}:`, error)
}
}
const targetDir = process.argv[2]
if (targetDir) {
postProcessor(targetDir, process.argv.includes("--dev"))
} else {
console.error("Please provide the build directory to run post processor on.")
process.exit(1)
}

View File

@@ -1,10 +1,15 @@
{
"name": "@sampadak/event-manager",
"version": "1.0.0",
"main": "src/index.tsx",
"type": "module",
"types": "./build/types/index.d.ts",
"main": "./build/cjs/index.js",
"module": "./build/es/index.js",
"scripts": {
"build": "tsc",
"test": "jest"
"dev": "vite --config vite-run.config.ts",
"start": "serve -s build",
"build": "./build-tools/build.sh",
"build:sm": "./build-tools/build.sh --dev"
},
"devDependencies": {
"jest": "^26.0.0"

View File

@@ -1,30 +1,118 @@
import { MutableRefObject } from "react"
import { ChunkInfo, FlexContentType, FunctionType, TextInfo } from "@armco/types"
import { calculateOffset } from "@sampadak/selection-manager"
import {
Dispatch,
FormEvent,
KeyboardEvent,
MutableRefObject,
RefObject,
SetStateAction,
} from "react"
import { v4 as uuid } from "uuid"
import {
ArEditorEvents,
ChunkInfo,
EventHandler,
EventHandlerCollection,
EventHandlers,
FlexContentState,
FlexContentType,
FunctionType,
TextInfo,
} from "@armco/types"
import {
calculateOffset,
getNormalizedCursorPosition,
} from "@sampadak/selection-manager"
import { applyTool } from "@sampadak/formatter"
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)
export const shortcuts: { [key: string]: ArEditorEvents } = {
b: ArEditorEvents.BOLD, // Bold
i: ArEditorEvents.ITALIC, // Italic
u: ArEditorEvents.UNDERLINE, // Underline
s: ArEditorEvents.STRIKETHROUGH, // Strikethrough
z: ArEditorEvents.SUBSCRIPT, // Subscript
y: ArEditorEvents.SUPERSCRIPT, // Superscript
}
export const onKeyDown = (
e: KeyboardEvent<HTMLDivElement>,
state: FlexContentState,
activeChunkRef: RefObject<ChunkInfo>,
) => {
if (DEFAULT_PREVENTERS.includes(e.key)) e.preventDefault()
if (e.key === "Enter") {
return (
eventHandlers[ArEditorEvents.ENTER] as EventHandlerCollection
).enterHandler(state, activeChunkRef)
} else if (e.key === "Backspace") {
return (
eventHandlers[ArEditorEvents.BACKSPACE] as EventHandlerCollection
).backspaceHandler(state, activeChunkRef)
} else if (e.key === "Delete") {
return (
eventHandlers[ArEditorEvents.DELETE] as EventHandlerCollection
).deleteHandler(state, activeChunkRef)
} else if ((e.ctrlKey || e.metaKey) && shortcuts[e.key.toUpperCase()]) {
return (eventHandlers[ArEditorEvents.FORMAT] as EventHandler)(state)
} else {
return state
}
}
// Input event is handled in before input, since by the time input is called, some of the required pre-processing is lost
// hence cursorPosition is outdated
// and should be incremented by length of added text
export const handleInput = (
input: string,
state: FlexContentState,
activeChunkRef: RefObject<ChunkInfo>,
) => {
const { registry, cursorPosition } = state
const selection = window.getSelection()
if (selection) {
const range = selection.getRangeAt(0)
let anchorText = (selection.anchorNode as HTMLElement).textContent
anchorText =
anchorText?.slice(0, cursorPosition) +
input +
anchorText?.slice(cursorPosition)
state.cursorPosition = (cursorPosition || 0) + 1
const startContainer = range.startContainer
if (registry && activeChunkRef.current) {
const textConfig = registry[activeChunkRef.current.parent]
textConfig.text = anchorText // TODO: Fix this value, might only capture chunk's text
if (
startContainer?.parentNode &&
!(startContainer.parentNode as HTMLElement).classList.contains(
"ar-FlexContent",
)
) {
textConfig.startContainer = startContainer
}
const activeText = registry[activeChunkRef.current.parent]
const activeChunk = activeText?.chunks[activeChunkRef.current.id]
activeChunk.text = anchorText
activeChunk.end = activeChunk.start + activeChunk.text.length
}
return state
}
}
export const splitChunkAtCursor = (
selection: Selection,
contentState: {
entireSelectionFormatted?: boolean
contentRegistry: FlexContentType
},
contentRegistry: FlexContentType,
activeChunkRef: MutableRefObject<ChunkInfo | undefined>,
updateContentState: FunctionType,
) => {
if (!activeChunkRef.current || !contentState?.contentRegistry) return
const selection = window.getSelection()
if (!activeChunkRef.current || !contentRegistry || !selection) return
const cursorPosition = selection.anchorOffset
const normalizedCursorPosition = getNormalizedCursorPosition()
const activeChunk = activeChunkRef.current
const activeTextConfig = contentState.contentRegistry[
activeChunk.parent
] as TextInfo
const normalizedCursorPosition =
selection.anchorNode &&
calculateOffset(
selection.getRangeAt(0),
selection.anchorNode,
cursorPosition,
)
const activeTextConfig = contentRegistry[activeChunk.parent] as TextInfo
const newTextConfigId = uuid()
const order = activeTextConfig.order + 1
const newTextConfig: TextInfo = {
@@ -63,12 +151,123 @@ export const splitChunkAtCursor = (
newTextConfig.chunks[chunkId] = activeTextConfig.chunks[chunkId]
delete activeTextConfig.chunks[chunkId]
}
const textConfigs = Object.values(contentState.contentRegistry)
const textConfigs = Object.values(contentRegistry)
textConfigs.forEach((textConfig) => {
textConfig.order >= order && textConfig.order++
})
// Update the content state
contentState.contentRegistry[newTextConfigId] = newTextConfig
updateContentState({ ...contentState })
}
contentRegistry[newTextConfigId] = newTextConfig
return contentRegistry
}
// Define event handlers
export const eventHandlers: EventHandlers = {
[ArEditorEvents.CLICK]: {
handler1: () => console.log("Handler 1 for Click event triggered!"),
},
[ArEditorEvents.INPUT]: {
handleInput,
},
[ArEditorEvents.KEYDOWN]: {
onKeyDown,
},
[ArEditorEvents.ENTER]: {
enterHandler: (state, activeChunkRef) => {
return splitChunkAtCursor(state.registry, activeChunkRef) || state
},
},
[ArEditorEvents.BACKSPACE]: {
backspaceHandler: (state, activeChunkRef) => {
const { registry, cursorPosition } = state
const selection = window.getSelection()
if (selection) {
const range = selection.getRangeAt(0)
let anchorText = (selection.anchorNode as HTMLElement).textContent
anchorText =
anchorText &&
anchorText?.slice(0, cursorPosition - 1) +
anchorText?.slice(cursorPosition)
state.cursorPosition -= 1
const startContainer = range.startContainer
if (registry && activeChunkRef.current) {
const textConfig = registry[activeChunkRef.current.parent]
textConfig.text = anchorText // TODO: Fix this value, might only capture chunk's text
if (
startContainer?.parentNode &&
!(startContainer.parentNode as HTMLElement).classList.contains(
"ar-FlexContent",
)
) {
textConfig.startContainer = startContainer
}
const activeText = registry[activeChunkRef.current.parent]
const activeChunk = activeText?.chunks[activeChunkRef.current.id]
activeChunk.text = anchorText
activeChunk.end = activeChunk.start + activeChunk.text.length
}
return state
}
},
},
[ArEditorEvents.DELETE]: {
deleteHandler: (state) => {
const range = window.getSelection()?.getRangeAt(0)
if (range) {
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()
}
}
}
return state
},
},
[ArEditorEvents.FORMAT]: (contentState) => {
console.log("Formatting event triggered!")
return contentState
},
[ArEditorEvents.BOLD]: {
handler: ({ selectionInfoRef, contentState }) => {
selectionInfoRef.current &&
contentState &&
applyTool(
"bold",
true,
selectionInfoRef.current,
contentState,
(state) => {
selectionInfoRef.current = null
return state
},
)
},
},
[ArEditorEvents.ITALIC]: {
handler: () => console.log("Italic handler triggered!"),
},
[ArEditorEvents.UNDERLINE]: {
handler: () => console.log("Underline handler triggered!"),
},
[ArEditorEvents.STRIKETHROUGH]: {
handler: () => console.log("Strikethrough handler triggered!"),
},
[ArEditorEvents.SUBSCRIPT]: {
handler: () => console.log("Subscript handler triggered!"),
},
[ArEditorEvents.SUPERSCRIPT]: {
handler: () => console.log("Superscript handler triggered!"),
},
// Add more handlers as needed
}

View File

@@ -1,66 +1,62 @@
import { EventHandlerOptions, EventHandlers } from "./types"
import {
ArEditorEvents,
EventHandlerCollection,
EventHandlerOptions,
} from "@armco/types"
import { eventHandlers, shortcuts } from "./handlers"
// Define event types
enum ArEventTypes {
CLICK = "click",
INPUT = "input",
SELECTIONCHANGE = "selectionchange",
KEYDOWN = "keydown",
BOLD = "bold",
ITALIC = "italic",
UNDERLINE = "underline",
STRIKETHROUGH = "strikethrough",
SUBSCRIPT = "subscript",
SUPERSCRIPT = "superscript",
}
// Define event handlers
const eventHandlers: EventHandlers = {
[ArEventTypes.CLICK]: {
handler1: () => console.log("Handler 1 for Click event triggered!"),
handler2: () => console.log("Handler 2 for Click event triggered!"),
},
[ArEventTypes.INPUT]: {
handler1: () => console.log("Handler 1 for Input event triggered!"),
handler2: () => console.log("Handler 2 for Input event triggered!"),
},
[ArEventTypes.KEYDOWN]: {
handler1: () => console.log("Handler 1 for Keydown event triggered!"),
handler2: () => console.log("Handler 2 for Keydown event triggered!"),
},
[ArEventTypes.BOLD]: {
handler: () => console.log("Bold handler triggered!"),
},
[ArEventTypes.ITALIC]: {
handler: () => console.log("Italic handler triggered!"),
},
[ArEventTypes.UNDERLINE]: {
handler: () => console.log("Underline handler triggered!"),
},
// Add more handlers as needed
}
// Define keyboard shortcut handlers
const macShortcuts: { [key: string]: ArEventTypes } = {
"cmd+b": ArEventTypes.BOLD, // Bold
"cmd+i": ArEventTypes.ITALIC, // Italic
"cmd+u": ArEventTypes.UNDERLINE, // Underline
"cmd+s": ArEventTypes.STRIKETHROUGH, // Strikethrough
"cmd+z": ArEventTypes.SUBSCRIPT, // Subscript
"cmd+y": ArEventTypes.SUPERSCRIPT, // Superscript
}
const windowsShortcuts: { [key: string]: ArEventTypes } = {
"ctrl+b": ArEventTypes.BOLD, // Bold
"ctrl+i": ArEventTypes.ITALIC, // Italic
"ctrl+u": ArEventTypes.UNDERLINE, // Underline
"ctrl+s": ArEventTypes.STRIKETHROUGH, // Strikethrough
"ctrl+z": ArEventTypes.SUBSCRIPT, // Subscript
"ctrl+y": ArEventTypes.SUPERSCRIPT, // Superscript
}
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform)
/**
* Utility designed to manage and hook various event handlers and keyboard shortcuts in a web application.
* It provides a structured way to define, include, and exclude event handlers based on specific criteria.
*
* **Event Hooking**: The `hook` method allows for the inclusion and exclusion of specific event handlers based on provided options (`EventHandlerOptions`).
* It processes the include and exclude options to determine which handlers should be hooked.
*
* **Include and Exclude Options**:
*
* - **Include Options**: Specifies which event handlers to include by providing an array of event types and handler names.
* - **Exclude Options**: Specifies which event handlers to exclude by providing an array of event types and handler names.
* Handlers present in both include and exclude lists are excluded, with a warning logged to the console.
*
* **Event Handlers**: Event handlers are hooked to the document based on the processed include and exclude lists.
* Handlers are defined in a collection (`eventHandlers`) and are hooked to their respective event types.
*
* **Platform-Specific Shortcuts**:
*
* The utility detects the user's platform (Mac or Windows) and hooks the appropriate keyboard shortcuts.
* Keyboard shortcuts are mapped to specific event types and are hooked to the document (DOCUMENT ONLY).
* When a keyboard shortcut is detected, the corresponding event handler is triggered, and the default action for the shortcut is prevented.
*
* **Including Event Handlers**: Specify which event handlers to include by providing an `include` option with the event type and handler names.
*
* **Excluding Event Handlers**: Specify which event handlers to exclude by providing an `exclude` option with the event type and handler names.
*
* **Hooking Event Handlers**: Call the `hook` method with the desired options to hook the event handlers and keyboard shortcuts.
*
* To apply default formatting handlers like bold, italic, underline etc., don't pass options
*
* @example
* ```typescript
* import EventsManager, { ArEventTypes } from './index';
*
* EventsManager.hook({
* include: [
* { eventType: ArEventTypes.CLICK, handlerNames: ['handler1'] },
* { eventType: ArEventTypes.INPUT, handlerNames: ['handler2'] },
* ],
* exclude: [
* { eventType: ArEventTypes.KEYDOWN, handlerNames: ['handler1'] },
* ],
* });
* ```
*/
class EventsManager {
public static hook(options?: EventHandlerOptions) {
public static hook(
options?: EventHandlerOptions | null,
dependencies?: { [key: string]: any },
) {
const includedHandlers = new Set<string>()
const excludedHandlers = new Set<string>()
@@ -95,30 +91,24 @@ class EventsManager {
if (excludedHandlers.has(handlerKey)) {
continue
}
document.addEventListener(eventType, handler)
// document.addEventListener(eventType, () => handler(dependencies))
}
}
// Determine platform and hook keyboard shortcut handlers
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform)
const shortcuts = isMac ? macShortcuts : windowsShortcuts
document.addEventListener("keydown", (event) => {
const key = `${isMac ? "cmd" : "ctrl"}+${event.key.toLowerCase()}`
const eventType = shortcuts[key]
if (eventType && !excludedHandlers.has(`${eventType}:handler`)) {
if (
includedHandlers.size === 0 ||
includedHandlers.has(`${eventType}:handler`)
) {
eventHandlers[eventType].handler()
event.preventDefault() // Prevent default action for the shortcut
}
}
})
// DEFAULT HANDLERS: Hook formatting handler (single keydown handler that checks key press and
// calls corresponding handler for eg. for bold and italic formatting)
// document.addEventListener("keydown", (event) => {
// if (event.ctrlKey || event.metaKey) {
// const eventType = shortcuts[event.key.toLowerCase()]
// if (eventType && eventHandlers[eventType]) {
// event.preventDefault() // Prevent default action for the shortcut
// ;(eventHandlers[eventType] as EventHandlerCollection).handler(
// dependencies,
// )
// }
// }
// })
}
}
// Export the module
export default EventsManager
export { ArEventTypes }

View File

@@ -1,10 +0,0 @@
export type EventHandlerOptions = {
include?: { eventType: ArEventTypes; handlerNames: string[] }[]
exclude?: { eventType: ArEventTypes; handlerNames: string[] }[]
}
export interface EventHandlers {
[key: string]: {
[handlerName: string]: () => void
}
}

View File

@@ -0,0 +1,27 @@
{
"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"]
}

View File

@@ -0,0 +1,41 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|Test).{ts,tsx}")),
},
sourcemap: true,
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -0,0 +1,20 @@
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
open: true,
},
build: {
outDir: "build",
sourcemap: true,
},
test: {
globals: true,
environment: "jsdom",
setupFiles: "src/setupTests",
mockReset: true,
},
})

View File

@@ -0,0 +1,40 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|Test).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Get the directory of the current script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
DEV_FLAG=""
# Parse arguments
for arg in "$@"
do
case $arg in
--dev)
DEV_FLAG="--dev"
shift # Remove --dev from processing
;;
esac
done
echo "[BUILD:SH] Dev flag is: $DEV_FLAG"
echo "[BUILD:SH] Removing build if exists"
rm -rf build
echo "[BUILD:SH] Checking TS Types"
npx tsc
echo "[BUILD:SH] Initiating build..."
# Conditionally use vite-dev.config.ts if --dev flag is present
if [ "$DEV_FLAG" == "--dev" ]; then
vite build --config vite-dev.config.ts
else
vite build
fi

View File

@@ -1,14 +1,18 @@
{
"name": "@sampadak/formatter",
"version": "1.0.0",
"main": "build/index.js",
"types": "./build/types/index.d.ts",
"main": "./build/cjs/index.js",
"module": "./build/es/index.js",
"scripts": {
"build": "rm -rf build && tsc",
"test": "jest"
"dev": "vite --config vite-run.config.ts",
"start": "serve -s build",
"build": "./build-tools/build.sh",
"build:sm": "./build-tools/build.sh --dev"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"jest": "^26.0.0"

View File

@@ -62,10 +62,7 @@ export const applyTool = (
format: keyof TextFormat,
formatValue: string | boolean | number | undefined,
selectionInfo: SelectionInfo,
contentState: {
entireSelectionFormatted?: boolean
contentRegistry: FlexContentType
},
contentState: FlexContentType,
callback: FunctionType,
) => {
if (selectionInfo) {
@@ -83,7 +80,7 @@ export const applyTool = (
selectionInfo,
contentStateClone.contentRegistry,
)
contentStateClone.entireSelectionFormatted = entireSelectionFormatted
// contentStateClone.entireSelectionFormatted = entireSelectionFormatted
selectedNodes?.forEach((id: string) => {
const currentConfig = contentStateClone.contentRegistry[id]

View File

@@ -1,9 +1,25 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"noEmit": false,
"outDir": "build"
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"downlevelIteration": true,
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}

View File

@@ -0,0 +1,42 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|Test).{ts,tsx}")),
},
sourcemap: true,
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -0,0 +1,20 @@
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
open: true,
},
build: {
outDir: "build",
sourcemap: true,
},
test: {
globals: true,
environment: "jsdom",
setupFiles: "src/setupTests",
mockReset: true,
},
})

View File

@@ -0,0 +1,41 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|Test).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -0,0 +1,27 @@
{
"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"]
}

View File

@@ -0,0 +1,27 @@
{
"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"]
}

View File

@@ -7,8 +7,8 @@
"test": "jest"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"jest": "^26.0.0"

View File

@@ -0,0 +1,27 @@
{
"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"]
}

View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Get the directory of the current script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
DEV_FLAG=""
# Parse arguments
for arg in "$@"
do
case $arg in
--dev)
DEV_FLAG="--dev"
shift # Remove --dev from processing
;;
esac
done
echo "[BUILD:SH] Dev flag is: $DEV_FLAG"
echo "[BUILD:SH] Removing build if exists"
rm -rf build
echo "[BUILD:SH] Checking TS Types"
npx tsc
echo "[BUILD:SH] Initiating build..."
# Conditionally use vite-dev.config.ts if --dev flag is present
if [ "$DEV_FLAG" == "--dev" ]; then
vite build --config vite-dev.config.ts
else
vite build
fi
cp src/types.d.ts build/types

View File

@@ -1,11 +1,12 @@
{
"name": "@sampadak/selection-manager",
"version": "1.0.0",
"main": "build/index.js",
"types": "build/index.d.ts",
"main": "build/cjs/index.js",
"module": "build/es/index.js",
"types": "build/types/index.d.ts",
"scripts": {
"build": "rm -rf build && tsc && cp src/types.d.ts build",
"test": "jest"
"build": "./build-tools/build.sh",
"build:sm": "./build-tools/build.sh --dev"
},
"devDependencies": {
"jest": "^26.0.0"

View File

@@ -3,26 +3,14 @@ import { ISelectionManager } from "./types"
import { processCaret, processRange } from "./util"
class SelectionManager {
static getSelection(
contentRegistry: FlexContentType,
doc?: Document,
win?: Window,
atomicContentSelector?: string,
): SelectionInfo | null {
const docObj = doc || document
const selection = (win || window).getSelection()
atomicContentSelector = atomicContentSelector || "ar-Text"
static getSelection(contentRegistry: FlexContentType): SelectionInfo | null {
const selection = window.getSelection()
if (selection) {
if (selection.type === "Range") {
return processRange(
selection,
docObj,
contentRegistry,
atomicContentSelector,
)
} else if (selection.type === "Caret" && contentRegistry) {
return processCaret(selection, contentRegistry, atomicContentSelector)
}
// if (selection.type === "Range") {
// return processRange(docObj, contentRegistry, atomicContentSelector)
// } else if (selection.type === "Caret" && contentRegistry) {
// return processCaret(contentRegistry, atomicContentSelector)
// }
}
return null
}
@@ -44,4 +32,4 @@ class SelectionManager {
}
export default SelectionManager as ISelectionManager
export * from "./util"
export * from "./util"

View File

@@ -1,22 +1,59 @@
import { FlexContentType, SelectionInfo, TextInfo } from "@armco/types"
export const getTextContainerId = (
node: Node | HTMLElement,
atomicContentSelector: string,
) => {
export const getTextContainerId = (node: Node | HTMLElement) => {
const textElementNode =
node.nodeType === Node.TEXT_NODE
? node.parentElement?.closest(atomicContentSelector)
? node.parentElement?.closest(".ar-Text")
: node
return (textElementNode as HTMLElement)?.id
}
export function getActiveTextAndChunk(
contentRegistry: FlexContentType,
normalizedCursorPosition: number | null,
) {
const anchorNode = window.getSelection()?.anchorNode
const activeText =
anchorNode && contentRegistry[getTextContainerId(anchorNode)]
let activeChunk
if (activeText?.chunks && normalizedCursorPosition !== null) {
activeChunk = Object.values((activeText as TextInfo).chunks).find(
(chunk) =>
chunk.start <= normalizedCursorPosition &&
chunk.end >= normalizedCursorPosition,
)
} else {
// activeChunk
}
// if (!activeChunk) {
console.log(contentRegistry)
console.log(normalizedCursorPosition)
// }
return { activeText, activeChunk }
}
export function getCursorPosition() {
return window.getSelection()?.anchorOffset || 0
}
export function getNormalizedCursorPosition() {
const selection = window.getSelection()
if (selection?.anchorNode) {
return calculateOffset(
selection.getRangeAt(0),
selection.anchorNode,
selection.anchorOffset,
)
}
return null
}
export const processRange = (
selection: Selection,
docObj: Document,
contentRegistry: FlexContentType,
atomicContentSelector: string,
): SelectionInfo => {
const selection = window.getSelection()
const selectionInfo: SelectionInfo = {
startOffset: -1,
endOffset: -1,
@@ -28,54 +65,50 @@ export const processRange = (
raw: selection,
}
const range = selection.getRangeAt(0)
const content = range.cloneContents()
if (selection) {
const range = selection.getRangeAt(0)
const content = range.cloneContents()
if (
content.childNodes.length === 1 &&
content.firstChild?.nodeType === Node.TEXT_NODE
) {
const parent = range.startContainer.parentElement
?.closest(atomicContentSelector)
?.cloneNode(true)
parent && content.replaceChild(parent, content.firstChild)
}
if (
content.childNodes.length === 1 &&
content.firstChild?.nodeType === Node.TEXT_NODE
) {
const parent = range.startContainer.parentElement
?.closest(atomicContentSelector)
?.cloneNode(true)
parent && content.replaceChild(parent, content.firstChild)
}
const startParentId = getTextContainerId(
range.startContainer,
atomicContentSelector,
)
const endParentId = getTextContainerId(
range.endContainer,
atomicContentSelector,
)
const startParentId = getTextContainerId(range.startContainer)
const endParentId = getTextContainerId(range.endContainer)
if (startParentId && endParentId) {
const startOffset = calculateOffset(
range,
range.startContainer,
range.startOffset,
)
const endOffset = calculateOffset(
range,
range.endContainer,
range.endOffset,
)
if (startParentId && endParentId) {
const startOffset = calculateOffset(
range,
range.startContainer,
range.startOffset,
)
const endOffset = calculateOffset(
range,
range.endContainer,
range.endOffset,
)
// Update selectionInfo properties
selectionInfo.startParentId = startParentId
selectionInfo.endParentId = endParentId
selectionInfo.startOffset = startOffset
selectionInfo.endOffset = endOffset
// Update selectionInfo properties
selectionInfo.startParentId = startParentId
selectionInfo.endParentId = endParentId
selectionInfo.startOffset = startOffset
selectionInfo.endOffset = endOffset
const walker = docObj.createTreeWalker(content, NodeFilter.SHOW_ELEMENT)
const walker = docObj.createTreeWalker(content, NodeFilter.SHOW_ELEMENT)
while (walker.nextNode()) {
const currNode = walker.currentNode as HTMLElement
const node = (currNode?.closest(atomicContentSelector) ||
range.commonAncestorContainer) as HTMLElement
if (node) {
selectionInfo.selectedNodes.add(node.id)
while (walker.nextNode()) {
const currNode = walker.currentNode as HTMLElement
const node = (currNode?.closest(atomicContentSelector) ||
range.commonAncestorContainer) as HTMLElement
if (node) {
selectionInfo.selectedNodes.add(node.id)
}
}
}
}
@@ -84,65 +117,61 @@ export const processRange = (
}
export const processCaret = (
selection: Selection,
contentRegistry: FlexContentType,
atomicContentSelector: string, // Eg. .ar-Text
): SelectionInfo | null => {
if (selection.anchorNode) {
const selectionInfo: SelectionInfo = {
startOffset: -1,
endOffset: -1,
startParentId: "",
endParentId: "",
startChunkId: "",
endChunkId: "",
selectedNodes: new Set(),
raw: selection,
}
let anchorNode: Node | HTMLElement | null = selection.anchorNode
if (
anchorNode.nodeType === Node.ELEMENT_NODE &&
(anchorNode as HTMLElement).classList.contains("ar-FlexContent")
) {
// Implies there's no text yet, so we get first text config as
// at least 1 is always expected to be present
anchorNode = (anchorNode as HTMLElement).querySelector(
atomicContentSelector,
)
}
const normalizedCursorPosition =
anchorNode &&
calculateOffset(
selection.getRangeAt(0),
anchorNode,
selection.anchorOffset,
)
const activeText =
anchorNode &&
contentRegistry[getTextContainerId(anchorNode, atomicContentSelector)]
const activeChunk =
activeText?.chunks && normalizedCursorPosition !== null
? Object.values((activeText as TextInfo).chunks).find(
(chunk) =>
chunk.start <= normalizedCursorPosition &&
chunk.end >= normalizedCursorPosition,
)
: undefined
if (normalizedCursorPosition) {
selectionInfo.startOffset = normalizedCursorPosition
selectionInfo.endOffset = normalizedCursorPosition
}
if (activeText && activeChunk) {
selectionInfo.startParentId = activeText.id
selectionInfo.endParentId = activeText.id
selectionInfo.startChunkId = activeChunk.id
selectionInfo.endChunkId = activeChunk.id
}
return selectionInfo
}
// const selection = window.getSelection()
// if (selection?.anchorNode) {
// const selectionInfo: SelectionInfo = {
// startOffset: -1,
// endOffset: -1,
// startParentId: "",
// endParentId: "",
// startChunkId: "",
// endChunkId: "",
// selectedNodes: new Set(),
// raw: selection,
// }
// let anchorNode: Node | HTMLElement | null = selection.anchorNode
// if (
// anchorNode.nodeType === Node.ELEMENT_NODE &&
// (anchorNode as HTMLElement).classList.contains("ar-FlexContent")
// ) {
// // Implies there's no text yet, so we get first text config as
// // at least 1 is always expected to be present
// anchorNode = (anchorNode as HTMLElement).querySelector(
// atomicContentSelector,
// )
// }
// const normalizedCursorPosition = getNormalizedCursorPosition()
// const { activeText, activeChunk } = getActiveTextAndChunk(
// contentRegistry,
// atomicContentSelector,
// normalizedCursorPosition,
// )
// if (normalizedCursorPosition !== null) {
// selectionInfo.startOffset = normalizedCursorPosition
// selectionInfo.endOffset = normalizedCursorPosition
// }
// if (activeText && activeChunk) {
// selectionInfo.startParentId = activeText.id
// selectionInfo.endParentId = activeText.id
// selectionInfo.startChunkId = activeChunk.id
// selectionInfo.endChunkId = activeChunk.id
// }
// return selectionInfo
// }
return null
}
/**
* The calculateOffset function calculates the cursor position considering the entire text length of
* all the chunks within the parent element with the class .ar-Text.
*
* @param range Range at index 0 of current selection
* @param container chunk within which cursor position is known and used to calculate within parent
* @param offset cursor position
* @returns number representing cursor position within parent
*/
export function calculateOffset(
range: Range,
container: Node,

View File

@@ -1,8 +1,27 @@
{
"extends": "../../tsconfig.json",
"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,
"outDir": "build"
"noEmit": true,
"jsx": "react-jsx",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "es5",
},
"include": ["src"],
"include": ["src"]
}

View File

@@ -0,0 +1,36 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d).{ts,tsx}")),
},
sourcemap: true,
rollupOptions: {
treeshake: true,
external: [new RegExp("@armco/*"), "uuid"],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -0,0 +1,20 @@
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
open: true,
},
build: {
outDir: "build",
sourcemap: true,
},
test: {
globals: true,
environment: "jsdom",
setupFiles: "src/setupTests",
mockReset: true,
},
})

View File

@@ -0,0 +1,41 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|index).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -0,0 +1,27 @@
{
"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"]
}

View File

@@ -7,8 +7,8 @@
"test": "jest"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"jest": "^26.0.0"

View File

@@ -0,0 +1,27 @@
{
"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"]
}

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Get the directory of the current script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
DEV_FLAG=""
# Parse arguments
for arg in "$@"
do
case $arg in
--dev)
DEV_FLAG="--dev"
shift # Remove --dev from processing
;;
esac
done
echo "[BUILD:SH] Dev flag is: $DEV_FLAG"
echo "[BUILD:SH] Removing build if exists"
rm -rf build
echo "[BUILD:SH] Checking TS Types"
npx tsc
echo "[BUILD:SH] Initiating build..."
# Conditionally use vite-dev.config.ts if --dev flag is present
if [ "$DEV_FLAG" == "--dev" ]; then
vite build --config vite-dev.config.ts
else
vite build
fi

View File

@@ -1,14 +1,18 @@
{
"name": "@sampadak/ui",
"version": "1.0.0",
"main": "src/index.tsx",
"types": "./build/types/FlexContent.d.ts",
"main": "./build/cjs/FlexContent.js",
"module": "./build/es/FlexContent.js",
"scripts": {
"build": "tsc",
"test": "jest"
"dev": "vite --config vite-run.config.ts",
"start": "serve -s build",
"build": "./build-tools/build.sh",
"build:sm": "./build-tools/build.sh --dev"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"jest": "^26.0.0"

View File

@@ -4,8 +4,8 @@
*
*/
import {
CompositionEvent,
FormEvent,
memo,
useCallback,
useEffect,
useMemo,
@@ -18,37 +18,33 @@ import {
ArPopoverSlots,
ChunkInfo,
CompInfo,
EventHandlerCollection,
FlexContentProps,
FlexContentState,
FlexContentType,
LibRepoType,
SelectionInfo,
TextInfo,
TextProps,
} from "@armco/types"
import {
applyTool, // Formatting
} from "@sampadak/formatter"
// DomHelper,
// PopoverV2,
import selectionManager from "@sampadak/selection-manager"
// splitChunkAtCursor, // Formatting
// TextFormatter,
import * as atoms from "../../atoms"
import * as molecules from "../../molecules"
import * as components from "@armco/components"
import { PopoverV2 } from "@armco/components"
import { eventHandlers } from "@sampadak/event-manager/handlers"
import EventManager from "@sampadak/event-manager"
import selectionManager, {
getActiveTextAndChunk,
getCursorPosition,
getNormalizedCursorPosition,
} from "@sampadak/selection-manager"
import TextFormatter from "./TextFormatter"
import MemoWrapper from "./MemoWrapper"
import "./FlexContent.scss"
const demoDummyState: {
entireSelectionFormatted?: boolean
contentRegistry: FlexContentType
} = {
contentRegistry: {},
}
const demoDummyState: FlexContentType = {}
Array.from({ length: 4 }, (i: number) => {
const id = uuid()
demoDummyState.contentRegistry[id] = {
demoDummyState[id] = {
id,
name: "Text",
text: `lorem ipsum${i}`,
order: i,
chunks: {},
@@ -57,37 +53,16 @@ Array.from({ length: 4 }, (i: number) => {
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 {
@@ -96,211 +71,117 @@ const FlexContent = (props: FlexContentProps): JSX.Element => {
hasFloatingFormatter,
content = initText,
demo,
dev,
onCommit,
theme,
} = props
const [contentState, updateContentState] = useState<{
entireSelectionFormatted?: boolean
contentRegistry: FlexContentType
}>()
const [state, setState] = useState<FlexContentState>({
cursorPosition: 0,
registry: {},
})
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(
() =>
contentState?.contentRegistry && selectionManager.getSelection(
contentState.contentRegistry,
docObj,
winObj,
".ar-Text",
),
[contentState, docObj, winObj],
)
const repo: { [key: string]: any } = useMemo(() => ({ ...components }), [])
const memoizedSelectionHandler = useCallback(() => {
const { activeChunk } = getActiveTextAndChunk(
state.registry,
getNormalizedCursorPosition(),
)
state.cursorPosition = getCursorPosition()
activeChunkRef.current = activeChunk
selectionInfoRef.current = selectionManager.getSelection(state.registry)
setState({ ...state })
}, [state.registry])
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,
}
const useContent: FlexContentType = demo ? demoDummyState : content || {}
useContent &&
Object.values(useContent).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)
}
})
setState({ registry: 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])
document?.addEventListener("selectionchange", memoizedSelectionHandler)
return () =>
document?.removeEventListener("selectionchange", memoizedSelectionHandler)
}, [memoizedSelectionHandler])
useEffect(() => {
if (flexContentRef.current) {
flexContentRef.current &&
setTimeout(() => flexContentRef.current?.focus(), 10)
}
}, [flexContentRef])
const textConfigs =
contentState &&
Object.entries(contentState.contentRegistry as FlexContentType)
const textConfigs = Object.entries(state.registry)
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}
onMouseUp={() =>
window?.getSelection()?.toString().length && displayFormatter(true)
}
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()
}
}
}
}
const updatedState = (
eventHandlers.keydown as EventHandlerCollection
).onKeyDown(e, state, activeChunkRef)
setState({ ...updatedState })
}}
onBeforeInput={(e) => {
e.preventDefault()
const updatedState = (
eventHandlers.input as EventHandlerCollection
).handleInput(
(e as CompositionEvent<HTMLInputElement>).data,
state,
activeChunkRef,
)
setState({ ...updatedState })
onCommit && onCommit(flexContentRef, state.registry, activeChunkRef)
keyRef.current = undefined
}}
onBeforeInput={handleInput}
contentEditable={isEditable}
suppressContentEditableWarning={true}
>
{textConfigs?.map((entry) => {
const props: TextProps = {
descriptor: entry[1] as TextInfo,
descriptor: {
...(entry[1] as TextInfo),
},
allowFormatting,
isEditable,
demo,
}
if (
activeChunkRef.current &&
Object.keys(entry[1].chunks).indexOf(activeChunkRef.current.id) > -1
) {
props.descriptor.cursorPosition = state.cursorPosition
}
return (
<MemoWrapper
key={`flex-content-item-${entry[0]}`}
componentName={entry[1].name}
repo={repo}
Component={repo[entry[1].name || "Text"]}
props={props}
/>
)
@@ -312,7 +193,7 @@ const FlexContent = (props: FlexContentProps): JSX.Element => {
<>
{isEditable && allowFormatting && hasFloatingFormatter ? (
<PopoverV2
classes="align-self-start w-100 border-bottom"
classes={`align-self-start w-100${dev ? " flex-grow-0" : ""}`}
contentClasses="invert-bg"
isOpen={formatterDisplayed}
onClose={() => displayFormatter(false)}
@@ -327,9 +208,9 @@ const FlexContent = (props: FlexContentProps): JSX.Element => {
activeFormats={activeChunkRef.current?.formats}
demo={demo}
slot={ArPopoverSlots.POPOVER}
content={contentState}
content={state.registry}
callback={(state) => {
updateContentState(state)
setState({ ...state })
selectionInfoRef.current = null
}}
selectionInfo={selectionInfoRef.current}
@@ -340,43 +221,49 @@ const FlexContent = (props: FlexContentProps): JSX.Element => {
) : (
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>
{dev && (
<>
<hr />
<div className="vw-100 flex-grow-1 overflow-auto p-3">
<div className="row">
<div className="col-4">
<h6>Content Registry</h6>
<pre className="my-3">
{JSON.stringify(
state.registry,
(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>
</div>
</>
)}
</>
)
}

View File

@@ -0,0 +1,19 @@
import { TextProps } from "@armco/types"
import { ComponentType, memo } from "react"
const MemoWrapper = memo(
(props: { props: TextProps; Component: ComponentType }) => {
const { Component, props: componentProps } = props
return Component ? (
<Component key={componentProps.descriptor.id} {...componentProps} />
) : null
},
(prevProps, props) => {
return (
JSON.stringify(prevProps.props.descriptor.chunks) !==
JSON.stringify(props.props.descriptor.chunks)
)
},
)
export default MemoWrapper

6
packages/ui/src/Test.tsx Normal file
View File

@@ -0,0 +1,6 @@
import ReactDOM from "react-dom/client"
import FlexContent from "./FlexContent"
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(<FlexContent />)

View File

@@ -4,8 +4,9 @@ import {
TextFormatterProps,
TextFormat,
} from "@armco/types"
import { applyTool } from "@sampadak/formatter"
import TextTool from "./TextTool"
import "./TextFormatter.component.scss"
import "./TextFormatter.scss"
const editToolGroups: {
[key: string]: {

View File

@@ -5,13 +5,8 @@ import {
ListItemContent,
TextToolProps,
} from "@armco/types"
import {
ColorSelectorRadio,
List,
LoadableIcon,
PopoverV2,
Tooltip,
} from "../.."
import { List, Icon, PopoverV2, Tooltip } from "@armco/components"
import ColorSelectorRadio from "./ColorSelectorRadio"
const popovers = (restTools?: Array<ListItemContent>, theme?: ArThemes) => ({
textColor: (
@@ -90,20 +85,24 @@ const TextTool = ({
onMouseLeave={() => setHovered(false)}
>
<span>
<LoadableIcon
size={(size || "1rem") as string}
<Icon
icon={icon}
color={iconColor}
attributes={{
size: (size || "1rem") as string,
colors: { fillColor: iconColor },
}}
/>
{category === "color" && (
<div className="ar-TextTool__color-tool-indicator" />
)}
</span>
{subIcon && (
<LoadableIcon
size={subIconSize || "0.8rem"}
<Icon
icon={subIcon}
color={iconColor}
attributes={{
size: subIconSize || "0.8rem",
colors: { fillColor: iconColor },
}}
/>
)}
</span>

View File

@@ -14,7 +14,7 @@
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"noEmit": false,
"noEmit": true,
"jsx": "react-jsx",
"lib": [
"dom",
@@ -23,6 +23,5 @@
],
"target": "es5",
},
"include": ["src"],
"exclude": ["build", "plop-templates", "node_modules", "src/**/*.test.*", "src/**/*.spec.*", "src/stories", "scripts"]
"include": ["src"]
}

View File

@@ -0,0 +1,42 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|index).{ts,tsx}")),
},
sourcemap: true,
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -0,0 +1,20 @@
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
open: true,
},
build: {
outDir: "build",
sourcemap: true,
},
test: {
globals: true,
environment: "jsdom",
setupFiles: "src/setupTests",
mockReset: true,
},
})

View File

@@ -0,0 +1,41 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|index).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

View File

@@ -1,12 +0,0 @@
import { useRoutes } from "react-router-dom"
import * as pages from "./pages"
import Helper from "./utils/helper"
import ROUTES from "./routes"
Helper.populateComponentsInRoutes(ROUTES, pages)
interface RouterProps {}
const Router = (props: RouterProps): JSX.Element | null => useRoutes(ROUTES)
export default Router

View File

@@ -1,6 +0,0 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
import type { RootState, AppDispatch } from "./store"
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@@ -1,3 +0,0 @@
/* PLOP_INJECT_IMPORT */
export /* PLOP_INJECT_EXPORT */ {}

View File

@@ -1,9 +0,0 @@
const ROUTES = [
{
path: "/",
class: "landing",
element: "Home",
},
]
export default ROUTES

View File

@@ -1,18 +0,0 @@
html, body, #root {
height: 100%;
width: 100%;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -1,14 +0,0 @@
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"
export const store = configureStore({
reducer: {},
})
export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>

View File

@@ -1,6 +0,0 @@
interface RouteConfig {
path: String
class?: String
element: String | JSX.Element | null
children?: Array<RouteConfig>
}

View File

@@ -1,15 +0,0 @@
class Helper {
static populateComponentsInRoutes(routes: RouteConfig[], components: any) {
routes &&
routes.forEach((route) => {
const Component: JSX.ElementType =
components[route.element as keyof object]
route.element = <Component />
if (route.children) {
Helper.populateComponentsInRoutes(route.children, components)
}
})
}
}
export default Helper

View File

@@ -1,19 +1,14 @@
import React from "react"
import ReactDOM from "react-dom/client"
import { BrowserRouter } from "react-router-dom"
import { Provider } from "react-redux"
import { store } from "./app/store"
import Router from "./app/Router"
import "./app/static/styles/global.scss"
import Editor from "@sampadak/ui"
import "@armco/components/build/es/index.css"
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<Router />
</Provider>
</BrowserRouter>
<div className="d-flex flex-column vh-100">
<Editor dev isEditable allowFormatting hasFloatingFormatter />
</div>
</React.StrictMode>,
)

View File

@@ -4,7 +4,7 @@
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: "development" | "production" | "test"
readonly NODE_ENV: "development" | "production"
readonly PUBLIC_URL: string
}
}

View File

@@ -20,6 +20,6 @@
"noEmit": false,
"jsx": "react-jsx"
},
"include": ["src", "packages"],
"include": ["src"],
"exclude": ["**/build"]
}

42
vite-dev.config.ts Normal file
View File

@@ -0,0 +1,42 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|index).{ts,tsx}")),
},
sourcemap: true,
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

20
vite-run.config.ts Normal file
View File

@@ -0,0 +1,20 @@
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
open: true,
},
build: {
outDir: "build",
sourcemap: true,
},
test: {
globals: true,
environment: "jsdom",
setupFiles: "src/setupTests",
mockReset: true,
},
})

View File

@@ -1,20 +1,41 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
open: true,
},
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
sourcemap: true,
},
test: {
globals: true,
environment: "jsdom",
setupFiles: "src/setupTests",
mockReset: true,
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|index).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})