npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@layers-app/editor

v0.6.19

Published

LayersTextEditor is a text editor for web applications written in JavaScript, with a focus on reliability, accessibility, and performance.

Downloads

2,203

Readme

LayersTextEditor

LayersTextEditor is a text editor for web applications written in JavaScript, with a focus on reliability, accessibility, and performance.

Installation

To install the package, run one of the following commands:

Use npm:

npm install @layers-app/editor

If you plan to use the Swagger node, install the additional package: npm install swagger-ui-react.

Use yarn:

yarn add @layers-app/editor

Initializing the text editor

import { Editor } from '@layers-app/editor';

By default, LayersTextEditor works with an object and can return either an object or HTML.

Example with an object:

const text = 'Hello world';

const json = {
  root: {
    children: [
      {
        children: [
          {
            detail: 0,
            format: 0,
            mode: 'normal',
            style: '',
            text: text,
            type: 'text',
            version: 1,
          },
        ],
        direction: 'ltr',
        format: '',
        indent: 0,
        type: 'paragraph',
        version: 1,
      },
    ],
    direction: 'ltr',
    format: '',
    indent: 0,
    type: 'root',
    version: 1,
  },
};

const onChange = (
  data, // json
) => <Editor initialContent={json} onChange={onChange} />;

You can also pass an HTML string to the editor.

Example with HTML:

const html = `
<h2 dir="ltr" style="text-align: left;">
   <span style="background-color: rgb(248, 231, 28); font-family: &quot;Trebuchet MS&quot;; white-space: pre-wrap;">Hello</span>
</h2>
<h2 dir="ltr">
   <br>
</h2>
<p dir="ltr">
   <br>
</p>
<p dir="ltr">
   <span style="font-size: 21px; white-space: pre-wrap;">world</span>
</p>
`

const onChange = (data) => // json

<Editor initialContent={html} onChange={onChange} />

The output of the data in the onChange function is controlled by the outputFormat property. outputFormat can be either "html" or "json". Example with outputFormat:

const html = `
<h2 dir="ltr" style="text-align: left;">
   <span style="background-color: rgb(248, 231, 28); font-family: &quot;Trebuchet MS&quot;; white-space: pre-wrap;">Hello</span>
</h2>
<h2 dir="ltr">
   <br>
</h2>
<p dir="ltr">
   <br>
</p>
<p dir="ltr">
   <span style="font-size: 21px; white-space: pre-wrap;">world</span>
</p>
`

const onChange = (data: string, text?: string) => {
  // data - html from editor
  // text - text from editor
}


<Editor initialContent={html} outputFormat="html" onChange={onChange} />

Use StylesProvider to add styling to your HTML content.

  <StylesProvider>
      <div
        dangerouslySetInnerHTML={{ __html: '<p>Your html here</p>' }}
      />
    </StylesProvider>

Image upload

To start working with image uploads, use the fetchUploadMedia function, which takes three parameters: file, success, and error. After successfully uploading the image to your service, you should call the success function and pass two required arguments: the URL of the image and its ID. Optional: You can also pass two optional parameters: signal and onProgress. The signal allows you to cancel an ongoing upload using an AbortController, and onProgress provides the current upload progress in percent — useful for displaying a progress bar or loading state.

  const fetchUploadMedia = async (
    file: File,
    success: (url: string, id: string) => void,
    error?: (error?: Error) => void
  ) => {
    const formData = new FormData();
    formData.append('File', file);
    formData.append('FileAccessModifier', '0');

    try {
      const response = await fetch('/api/v1/Files/Upload', {
        method: 'POST',
        body: formData,
        credentials: 'include'
      });

      if (!response.ok) {
        throw new Error('File upload failed');
      }

      const data = await response.json();
      const { Id, Url } = data;

      success(Url, Id);
    } catch (err) {
      if (error) {
        if (err instanceof Error) {
          error(err);
        } else {
          error(new Error('An unknown error occurred'));
        }
      }
    }
  };

  const fetchUploadMedia = async (
  file: File,
  success: (url: string, id: string, data: any) => void,
  error?: (err: Error) => void,
  signal?: AbortSignal,
  onProgress?: (percent: number) => void,
) => {
    const formData = new FormData();
    formData.append('File', file);
    formData.append('FileAccessModifier', '0');

  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/api/v1/Files/Upload');
  xhr.withCredentials = true;

  if (signal) signal.addEventListener('abort', () => xhr.abort());
  if (onProgress) {
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
    };
  }

  xhr.onload = () => {
    try {
      const data = JSON.parse(xhr.responseText);
      success(`/v1/attachments/${data.id}`, data.id, data);
    } catch {
      error?.(new Error('Invalid response'));
    }
  };

  xhr.onerror = () => error?.(new Error('Upload error')));
  xhr.send(formData);
};


