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

nol-editor

v1.5.0

Published

의존성 없는 가벼운 HTML 리치 텍스트(WYSIWYG) 에디터 - A tiny dependency-free HTML rich text editor with drag & drop upload, tables and server upload API

Readme

NolEditor

A small, free HTML rich text (WYSIWYG) editor. Written in plain JavaScript with zero dependencies, and released under the MIT license for anyone to use, modify, and redistribute.

22.6KB JS + 3.2KB CSS, gzipped. No jQuery, no bundler, no build step required.

한국어 문서 · Documentation

NolEditor


What's in it

Writing — bold, italic, underline, strikethrough, superscript and subscript; font family, size and line height; text color and highlight; headings and code blocks; blockquotes; four alignments; indentation; bullet lists, numbered lists, and checklists

Inserting — links, images, video (YouTube, Vimeo, mp4), tables, horizontal rules, emoji and special characters

Tablespick the size by dragging across a grid, or type the numbers. Put the caret in a table and a toolbar appears with row and column insert/delete, cell merge and split, header row toggle, and cell background color. Drag across cells to select a region, then merge it with one click. Tab moves between cells; pressing it in the last cell appends a row.

| Drag to pick a size | Merge cells | | --- | --- | | Table size picker | Cell merge |

Imagesdrop files straight into the editor, paste screenshots from the clipboard, or pick them from a dialog. Inserted images resize by dragging a corner, and a bubble menu handles alignment, width presets, and alt text.

Uploads — a single upload option wires all of that to your own server API, with a progress indicator built in. Unusual flows like S3 presigned URLs can replace the whole transfer via handler. Leave it unset and files are inlined as base64.

| Drop files in | Upload progress | | --- | --- | | Drop zone | Upload progress |

Languages — the interface ships in 14 languages. English is built in; the rest load as separate ~2KB files, so you never pay for languages you don't use. Any string can be overridden. See Languages.

