mirror of
https://github.com/codex-team/editor.js
synced 2026-03-16 07:35:48 +01:00
fix: lint issues in dom.ts
This commit is contained in:
parent
7ad1eed3bb
commit
4e7e384375
10 changed files with 428 additions and 58 deletions
23
.cursor/rules/do-not-modify-configs.mdc
Normal file
23
.cursor/rules/do-not-modify-configs.mdc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Rule: DO NOT MODIFY configuration files unless explicitly instructed
|
||||
|
||||
## Description
|
||||
You MUST **never modify any configuration files** (such as `vite.config.ts`, `tsconfig.json`, `.eslintrc`, `package.json`, `.env`, etc.) **unless explicitly told to do so** in the current request or accompanying instructions.
|
||||
|
||||
## Examples
|
||||
|
||||
✅ **Allowed**
|
||||
- Editing TypeScript source files, tests, or component code.
|
||||
- Updating imports, logic, or styles within non-config files.
|
||||
- Adding configuration changes **only when explicitly requested** (e.g., “Add a new alias in `vite.config.ts`”).
|
||||
|
||||
❌ **Not Allowed**
|
||||
- Modifying or creating any config files without explicit instruction.
|
||||
- Automatically adding dependencies or changing build/test settings.
|
||||
- Altering environment variables or global project settings without being told to.
|
||||
|
||||
## Enforcement
|
||||
If you believe a configuration change might be required, **ask for confirmation first** before proceeding.
|
||||
23
.cursor/rules/fix-problems.mdc
Normal file
23
.cursor/rules/fix-problems.mdc
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Fix Problems Policy
|
||||
|
||||
## Core Principle
|
||||
VERY IMPORTANT: When encountering ANY problem in the code—such as TypeScript errors, linting issues, runtime bugs, accessibility violations, or performance problems—you or any other problem MUST find a proper way to fix it. Do NOT silence, suppress, or avoid the problem using workarounds like `// @ts-ignore`, `any` types, or ignoring linter warnings.
|
||||
|
||||
## Preferred Approaches
|
||||
- **Refactor for correctness**: Resolve issues by improving the code structure, using precise types, type guards, proper error handling, and best practices.
|
||||
- **Investigate root causes**: Use tools like debugging, logging, or code searches to understand why the problem occurs before fixing it.
|
||||
- **Align with existing rules**: Follow related policies such as the Fix TypeScript Errors Policy (adapt for other languages), ESLint configurations, and accessibility guidelines.
|
||||
- **Test the fix**: After fixing, verify with tests, linting runs (e.g., `yarn lint:fix`), or manual checks to ensure the problem is truly resolved without introducing new issues.
|
||||
|
||||
## When to Apply
|
||||
- During any code editing, reviewing, or generation task.
|
||||
- Proactively scan for and fix problems in affected files using available tools (e.g., read_lints, grep, codebase_search).
|
||||
- If a problem persists after reasonable efforts, document it clearly and suggest next steps rather than suppressing it.
|
||||
|
||||
## Notes
|
||||
- This policy promotes robust, high-quality code that is easier to maintain and less prone to future issues.
|
||||
- If unsure how to fix a problem, use tools to gather more information or break it into smaller, solvable parts rather than bypassing it.
|
||||
119
.cursor/rules/src/frontend/accessibility.mdc
Normal file
119
.cursor/rules/src/frontend/accessibility.mdc
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
description: Enforce accessibility best practices so all users can use the application
|
||||
---
|
||||
|
||||
### Accessibility guidance (must follow)
|
||||
|
||||
- Semantics first
|
||||
- Prefer semantic HTML (`button`, `a`, `nav`, `main`, `header`, `footer`, `ul/ol/li`, `table/th/td`) over generic `div`/`span`.
|
||||
- Use `button` for actions and `a`/`Link` for navigation. Do not use click handlers on non-interactive elements. If unavoidable, add `role="button"`, `tabIndex={0}`, and keyboard handlers for Enter/Space.
|
||||
|
||||
- Keyboard support
|
||||
- All interactive controls must be reachable via Tab and operable via keyboard.
|
||||
- Do not remove focus outlines. If customizing, ensure visible `:focus-visible` styles with sufficient contrast.
|
||||
- Preserve a logical tab order; avoid `tabIndex` > 0.
|
||||
|
||||
- Focus management
|
||||
- On opening modals/drawers/popovers: move focus inside, trap focus, and restore focus to the trigger on close.
|
||||
- Provide a skip link to main content (e.g., `href="#main"`) and landmark roles (`<main>`, `<nav>`, `<header>`, `<footer>`).
|
||||
|
||||
- Images and media
|
||||
- Every `img` must have an appropriate `alt`. If decorative, use `alt=""` and `aria-hidden="true"`.
|
||||
- Provide captions/subtitles for video/audio when applicable.
|
||||
- For lazy-loaded images with skeletons, mark skeletons `aria-hidden="true"` and set container `aria-busy` while loading.
|
||||
|
||||
- Forms
|
||||
- Inputs require visible labels bound via `<label htmlFor>` or `aria-label`/`aria-labelledby`.
|
||||
- Indicate errors with `aria-invalid` and associate helper/error text via `aria-describedby`.
|
||||
|
||||
- Live updates and async content
|
||||
- For dynamic status (loading/completion), use `aria-live="polite"` (or `assertive` if critical).
|
||||
- Spinners should have `aria-label` or be hidden (`aria-hidden="true"`) with a separate live region announcing status.
|
||||
|
||||
- Headings and structure
|
||||
- Maintain a logical heading hierarchy without skipping levels.
|
||||
- Use list semantics for collections.
|
||||
|
||||
- Color and contrast
|
||||
- Ensure WCAG 2.1 AA contrast: 4.5:1 for normal text and 3:1 for large or bold text and UI components, including focus and hover states. When placing text over images, add an overlay or background.
|
||||
- Do not convey information by color alone; add icons/text.
|
||||
|
||||
- Motion and reduced motion
|
||||
- Respect `prefers-reduced-motion: reduce`. Disable or simplify non-essential animations.
|
||||
- In React animations, gate effects with `gsap.matchMedia('(prefers-reduced-motion: no-preference)')` and provide a reduced-motion path.
|
||||
- Example usage exists in [AnnouncementsFeedContent.tsx](mdc:src/frontend/src/features/AnnouncementsFeed/ui/AnnouncementsFeedContent.tsx).
|
||||
- Vanilla CSS example using the `prefers-reduced-motion` media query:
|
||||
|
||||
```css
|
||||
/* Default animations */
|
||||
.card {
|
||||
transition: transform 300ms ease, opacity 300ms ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* Reduced motion: remove transforms and long transitions */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation: none !important;
|
||||
transition-duration: 0.01ms !important; /* effectively no transition */
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
.card:hover {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Tables and data
|
||||
- Use `<th scope>` for headers, provide captions when helpful. Avoid layout tables.
|
||||
|
||||
- Testing
|
||||
- Prefer `@testing-library` queries by role/name (`getByRole`, `getByLabelText`) to reflect real accessibility.
|
||||
|
||||
### React implementation tips
|
||||
|
||||
- Announce route changes by updating `document.title` and placing page content in a `<main id="main">` region.
|
||||
- When building composite widgets (tabs, accordions), follow the relevant ARIA patterns (roles, `aria-selected`, `aria-controls`) only when semantics are not achievable with native elements.
|
||||
- For card components that wrap links, ensure the entire card is a single focusable link (as with `Link`) and include descriptive link text or `aria-label` if needed.
|
||||
|
||||
### Code patterns
|
||||
|
||||
```tsx
|
||||
// Accessible button vs. link
|
||||
<button type="button" onClick={handleAction}>Do action</button>
|
||||
<Link to="/path">Go to details</Link>
|
||||
|
||||
// Custom interactive element (only if you cannot use <button>)
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
const isEnter = e.key === 'Enter' || e.code === 'Enter';
|
||||
const isSpace = e.key === ' ' || e.key === 'Spacebar' || e.code === 'Space';
|
||||
|
||||
if (isEnter) {
|
||||
onClick();
|
||||
}
|
||||
if (isSpace) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
onKeyUp={(e) => {
|
||||
const isSpace = e.key === ' ' || e.key === 'Spacebar' || e.code === 'Space';
|
||||
if (isSpace) {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
onClick={onClick}
|
||||
/>
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Use ARIA to enhance semantics, not replace them. Avoid redundant roles on native elements.
|
||||
- If a component is purely decorative (e.g., background clouds), set `aria-hidden="true"` and remove from the tab order.
|
||||
27
.cursor/rules/src/frontend/code-style-eslint.mdc
Normal file
27
.cursor/rules/src/frontend/code-style-eslint.mdc
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
title: Frontend ESLint Code Style
|
||||
alwaysApply: true
|
||||
description: Defer all code style decisions to the project's ESLint configuration; do not invent new style rules
|
||||
---
|
||||
|
||||
### Code Style Source of Truth
|
||||
|
||||
- **Always defer to ESLint configuration** for any code style, formatting, or lint rules.
|
||||
- **Do not create or enforce custom style rules** beyond what ESLint (and its plugins) already defines in this repo.
|
||||
|
||||
### Where the rules live
|
||||
|
||||
- Frontend config: [eslint.config.js](mdc:src/frontend/eslint.config.js)
|
||||
- AppShell config: [.eslintrc.js](mdc:src/Dodo.KnowledgeBase.Web/appshell/.eslintrc.js)
|
||||
|
||||
### How to apply
|
||||
|
||||
- When unsure about style (imports order, quote style, indentation, prop ordering, hooks rules, etc.), consult the ESLint configs above and follow them as-is.
|
||||
- Prefer using the repo scripts to validate/fix:
|
||||
- `yarn lint`
|
||||
- `yarn lint:fix`
|
||||
|
||||
### Notes
|
||||
|
||||
- If ESLint and Prettier interact, follow the ESLint-integrated Prettier setup from the configs.
|
||||
- For styles-in-JS (e.g., styled-components), follow any ESLint plugin guidance present; do not invent property ordering rules.
|
||||
22
.cursor/rules/src/frontend/fix-typescript-errors.mdc
Normal file
22
.cursor/rules/src/frontend/fix-typescript-errors.mdc
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
globs: *.ts,*.tsx
|
||||
description: Enforce fixing TypeScript errors by improving code quality, not suppressing them
|
||||
---
|
||||
|
||||
# Fix TypeScript Errors Policy
|
||||
|
||||
- **Core Principle**: Always resolve TypeScript errors by refactoring code to be type-safe, rather than suppressing them with `any`, `// @ts-ignore`, or similar workarounds.
|
||||
- **Preferred Approaches**:
|
||||
- Use precise types, type guards, discriminated unions, and proper narrowing to eliminate errors.
|
||||
- Avoid the non-null assertion operator (`!`) and `any` types as per project guidelines.
|
||||
- Refactor functions, components, and logic to align with TypeScript's type system.
|
||||
- **When to Apply**:
|
||||
- For any TypeScript files (`.ts`, `.tsx`), prioritize fixing errors during edits.
|
||||
- After making changes, run `yarn lint:fix` or similar commands to ensure compliance.
|
||||
- **Alignment with Existing Rules**:
|
||||
- This reinforces the ESLint Fix-First Policy: Fix issues flagged by TypeScript/ESLint by improving code, not silencing linters.
|
||||
- Ensure accessibility and best practices are maintained while resolving types.
|
||||
- **Notes**:
|
||||
- If a TypeScript error persists after reasonable refactoring, consult the ESLint configuration or seek clarification on intended behavior, but do not suppress it locally.
|
||||
- Promote code that is both type-safe and adheres to React/JS best practices.
|
||||
69
.cursor/rules/src/frontend/frontend-simplicity.mdc
Normal file
69
.cursor/rules/src/frontend/frontend-simplicity.mdc
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
globs: "*.ts","*.tsx","*.js","*.jsx","src/frontend/**"
|
||||
description: "Frontend development principle: Keep solutions simple and avoid overengineering"
|
||||
---
|
||||
|
||||
# Frontend Simplicity Principle
|
||||
|
||||
When working on frontend tasks, prioritize simple, straightforward solutions over complex implementations.
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Keep it simple
|
||||
- **Prefer basic approaches**: Choose standard patterns over custom abstractions unless there's a clear benefit
|
||||
- **Avoid premature optimization**: Don't add complexity for performance gains that haven't been measured
|
||||
- **Use existing libraries**: Leverage well-established libraries rather than building custom solutions
|
||||
|
||||
### Component design
|
||||
- **Single responsibility**: Components should do one thing well
|
||||
- **Avoid deep nesting**: Keep component trees shallow and manageable
|
||||
- **Prefer composition over inheritance**: Use composition patterns for reusable behavior
|
||||
|
||||
### State management
|
||||
- **Local state first**: Use local component state before reaching for global state management
|
||||
- **Simple patterns**: Prefer useState/useReducer over complex state machines unless necessary
|
||||
- **Avoid over-abstraction**: Don't create unnecessary abstractions for simple state logic
|
||||
|
||||
### Code organization
|
||||
- **Clear naming**: Use descriptive names that explain the purpose
|
||||
- **Minimal files**: Avoid splitting simple features across multiple files
|
||||
- **Straightforward logic**: Write code that's easy to follow and debug
|
||||
|
||||
### When complexity is justified
|
||||
Only add complexity when:
|
||||
- It solves a measured performance problem
|
||||
- It significantly improves user experience
|
||||
- It enables critical functionality
|
||||
- The team agrees it's necessary
|
||||
|
||||
## Examples
|
||||
|
||||
```tsx
|
||||
// ✅ Simple and clear
|
||||
const UserProfile = ({ user }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{user.name}</h2>
|
||||
{isEditing ? (
|
||||
<EditForm onSave={() => setIsEditing(false)} />
|
||||
) : (
|
||||
<button onClick={() => setIsEditing(true)}>Edit</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ❌ Overcomplicated
|
||||
const UserProfile = ({ user }) => {
|
||||
const [state, dispatch] = useReducer(profileReducer, initialProfileState);
|
||||
const editingContext = useContext(EditingContext);
|
||||
const formManager = useFormManager();
|
||||
|
||||
// Complex logic that could be simplified...
|
||||
};
|
||||
```
|
||||
|
||||
Remember: Code is read more than it's written. Choose the solution that future developers can understand quickly.
|
||||
24
.cursor/rules/src/frontend/lint-fix-policy.mdc
Normal file
24
.cursor/rules/src/frontend/lint-fix-policy.mdc
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
description: Policy for handling ESLint issues by preferring autofix with yarn lint:fix
|
||||
---
|
||||
|
||||
# Lint Fix Policy
|
||||
|
||||
When encountering ANY ESLint problem:
|
||||
|
||||
## Core Steps
|
||||
1. **ALWAYS try autofix first**: Run `yarn lint:fix` (or the equivalent command for the subproject) to automatically resolve the issue.
|
||||
- For frontend: From the workspace root, run `cd packages/frontend && yarn lint:fix`
|
||||
- If targeting specific files: `cd packages/frontend && yarn eslint "path/to/file.tsx" --fix`
|
||||
|
||||
2. **ONLY manual fix if autofix fails**: If `yarn lint:fix` does not resolve the issue, manually edit the code to comply with ESLint rules.
|
||||
- Defer to the ESLint configuration as the source of truth: [eslint.config.js](mdc:eslint.config.js) for frontend.
|
||||
- Do not invent custom style rules; follow ESLint and integrated Prettier setups exactly.
|
||||
- After manual fixes, re-run `yarn lint` to verify resolution.
|
||||
|
||||
## Notes
|
||||
- Prefer `yarn lint:fix` over ad-hoc formatting to ensure consistency.
|
||||
- If ESLint interacts with Prettier, let ESLint enforce the rules.
|
||||
- For uncertainty, consult ESLint configs before manual changes.
|
||||
- Proactively use this during code edits, reviews, or generations to maintain high-quality code.
|
||||
37
.cursor/rules/test/e2e-best-practices.mdc
Normal file
37
.cursor/rules/test/e2e-best-practices.mdc
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
globs: tests/**/*.spec.ts,tests/**/*.ts
|
||||
description: Playwright end-to-end testing patterns and expectations
|
||||
---
|
||||
# Playwright E2E Tests
|
||||
|
||||
- **Use shared fixtures**
|
||||
- Import `test`/`expect` from `[fixtures.ts](mdc:tests/Dodo.KnowledgeBase.Ui/fixtures/fixtures.ts)` so page objects, API helpers, and auth utilities stay consistent.
|
||||
- Prefer the `storedCookies` fixture when a test needs an authenticated context to avoid duplicate login work.
|
||||
|
||||
- **Lean on helpers and page objects**
|
||||
- Reuse the page-object classes in `[pages/](mdc:tests/Dodo.KnowledgeBase.Ui/pages)` for interactions; add new methods there instead of ad-hoc selectors in specs.
|
||||
- Wrap navigation, assertions, and Allure metadata with `test.step` via `[utils/helpers.ts](mdc:tests/Dodo.KnowledgeBase.Ui/utils/helpers.ts)` for richer reporting.
|
||||
|
||||
- **Prefer resilient, accessible locators**
|
||||
- Target elements by role, label, or text when possible (e.g., `page.getByRole('button', { name: '...' })`) before falling back to CSS/XPath.
|
||||
- Mirror the app’s accessibility requirements—favor semantic selectors over brittle DOM structure hooks.
|
||||
|
||||
- **Keep tests focused and deterministic**
|
||||
- Scope each spec to a single feature/flow; move common setup into `test.beforeEach` blocks using helpers.
|
||||
|
||||
- **Leverage configuration**
|
||||
- Align new suites with existing Playwright projects defined in `[playwright.config.ts](mdc:playwright.config.ts)`; extend `testMatch` rather than spinning up new configs.
|
||||
- Respect shared `use` options (locale, screenshots, traces) to keep reports uniform.
|
||||
|
||||
- **AVOID using mocks unless it's necessary**
|
||||
- When writing tests prefer actual data instead of using mocks to test actual behavior.
|
||||
|
||||
- **Do not @allure.id to tests**
|
||||
- Adding @allure.id is handled on the user's side DO NOT add it yourself.
|
||||
|
||||
- **Document Allure hierarchy**
|
||||
- Call `Helpers.addAllureHierarchy` at suite setup (see `[auth-tests.spec.ts](mdc:tests/Dodo.KnowledgeBase.Ui/auth-tests.spec.ts)`) so new tests appear correctly in TestOps.
|
||||
|
||||
- **Running locally**
|
||||
- Follow the workflow in `[README.md](mdc:README.md#L51)` (`yarn --cwd src/frontend serve` + `yarn e2e:ui`) when validating new specs.
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
},
|
||||
"rules": {
|
||||
"jsdoc/require-returns-type": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": "warn",
|
||||
"@typescript-eslint/strict-boolean-expressions": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/consistent-type-exports": "error"
|
||||
},
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
],
|
||||
"rules": {
|
||||
"quotes": [1, "double"],
|
||||
"semi": [1, "never"],
|
||||
"semi": [1, "never"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default class Dom {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
public static isSingleTag(tag: HTMLElement): boolean {
|
||||
return tag.tagName && [
|
||||
return Boolean(tag.tagName) && [
|
||||
'AREA',
|
||||
'BASE',
|
||||
'BR',
|
||||
|
|
@ -40,10 +40,7 @@ export default class Dom {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
public static isLineBreakTag(element: HTMLElement): element is HTMLBRElement {
|
||||
return element && element.tagName && [
|
||||
'BR',
|
||||
'WBR',
|
||||
].includes(element.tagName);
|
||||
return !!element && ['BR', 'WBR'].includes(element.tagName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -54,7 +51,7 @@ export default class Dom {
|
|||
* @param {object} [attributes] - any attributes
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
public static make(tagName: string, classNames: string | (string | undefined)[] | null = null, attributes: object = {}): HTMLElement {
|
||||
public static make(tagName: string, classNames: string | (string | undefined)[] | null = null, attributes: Record<string, string | number | boolean | null | undefined> = {}): HTMLElement {
|
||||
const el = document.createElement(tagName);
|
||||
|
||||
if (Array.isArray(classNames)) {
|
||||
|
|
@ -67,7 +64,17 @@ export default class Dom {
|
|||
|
||||
for (const attrName in attributes) {
|
||||
if (Object.prototype.hasOwnProperty.call(attributes, attrName)) {
|
||||
el[attrName] = attributes[attrName];
|
||||
const value = attributes[attrName];
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attrName in el) {
|
||||
(el as unknown as Record<string, unknown>)[attrName] = value;
|
||||
} else {
|
||||
el.setAttribute(attrName, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,16 +135,16 @@ export default class Dom {
|
|||
const temp = document.createElement('div'),
|
||||
parent = el1.parentNode;
|
||||
|
||||
parent.insertBefore(temp, el1);
|
||||
parent?.insertBefore(temp, el1);
|
||||
|
||||
// move el1 to right before el2
|
||||
parent.insertBefore(el1, el2);
|
||||
parent?.insertBefore(el1, el2);
|
||||
|
||||
// move el2 to right before where el1 used to be
|
||||
parent.insertBefore(el2, temp);
|
||||
parent?.insertBefore(el2, temp);
|
||||
|
||||
// remove temporary marker node
|
||||
parent.removeChild(temp);
|
||||
parent?.removeChild(temp);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -216,50 +223,61 @@ export default class Dom {
|
|||
* @returns - it can be text Node or Element Node, so that caret will able to work with it
|
||||
* Can return null if node is Document or DocumentFragment, or node is not attached to the DOM
|
||||
*/
|
||||
public static getDeepestNode(node: Node, atLast = false): Node | null {
|
||||
public static getDeepestNode(node: Node | null, atLast = false): Node | null {
|
||||
/**
|
||||
* Current function have two directions:
|
||||
* - starts from first child and every time gets first or nextSibling in special cases
|
||||
* - starts from last child and gets last or previousSibling
|
||||
* - starts from first child and every time gets first or nextSibling in special cases
|
||||
* - starts from last child and gets last or previousSibling
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const child = atLast ? 'lastChild' : 'firstChild',
|
||||
sibling = atLast ? 'previousSibling' : 'nextSibling';
|
||||
|
||||
if (node && node.nodeType === Node.ELEMENT_NODE && node[child]) {
|
||||
let nodeChild = node[child] as Node;
|
||||
|
||||
/**
|
||||
* special case when child is single tag that can't contain any content
|
||||
*/
|
||||
if (
|
||||
Dom.isSingleTag(nodeChild as HTMLElement) &&
|
||||
!Dom.isNativeInput(nodeChild) &&
|
||||
!Dom.isLineBreakTag(nodeChild as HTMLElement)
|
||||
) {
|
||||
/**
|
||||
* 1) We need to check the next sibling. If it is Node Element then continue searching for deepest
|
||||
* from sibling
|
||||
*
|
||||
* 2) If single tag's next sibling is null, then go back to parent and check his sibling
|
||||
* In case of Node Element continue searching
|
||||
*
|
||||
* 3) If none of conditions above happened return parent Node Element
|
||||
*/
|
||||
if (nodeChild[sibling]) {
|
||||
nodeChild = nodeChild[sibling];
|
||||
} else if (nodeChild.parentNode[sibling]) {
|
||||
nodeChild = nodeChild.parentNode[sibling];
|
||||
} else {
|
||||
return nodeChild.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getDeepestNode(nodeChild, atLast);
|
||||
if (node === null || node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return node;
|
||||
}
|
||||
|
||||
return node;
|
||||
const nodeChildProperty = node[child];
|
||||
|
||||
if (nodeChildProperty === null) {
|
||||
return node;
|
||||
}
|
||||
|
||||
let nodeChild = nodeChildProperty as Node;
|
||||
|
||||
/**
|
||||
* special case when child is single tag that can't contain any content
|
||||
*/
|
||||
if (
|
||||
Dom.isSingleTag(nodeChild as HTMLElement) &&
|
||||
!Dom.isNativeInput(nodeChild) &&
|
||||
!Dom.isLineBreakTag(nodeChild as HTMLElement)
|
||||
) {
|
||||
/**
|
||||
* 1) We need to check the next sibling. If it is Node Element then continue searching for deepest
|
||||
* from sibling
|
||||
*
|
||||
* 2) If single tag's next sibling is null, then go back to parent and check his sibling
|
||||
* In case of Node Element continue searching
|
||||
*
|
||||
* 3) If none of conditions above happened return parent Node Element
|
||||
*/
|
||||
const siblingNode = nodeChild[sibling];
|
||||
|
||||
if (siblingNode) {
|
||||
nodeChild = siblingNode;
|
||||
} else {
|
||||
const parentSiblingNode = nodeChild.parentNode?.[sibling];
|
||||
|
||||
if (!parentSiblingNode) {
|
||||
return nodeChild.parentNode;
|
||||
}
|
||||
nodeChild = parentSiblingNode;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getDeepestNode(nodeChild, atLast);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -274,7 +292,7 @@ export default class Dom {
|
|||
return false;
|
||||
}
|
||||
|
||||
return node && node.nodeType && node.nodeType === Node.ELEMENT_NODE;
|
||||
return node != null && node.nodeType != null && node.nodeType === Node.ELEMENT_NODE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -289,7 +307,7 @@ export default class Dom {
|
|||
return false;
|
||||
}
|
||||
|
||||
return node && node.nodeType && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
||||
return node != null && node.nodeType != null && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -315,7 +333,7 @@ export default class Dom {
|
|||
'TEXTAREA',
|
||||
];
|
||||
|
||||
return target && target.tagName ? nativeInputs.includes(target.tagName) : false;
|
||||
return target != null && typeof target.tagName === 'string' ? nativeInputs.includes(target.tagName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -357,7 +375,7 @@ export default class Dom {
|
|||
* @returns {boolean} true if it is empty
|
||||
*/
|
||||
public static isNodeEmpty(node: Node, ignoreChars?: string): boolean {
|
||||
let nodeText;
|
||||
let nodeText: string | undefined;
|
||||
|
||||
if (this.isSingleTag(node as HTMLElement) && !this.isLineBreakTag(node as HTMLElement)) {
|
||||
return false;
|
||||
|
|
@ -366,14 +384,14 @@ export default class Dom {
|
|||
if (this.isElement(node) && this.isNativeInput(node)) {
|
||||
nodeText = (node as HTMLInputElement).value;
|
||||
} else {
|
||||
nodeText = node.textContent.replace('\u200B', '');
|
||||
nodeText = node.textContent?.replace('\u200B', '');
|
||||
}
|
||||
|
||||
if (ignoreChars) {
|
||||
nodeText = nodeText.replace(new RegExp(ignoreChars, 'g'), '');
|
||||
nodeText = nodeText?.replace(new RegExp(ignoreChars, 'g'), '');
|
||||
}
|
||||
|
||||
return nodeText.length === 0;
|
||||
return (nodeText?.length ?? 0) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -403,12 +421,14 @@ export default class Dom {
|
|||
const treeWalker = [ node ];
|
||||
|
||||
while (treeWalker.length > 0) {
|
||||
node = treeWalker.shift();
|
||||
const currentNode = treeWalker.shift();
|
||||
|
||||
if (!node) {
|
||||
if (!currentNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node = currentNode;
|
||||
|
||||
if (this.isLeaf(node) && !this.isNodeEmpty(node, ignoreChars)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -450,7 +470,7 @@ export default class Dom {
|
|||
return (node as Text).length;
|
||||
}
|
||||
|
||||
return node.textContent.length;
|
||||
return node.textContent?.length ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -518,7 +538,7 @@ export default class Dom {
|
|||
wrapper = data;
|
||||
}
|
||||
|
||||
const check = (element: HTMLElement): boolean => {
|
||||
const check = (element: Element): boolean => {
|
||||
return !Dom.blockElements.includes(element.tagName.toLowerCase()) &&
|
||||
Array.from(element.children).every(check);
|
||||
};
|
||||
|
|
@ -539,7 +559,7 @@ export default class Dom {
|
|||
|
||||
return Array.from(parent.children).reduce((result, element) => {
|
||||
return [...result, ...Dom.getDeepestBlockElements(element as HTMLElement)];
|
||||
}, []);
|
||||
}, [] as HTMLElement[]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -550,7 +570,13 @@ export default class Dom {
|
|||
*/
|
||||
public static getHolder(element: string | HTMLElement): HTMLElement {
|
||||
if (_.isString(element)) {
|
||||
return document.getElementById(element);
|
||||
const holder = document.getElementById(element);
|
||||
|
||||
if (holder === null) {
|
||||
throw new Error(`Element with id "${element}" not found`);
|
||||
}
|
||||
|
||||
return holder;
|
||||
}
|
||||
|
||||
return element;
|
||||
|
|
@ -572,7 +598,7 @@ export default class Dom {
|
|||
* @todo handle case when editor initialized in scrollable popup
|
||||
* @param el - element to compute offset
|
||||
*/
|
||||
public static offset(el): { top: number; left: number; right: number; bottom: number } {
|
||||
public static offset(el: Element): { top: number; left: number; right: number; bottom: number } {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
|
@ -682,13 +708,13 @@ export function isCollapsedWhitespaces(textContent: string): boolean {
|
|||
* The calculation formula is as follows:
|
||||
*
|
||||
* 1. Calculate the baseline offset:
|
||||
* - Typically, the baseline is about 80% of the `fontSize` from the top of the text, as this is a common average for many fonts.
|
||||
* - Typically, the baseline is about 80% of the `fontSize` from the top of the text, as this is a common average for many fonts.
|
||||
*
|
||||
* 2. Calculate the additional space due to `lineHeight`:
|
||||
* - If the `lineHeight` is greater than the `fontSize`, the extra space is evenly distributed above and below the text. This extra space is `(lineHeight - fontSize) / 2`.
|
||||
* - If the `lineHeight` is greater than the `fontSize`, the extra space is evenly distributed above and below the text. This extra space is `(lineHeight - fontSize) / 2`.
|
||||
*
|
||||
* 3. Calculate the total baseline Y coordinate:
|
||||
* - Sum of `marginTop`, `borderTopWidth`, `paddingTop`, the extra space due to `lineHeight`, and the baseline offset.
|
||||
* - Sum of `marginTop`, `borderTopWidth`, `paddingTop`, the extra space due to `lineHeight`, and the baseline offset.
|
||||
*
|
||||
* @param element - The element to calculate the baseline for.
|
||||
* @returns {number} - The Y coordinate of the text baseline from the top of the element's margin box.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue