First Commit
This commit is contained in:
@@ -9,6 +9,5 @@
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
lerna.json
Normal file
6
lerna.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"packages": [
|
||||
"src/packages/*"
|
||||
],
|
||||
"version": "independent"
|
||||
}
|
||||
11118
package-lock.json
generated
11118
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"name": "@armco/react-vite-rtk-template",
|
||||
"name": "@armco/sampadak",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"src/packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
@@ -25,16 +28,21 @@
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.13.0"
|
||||
"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",
|
||||
@@ -75,7 +83,7 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template/issues"
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/sampadak/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template#readme"
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/sampadak#readme"
|
||||
}
|
||||
|
||||
7
src/app/Router.js
Normal file
7
src/app/Router.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useRoutes } from "react-router-dom";
|
||||
import * as pages from "./pages";
|
||||
import Helper from "./utils/helper";
|
||||
import ROUTES from "./routes";
|
||||
Helper.populateComponentsInRoutes(ROUTES, pages);
|
||||
var Router = function (props) { return useRoutes(ROUTES); };
|
||||
export default Router;
|
||||
1
src/app/config/constants.js
Normal file
1
src/app/config/constants.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
4
src/app/hooks.js
Normal file
4
src/app/hooks.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export var useAppDispatch = useDispatch;
|
||||
export var useAppSelector = useSelector;
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-Home {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
export interface HomeState {}
|
||||
|
||||
const initialState: HomeState = {}
|
||||
|
||||
export const homeSlice = createSlice({
|
||||
name: "home",
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {},
|
||||
})
|
||||
|
||||
export const { increment } = homeSlice.actions
|
||||
|
||||
export default homeSlice.reducer
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import Home from "./Home"
|
||||
|
||||
describe("Home", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react"
|
||||
import "./Home.module.scss"
|
||||
|
||||
interface HomeProps {}
|
||||
|
||||
const Home = props => {
|
||||
return (
|
||||
<div className="c-Home">
|
||||
In Page Home
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -1,3 +0,0 @@
|
||||
import Home from "./Home.jsx"
|
||||
|
||||
export default Home
|
||||
2
src/app/pages/index.js
Normal file
2
src/app/pages/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
export {};
|
||||
@@ -1,7 +1,3 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
import Home from "./Home"
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
Home,
|
||||
}
|
||||
export /* PLOP_INJECT_EXPORT */ {}
|
||||
|
||||
8
src/app/routes.js
Normal file
8
src/app/routes.js
Normal file
@@ -0,0 +1,8 @@
|
||||
var ROUTES = [
|
||||
{
|
||||
path: "/",
|
||||
class: "landing",
|
||||
element: "Home",
|
||||
},
|
||||
];
|
||||
export default ROUTES;
|
||||
4
src/app/store.js
Normal file
4
src/app/store.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
export var store = configureStore({
|
||||
reducer: {},
|
||||
});
|
||||
@@ -1,10 +1,7 @@
|
||||
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"
|
||||
import counterReducer from "../features/counter/counterSlice"
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
counter: counterReducer,
|
||||
},
|
||||
reducer: {},
|
||||
})
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
17
src/app/utils/helper.js
Normal file
17
src/app/utils/helper.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
var Helper = /** @class */ (function () {
|
||||
function Helper() {
|
||||
}
|
||||
Helper.populateComponentsInRoutes = function (routes, components) {
|
||||
routes &&
|
||||
routes.forEach(function (route) {
|
||||
var Component = components[route.element];
|
||||
route.element = _jsx(Component, {});
|
||||
if (route.children) {
|
||||
Helper.populateComponentsInRoutes(route.children, components);
|
||||
}
|
||||
});
|
||||
};
|
||||
return Helper;
|
||||
}());
|
||||
export default Helper;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import { useAppSelector, useAppDispatch } from "../../app/hooks"
|
||||
import {
|
||||
decrement,
|
||||
increment,
|
||||
incrementByAmount,
|
||||
incrementAsync,
|
||||
incrementIfOdd,
|
||||
selectCount,
|
||||
} from "./counterSlice"
|
||||
|
||||
export function Counter() {
|
||||
const count = useAppSelector(selectCount)
|
||||
const dispatch = useAppDispatch()
|
||||
const [incrementAmount, setIncrementAmount] = useState("2")
|
||||
|
||||
const incrementValue = Number(incrementAmount) || 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Decrement value"
|
||||
onClick={() => dispatch(decrement())}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="value">{count}</span>
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Increment value"
|
||||
onClick={() => dispatch(increment())}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<input
|
||||
className="textbox"
|
||||
aria-label="Set increment amount"
|
||||
value={incrementAmount}
|
||||
onChange={(e) => setIncrementAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
||||
>
|
||||
Add Amount
|
||||
</button>
|
||||
<button
|
||||
className="asyncButton"
|
||||
onClick={() => dispatch(incrementAsync(incrementValue))}
|
||||
>
|
||||
Add Async
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
||||
>
|
||||
Add If Odd
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// A mock function to mimic making an async request for data
|
||||
export function fetchCount(amount = 1) {
|
||||
return new Promise<{ data: number }>((resolve) =>
|
||||
setTimeout(() => resolve({ data: amount }), 500),
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import counterReducer, {
|
||||
CounterState,
|
||||
increment,
|
||||
decrement,
|
||||
incrementByAmount,
|
||||
} from "./counterSlice"
|
||||
|
||||
describe("counter reducer", () => {
|
||||
const initialState: CounterState = {
|
||||
value: 3,
|
||||
status: "idle",
|
||||
}
|
||||
it("should handle initial state", () => {
|
||||
expect(counterReducer(undefined, { type: "unknown" })).toEqual({
|
||||
value: 0,
|
||||
status: "idle",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle increment", () => {
|
||||
const actual = counterReducer(initialState, increment())
|
||||
expect(actual.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should handle decrement", () => {
|
||||
const actual = counterReducer(initialState, decrement())
|
||||
expect(actual.value).toEqual(2)
|
||||
})
|
||||
|
||||
it("should handle incrementByAmount", () => {
|
||||
const actual = counterReducer(initialState, incrementByAmount(2))
|
||||
expect(actual.value).toEqual(5)
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { RootState, AppThunk } from "../../app/store"
|
||||
import { fetchCount } from "./counterAPI"
|
||||
|
||||
export interface CounterState {
|
||||
value: number
|
||||
status: "idle" | "loading" | "failed"
|
||||
}
|
||||
|
||||
const initialState: CounterState = {
|
||||
value: 0,
|
||||
status: "idle",
|
||||
}
|
||||
|
||||
// The function below is called a thunk and allows us to perform async logic. It
|
||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
||||
// code can then be executed and other actions can be dispatched. Thunks are
|
||||
// typically used to make async requests.
|
||||
export const incrementAsync = createAsyncThunk(
|
||||
"counter/fetchCount",
|
||||
async (amount: number) => {
|
||||
const response = await fetchCount(amount)
|
||||
// The value we return becomes the `fulfilled` action payload
|
||||
return response.data
|
||||
},
|
||||
)
|
||||
|
||||
export const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
initialState,
|
||||
// The `reducers` field lets us define reducers and generate associated actions
|
||||
reducers: {
|
||||
increment: (state) => {
|
||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
||||
// doesn"t actually mutate the state because it uses the Immer library,
|
||||
// which detects changes to a "draft state" and produces a brand new
|
||||
// immutable state based off those changes
|
||||
state.value += 1
|
||||
},
|
||||
decrement: (state) => {
|
||||
state.value -= 1
|
||||
},
|
||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
// The `extraReducers` field lets the slice handle actions defined elsewhere,
|
||||
// including actions generated by createAsyncThunk or in other slices.
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(incrementAsync.pending, (state) => {
|
||||
state.status = "loading"
|
||||
})
|
||||
.addCase(incrementAsync.fulfilled, (state, action) => {
|
||||
state.status = "idle"
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(incrementAsync.rejected, (state) => {
|
||||
state.status = "failed"
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions
|
||||
|
||||
// The function below is called a selector and allows us to select a value from
|
||||
// the state. Selectors can also be defined inline where they"re used instead of
|
||||
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
|
||||
export const selectCount = (state: RootState) => state.counter.value
|
||||
|
||||
// We can also write thunks by hand, which may contain both sync and async logic.
|
||||
// Here"s an example of conditionally dispatching actions based on current state.
|
||||
export const incrementIfOdd =
|
||||
(amount: number): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
const currentValue = selectCount(getState())
|
||||
if (currentValue % 2 === 1) {
|
||||
dispatch(incrementByAmount(amount))
|
||||
}
|
||||
}
|
||||
|
||||
export default counterSlice.reducer
|
||||
10
src/index.js
Normal file
10
src/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
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";
|
||||
var root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(_jsx(React.StrictMode, { children: _jsx(BrowserRouter, { children: _jsx(Provider, { store: store, children: _jsx(Router, {}) }) }) }));
|
||||
12
src/packages/collaboration/package.json
Normal file
12
src/packages/collaboration/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@sampadak/collaboration",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/collaboration/src/index.js
Normal file
1
src/packages/collaboration/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/collaboration/src/index.tsx
Normal file
0
src/packages/collaboration/src/index.tsx
Normal file
16
src/packages/core/package.json
Normal file
16
src/packages/core/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@sampadak/core",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/core/src/index.js
Normal file
1
src/packages/core/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./utils";
|
||||
1
src/packages/core/src/index.ts
Normal file
1
src/packages/core/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./utils"
|
||||
21
src/packages/core/src/utils/helper.js
Normal file
21
src/packages/core/src/utils/helper.js
Normal 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);
|
||||
};
|
||||
}
|
||||
25
src/packages/core/src/utils/helper.ts
Normal file
25
src/packages/core/src/utils/helper.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait?: number,
|
||||
immediate: boolean = false,
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout
|
||||
|
||||
return function (...args: Parameters<T>): void {
|
||||
const later = () => {
|
||||
timeout = undefined!
|
||||
|
||||
if (!immediate) {
|
||||
func(...args)
|
||||
}
|
||||
}
|
||||
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (immediate && !timeout) {
|
||||
func(...args)
|
||||
}
|
||||
|
||||
timeout = setTimeout(later, wait || 1000)
|
||||
}
|
||||
}
|
||||
1
src/packages/core/src/utils/index.js
Normal file
1
src/packages/core/src/utils/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./helper";
|
||||
1
src/packages/core/src/utils/index.ts
Normal file
1
src/packages/core/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./helper"
|
||||
12
src/packages/event-manager/package.json
Normal file
12
src/packages/event-manager/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@sampadak/event-manager",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/event-manager/src/index.js
Normal file
1
src/packages/event-manager/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/event-manager/src/index.tsx
Normal file
0
src/packages/event-manager/src/index.tsx
Normal file
17
src/packages/formatter/package.json
Normal file
17
src/packages/formatter/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@sampadak/formatter",
|
||||
"version": "1.0.0",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"build": "rm -rf build && tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
},
|
||||
"files": ["build"]
|
||||
}
|
||||
93
src/packages/formatter/src/index.js
Normal file
93
src/packages/formatter/src/index.js
Normal file
@@ -0,0 +1,93 @@
|
||||
var __assign = (this && this.__assign) || function () {
|
||||
__assign = Object.assign || function(t) {
|
||||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||||
s = arguments[i];
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
||||
t[p] = s[p];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
return __assign.apply(this, arguments);
|
||||
};
|
||||
import { v4 as uuid } from "uuid";
|
||||
export var findOverlappedChunksInSingleText = function (chunks, start, end) {
|
||||
return Object.values(chunks).filter(function (chunk) { return chunk.end > start && chunk.start < end; });
|
||||
};
|
||||
export var findOverlappedChunks = function (contentRegistry, selectionInfo) {
|
||||
var selectedNodes = selectionInfo.selectedNodes, startParentId = selectionInfo.startParentId, endParentId = selectionInfo.endParentId, startOffset = selectionInfo.startOffset, endOffset = selectionInfo.endOffset;
|
||||
return Array.from(selectedNodes).reduce(function (acc, id) {
|
||||
var _a;
|
||||
var currentConfig = contentRegistry[id];
|
||||
var start = id !== startParentId ? 0 : startOffset;
|
||||
var end = id !== endParentId
|
||||
? (((_a = currentConfig.text) === null || _a === void 0 ? void 0 : _a.length) || 0) - 1
|
||||
: endOffset;
|
||||
acc = acc.concat(findOverlappedChunksInSingleText(currentConfig.chunks, start, end));
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
export var isEntireSelectionFormatted = function (format, selectionInfo, contentRegistry) {
|
||||
// Flatten all chunks from selected nodes
|
||||
var allSelectedChunks = findOverlappedChunks(contentRegistry, selectionInfo);
|
||||
// Check if all chunks in the selection already have the format
|
||||
var entireSelectionFormatted = allSelectedChunks.length > 0 &&
|
||||
allSelectedChunks.every(function (chunk) { return format in chunk.formats; });
|
||||
return entireSelectionFormatted;
|
||||
};
|
||||
export var applyTool = function (format, formatValue, selectionInfo, contentState, callback) {
|
||||
if (selectionInfo) {
|
||||
var contentStateClone_1 = __assign({}, contentState);
|
||||
var selectedNodes = selectionInfo.selectedNodes, startParentId_1 = selectionInfo.startParentId, endParentId_1 = selectionInfo.endParentId, startOffset_1 = selectionInfo.startOffset, endOffset_1 = selectionInfo.endOffset;
|
||||
var entireSelectionFormatted_1 = isEntireSelectionFormatted(format, selectionInfo, contentStateClone_1.contentRegistry);
|
||||
contentStateClone_1.entireSelectionFormatted = entireSelectionFormatted_1;
|
||||
selectedNodes === null || selectedNodes === void 0 ? void 0 : selectedNodes.forEach(function (id) {
|
||||
var _a;
|
||||
var currentConfig = contentStateClone_1.contentRegistry[id];
|
||||
handleNewChunk(currentConfig.chunks, [format, true], // TODO: Retain value as true, this will change for color, backgroundColor and fontSize
|
||||
startParentId_1 === id ? startOffset_1 : 0, endParentId_1 === id
|
||||
? endOffset_1
|
||||
: ((_a = currentConfig.text) === null || _a === void 0 ? void 0 : _a.length) - 1, entireSelectionFormatted_1, currentConfig.text);
|
||||
});
|
||||
Array.from(selectedNodes).length > 0 &&
|
||||
callback &&
|
||||
callback(contentStateClone_1);
|
||||
}
|
||||
};
|
||||
var handleNewChunk = function (chunksMap, formatDetails, start, end, entireSelectionFormatted, text) {
|
||||
var format = formatDetails[0], value = formatDetails[1];
|
||||
// Find all overlapped chunks, chunk out first and/or last chunks, splitting based on selection start and end and whether applied format is same or different
|
||||
var overlappedChunks = findOverlappedChunksInSingleText(chunksMap, start, end);
|
||||
overlappedChunks.sort(function (a, b) { return a.start - b.start; });
|
||||
function createRestChunk(chunk, start, end, isLeftOverlap) {
|
||||
var chunkId = uuid();
|
||||
var restChunk = {
|
||||
id: chunkId,
|
||||
start: isLeftOverlap ? chunk.start : end,
|
||||
end: isLeftOverlap ? start : chunk.end,
|
||||
text: text.substring(isLeftOverlap ? chunk.start : end, isLeftOverlap ? start : chunk.end),
|
||||
formats: __assign({}, chunk.formats),
|
||||
parent: chunk.parent,
|
||||
};
|
||||
chunksMap[chunkId] = restChunk;
|
||||
if (isLeftOverlap) {
|
||||
chunk.start = start;
|
||||
chunk.text = chunk.text.substring(start);
|
||||
}
|
||||
else {
|
||||
chunk.end = end;
|
||||
}
|
||||
}
|
||||
// entireSelectionFormatted && remove format from overlapped chunk, shrink the before/after chunks
|
||||
// !entireSelectionFormatted && add format to all the chunks, if a chunk ends up becoming similar to previous one, extend the previous chunk, ignore current one
|
||||
overlappedChunks.forEach(function (c) {
|
||||
if (format in c.formats === entireSelectionFormatted) {
|
||||
c.start < start && createRestChunk(c, c.start, start, true);
|
||||
c.end > end && createRestChunk(c, end, c.end, false);
|
||||
chunksMap[c.id] = c;
|
||||
// Full contained or shrunk to be fully contained
|
||||
entireSelectionFormatted
|
||||
? delete c.formats[format]
|
||||
: (c.formats[format] = value !== undefined ? value : true);
|
||||
}
|
||||
});
|
||||
};
|
||||
164
src/packages/formatter/src/index.ts
Normal file
164
src/packages/formatter/src/index.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { v4 as uuid } from "uuid"
|
||||
import {
|
||||
ChunkInfo,
|
||||
FlexContentType,
|
||||
FunctionType,
|
||||
SelectionInfo,
|
||||
TextFormat,
|
||||
} from "@armco/types"
|
||||
|
||||
export const findOverlappedChunksInSingleText = (
|
||||
chunks: { [key: string]: ChunkInfo },
|
||||
start: number,
|
||||
end: number,
|
||||
) => {
|
||||
return Object.values(chunks).filter(
|
||||
(chunk) => chunk.end > start && chunk.start < end,
|
||||
)
|
||||
}
|
||||
|
||||
export const findOverlappedChunks = (
|
||||
contentRegistry: FlexContentType,
|
||||
selectionInfo: SelectionInfo,
|
||||
) => {
|
||||
const { selectedNodes, startParentId, endParentId, startOffset, endOffset } =
|
||||
selectionInfo
|
||||
return Array.from(selectedNodes).reduce(
|
||||
(acc: Array<ChunkInfo>, id: string) => {
|
||||
const currentConfig = contentRegistry[id]
|
||||
const start = id !== startParentId ? 0 : startOffset
|
||||
const end =
|
||||
id !== endParentId
|
||||
? ((currentConfig.text as string)?.length || 0) - 1
|
||||
: endOffset
|
||||
acc = acc.concat(
|
||||
findOverlappedChunksInSingleText(currentConfig.chunks, start, end),
|
||||
)
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
export const isEntireSelectionFormatted = (
|
||||
format: keyof TextFormat,
|
||||
selectionInfo: SelectionInfo,
|
||||
contentRegistry: FlexContentType,
|
||||
) => {
|
||||
// Flatten all chunks from selected nodes
|
||||
const allSelectedChunks: Array<ChunkInfo> = findOverlappedChunks(
|
||||
contentRegistry,
|
||||
selectionInfo,
|
||||
)
|
||||
|
||||
// Check if all chunks in the selection already have the format
|
||||
const entireSelectionFormatted =
|
||||
allSelectedChunks.length > 0 &&
|
||||
allSelectedChunks.every((chunk) => format in chunk.formats)
|
||||
return entireSelectionFormatted
|
||||
}
|
||||
|
||||
export const applyTool = (
|
||||
format: keyof TextFormat,
|
||||
formatValue: string | boolean | number | undefined,
|
||||
selectionInfo: SelectionInfo,
|
||||
contentState: {
|
||||
entireSelectionFormatted?: boolean
|
||||
contentRegistry: FlexContentType
|
||||
},
|
||||
callback: FunctionType,
|
||||
) => {
|
||||
if (selectionInfo) {
|
||||
const contentStateClone = { ...contentState }
|
||||
const {
|
||||
selectedNodes,
|
||||
startParentId,
|
||||
endParentId,
|
||||
startOffset,
|
||||
endOffset,
|
||||
} = selectionInfo
|
||||
|
||||
const entireSelectionFormatted = isEntireSelectionFormatted(
|
||||
format,
|
||||
selectionInfo,
|
||||
contentStateClone.contentRegistry,
|
||||
)
|
||||
contentStateClone.entireSelectionFormatted = entireSelectionFormatted
|
||||
|
||||
selectedNodes?.forEach((id: string) => {
|
||||
const currentConfig = contentStateClone.contentRegistry[id]
|
||||
handleNewChunk(
|
||||
currentConfig.chunks,
|
||||
[format, true], // TODO: Retain value as true, this will change for color, backgroundColor and fontSize
|
||||
startParentId === id ? startOffset : 0,
|
||||
endParentId === id
|
||||
? endOffset
|
||||
: (currentConfig.text as string)?.length - 1,
|
||||
entireSelectionFormatted,
|
||||
currentConfig.text,
|
||||
)
|
||||
})
|
||||
Array.from(selectedNodes).length > 0 &&
|
||||
callback &&
|
||||
callback(contentStateClone)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewChunk = (
|
||||
chunksMap: { [key: string]: ChunkInfo },
|
||||
formatDetails: [keyof TextFormat, string | boolean | number],
|
||||
start: number,
|
||||
end: number,
|
||||
entireSelectionFormatted: boolean,
|
||||
text: string,
|
||||
) => {
|
||||
const [format, value] = formatDetails
|
||||
// Find all overlapped chunks, chunk out first and/or last chunks, splitting based on selection start and end and whether applied format is same or different
|
||||
const overlappedChunks = findOverlappedChunksInSingleText(
|
||||
chunksMap,
|
||||
start,
|
||||
end,
|
||||
)
|
||||
overlappedChunks.sort((a, b) => a.start - b.start)
|
||||
function createRestChunk(
|
||||
chunk: any,
|
||||
start: number,
|
||||
end: number,
|
||||
isLeftOverlap: boolean,
|
||||
) {
|
||||
const chunkId = uuid()
|
||||
const restChunk = {
|
||||
id: chunkId,
|
||||
start: isLeftOverlap ? chunk.start : end,
|
||||
end: isLeftOverlap ? start : chunk.end,
|
||||
text: text.substring(
|
||||
isLeftOverlap ? chunk.start : end,
|
||||
isLeftOverlap ? start : chunk.end,
|
||||
),
|
||||
formats: { ...chunk.formats },
|
||||
parent: chunk.parent,
|
||||
}
|
||||
chunksMap[chunkId] = restChunk
|
||||
|
||||
if (isLeftOverlap) {
|
||||
chunk.start = start
|
||||
chunk.text = chunk.text.substring(start)
|
||||
} else {
|
||||
chunk.end = end
|
||||
}
|
||||
}
|
||||
// entireSelectionFormatted && remove format from overlapped chunk, shrink the before/after chunks
|
||||
// !entireSelectionFormatted && add format to all the chunks, if a chunk ends up becoming similar to previous one, extend the previous chunk, ignore current one
|
||||
overlappedChunks.forEach((c) => {
|
||||
if (format in c.formats === entireSelectionFormatted) {
|
||||
c.start < start && createRestChunk(c, c.start, start, true)
|
||||
c.end > end && createRestChunk(c, end, c.end, false)
|
||||
|
||||
chunksMap[c.id as string] = c
|
||||
// Full contained or shrunk to be fully contained
|
||||
entireSelectionFormatted
|
||||
? delete c.formats[format]
|
||||
: ((c.formats as any)[format] = value !== undefined ? value : true)
|
||||
}
|
||||
})
|
||||
}
|
||||
9
src/packages/formatter/tsconfig.json
Normal file
9
src/packages/formatter/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"noEmit": false,
|
||||
"outDir": "build"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
12
src/packages/history/package.json
Normal file
12
src/packages/history/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@sampadak/history",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/history/src/index.js
Normal file
1
src/packages/history/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/history/src/index.tsx
Normal file
0
src/packages/history/src/index.tsx
Normal file
12
src/packages/persistence/package.json
Normal file
12
src/packages/persistence/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@sampadak/persistence",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/persistence/src/index.js
Normal file
1
src/packages/persistence/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/persistence/src/index.tsx
Normal file
0
src/packages/persistence/src/index.tsx
Normal file
16
src/packages/plugin-manager/package.json
Normal file
16
src/packages/plugin-manager/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@sampadak/plugin-manager",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/plugin-manager/src/index.js
Normal file
1
src/packages/plugin-manager/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/plugin-manager/src/index.tsx
Normal file
0
src/packages/plugin-manager/src/index.tsx
Normal file
16
src/packages/selection-manager/package.json
Normal file
16
src/packages/selection-manager/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@sampadak/selection-manager",
|
||||
"version": "1.0.0",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rm -rf build && tsc && cp src/types.d.ts build",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
]
|
||||
}
|
||||
36
src/packages/selection-manager/src/index.js
Normal file
36
src/packages/selection-manager/src/index.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { processCaret, processRange } from "./util";
|
||||
var SelectionManager = /** @class */ (function () {
|
||||
function SelectionManager() {
|
||||
}
|
||||
SelectionManager.getSelection = function (contentRegistry, doc, win, atomicContentSelector) {
|
||||
var docObj = doc || document;
|
||||
var selection = (win || window).getSelection();
|
||||
atomicContentSelector = atomicContentSelector || "ar-Text";
|
||||
if (selection) {
|
||||
if (selection.type === "Range") {
|
||||
return processRange(selection, docObj, contentRegistry, atomicContentSelector);
|
||||
}
|
||||
else if (selection.type === "Caret" && contentRegistry) {
|
||||
return processCaret(selection, contentRegistry, atomicContentSelector);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
SelectionManager.setSelection = function (range) {
|
||||
throw new Error("Method not implemented.");
|
||||
};
|
||||
SelectionManager.clearSelection = function () {
|
||||
throw new Error("Method not implemented.");
|
||||
};
|
||||
SelectionManager.selectAll = function () {
|
||||
throw new Error("Method not implemented.");
|
||||
};
|
||||
SelectionManager.getSelectedText = function () {
|
||||
throw new Error("Method not implemented.");
|
||||
};
|
||||
SelectionManager.hasSelection = function () {
|
||||
throw new Error("Method not implemented.");
|
||||
};
|
||||
return SelectionManager;
|
||||
}());
|
||||
export default SelectionManager;
|
||||
46
src/packages/selection-manager/src/index.ts
Normal file
46
src/packages/selection-manager/src/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { FlexContentType, SelectionInfo } from "@armco/types"
|
||||
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"
|
||||
if (selection) {
|
||||
if (selection.type === "Range") {
|
||||
return processRange(
|
||||
selection,
|
||||
docObj,
|
||||
contentRegistry,
|
||||
atomicContentSelector,
|
||||
)
|
||||
} else if (selection.type === "Caret" && contentRegistry) {
|
||||
return processCaret(selection, contentRegistry, atomicContentSelector)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
static setSelection(range: Range): void {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
static clearSelection(): void {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
static selectAll(): void {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
static getSelectedText(): string {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
static hasSelection(): boolean {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectionManager as ISelectionManager
|
||||
15
src/packages/selection-manager/src/types.d.ts
vendored
Normal file
15
src/packages/selection-manager/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { FlexContentType, SelectionInfo } from "@armco/types"
|
||||
|
||||
export interface ISelectionManager {
|
||||
getSelection(
|
||||
contentRegistry: FlexContentType,
|
||||
doc?: Document,
|
||||
win?: Window,
|
||||
atomicContentSelector?: string,
|
||||
): SelectionInfo
|
||||
setSelection(range: Range): void
|
||||
clearSelection(): void
|
||||
selectAll(): void
|
||||
getSelectedText(): string
|
||||
hasSelection(): boolean
|
||||
}
|
||||
104
src/packages/selection-manager/src/util.js
Normal file
104
src/packages/selection-manager/src/util.js
Normal file
@@ -0,0 +1,104 @@
|
||||
export var getTextContainerId = function (node, atomicContentSelector) {
|
||||
var _a;
|
||||
var textElementNode = node.nodeType === Node.TEXT_NODE
|
||||
? (_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.closest(atomicContentSelector)
|
||||
: node;
|
||||
return textElementNode === null || textElementNode === void 0 ? void 0 : textElementNode.id;
|
||||
};
|
||||
export var processRange = function (selection, docObj, contentRegistry, atomicContentSelector) {
|
||||
var _a, _b, _c;
|
||||
var selectionInfo = {
|
||||
startOffset: -1,
|
||||
endOffset: -1,
|
||||
startParentId: "",
|
||||
endParentId: "",
|
||||
startChunkId: "",
|
||||
endChunkId: "",
|
||||
selectedNodes: new Set(),
|
||||
raw: selection,
|
||||
};
|
||||
var range = selection.getRangeAt(0);
|
||||
var content = range.cloneContents();
|
||||
if (content.childNodes.length === 1 &&
|
||||
((_a = content.firstChild) === null || _a === void 0 ? void 0 : _a.nodeType) === Node.TEXT_NODE) {
|
||||
var parent_1 = (_c = (_b = range.startContainer.parentElement) === null || _b === void 0 ? void 0 : _b.closest(atomicContentSelector)) === null || _c === void 0 ? void 0 : _c.cloneNode(true);
|
||||
parent_1 && content.replaceChild(parent_1, content.firstChild);
|
||||
}
|
||||
var startParentId = getTextContainerId(range.startContainer, atomicContentSelector);
|
||||
var endParentId = getTextContainerId(range.endContainer, atomicContentSelector);
|
||||
if (startParentId && endParentId) {
|
||||
var startOffset = calculateOffset(range, range.startContainer, range.startOffset);
|
||||
var endOffset = calculateOffset(range, range.endContainer, range.endOffset);
|
||||
// Update selectionInfo properties
|
||||
selectionInfo.startParentId = startParentId;
|
||||
selectionInfo.endParentId = endParentId;
|
||||
selectionInfo.startOffset = startOffset;
|
||||
selectionInfo.endOffset = endOffset;
|
||||
var walker = docObj.createTreeWalker(content, NodeFilter.SHOW_ELEMENT);
|
||||
while (walker.nextNode()) {
|
||||
var currNode = walker.currentNode;
|
||||
var node = ((currNode === null || currNode === void 0 ? void 0 : currNode.closest(atomicContentSelector)) ||
|
||||
range.commonAncestorContainer);
|
||||
if (node) {
|
||||
selectionInfo.selectedNodes.add(node.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectionInfo;
|
||||
};
|
||||
export var processCaret = function (selection, contentRegistry, atomicContentSelector) {
|
||||
if (selection.anchorNode) {
|
||||
var selectionInfo = {
|
||||
startOffset: -1,
|
||||
endOffset: -1,
|
||||
startParentId: "",
|
||||
endParentId: "",
|
||||
startChunkId: "",
|
||||
endChunkId: "",
|
||||
selectedNodes: new Set(),
|
||||
raw: selection,
|
||||
};
|
||||
var anchorNode = selection.anchorNode;
|
||||
if (anchorNode.nodeType === Node.ELEMENT_NODE &&
|
||||
anchorNode.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.querySelector(atomicContentSelector);
|
||||
}
|
||||
var normalizedCursorPosition_1 = anchorNode &&
|
||||
calculateOffset(selection.getRangeAt(0), anchorNode, selection.anchorOffset);
|
||||
var activeText = anchorNode &&
|
||||
contentRegistry[getTextContainerId(anchorNode, atomicContentSelector)];
|
||||
var activeChunk = (activeText === null || activeText === void 0 ? void 0 : activeText.chunks) && normalizedCursorPosition_1 !== null
|
||||
? Object.values(activeText.chunks).find(function (chunk) {
|
||||
return chunk.start <= normalizedCursorPosition_1 &&
|
||||
chunk.end >= normalizedCursorPosition_1;
|
||||
})
|
||||
: undefined;
|
||||
if (normalizedCursorPosition_1) {
|
||||
selectionInfo.startOffset = normalizedCursorPosition_1;
|
||||
selectionInfo.endOffset = normalizedCursorPosition_1;
|
||||
}
|
||||
if (activeText && activeChunk) {
|
||||
selectionInfo.startParentId = activeText.id;
|
||||
selectionInfo.endParentId = activeText.id;
|
||||
selectionInfo.startChunkId = activeChunk.id;
|
||||
selectionInfo.endChunkId = activeChunk.id;
|
||||
}
|
||||
return selectionInfo;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export function calculateOffset(range, container, offset) {
|
||||
var _a;
|
||||
var relativeTo = container.nodeType === Node.TEXT_NODE
|
||||
? (_a = container.parentElement) === null || _a === void 0 ? void 0 : _a.closest(".ar-Text")
|
||||
: container.closest(".ar-Text");
|
||||
if (relativeTo) {
|
||||
var preCaretRange = range.cloneRange();
|
||||
preCaretRange.selectNodeContents(relativeTo);
|
||||
preCaretRange.setEnd(container, offset);
|
||||
return preCaretRange.toString().length;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
162
src/packages/selection-manager/src/util.ts
Normal file
162
src/packages/selection-manager/src/util.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { FlexContentType, SelectionInfo, TextInfo } from "@armco/types"
|
||||
|
||||
export const getTextContainerId = (
|
||||
node: Node | HTMLElement,
|
||||
atomicContentSelector: string,
|
||||
) => {
|
||||
const textElementNode =
|
||||
node.nodeType === Node.TEXT_NODE
|
||||
? node.parentElement?.closest(atomicContentSelector)
|
||||
: node
|
||||
return (textElementNode as HTMLElement)?.id
|
||||
}
|
||||
|
||||
export const processRange = (
|
||||
selection: Selection,
|
||||
docObj: Document,
|
||||
contentRegistry: FlexContentType,
|
||||
atomicContentSelector: string,
|
||||
): SelectionInfo => {
|
||||
const selectionInfo: SelectionInfo = {
|
||||
startOffset: -1,
|
||||
endOffset: -1,
|
||||
startParentId: "",
|
||||
endParentId: "",
|
||||
startChunkId: "",
|
||||
endChunkId: "",
|
||||
selectedNodes: new Set(),
|
||||
raw: 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)
|
||||
}
|
||||
|
||||
const startParentId = getTextContainerId(
|
||||
range.startContainer,
|
||||
atomicContentSelector,
|
||||
)
|
||||
const endParentId = getTextContainerId(
|
||||
range.endContainer,
|
||||
atomicContentSelector,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectionInfo
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function calculateOffset(
|
||||
range: Range,
|
||||
container: Node,
|
||||
offset: number,
|
||||
): number {
|
||||
const relativeTo =
|
||||
container.nodeType === Node.TEXT_NODE
|
||||
? container.parentElement?.closest(".ar-Text")
|
||||
: (container as HTMLElement).closest(".ar-Text")
|
||||
if (relativeTo) {
|
||||
const preCaretRange = range.cloneRange()
|
||||
preCaretRange.selectNodeContents(relativeTo)
|
||||
preCaretRange.setEnd(container, offset)
|
||||
return preCaretRange.toString().length
|
||||
}
|
||||
return -1
|
||||
}
|
||||
8
src/packages/selection-manager/tsconfig.json
Normal file
8
src/packages/selection-manager/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "build"
|
||||
},
|
||||
"include": ["src"],
|
||||
}
|
||||
12
src/packages/syntax-highlighter/package.json
Normal file
12
src/packages/syntax-highlighter/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@sampadak/syntax-highlighter",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/syntax-highlighter/src/index.js
Normal file
1
src/packages/syntax-highlighter/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/syntax-highlighter/src/index.tsx
Normal file
0
src/packages/syntax-highlighter/src/index.tsx
Normal file
16
src/packages/toolbar/package.json
Normal file
16
src/packages/toolbar/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@sampadak/toolbar",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/toolbar/src/index.js
Normal file
1
src/packages/toolbar/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/toolbar/src/index.tsx
Normal file
0
src/packages/toolbar/src/index.tsx
Normal file
16
src/packages/ui/package.json
Normal file
16
src/packages/ui/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@sampadak/ui",
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^26.0.0"
|
||||
}
|
||||
}
|
||||
1
src/packages/ui/src/index.js
Normal file
1
src/packages/ui/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";
|
||||
0
src/packages/ui/src/index.tsx
Normal file
0
src/packages/ui/src/index.tsx
Normal file
5
src/setupTests.js
Normal file
5
src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom";
|
||||
@@ -17,10 +17,9 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"noEmit": false,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"],
|
||||
"exclude": ["**/build"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user