├── .eslintignore ├── packages ├── quill │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── assets │ │ │ ├── favicon.png │ │ │ ├── icons │ │ │ │ ├── dropdown.svg │ │ │ │ ├── mention.svg │ │ │ │ ├── comment.svg │ │ │ │ ├── redo.svg │ │ │ │ ├── undo.svg │ │ │ │ ├── attachment.svg │ │ │ │ ├── more.svg │ │ │ │ ├── table-merge-cells.svg │ │ │ │ ├── underline.svg │ │ │ │ ├── italic.svg │ │ │ │ ├── align-center.svg │ │ │ │ ├── align-justify.svg │ │ │ │ ├── align-left.svg │ │ │ │ ├── align-right.svg │ │ │ │ ├── code.svg │ │ │ │ ├── float-full.svg │ │ │ │ ├── image.svg │ │ │ │ ├── float-center.svg │ │ │ │ ├── table-border-all.svg │ │ │ │ ├── color.svg │ │ │ │ ├── emoji.svg │ │ │ │ ├── bold.svg │ │ │ │ ├── outdent.svg │ │ │ │ ├── hashtag.svg │ │ │ │ ├── indent.svg │ │ │ │ ├── size-decrease.svg │ │ │ │ ├── speech.svg │ │ │ │ ├── horizontal-rule.svg │ │ │ │ ├── authorship.svg │ │ │ │ ├── blockquote.svg │ │ │ │ ├── clean.svg │ │ │ │ ├── direction-ltr.svg │ │ │ │ ├── direction-rtl.svg │ │ │ │ ├── size-increase.svg │ │ │ │ ├── float-left.svg │ │ │ │ ├── table-unmerge-cells.svg │ │ │ │ ├── link.svg │ │ │ │ ├── list-bullet.svg │ │ │ │ ├── float-right.svg │ │ │ │ ├── list-check.svg │ │ │ │ ├── table-insert-columns.svg │ │ │ │ ├── audio.svg │ │ │ │ ├── size.svg │ │ │ │ ├── table.svg │ │ │ │ ├── spacing.svg │ │ │ │ ├── table-insert-rows.svg │ │ │ │ ├── table-delete-columns.svg │ │ │ │ ├── table-delete-rows.svg │ │ │ │ ├── strike.svg │ │ │ │ ├── header.svg │ │ │ │ ├── superscript.svg │ │ │ │ ├── header-2.svg │ │ │ │ ├── header-4.svg │ │ │ │ ├── subscript.svg │ │ │ │ ├── header-6.svg │ │ │ │ ├── map.svg │ │ │ │ ├── font.svg │ │ │ │ ├── list-ordered.svg │ │ │ │ ├── embed.svg │ │ │ │ ├── header-5.svg │ │ │ │ ├── table-insert-cells.svg │ │ │ │ ├── header-3.svg │ │ │ │ ├── table-delete-cells.svg │ │ │ │ ├── video.svg │ │ │ │ ├── formula.svg │ │ │ │ ├── table-border-none.svg │ │ │ │ ├── table-border-right.svg │ │ │ │ ├── table-border-bottom.svg │ │ │ │ └── table-border-outside.svg │ │ │ ├── bubble │ │ │ │ ├── toolbar.styl │ │ │ │ └── tooltip.styl │ │ │ ├── snow.styl │ │ │ ├── snow │ │ │ │ ├── toolbar.styl │ │ │ │ └── tooltip.styl │ │ │ └── bubble.styl │ │ ├── core │ │ │ ├── instances.ts │ │ │ ├── module.ts │ │ │ ├── logger.ts │ │ │ ├── utils │ │ │ │ └── createRegistryWithFormats.ts │ │ │ ├── theme.ts │ │ │ └── composition.ts │ │ ├── blots │ │ │ ├── container.ts │ │ │ ├── text.ts │ │ │ ├── break.ts │ │ │ └── inline.ts │ │ ├── types.d.ts │ │ ├── formats │ │ │ ├── italic.ts │ │ │ ├── strike.ts │ │ │ ├── underline.ts │ │ │ ├── blockquote.ts │ │ │ ├── header.ts │ │ │ ├── background.ts │ │ │ ├── size.ts │ │ │ ├── align.ts │ │ │ ├── direction.ts │ │ │ ├── font.ts │ │ │ ├── bold.ts │ │ │ ├── script.ts │ │ │ ├── color.ts │ │ │ ├── formula.ts │ │ │ ├── indent.ts │ │ │ ├── link.ts │ │ │ ├── image.ts │ │ │ ├── video.ts │ │ │ ├── list.ts │ │ │ └── code.ts │ │ ├── modules │ │ │ └── normalizeExternalHTML │ │ │ │ ├── index.ts │ │ │ │ └── normalizers │ │ │ │ └── googleDocs.ts │ │ ├── ui │ │ │ ├── icon-picker.ts │ │ │ └── color-picker.ts │ │ └── core.ts │ ├── test │ │ ├── unit │ │ │ ├── __helpers__ │ │ │ │ ├── cleanup.ts │ │ │ │ ├── utils.ts │ │ │ │ ├── vitest.d.ts │ │ │ │ ├── factory.ts │ │ │ │ └── expect.ts │ │ │ ├── vitest.config.ts │ │ │ ├── formats │ │ │ │ ├── bold.spec.ts │ │ │ │ ├── script.spec.ts │ │ │ │ ├── header.spec.ts │ │ │ │ ├── indent.spec.ts │ │ │ │ ├── align.spec.ts │ │ │ │ └── color.spec.ts │ │ │ ├── modules │ │ │ │ ├── normalizeExternalHTML │ │ │ │ │ └── normalizers │ │ │ │ │ │ ├── googleDocs.spec.ts │ │ │ │ │ │ └── msWord.spec.ts │ │ │ │ └── uiNode.spec.ts │ │ │ ├── core │ │ │ │ ├── composition.spec.ts │ │ │ │ └── emitter.spec.ts │ │ │ └── blots │ │ │ │ └── inline.spec.ts │ │ ├── fuzz │ │ │ ├── vitest.config.ts │ │ │ └── __helpers__ │ │ │ │ └── utils.ts │ │ └── e2e │ │ │ ├── utils │ │ │ └── index.ts │ │ │ ├── __dev_server__ │ │ │ └── webpack.config.cjs │ │ │ └── fixtures │ │ │ ├── utils │ │ │ └── Locker.ts │ │ │ └── index.ts │ ├── babel.config.cjs │ ├── tsconfig.json │ ├── scripts │ │ ├── build │ │ └── babel-svg-inline-import.cjs │ ├── .eslintrc.json │ ├── playwright.config.ts │ ├── webpack.config.cjs │ ├── LICENSE │ └── webpack.common.cjs └── website │ ├── public │ ├── CNAME │ ├── robots.txt │ └── assets │ │ ├── fonts │ │ ├── sailec.woff2 │ │ ├── sailec-bold.woff2 │ │ ├── sofia-pro.woff2 │ │ ├── sailec-light.woff2 │ │ └── sofia-pro-bold.woff2 │ │ └── images │ │ ├── favicon.ico │ │ ├── footer.png │ │ ├── users.png │ │ ├── blog │ │ ├── bubble.png │ │ ├── color.png │ │ ├── formula.png │ │ ├── syntax.png │ │ ├── theme-1.png │ │ └── theme-2.png │ │ ├── brand-asset.png │ │ └── logo.svg │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── src │ ├── playground │ │ ├── snow │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── playground.json │ │ ├── react │ │ │ ├── styles.css │ │ │ ├── playground.json │ │ │ ├── Editor.js │ │ │ └── App.js │ │ ├── custom-formats │ │ │ ├── index.js │ │ │ ├── playground.json │ │ │ └── index.css │ │ └── form │ │ │ ├── playground.json │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ └── index.js │ ├── utils │ │ ├── slug.js │ │ ├── replaceCDN.js │ │ └── flattenData.js │ ├── components │ │ ├── OpenSource.module.scss │ │ ├── Hint.module.scss │ │ ├── Hint.jsx │ │ ├── Link.jsx │ │ ├── NoSSR.jsx │ │ ├── PostLayout.module.scss │ │ ├── OpenSource.jsx │ │ ├── Layout.jsx │ │ ├── GitHub.module.scss │ │ ├── PlaygroundLayout.module.scss │ │ ├── SEO.jsx │ │ ├── Editor.jsx │ │ ├── ActiveLink.jsx │ │ ├── GitHub.jsx │ │ ├── ClickOutsideHandler.jsx │ │ └── Heading.jsx │ ├── pages │ │ ├── 404.jsx │ │ ├── docs.jsx │ │ ├── playground.jsx │ │ ├── playground │ │ │ └── [...id].module.scss │ │ ├── _app.jsx │ │ ├── variables.scss │ │ ├── standalone │ │ │ ├── snow.mdx │ │ │ ├── bubble.mdx │ │ │ └── stress.mdx │ │ ├── docs │ │ │ └── [...id].jsx │ │ └── _document.jsx │ ├── svg │ │ ├── external-link.svg │ │ ├── breadcrumb-arrow.svg │ │ ├── users │ │ │ ├── gem.svg │ │ │ ├── microsoft.svg │ │ │ ├── typeform.svg │ │ │ ├── vox-media.svg │ │ │ ├── miro.svg │ │ │ ├── zoom.svg │ │ │ ├── grammarly.svg │ │ │ ├── apollo.svg │ │ │ ├── linkedin.svg │ │ │ ├── front.svg │ │ │ ├── figma.svg │ │ │ ├── slab.svg │ │ │ ├── airtable.svg │ │ │ ├── slack.svg │ │ │ ├── calendly.svg │ │ │ └── mode.svg │ │ ├── dropdown.svg │ │ ├── octocat.svg │ │ ├── logo.svg │ │ └── x.svg │ └── data │ │ ├── playground.tsx │ │ └── api.tsx │ ├── content │ ├── blog │ │ ├── an-official-cdn-for-quill.mdx │ │ ├── the-state-of-quill-and-2-0.mdx │ │ ├── quill-1-0-beta-release.mdx │ │ └── are-we-there-yet-to-1-0.mdx │ └── docs │ │ ├── quickstart.mdx │ │ ├── modules.mdx │ │ └── modules │ │ └── syntax.mdx │ ├── env.js │ └── package.json ├── .npmignore ├── .gitignore ├── .github ├── workflows │ ├── pull-request.yml │ ├── main.yml │ ├── changelog.yml │ ├── label.yml │ ├── release.yml │ └── _test.yml ├── release.yml └── ISSUE_TEMPLATE.md ├── scripts └── utils │ └── configGit.mjs ├── tsconfig.json ├── package.json └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | scripts/ 3 | -------------------------------------------------------------------------------- /packages/quill/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/website/public/CNAME: -------------------------------------------------------------------------------- 1 | quilljs.com -------------------------------------------------------------------------------- /packages/website/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next" 3 | } 4 | -------------------------------------------------------------------------------- /packages/website/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /packages/quill/README.md: -------------------------------------------------------------------------------- 1 | # Quill 2 | 3 | This is the main package of Quill. 4 | -------------------------------------------------------------------------------- /packages/website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .next 4 | out 5 | certificates -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | .* 4 | .github 5 | .vscode 6 | docs 7 | test 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /packages/website/README.md: -------------------------------------------------------------------------------- 1 | # Quill Official Website 2 | 3 | [https://quilljs.com](https://quilljs.com) 4 | -------------------------------------------------------------------------------- /packages/website/src/playground/snow/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/quill/src/assets/favicon.png -------------------------------------------------------------------------------- /packages/quill/src/core/instances.ts: -------------------------------------------------------------------------------- 1 | import type Quill from '../core.js'; 2 | 3 | export default new WeakMapabc
'); 14 | const bold = document.createElement('b'); 15 | bold.appendChild(scroll.domNode.firstChild?.childNodes[1] as Node); 16 | scroll.domNode.firstChild?.insertBefore( 17 | bold, 18 | scroll.domNode.firstChild.lastChild, 19 | ); 20 | scroll.update(); 21 | expect(scroll.domNode).toEqualHTML('abc
'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/website/src/components/NoSSR.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useState } from 'react'; 2 | 3 | const useEnhancedEffect = 4 | typeof window !== 'undefined' && process.env.NODE_ENV !== 'test' 5 | ? useLayoutEffect 6 | : useEffect; 7 | 8 | const NoSSR = ({ children, defer, fallback }) => { 9 | const [isMounted, setMountedState] = useState(false); 10 | 11 | useEnhancedEffect(() => { 12 | if (!defer) setMountedState(true); 13 | }, [defer]); 14 | 15 | useEffect(() => { 16 | if (defer) setMountedState(true); 17 | }, [defer]); 18 | 19 | return isMounted ? children : fallback; 20 | }; 21 | 22 | export const withoutSSR = (Component) => { 23 | const Comp = (props) => ( 24 |Hello World!
15 |Some initial bold text
16 |List items:
28 |a2 + b2 = c2
'), 16 | ); 17 | editor.formatText(6, 1, { script: 'super' }); 18 | expect(editor.scroll.domNode).toEqualHTML( 19 | 'a2 + b2 = c2
', 20 | ); 21 | }); 22 | 23 | test('remove', () => { 24 | const editor = new Editor( 25 | createScroll('a2 + b2
'), 26 | ); 27 | editor.formatText(1, 1, { script: false }); 28 | expect(editor.scroll.domNode).toEqualHTML('a2 + b2
'); 29 | }); 30 | 31 | test('replace', () => { 32 | const editor = new Editor( 33 | createScroll('a2 + b2
'), 34 | ); 35 | editor.formatText(1, 1, { script: 'sub' }); 36 | expect(editor.scroll.domNode).toEqualHTML( 37 | 'a2 + b2
', 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'npm version. Examples: "2.0.0", "2.0.0-beta.0". To deploy an experimental version, type "experimental".' 8 | default: "experimental" 9 | required: true 10 | dry-run: 11 | description: "Only create a tarball, do not publish to npm or create a release on GitHub." 12 | type: boolean 13 | default: true 14 | required: true 15 | 16 | permissions: 17 | contents: write 18 | 19 | jobs: 20 | test: 21 | uses: ./.github/workflows/_test.yml 22 | 23 | release: 24 | runs-on: ubuntu-latest 25 | needs: test 26 | 27 | steps: 28 | - name: Git checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Use Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 20 35 | 36 | - run: npm ci 37 | - run: ./scripts/release.js --version ${{ github.event.inputs.version }} ${{ github.event.inputs.dry-run == 'true' && '--dry-run' || '' }} 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | 42 | - name: Archive npm package tarball 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: npm 46 | path: | 47 | packages/quill/dist/*.tgz 48 | -------------------------------------------------------------------------------- /packages/quill/src/formats/link.ts: -------------------------------------------------------------------------------- 1 | import Inline from '../blots/inline.js'; 2 | 3 | class Link extends Inline { 4 | static blotName = 'link'; 5 | static tagName = 'A'; 6 | static SANITIZED_URL = 'about:blank'; 7 | static PROTOCOL_WHITELIST = ['http', 'https', 'mailto', 'tel', 'sms']; 8 | 9 | static create(value: string) { 10 | const node = super.create(value) as HTMLElement; 11 | node.setAttribute('href', this.sanitize(value)); 12 | node.setAttribute('rel', 'noopener noreferrer'); 13 | node.setAttribute('target', '_blank'); 14 | return node; 15 | } 16 | 17 | static formats(domNode: HTMLElement) { 18 | return domNode.getAttribute('href'); 19 | } 20 | 21 | static sanitize(url: string) { 22 | return sanitize(url, this.PROTOCOL_WHITELIST) ? url : this.SANITIZED_URL; 23 | } 24 | 25 | format(name: string, value: unknown) { 26 | if (name !== this.statics.blotName || !value) { 27 | super.format(name, value); 28 | } else { 29 | // @ts-expect-error 30 | this.domNode.setAttribute('href', this.constructor.sanitize(value)); 31 | } 32 | } 33 | } 34 | 35 | function sanitize(url: string, protocols: string[]) { 36 | const anchor = document.createElement('a'); 37 | anchor.href = url; 38 | const protocol = anchor.href.slice(0, anchor.href.indexOf(':')); 39 | return protocols.indexOf(protocol) > -1; 40 | } 41 | 42 | export { Link as default, sanitize }; 43 | -------------------------------------------------------------------------------- /packages/quill/test/e2e/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import { test as base } from '@playwright/test'; 2 | import EditorPage from '../pageobjects/EditorPage.js'; 3 | import Composition from './Composition.js'; 4 | import Locker from './utils/Locker.js'; 5 | import Clipboard from './Clipboard.js'; 6 | 7 | export const test = base.extend<{ 8 | editorPage: EditorPage; 9 | clipboard: Clipboard; 10 | composition: Composition; 11 | }>({ 12 | editorPage: ({ page }, use) => { 13 | use(new EditorPage(page)); 14 | }, 15 | composition: ({ page, browserName }, use) => { 16 | test.fail( 17 | browserName !== 'chromium', 18 | 'CDPSession is only available in Chromium', 19 | ); 20 | 21 | use(new Composition(page, browserName)); 22 | }, 23 | clipboard: [ 24 | async ({ page }, use) => { 25 | const locker = new Locker('clipboard'); 26 | await locker.lock(); 27 | await use(new Clipboard(page)); 28 | await locker.release(); 29 | }, 30 | { timeout: 30000 }, 31 | ], 32 | }); 33 | 34 | export const CHAPTER = 'Chapter 1. Loomings.'; 35 | export const P1 = 36 | 'Call me Ishmael. Some years ago—never mind how long precisely-having little or no money in my purse, and nothing particular to interest me on shore.'; 37 | export const P2 = 38 | 'There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs—commerce surrounds it with her surf.'; 39 | -------------------------------------------------------------------------------- /packages/website/src/data/api.tsx: -------------------------------------------------------------------------------- 1 | const items = [ 2 | { 3 | title: 'Content', 4 | hashes: [ 5 | 'deleteText', 6 | 'getContents', 7 | 'getLength', 8 | 'getText', 9 | 'getSemanticHTML', 10 | 'insertEmbed', 11 | 'insertText', 12 | 'setContents', 13 | 'setText', 14 | 'updateContents', 15 | ], 16 | }, 17 | { 18 | title: 'Formatting', 19 | hashes: ['format', 'formatLine', 'formatText', 'getFormat', 'removeFormat'], 20 | }, 21 | { 22 | title: 'Selection', 23 | hashes: [ 24 | 'getBounds', 25 | 'getSelection', 26 | 'setSelection', 27 | 'scrollSelectionIntoView', 28 | ], 29 | }, 30 | { 31 | title: 'Editor', 32 | hashes: [ 33 | 'blur', 34 | 'focus', 35 | 'disable', 36 | 'enable', 37 | 'hasFocus', 38 | 'update', 39 | 'scrollRectIntoView-experimental', 40 | ], 41 | }, 42 | { 43 | title: 'Events', 44 | hashes: [ 45 | 'text-change', 46 | 'selection-change', 47 | 'editor-change', 48 | 'off', 49 | 'on', 50 | 'once', 51 | ], 52 | }, 53 | { 54 | title: 'Model', 55 | hashes: ['find', 'getIndex', 'getLeaf', 'getLine', 'getLines'], 56 | }, 57 | { 58 | title: 'Extension', 59 | hashes: ['debug', 'import', 'register', 'addContainer', 'getModule'], 60 | }, 61 | ]; 62 | 63 | export default items; 64 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/formula.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "2.0.3", 4 | "description": "Quill official website", 5 | "private": true, 6 | "homepage": "https://quilljs.com", 7 | "keywords": [], 8 | "license": "BSD-3-Clause", 9 | "scripts": { 10 | "dev": "PORT=$npm_package_config_ports_website next dev", 11 | "start": "npm run dev", 12 | "build": "next build", 13 | "lint": "next lint", 14 | "serve": "npm run serve" 15 | }, 16 | "dependencies": { 17 | "@codesandbox/sandpack-react": "^2.11.3", 18 | "@docsearch/react": "^3.5.2", 19 | "@mdx-js/loader": "^3.0.0", 20 | "@mdx-js/mdx": "^2.1.5", 21 | "@mdx-js/react": "^2.3.0", 22 | "@next/mdx": "^14.0.4", 23 | "@next/third-parties": "^14.1.0", 24 | "@radix-ui/react-icons": "^1.3.0", 25 | "@radix-ui/themes": "^2.0.3", 26 | "@svgr/webpack": "^8.1.0", 27 | "@types/mdx": "^2.0.10", 28 | "classnames": "^2.3.2", 29 | "eslint-config-next": "^14.1.0", 30 | "lz-string": "^1.5.0", 31 | "next": "^14.0.4", 32 | "next-mdx-remote": "^4.4.1", 33 | "react": "^18.2.0", 34 | "react-dom": "^18.2.0", 35 | "react-helmet": "^6.1.0", 36 | "slugify": "^1.6.5" 37 | }, 38 | "prettier": { 39 | "singleQuote": true 40 | }, 41 | "devDependencies": { 42 | "http-proxy": "^1.18.1", 43 | "prism-react-renderer": "^2.3.0", 44 | "prismjs": "^1.29.0", 45 | "sass": "^1.55.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/website/src/components/GitHub.jsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { useEffect, useState } from 'react'; 3 | import OctocatIcon from '../svg/octocat.svg'; 4 | import * as styles from './GitHub.module.scss'; 5 | 6 | const placeholderCount = (37622).toLocaleString(); 7 | 8 | const GitHub = ({ dark = false }) => { 9 | const [count, setCount] = useState(placeholderCount); 10 | 11 | useEffect(() => { 12 | fetch( 13 | 'https://api.github.com/search/repositories?q=quill+user:slab+repo:quill&sort=stars&order=desc', 14 | ) 15 | .then((response) => response.json()) 16 | .then((data) => { 17 | if (data.items && data.items[0].full_name === 'slab/quill') { 18 | setCount(data.items[0].stargazers_count.toLocaleString()); 19 | } 20 | }); 21 | }, []); 22 | 23 | return ( 24 |Hello World!
', 10 | createRegistry([Bold, Italic]), 11 | ); 12 | scroll.formatAt(0, 1, 'bold', true); 13 | scroll.formatAt(0, 1, 'italic', true); 14 | scroll.formatAt(2, 1, 'italic', true); 15 | scroll.formatAt(2, 1, 'bold', true); 16 | expect(scroll.domNode).toEqualHTML( 17 | 'Hello World!
', 18 | ); 19 | }); 20 | 21 | test('reorder', () => { 22 | const scroll = createScroll( 23 | '0123
', 24 | createRegistry([Bold, Italic]), 25 | ); 26 | const p = scroll.domNode.firstChild as HTMLParagraphElement; 27 | const em = document.createElement('em'); 28 | Array.from(p.childNodes).forEach((node) => { 29 | em.appendChild(node); 30 | }); 31 | p.appendChild(em); 32 | expect(scroll.domNode).toEqualHTML('0123
'); 33 | scroll.update(); 34 | expect(scroll.domNode).toEqualHTML( 35 | '0123
', 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/quill/scripts/babel-svg-inline-import.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { dirname, resolve } = require('path'); 3 | const { optimize } = require('svgo'); 4 | 5 | module.exports = ({ types: t }) => { 6 | class BabelSVGInlineImport { 7 | constructor() { 8 | return { 9 | visitor: { 10 | ImportDeclaration: { 11 | exit(path, state) { 12 | const givenPath = path.node.source.value; 13 | if (!givenPath.endsWith('.svg')) { 14 | return; 15 | } 16 | const specifier = path.node.specifiers[0]; 17 | const id = specifier.local.name; 18 | const reference = state && state.file && state.file.opts.filename; 19 | const absolutePath = resolve(dirname(reference), givenPath); 20 | const content = optimize( 21 | fs.readFileSync(absolutePath).toString(), 22 | { plugins: [] }, 23 | ).data; 24 | 25 | const variableValue = t.stringLiteral(content); 26 | const variable = t.variableDeclarator( 27 | t.identifier(id), 28 | variableValue, 29 | ); 30 | 31 | path.replaceWith({ 32 | type: 'VariableDeclaration', 33 | kind: 'const', 34 | declarations: [variable], 35 | }); 36 | }, 37 | }, 38 | }, 39 | }; 40 | } 41 | } 42 | 43 | return new BabelSVGInlineImport(); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/quill/test/unit/__helpers__/factory.ts: -------------------------------------------------------------------------------- 1 | import { Registry } from 'parchment'; 2 | import type { Attributor } from 'parchment'; 3 | 4 | import Block from '../../../src/blots/block.js'; 5 | import Break from '../../../src/blots/break.js'; 6 | import Cursor from '../../../src/blots/cursor.js'; 7 | import Scroll from '../../../src/blots/scroll.js'; 8 | import TextBlot from '../../../src/blots/text.js'; 9 | import ListItem, { ListContainer } from '../../../src/formats/list.js'; 10 | import Inline from '../../../src/blots/inline.js'; 11 | import Emitter from '../../../src/core/emitter.js'; 12 | import { normalizeHTML } from './utils.js'; 13 | 14 | export const createRegistry = (formats: unknown[] = []) => { 15 | const registry = new Registry(); 16 | 17 | formats.forEach((format) => { 18 | registry.register(format as Attributor); 19 | }); 20 | registry.register(Block); 21 | registry.register(Break); 22 | registry.register(Cursor); 23 | registry.register(Inline); 24 | registry.register(Scroll); 25 | registry.register(TextBlot); 26 | registry.register(ListContainer); 27 | registry.register(ListItem); 28 | 29 | return registry; 30 | }; 31 | 32 | export const createScroll = ( 33 | html: string | { html: string }, 34 | registry = createRegistry(), 35 | container = document.body, 36 | ) => { 37 | const emitter = new Emitter(); 38 | const root = container.appendChild(document.createElement('div')); 39 | root.innerHTML = normalizeHTML(html); 40 | const scroll = new Scroll(registry, root, { 41 | emitter, 42 | }); 43 | return scroll; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/quill/src/assets/snow/tooltip.styl: -------------------------------------------------------------------------------- 1 | tooltipMargin = 8px 2 | 3 | .ql-snow 4 | .ql-tooltip 5 | background-color: #fff 6 | border: 1px solid borderColor 7 | box-shadow: 0px 0px 5px shadowColor 8 | color: textColor 9 | padding: 5px 12px 10 | white-space: nowrap 11 | &::before 12 | content: "Visit URL:" 13 | line-height: 26px 14 | margin-right: tooltipMargin 15 | input[type=text] 16 | display: none 17 | border: 1px solid borderColor 18 | font-size: 13px 19 | height: 26px 20 | margin: 0px 21 | padding: 3px 5px 22 | width: 170px 23 | a.ql-preview 24 | display: inline-block 25 | max-width: 200px 26 | overflow-x: hidden 27 | text-overflow: ellipsis 28 | vertical-align: top 29 | a.ql-action::after 30 | border-right: 1px solid borderColor 31 | content: 'Edit' 32 | margin-left: tooltipMargin*2 33 | padding-right: tooltipMargin 34 | a.ql-remove::before 35 | content: 'Remove' 36 | margin-left: tooltipMargin 37 | a 38 | line-height: 26px 39 | .ql-tooltip.ql-editing 40 | a.ql-preview, a.ql-remove 41 | display: none 42 | input[type=text] 43 | display: inline-block 44 | a.ql-action::after 45 | border-right: 0px 46 | content: 'Save' 47 | padding-right: 0px 48 | .ql-tooltip[data-mode=link]::before 49 | content: "Enter link:" 50 | .ql-tooltip[data-mode=formula]::before 51 | content: "Enter formula:" 52 | .ql-tooltip[data-mode=video]::before 53 | content: "Enter video:" 54 | -------------------------------------------------------------------------------- /packages/quill/src/formats/image.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBlot } from 'parchment'; 2 | import { sanitize } from './link.js'; 3 | 4 | const ATTRIBUTES = ['alt', 'height', 'width']; 5 | 6 | class Image extends EmbedBlot { 7 | static blotName = 'image'; 8 | static tagName = 'IMG'; 9 | 10 | static create(value: string) { 11 | const node = super.create(value) as Element; 12 | if (typeof value === 'string') { 13 | node.setAttribute('src', this.sanitize(value)); 14 | } 15 | return node; 16 | } 17 | 18 | static formats(domNode: Element) { 19 | return ATTRIBUTES.reduce( 20 | (formats: Record0123
')); 17 | editor.formatText(4, 1, { header: 1 }); 18 | expect(editor.getDelta()).toEqual( 19 | new Delta().insert('0123', { italic: true }).insert('\n', { header: 1 }), 20 | ); 21 | expect(editor.scroll.domNode).toEqualHTML('0123
'); 31 | }); 32 | 33 | test('change', () => { 34 | const editor = new Editor(createScroll('1. item 1
14 |item 2
15 |item 3 in another list
16 |Plain paragraph
17 |the last item
18 | 19 | 20 | `; 21 | 22 | const doc = new DOMParser().parseFromString(html, 'text/html'); 23 | normalize(doc); 24 | expect(doc.body.children).toMatchInlineSnapshot(` 25 | HTMLCollection [ 26 |48 | Plain paragraph 49 |
, 50 |\n${escapeText(this.code(index, length))}\n`;
30 | }
31 | }
32 |
33 | class CodeBlock extends Block {
34 | static TAB = ' ';
35 |
36 | static register() {
37 | Quill.register(CodeBlockContainer);
38 | }
39 | }
40 |
41 | class Code extends Inline {}
42 | Code.blotName = 'code';
43 | Code.tagName = 'CODE';
44 |
45 | CodeBlock.blotName = 'code-block';
46 | CodeBlock.className = 'ql-code-block';
47 | CodeBlock.tagName = 'DIV';
48 | CodeBlockContainer.blotName = 'code-block-container';
49 | CodeBlockContainer.className = 'ql-code-block-container';
50 | CodeBlockContainer.tagName = 'DIV';
51 |
52 | CodeBlockContainer.allowedChildren = [CodeBlock];
53 |
54 | CodeBlock.allowedChildren = [TextBlot, Break, Cursor];
55 | CodeBlock.requiredContainer = CodeBlockContainer;
56 |
57 | export { Code, CodeBlockContainer, CodeBlock as default };
58 |
--------------------------------------------------------------------------------
/packages/website/src/pages/_document.jsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document';
2 | import Script from 'next/script';
3 | import { getSandpackCssText } from '@codesandbox/sandpack-react';
4 |
5 | export default function Document() {
6 | return (
7 |
8 |
9 |
13 |
17 |
21 |
26 |
27 |
28 |
29 |
33 |
37 |
41 |
45 |
50 |
51 |
52 |
53 | abc
')); 49 | editor.formatText(3, 1, { indent: 1 }); 50 | expect(editor.getDelta()).toEqual( 51 | new Delta().insert('abc').insert('\n', { indent: 1 }), 52 | ); 53 | expect(editor.scroll.domNode).toEqualHTML(`abc
`); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-border-outside.svg: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /packages/quill/test/unit/formats/align.spec.ts: -------------------------------------------------------------------------------- 1 | import Delta from 'quill-delta'; 2 | import Editor from '../../../src/core/editor.js'; 3 | import { describe, test, expect } from 'vitest'; 4 | import { 5 | createRegistry, 6 | createScroll as baseCreateScroll, 7 | } from '../__helpers__/factory.js'; 8 | import { AlignClass } from '../../../src/formats/align.js'; 9 | 10 | const createScroll = (html: string) => 11 | baseCreateScroll(html, createRegistry([AlignClass])); 12 | 13 | describe('Align', () => { 14 | test('add', () => { 15 | const editor = new Editor(createScroll('0123
')); 16 | editor.formatText(4, 1, { align: 'center' }); 17 | expect(editor.getDelta()).toEqual( 18 | new Delta().insert('0123').insert('\n', { align: 'center' }), 19 | ); 20 | expect(editor.scroll.domNode).toEqualHTML( 21 | '0123
', 22 | ); 23 | }); 24 | 25 | test('remove', () => { 26 | const editor = new Editor( 27 | createScroll('0123
'), 28 | ); 29 | editor.formatText(4, 1, { align: false }); 30 | expect(editor.getDelta()).toEqual(new Delta().insert('0123\n')); 31 | expect(editor.scroll.domNode).toEqualHTML('0123
'); 32 | }); 33 | 34 | test('whitelist', () => { 35 | const editor = new Editor( 36 | createScroll('0123
'), 37 | ); 38 | editor.formatText(4, 1, { align: 'middle' }); 39 | expect(editor.getDelta()).toEqual( 40 | new Delta().insert('0123').insert('\n', { align: 'center' }), 41 | ); 42 | expect(editor.scroll.domNode).toEqualHTML( 43 | '0123
', 44 | ); 45 | }); 46 | 47 | test('invalid scope', () => { 48 | const editor = new Editor(createScroll('0123
')); 49 | editor.formatText(1, 2, { align: 'center' }); 50 | expect(editor.getDelta()).toEqual(new Delta().insert('0123\n')); 51 | expect(editor.scroll.domNode).toEqualHTML('0123
'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/mode.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/blots/inline.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBlot, InlineBlot, Scope } from 'parchment'; 2 | import type { BlotConstructor } from 'parchment'; 3 | import Break from './break.js'; 4 | import Text from './text.js'; 5 | 6 | class Inline extends InlineBlot { 7 | static allowedChildren: BlotConstructor[] = [Inline, Break, EmbedBlot, Text]; 8 | // Lower index means deeper in the DOM tree, since not found (-1) is for embeds 9 | static order = [ 10 | 'cursor', 11 | 'inline', // Must be lower 12 | 'link', // Chrome wants to be lower 13 | 'underline', 14 | 'strike', 15 | 'italic', 16 | 'bold', 17 | 'script', 18 | 'code', // Must be higher 19 | ]; 20 | 21 | static compare(self: string, other: string) { 22 | const selfIndex = Inline.order.indexOf(self); 23 | const otherIndex = Inline.order.indexOf(other); 24 | if (selfIndex >= 0 || otherIndex >= 0) { 25 | return selfIndex - otherIndex; 26 | } 27 | if (self === other) { 28 | return 0; 29 | } 30 | if (self < other) { 31 | return -1; 32 | } 33 | return 1; 34 | } 35 | 36 | formatAt(index: number, length: number, name: string, value: unknown) { 37 | if ( 38 | Inline.compare(this.statics.blotName, name) < 0 && 39 | this.scroll.query(name, Scope.BLOT) 40 | ) { 41 | const blot = this.isolate(index, length); 42 | if (value) { 43 | blot.wrap(name, value); 44 | } 45 | } else { 46 | super.formatAt(index, length, name, value); 47 | } 48 | } 49 | 50 | optimize(context: { [key: string]: any }) { 51 | super.optimize(context); 52 | if ( 53 | this.parent instanceof Inline && 54 | Inline.compare(this.statics.blotName, this.parent.statics.blotName) > 0 55 | ) { 56 | const parent = this.parent.isolate(this.offset(), this.length()); 57 | // @ts-expect-error TODO: make isolate generic 58 | this.moveChildren(parent); 59 | parent.wrap(this); 60 | } 61 | } 62 | } 63 | 64 | export default Inline; 65 | -------------------------------------------------------------------------------- /packages/website/content/docs/modules.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modules 3 | --- 4 | 5 | Modules allow Quill's behavior and functionality to be customized. Several officially supported modules are available to pick and choose from, some with additional configuration options and APIs. Refer to their respective documentation pages for more details. 6 | 7 | To enable a module, simply include it in Quill's configuration. 8 | 9 | ```javascript 10 | const quill = new Quill('#editor', { 11 | modules: { 12 | history: { // Enable with custom configurations 13 | delay: 2500, 14 | userOnly: true 15 | }, 16 | syntax: true // Enable with default configuration 17 | } 18 | }); 19 | ``` 20 | 21 | The [Clipboard](/docs/modules/clipboard/), [Keyboard](/docs/modules/keyboard/), and [History](/docs/modules/history/) modules are required by Quill and do not need to be included explictly, but may be configured like any other module. 22 | 23 | 24 | ## Extending 25 | 26 | Modules may also be extended and re-registered, replacing the original module. Even required modules may be re-registered and replaced. 27 | 28 | ```javascript 29 | const Clipboard = Quill.import('modules/clipboard'); 30 | const Delta = Quill.import('delta'); 31 | 32 | class PlainClipboard extends Clipboard { 33 | convert(html = null) { 34 | if (typeof html === 'string') { 35 | this.container.innerHTML = html; 36 | } 37 | let text = this.container.innerText; 38 | this.container.innerHTML = ''; 39 | return new Delta().insert(text); 40 | } 41 | } 42 | 43 | Quill.register('modules/clipboard', PlainClipboard, true); 44 | 45 | // Will be created with instance of PlainClipboard 46 | const quill = new Quill('#editor'); 47 | ``` 48 | 49 |0123
')); 17 | editor.formatText(1, 2, { color: 'red' }); 18 | expect(editor.getDelta()).toEqual( 19 | new Delta().insert('0').insert('12', { color: 'red' }).insert('3\n'), 20 | ); 21 | expect(editor.scroll.domNode).toEqualHTML( 22 | '0123
', 23 | ); 24 | }); 25 | 26 | test('remove', () => { 27 | const editor = new Editor( 28 | createScroll('0123
'), 29 | ); 30 | editor.formatText(1, 2, { color: false }); 31 | const delta = new Delta() 32 | .insert('0') 33 | .insert('12', { bold: true }) 34 | .insert('3\n'); 35 | expect(editor.getDelta()).toEqual(delta); 36 | expect(editor.scroll.domNode).toEqualHTML('0123
'); 37 | }); 38 | 39 | test('remove unwrap', () => { 40 | const editor = new Editor( 41 | createScroll('0123
'), 42 | ); 43 | editor.formatText(1, 2, { color: false }); 44 | expect(editor.getDelta()).toEqual(new Delta().insert('0123\n')); 45 | expect(editor.scroll.domNode).toEqualHTML('0123
'); 46 | }); 47 | 48 | test('invalid scope', () => { 49 | const editor = new Editor(createScroll('0123
')); 50 | editor.formatText(4, 1, { color: 'red' }); 51 | expect(editor.getDelta()).toEqual(new Delta().insert('0123\n')); 52 | expect(editor.scroll.domNode).toEqualHTML('0123
'); 53 | }); 54 | }); 55 | --------------------------------------------------------------------------------