<Editor
  ...props
  fetchUploadMedia={fetchUploadMedia}
/>

Image Deletion

To have greater control over image deletion, pass an optional function fetchDeleteMedia to the editor, which accepts three parameters: id, success, and error. After successfully deleting the image from your service, the success function should be called.

  const fetchDeleteMedia = async (
    id: string,
    success: () => void,
    error?: (error?: Error) => void
  ) => {
    const body = { Ids: [id] };

    try {
      const response = await fetch('/api/v1/Documents/Delete', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(body),
        credentials: 'include'
      });

      await response.json();
      success();
    } catch (err) {
      if (error) {
        if (err instanceof Error) {
          error(err);
        } else {
          error(new Error('An unknown error occurred'));
        }
      }
    }
  };

<Editor
  ...props
   fetchUploadMedia={fetchUploadMedia}
   fetchDeleteMedia={fetchUploadMedia}
/>

Additional options for working with image uploads.

import { Editor, Dropzone } from "@sinups/editor-dsd";

const Content = () => (
      <Group justify="center" gap="xl" mih={220} style={{ pointerEvents: 'none' }}>
           {/*
      The components Dropzone.Accept, Dropzone.Reject, and Dropzone.Idle are visible only when the user performs specific actions:

Dropzone.Accept is visible only when the user drags a file that can be accepted into the drop zone.
Dropzone.Reject is visible only when the user drags a file that cannot be accepted into the drop zone.
Dropzone.Idle is visible when the user is not dragging any file into the drop zone.
          */}
            <Dropzone.Accept>
              <IconUpload
                style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)' }}
                stroke={1.5}
              />
            </Dropzone.Accept>
            <Dropzone.Reject>
              <IconX
                style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)' }}
                stroke={1.5}
              />
            </Dropzone.Reject>
            <Dropzone.Idle>
              <IconPhoto
                style={{ width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)' }}
                stroke={1.5}
              />
            </Dropzone.Idle>

            <div>
              <Text size="xl" inline>
                 Drag images here or click to select files
              </Text>
              <Text size="sm" c="dimmed" inline mt={7}>
               Attach as many files as you want, each file must not exceed{' '}                {maxFileSize} МБ.
              </Text>
            </div>
          </Group>
  );

<Editor
  ...props
   fetchUploadMedia={fetchUploadMedia}
   contentModalUploadImage={Content}
   maxFileSize={5}
   maxImageSizeError={() => {}}
/>

File upload

For uploading a file or audio, you might need the third parameter "data".

  const fetchUploadMedia = async (
    file: File,
    success: (url: string, id: string, data?: {
      contentType: string;
      fileSize: string;
      originalFileName: string;
    }) => void,
    error?: (error?: Error) => void
  ) => {
    const formData = new FormData();
    formData.append('File', file);
    formData.append('FileAccessModifier', '0');

    try {
      const response = await fetch('/api/v1/Files/Upload', {
        method: 'POST',
        body: formData,
        credentials: 'include'
      });

      if (!response.ok) {
        throw new Error('File upload failed');
      }

      const data = await response.json();
      const { Id, Url } = data;

      success(Url, Id, data);
    } catch (err) {
      if (error) {
        if (err instanceof Error) {
          error(err);
        } else {
          error(new Error('An unknown error occurred'));
        }
      }
    }
  };