Tools — find and replace, print, direct HTML source editing, fullscreen, dark mode, character count, a character limit, and markdown shortcuts (## , - , 1. , > , [] ) that format as you type

Undo that covers everything — the browser's own stack only sees execCommand, so merging cells or resizing an image was invisible to it. NolEditor keeps its own snapshots, groups a burst of typing into one step, and puts the caret back where the edit happened.

Works without a mouse — the toolbar is one tab stop with arrow keys inside it, toggles report aria-pressed, dialogs trap Tab and restore focus on Escape, and the colour, emoji and table-size grids are all keyboard-reachable.

Works on touch — the table size picker, cell merge drag and image resize handles run on pointer events, so a phone or tablet gets the same features a desktop does.

Also — pasting from Word, Hangul, or Google Docs strips the junk automatically; attaching to an existing <textarea> keeps its form value in sync; Korean and other IME input works correctly; TypeScript definitions are included.

Install

CDN

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/nol-editor@1/dist/nol-editor.min.css">
<script src="https://cdn.jsdelivr.net/npm/nol-editor@1/dist/nol-editor.min.js"></script>

npm

npm install nol-editor
import NolEditor from 'nol-editor';
import 'nol-editor/style.css';

Plain files

Copy dist/nol-editor.min.js and dist/nol-editor.min.css into your project.

Quick start

<div id="editor"></div>

<script>
  const editor = new NolEditor('#editor', {
    placeholder: 'Start writing...',
    height: 320,
    upload: { url: '/api/upload' },
    onChange: (html) => console.log(html)
  });
</script>

To upgrade an existing <textarea> in place:

<form method="post" action="/save">
  <textarea name="content"></textarea>
  <button type="submit">Save</button>
</form>

<script>
  new NolEditor('textarea[name=content]');
  // The edited HTML is mirrored into textarea.value and submits as-is.
</script>

Server uploads

new NolEditor('#editor', {
  upload: {
    url: '/api/upload',
    fieldName: 'file',
    headers: { Authorization: 'Bearer ' + token },
    maxSize: 10 * 1024 * 1024,
    accept: 'image/*'
  },
  onError: (err) => showToast(err.message)
});

Your endpoint receives multipart/form-data and returns JSON containing the URL:

{ "url": "/uploads/2026/abc.png" }

Working examples for Express, Spring Boot, PHP, Django, and S3 presigned URLs are in the upload guide.

Documentation

| Guide | Contents | | --- | --- | | Getting started | Install paths, saving and loading content | | API reference | Options, methods, events, shortcuts, theming | | File uploads | Server API integration, backend examples, S3 | | Framework integration | React, Vue, Next.js, Nuxt, Svelte, Angular | | Security | XSS defense, server sanitizing, upload safety, CSP |

Main options

| Option | Type | Default | Description | | --- | --- | --- | --- | | lang | string | object | 'en' | UI language code, or a custom string table | | markdown | boolean | true | Turn ## , - , > … into blocks as you type | | maxLength | number | — | Character limit; blocks typing, never editing | | columnResize | boolean | true | Drag a cell's right edge to resize its column | | historyLimit | number | 100 | Undo snapshots kept | | historyDelay | number | 400 | Idle ms before typing commits an undo step | | ariaLabel | string | from lang | Accessible name for the editing area | | toolbar | array | 'full' | 'basic' | 'minimal' | 'full' | Toolbar layout | | hide | string[] | — | Toolbar items to leave out | | buttons | object | — | Custom buttons, keyed by name | | placeholder | string | from lang | Text shown while empty | | height | number | string | 320 | Minimum height of the editing area | | maxHeight | number | string | null | Height at which the area starts scrolling | | value | string | '' | Initial HTML | | dark | boolean | false | Start in dark theme | | statusbar | boolean | true | Character-count bar at the bottom | | upload | object | null | Upload configuration | | allowedIframeHosts | string[] | YouTube etc. | Hosts allowed as iframe embeds | | onChange | function | null | (html, editor) | | onError | function | null | (error, editor) |

The full list lives in the API reference.

Customizing the toolbar

Presets

Three named layouts are built in, so the common cases need no array:

new NolEditor('#editor', { toolbar: 'basic' });    // 'full' (default), 'basic', 'minimal'

Hiding a few items

When you want everything except a couple of buttons, hide beats retyping the whole layout. A group left empty is dropped along with its divider, so you never get a stray separator:

new NolEditor('#editor', { hide: ['print', 'video', 'emoji'] });

An explicit layout

Each inner array is one group, with a divider drawn between groups:

new NolEditor('#editor', {
  toolbar: [
    ['bold', 'italic', 'underline'],
    ['ul', 'ol', 'checklist'],
    ['link', 'image', 'table'],
    ['source']
  ]
});

Your own buttons

Give a button a name, drop that name into the layout, and it behaves like any built-in one:

const editor = new NolEditor('#editor', {
  toolbar: [['bold', 'italic'], ['save']],
  buttons: {
    save: {
      icon: '<svg viewBox="0 0 24 24">…</svg>',   // or text: 'Save'
      title: 'Save the document',
      action: (editor) => post('/api/posts', editor.getHTML()),
      active: (editor) => !editor.isEmpty()       // optional highlight state
    }
  }
});

active is re-evaluated as the selection moves, the same way bold lights up inside bold text.

To make a button available to every editor on the page:

NolEditor.registerButton('save', { text: 'Save', action: (ed) => save(ed.getHTML()) });

A button passed in buttons overrides a global one of the same name.

Changing the toolbar at runtime

editor.setToolbar('minimal');          // swap layouts
editor.setToolbar([['bold'], ['source']]);
editor.hideItem('print');              // one name or an array
editor.showItem(['print', 'video']);
editor.addButton('save', { text: 'Save', action: onSave });

All of these are chainable and rebuild the toolbar in place, preserving the source and fullscreen states.

Main API

editor.getHTML()          // current HTML
editor.setHTML(html)      // replace content (sanitized first)
editor.getText()          // plain text
editor.insertHTML(html)   // insert at the caret
editor.clear()            // empty the editor
editor.isEmpty()          // true when there is no text or media
editor.focus()
editor.enable() / disable()
editor.uploadFiles(files) // push files through the upload pipeline
editor.findNext(q)
editor.replaceAll(q, r)
editor.toggleSource()
editor.toggleFullscreen()
editor.print()
editor.setDark(true)
editor.on('change', fn)   // events: change, upload, error
editor.destroy()          // remove and detach listeners

editor.setToolbar(layout) // swap the layout or preset
editor.hideItem(names)    // hide toolbar items
editor.showItem(names)    // bring them back
editor.addButton(name, def)

Static helpers:

NolEditor.registerLocale('fr-CA', { bold: 'Gras' });
NolEditor.registerButton('save', { text: 'Save', action: (ed) => save(ed) });
NolEditor.locales   // every registered language table
NolEditor.buttons   // every globally registered button

Keyboard shortcuts

| Key | Action | | --- | --- | | Ctrl/Cmd + B / I / U | Bold / italic / underline | | Ctrl/Cmd + K | Insert link | | Ctrl/Cmd + F | Find and replace | | Ctrl/Cmd + P | Print | | Ctrl/Cmd + Z / Y | Undo / redo | | Ctrl/Cmd + Shift + V | Paste without formatting | | Tab / Shift+Tab | Move between table cells, otherwise indent | | Esc | Close popover, exit fullscreen |

Languages

Fourteen languages ship with the editor:

| Code | Language | Code | Language | | --- | --- | --- | --- | | en | English (built in) | pt-BR | Português do Brasil | | ko | 한국어 | ru | Русский | | ja | 日本語 | it | Italiano | | zh-CN | 简体中文 | vi | Tiếng Việt | | zh-TW | 繁體中文 | id | Bahasa Indonesia | | es | Español | tr | Türkçe | | fr | Français | de | Deutsch |

Only English is bundled into the core, so a page never pays for languages it doesn't use. Load the one you need — each is about 2KB gzipped.

<script src="nol-editor.min.js"></script>
<script src="locales/ja.min.js"></script>
<script>
  new NolEditor('#editor', { lang: 'ja' });
</script>

With a bundler, import the locale and pass the table directly:

import NolEditor from 'nol-editor';
import ja from 'nol-editor/locales/ja';

new NolEditor('#editor', { lang: ja });

Naming a language you haven't loaded logs a warning and falls back to English rather than showing raw keys.

Adding or adjusting a language

lang also takes a plain object. Anything you leave out falls back to English, so a partial table is fine:

new NolEditor('#editor', {
  lang: {
    bold: 'Gras',
    'link.url': 'Adresse du lien',
    chars: (n) => `${n} caractères`
  }
});

To make a language reusable by code rather than passing the table every time, register it once:

import NolEditor from 'nol-editor';
import frCanada from './locales/fr-CA.js';

NolEditor.registerLocale('fr-CA', frCanada);
new NolEditor('#editor', { lang: 'fr-CA' });

Regional codes fall back sensibly: fr-CA uses fr if only that is loaded, and a bare zh picks up zh-CN.

NolEditor.locales.en lists every key, so it doubles as the template for a new translation. Copying a file from src/locales/ and sending a pull request is very welcome — the test suite checks that every locale covers the full key set, so nothing silently falls back.

Theming

Everything is driven by CSS variables — override them and you're done.

.noleditor {
  --nol-primary: #10b981;
  --nol-radius: 4px;
  --nol-border: #cbd5e1;
}

Security

setHTML(), pasting, and returning from source mode all strip <script>, on* handlers, javascript: URLs, and iframes from non-whitelisted hosts.

That said, client-side sanitizing is not a security boundary. An attacker can post to your API without going through the browser, so always sanitize again on the server before storing. Language-specific recipes are in the security guide.

Building from source

npm install

Installing runs the build, so dist/ ends up with UMD, ESM, CJS, minified builds, and type definitions.

| Command | What it does | | --- | --- | | npm run build | Regenerate dist/ | | npm test | 35 tests against a real Chromium | | npm run typecheck | Validate the shipped type definitions |

The tests exercise behavior that depends on contenteditable and document.execCommand, so they drive a real browser through Playwright rather than a simulated DOM. Chromium needs downloading once:

npx playwright install chromium

README screenshots are captured from the live editor. Regenerate them when the UI changes:

node tools/screenshots.mjs

Releasing

Publishing is tied to a version tag, so a release is an explicit act rather than something that happens on every push:

npm version minor && git push --follow-tags

That runs the release workflow, which refuses to publish unless the tag matches package.json, the type definitions check out and all the browser tests pass. It then publishes with provenance and drafts a GitHub release from the matching CHANGELOG section.

Authentication works either way: register the repository as a trusted publisher on npmjs.com and nothing needs storing, or save an automation token as the NPM_TOKEN secret. The very first publish of a package name has to be done by hand — npm cannot grant a trusted publisher for a package that does not exist yet.

Browser support

Current versions of Chrome, Edge, Firefox, and Safari, including mobile.

Known limitations

Cell selection on touch needs a short hold before it starts, because an immediate drag across cells is indistinguishable from scrolling the page.

License

MIT. Free for commercial use; just keep the copyright notice.