Connect AI

  const fetchPromptResult = async (
    prompt: string,
    success: (data: string) => void,
    error?: () => void,
  ) => {
    try {
      const response = await fetch(
        'https://domain/api/v1/openai/call-any-prompt',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',

            Authorization: 'token',
          },
          body: JSON.stringify({ prompt }),
        },
      );

      if (!response.ok) {
        const errText = await response.text();
        console.error('server error', errText);
        throw new Error('API failed');
      }

      const data = await response.json();

      success(data.content);
    } catch (err) {
      if (error) {
        error();
        console.error('Error');
      }
    }
  };

<Editor
  ...props
  fetchPromptResult={fetchPromptResult}
/>
<Editor
  {...props}
  ws={{
    url: 'https://wss.dudoc.io/', // WebSocket URL
    id: '322323', // Unique document ID
    user: userProfile, // Current user
    getActiveUsers: (users) => {
      // Returns active users editing the document
      setActiveUsers(users);
    },
  }}
/>

Reset editor content


import {  CLEAR_EDITOR_COMMAND } from './EditorLexical';

<>
   <button
      onClick={() => {
        if (editorRef.current) {
          editorRef.current.update(() => {
            editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
          });
        }
      }}
    >
      Reset
    </button>
    <Editor
    ...props
     editorRef={editorRef}
    />
<>

Testing Overview

This project includes comprehensive testing with both unit tests (Vitest) and end-to-end tests (Playwright). The testing setup ensures reliability across different browsers and environments.

Prerequisites

Before running tests, make sure you have installed all dependencies:

npm install

Unit Tests (Vitest)

Unit tests are written with Vitest and jsdom for testing individual components and utilities.

Run Unit Tests

# Run all unit tests
npm run test-unit

# Run unit tests in watch mode (auto-rerun on changes)
npm run test-unit-watch

Unit Test Files Location

  • __tests__/unit/ - Unit test files
  • Test files follow the pattern: *.test.ts or *.test.tsx

End-to-End Tests (Playwright)

E2E tests use Playwright to test the complete application flow in real browsers.

Run E2E Tests

# Run all E2E tests (WebKit only for CI optimization)
npm run test:e2e

# Run E2E tests with UI mode (interactive)
npm run test:e2e:ui

# Run E2E tests in debug mode
npm run test:e2e:debug

# Run E2E tests in headed mode (visible browser)
npm run test:e2e:headed

E2E Test Files Location

  • __tests__/e2e/ - End-to-end test files
  • __tests__/regression/ - Regression test files
  • Test files follow the pattern: *.spec.js, *.spec.mjs, or *.spec.ts

Browser Support

  • WebKit (Safari) - Primary browser for CI/CD
  • Chromium and Firefox - Available for local testing

Test Server

The test server automatically starts when running E2E tests:

# Manual test server start (if needed)
npm run start-test-server
  • URL: http://localhost:3000
  • Mode: Full editor mode with all features enabled
  • Environment: VITE_LAYERS=true

Test Configuration

Playwright Configuration

  • Config file: playwright.config.js
  • Test directory: ./__tests__/e2e/
  • Browser: WebKit (optimized for CI)
  • Base URL: http://localhost:3000
  • Timeout: 90 seconds per test
  • Retries: 2 retries in CI, 0 locally

Vitest Configuration

  • Config file: vitest.config.mts
  • Environment: jsdom
  • Setup file: vitest.setup.mts
  • Coverage: V8 provider

CI/CD Testing

Tests run automatically on:

  • Push to main or dev branches
  • Pull requests to main or dev branches
  • Manual trigger via GitHub Actions

GitHub Actions Workflow

  • File: .github/workflows/tests.yml
  • Runner: Ubuntu Latest
  • Node.js: Version 20
  • Browser caching: Playwright browsers cached for faster runs
  • Artifacts: Test reports and traces uploaded on completion

Test Examples

Basic E2E Test Structure

// __tests__/e2e/example.spec.mjs
import { test, expect } from '@playwright/test';
import { focusEditor } from '../utils/index.mjs';

test('Can type text in editor', async ({ page }) => {
  await page.goto('/');
  await focusEditor(page);

  const editor = page.locator('[contenteditable="true"]').first();
  await editor.type('Hello World');

  await expect(editor).toContainText('Hello World');
});

Unit Test Structure

// __tests__/unit/example.test.ts
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Editor } from '../src/Editor';

describe('Editor Component', () => {
  it('renders without crashing', () => {
    const { container } = render(<Editor />);
    expect(container).toBeTruthy();
  });
});

Debugging Tests

Debug E2E Tests

# Run with Playwright Inspector
npm run test:e2e:debug

# Run specific test file
npx playwright test __tests__/e2e/TextEntry.spec.mjs --debug

# Run with headed browser
npm run test:e2e:headed

View Test Reports

# Open HTML report (after running tests)
npx playwright show-report

# View test traces (for failed tests)
npx playwright show-trace test-results/[test-name]/trace.zip

Test Utilities

Common test utilities are available in __tests__/utils/index.mjs:

  • focusEditor(page) - Focus the main editor
  • selectAll(page) - Select all text in editor
  • moveLeft(page, count) - Move cursor left
  • selectCharacters(page, count) - Select specific number of characters
  • waitForSelector(page, selector) - Wait for element to appear

Performance

Test Execution Times

  • Unit Tests: ~10-30 seconds
  • E2E Tests (first run): ~3-4 minutes (includes browser installation)
  • E2E Tests (cached): ~1-2 minutes (uses cached browsers)

Optimization Features

  • Browser Caching: Playwright browsers cached in CI
  • Single Worker: Prevents race conditions in CI
  • WebKit Only: Faster than multi-browser matrix
  • Smart Retries: Auto-retry flaky tests

Troubleshooting

Common Issues

  1. Port conflicts: Ensure port 3000 is available
  2. Browser installation: Run npx playwright install if needed
  3. Test timeouts: Check if test server is running properly
  4. Certificate errors: Tests use HTTP to avoid HTTPS certificate issues

Reset Test Environment

# Clear Playwright cache
npx playwright install --force

# Reset node_modules
rm -rf node_modules package-lock.json
npm install
onChange: (value: string | object) => undefined - A function that triggers every time the editor content changes and returns an HTML string or an object depending on the outputFormat property.
debounce?: number - Defines how often the onChange function is called, in milliseconds.
onBlur: (value: string | object) => undefined - A function that triggers when the editor loses focus and returns an HTML string or an object depending on the outputFormat property.
outputFormat?: 'html' | 'json' - The outputFormat property defines whether we want to output an HTML string or a JSON object. The default is JSON.
initialContent: string | object - The initial content for the editor.
maxHeight?: number - Sets the height of the editor. The default is 100%.
mode?: 'simple' | 'default' | 'full' | 'editor' - The editor mode. Depending on the chosen mode, functionality may be restricted or extended. The default is default.
fetchUploadMedia?: (file: File, success: (url: string, id: string, error?: (error?: Error) => void) => void) - Function to upload an image to your service.
fetchDeleteMedia?: (id: string, success: () => void, error?: (error?: Error) => void) - Helper function to delete an image.
maxFileSize?: number - The maximum image size in megabytes.
contentModalUploadImage?: React.FunctionComponent - A React component to replace content in DropZone.
maxImageSizeError?: () => void - A function that is called if the image exceeds the maxFileSize.
disable?: boolean - Toggles the editor into read-only mode.
ws?: { url: string, id: string, user: { color: string, name: string }, getActiveUsers: (users) => void } - WebSocket settings: URL, document ID, current user details, and function to return active users editing the document.
editorRef?: { current: EditorType | null } - Reference to the editor.