├── .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 WeakMap(); 4 | -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sailec.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/fonts/sailec.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/favicon.ico -------------------------------------------------------------------------------- /packages/website/public/assets/images/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/footer.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/users.png -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sailec-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/fonts/sailec-bold.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sofia-pro.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/fonts/sofia-pro.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/blog/bubble.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/blog/color.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/blog/formula.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/blog/syntax.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/theme-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/blog/theme-1.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/theme-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/blog/theme-2.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/brand-asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/images/brand-asset.png -------------------------------------------------------------------------------- /packages/quill/test/unit/__helpers__/cleanup.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach } from 'vitest'; 2 | 3 | beforeEach(() => { 4 | document.body.innerHTML = ''; 5 | }); 6 | -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sailec-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/fonts/sailec-light.woff2 -------------------------------------------------------------------------------- /packages/website/src/utils/slug.js: -------------------------------------------------------------------------------- 1 | import slugify from 'slugify'; 2 | 3 | const slug = text => slugify(text, { lower: true }); 4 | 5 | export default slug; 6 | -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sofia-pro-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code/app-quill/main/packages/website/public/assets/fonts/sofia-pro-bold.woff2 -------------------------------------------------------------------------------- /packages/quill/src/blots/container.ts: -------------------------------------------------------------------------------- 1 | import { ContainerBlot } from 'parchment'; 2 | 3 | class Container extends ContainerBlot {} 4 | 5 | export default Container; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.eslintrc.json 3 | !.eslintignore 4 | !.npmignore 5 | !.gitignore 6 | !.github 7 | 8 | node_modules 9 | 10 | test-results/ 11 | playwright-report/ 12 | -------------------------------------------------------------------------------- /packages/quill/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | declare const QUILL_VERSION: string | undefined; 7 | -------------------------------------------------------------------------------- /packages/website/src/components/OpenSource.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .about { 3 | line-height: 1.2; 4 | } 5 | } 6 | 7 | .github { 8 | margin-top: 20px; 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | uses: ./.github/workflows/_test.yml 10 | -------------------------------------------------------------------------------- /packages/quill/src/formats/italic.ts: -------------------------------------------------------------------------------- 1 | import Bold from './bold.js'; 2 | 3 | class Italic extends Bold { 4 | static blotName = 'italic'; 5 | static tagName = ['EM', 'I']; 6 | } 7 | 8 | export default Italic; 9 | -------------------------------------------------------------------------------- /packages/quill/src/formats/strike.ts: -------------------------------------------------------------------------------- 1 | import Bold from './bold.js'; 2 | 3 | class Strike extends Bold { 4 | static blotName = 'strike'; 5 | static tagName = ['S', 'STRIKE']; 6 | } 7 | 8 | export default Strike; 9 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/mention.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/formats/underline.ts: -------------------------------------------------------------------------------- 1 | import Inline from '../blots/inline.js'; 2 | 3 | class Underline extends Inline { 4 | static blotName = 'underline'; 5 | static tagName = 'U'; 6 | } 7 | 8 | export default Underline; 9 | -------------------------------------------------------------------------------- /packages/quill/src/formats/blockquote.ts: -------------------------------------------------------------------------------- 1 | import Block from '../blots/block.js'; 2 | 3 | class Blockquote extends Block { 4 | static blotName = 'blockquote'; 5 | static tagName = 'blockquote'; 6 | } 7 | 8 | export default Blockquote; 9 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | concurrency: 8 | group: main-build 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | test: 13 | uses: ./.github/workflows/_test.yml 14 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/attachment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/website/src/pages/404.jsx: -------------------------------------------------------------------------------- 1 | import SEO from '../components/SEO'; 2 | 3 | const NotFound = () =>
Not Found
; 4 | 5 | export const Head = () => ; 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-merge-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/underline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/align-center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/align-justify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/align-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/align-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /scripts/utils/configGit.mjs: -------------------------------------------------------------------------------- 1 | import { $ } from "execa"; 2 | 3 | async function configGit() { 4 | await $`git config --global user.name ${"Zihua Li"}`; 5 | await $`git config --global user.email ${"635902+luin@users.noreply.github.com"}`; 6 | } 7 | 8 | export default configGit; 9 | -------------------------------------------------------------------------------- /packages/website/src/svg/external-link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/core/module.ts: -------------------------------------------------------------------------------- 1 | import type Quill from './quill.js'; 2 | 3 | abstract class Module { 4 | static DEFAULTS = {}; 5 | 6 | constructor( 7 | public quill: Quill, 8 | protected options: Partial = {}, 9 | ) {} 10 | } 11 | 12 | export default Module; 13 | -------------------------------------------------------------------------------- /packages/website/src/utils/replaceCDN.js: -------------------------------------------------------------------------------- 1 | import env from '../../env'; 2 | 3 | const replaceCDN = (value) => { 4 | return value.replace(/\{\{site\.(\w+)\}\}/g, (_, matched) => { 5 | return matched === 'cdn' ? process.env.cdn : env[matched]; 6 | }); 7 | }; 8 | 9 | export default replaceCDN; 10 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-border-all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/website/src/components/Hint.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 0 0 20px; 3 | border-left: 3px solid #45aad8; 4 | margin: 20px 0 30px; 5 | 6 | .title { 7 | color: #45aad8; 8 | margin-bottom: 4px; 9 | } 10 | 11 | :last-child { 12 | margin-bottom: 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/emoji.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/website/src/svg/breadcrumb-arrow.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/gem.svg: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /packages/website/src/components/Hint.jsx: -------------------------------------------------------------------------------- 1 | import * as styles from './Hint.module.scss'; 2 | 3 | const Hint = ({ children }) => { 4 | return ( 5 |
6 |
Note
7 | {children} 8 |
9 | ); 10 | }; 11 | 12 | export default Hint; 13 | -------------------------------------------------------------------------------- /packages/website/src/svg/dropdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/quill/test/unit/__helpers__/utils.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (ms: number) => 2 | new Promise((r) => { 3 | setTimeout(() => { 4 | r(); 5 | }, ms); 6 | }); 7 | 8 | export const normalizeHTML = (html: string | { html: string }) => 9 | typeof html === 'object' ? html.html : html.replace(/\n\s*/g, ''); 10 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/outdent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/website/src/playground/snow/index.js: -------------------------------------------------------------------------------- 1 | const quill = new Quill('#editor', { 2 | modules: { 3 | toolbar: [ 4 | [{ header: [1, 2, false] }], 5 | ['bold', 'italic', 'underline'], 6 | ['image', 'code-block'], 7 | ], 8 | }, 9 | placeholder: 'Compose an epic...', 10 | theme: 'snow', // or 'bubble' 11 | }); 12 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/hashtag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/indent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/size-decrease.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/speech.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/test/fuzz/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | resolve: { 5 | extensions: ['.ts', '.js'], 6 | }, 7 | test: { 8 | include: ['test/fuzz/**/*.spec.ts'], 9 | environment: 'jsdom', 10 | testTimeout: 40000, 11 | pool: 'threads', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/website/src/components/Link.jsx: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link'; 2 | import { Link as RadixLink } from '@radix-ui/themes'; 3 | 4 | const Link = ({ children, ...props }) => { 5 | return ( 6 | 7 | {children} 8 | 9 | ); 10 | }; 11 | 12 | export default Link; 13 | -------------------------------------------------------------------------------- /packages/quill/babel.config.cjs: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | 3 | module.exports = { 4 | presets: [ 5 | ['@babel/preset-env', { modules: false }], 6 | '@babel/preset-typescript', 7 | ], 8 | plugins: [ 9 | ['transform-define', { QUILL_VERSION: pkg.version }], 10 | './scripts/babel-svg-inline-import.cjs', 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/quill/src/formats/header.ts: -------------------------------------------------------------------------------- 1 | import Block from '../blots/block.js'; 2 | 3 | class Header extends Block { 4 | static blotName = 'header'; 5 | static tagName = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']; 6 | 7 | static formats(domNode: Element) { 8 | return this.tagName.indexOf(domNode.tagName) + 1; 9 | } 10 | } 11 | 12 | export default Header; 13 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/horizontal-rule.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/website/src/pages/docs.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/navigation'; 2 | import { useEffect } from 'react'; 3 | import docs from '../data/docs'; 4 | 5 | const Docs = () => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | router.replace(docs[0].url); 10 | }, [router]); 11 | 12 | return null; 13 | }; 14 | 15 | export default Docs; 16 | -------------------------------------------------------------------------------- /packages/quill/src/assets/bubble/toolbar.styl: -------------------------------------------------------------------------------- 1 | arrowWidth = 6px 2 | 3 | .ql-bubble 4 | .ql-toolbar 5 | .ql-formats 6 | margin: 8px 12px 8px 0px 7 | .ql-formats:first-child 8 | margin-left: 12px 9 | 10 | .ql-color-picker 11 | svg 12 | margin: 1px 13 | .ql-picker-item.ql-selected, .ql-picker-item:hover 14 | border-color: activeColor 15 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/authorship.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/blockquote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/website/src/playground/react/styles.css: -------------------------------------------------------------------------------- 1 | .controls { 2 | display: flex; 3 | border: 1px solid #ccc; 4 | border-top: 0; 5 | padding: 10px; 6 | } 7 | 8 | .controls-right { 9 | margin-left: auto; 10 | } 11 | 12 | .state { 13 | margin: 10px 0; 14 | font-family: monospace; 15 | } 16 | 17 | .state-title { 18 | color: #999; 19 | text-transform: uppercase; 20 | } 21 | -------------------------------------------------------------------------------- /packages/website/src/playground/custom-formats/index.js: -------------------------------------------------------------------------------- 1 | // Add fonts to whitelist 2 | const Font = Quill.import('formats/font'); 3 | // We do not add Aref Ruqaa since it is the default 4 | Font.whitelist = ['mirza', 'roboto']; 5 | Quill.register(Font, true); 6 | 7 | const quill = new Quill('#editor', { 8 | modules: { 9 | toolbar: '#toolbar', 10 | }, 11 | theme: 'snow', 12 | }); 13 | -------------------------------------------------------------------------------- /packages/quill/src/assets/snow.styl: -------------------------------------------------------------------------------- 1 | themeName = 'snow' 2 | activeColor = #06c 3 | borderColor = #ccc 4 | backgroundColor = #fff 5 | inactiveColor = #444 6 | shadowColor = #ddd 7 | textColor = #444 8 | 9 | @import './core' 10 | @import './base' 11 | @import './snow/*' 12 | 13 | .ql-snow 14 | a 15 | color: activeColor 16 | 17 | .ql-container.ql-snow 18 | border: 1px solid borderColor 19 | -------------------------------------------------------------------------------- /packages/website/src/pages/playground.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/navigation'; 2 | import { useEffect } from 'react'; 3 | import playground from '../data/playground'; 4 | 5 | const Playground = () => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | router.replace(playground[0].url); 10 | }, [router]); 11 | 12 | return null; 13 | }; 14 | 15 | export default Playground; 16 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/typeform.svg: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /packages/website/src/pages/playground/[...id].module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 40px auto 20px; 3 | } 4 | 5 | .wrapper { 6 | display: flex; 7 | border: 1px solid #ccc; 8 | height: 500px; 9 | 10 | .editor { 11 | flex: 1; 12 | border-right: 1px solid #ccc; 13 | display: flex; 14 | min-width: 0; 15 | } 16 | 17 | .preview { 18 | flex: 1; 19 | display: flex; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - quill-bot 5 | categories: 6 | - title: Bug Fixes 🛠 7 | labels: 8 | - change:bugfix 9 | - title: New Features 🎉 10 | labels: 11 | - change:feature 12 | - title: Documentation 📚 13 | labels: 14 | - change:documentation 15 | - title: Other Changes 16 | labels: 17 | - "*" 18 | -------------------------------------------------------------------------------- /packages/quill/test/unit/__helpers__/vitest.d.ts: -------------------------------------------------------------------------------- 1 | import type { Assertion, AsymmetricMatchersContaining } from 'vitest'; 2 | 3 | interface CustomMatchers { 4 | toEqualHTML(html: string, options?: { ignoreAttrs?: string[] }): R; 5 | } 6 | 7 | declare module 'vitest' { 8 | interface Assertion extends CustomMatchers {} 9 | interface AsymmetricMatchersContaining extends CustomMatchers {} 10 | } 11 | -------------------------------------------------------------------------------- /packages/website/src/playground/form/playground.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "static", 3 | "externalResources": [ 4 | "{{site.highlightjs}}/highlight.min.js", 5 | "{{site.highlightjs}}/styles/atom-one-dark.min.css", 6 | "{{site.katex}}/katex.min.js", 7 | "{{site.katex}}/katex.min.css", 8 | "{{site.cdn}}/quill.snow.css", 9 | "{{site.cdn}}/quill.bubble.css", 10 | "{{site.cdn}}/quill.js" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/website/src/playground/snow/playground.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "static", 3 | "externalResources": [ 4 | "{{site.highlightjs}}/highlight.min.js", 5 | "{{site.highlightjs}}/styles/atom-one-dark.min.css", 6 | "{{site.katex}}/katex.min.js", 7 | "{{site.katex}}/katex.min.css", 8 | "{{site.cdn}}/quill.snow.css", 9 | "{{site.cdn}}/quill.bubble.css", 10 | "{{site.cdn}}/quill.js" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/website/src/utils/flattenData.js: -------------------------------------------------------------------------------- 1 | function flattenData(root) { 2 | const data = []; 3 | const flatten = (i) => { 4 | i.forEach((child) => { 5 | if (child.url.includes('#')) return; 6 | data.push(child); 7 | if (child.children) { 8 | flatten(child.children); 9 | } 10 | }); 11 | }; 12 | 13 | flatten(root); 14 | return data; 15 | } 16 | 17 | export default flattenData; 18 | -------------------------------------------------------------------------------- /packages/quill/src/formats/background.ts: -------------------------------------------------------------------------------- 1 | import { ClassAttributor, Scope } from 'parchment'; 2 | import { ColorAttributor } from './color.js'; 3 | 4 | const BackgroundClass = new ClassAttributor('background', 'ql-bg', { 5 | scope: Scope.INLINE, 6 | }); 7 | const BackgroundStyle = new ColorAttributor('background', 'background-color', { 8 | scope: Scope.INLINE, 9 | }); 10 | 11 | export { BackgroundClass, BackgroundStyle }; 12 | -------------------------------------------------------------------------------- /packages/website/src/playground/form/index.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 500px; 3 | max-width: 100%; 4 | } 5 | 6 | .form-group { 7 | margin-bottom: 12px; 8 | } 9 | 10 | label { 11 | display: block; 12 | margin-bottom: 4px; 13 | } 14 | 15 | input { 16 | border: 1px solid #ccc; 17 | } 18 | 19 | input, 20 | .ql-editor { 21 | padding: 4px; 22 | font-size: 14px; 23 | } 24 | 25 | #editor { 26 | height: 130px; 27 | } 28 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/clean.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/direction-ltr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/direction-rtl.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/size-increase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/formats/size.ts: -------------------------------------------------------------------------------- 1 | import { ClassAttributor, Scope, StyleAttributor } from 'parchment'; 2 | 3 | const SizeClass = new ClassAttributor('size', 'ql-size', { 4 | scope: Scope.INLINE, 5 | whitelist: ['small', 'large', 'huge'], 6 | }); 7 | const SizeStyle = new StyleAttributor('size', 'font-size', { 8 | scope: Scope.INLINE, 9 | whitelist: ['10px', '18px', '32px'], 10 | }); 11 | 12 | export { SizeClass, SizeStyle }; 13 | -------------------------------------------------------------------------------- /packages/website/src/playground/custom-formats/playground.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "static", 3 | "externalResources": [ 4 | "{{site.highlightjs}}/highlight.min.js", 5 | "{{site.highlightjs}}/styles/atom-one-dark.min.css", 6 | "{{site.katex}}/katex.min.js", 7 | "{{site.katex}}/katex.min.css", 8 | "{{site.cdn}}/quill.snow.css", 9 | "{{site.cdn}}/quill.bubble.css", 10 | "{{site.cdn}}/quill.js" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/quill/test/fuzz/__helpers__/utils.ts: -------------------------------------------------------------------------------- 1 | export function randomInt(max: number) { 2 | return Math.floor(Math.random() * max); 3 | } 4 | 5 | export function choose(choices: T[]): T { 6 | return choices[randomInt(choices.length)]; 7 | } 8 | 9 | export function runFuzz(testCase: () => void) { 10 | const start = performance.now(); 11 | do { 12 | testCase(); 13 | } while (performance.now() - start < 30 * 1000); 14 | } 15 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/vox-media.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-unmerge-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/modules/normalizeExternalHTML/index.ts: -------------------------------------------------------------------------------- 1 | import googleDocs from './normalizers/googleDocs.js'; 2 | import msWord from './normalizers/msWord.js'; 3 | 4 | const NORMALIZERS = [msWord, googleDocs]; 5 | 6 | const normalizeExternalHTML = (doc: Document) => { 7 | if (doc.documentElement) { 8 | NORMALIZERS.forEach((normalize) => { 9 | normalize(doc); 10 | }); 11 | } 12 | }; 13 | 14 | export default normalizeExternalHTML; 15 | -------------------------------------------------------------------------------- /packages/website/src/playground/react/playground.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "react", 3 | "externalResources": [ 4 | "{{site.highlightjs}}/highlight.min.js", 5 | "{{site.highlightjs}}/styles/atom-one-dark.min.css", 6 | "{{site.katex}}/katex.min.js", 7 | "{{site.katex}}/katex.min.css", 8 | "{{site.cdn}}/quill.snow.css", 9 | "{{site.cdn}}/quill.bubble.css", 10 | "{{site.cdn}}/quill.js" 11 | ], 12 | "activeFile": "App.js" 13 | } 14 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/list-bullet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/quill/src/blots/text.ts: -------------------------------------------------------------------------------- 1 | import { TextBlot } from 'parchment'; 2 | 3 | class Text extends TextBlot {} 4 | 5 | // https://lodash.com/docs#escape 6 | const entityMap: Record = { 7 | '&': '&', 8 | '<': '<', 9 | '>': '>', 10 | '"': '"', 11 | "'": ''', 12 | }; 13 | 14 | function escapeText(text: string) { 15 | return text.replace(/[&<>"']/g, (s) => entityMap[s]); 16 | } 17 | 18 | export { Text as default, escapeText }; 19 | -------------------------------------------------------------------------------- /packages/quill/src/blots/break.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBlot } from 'parchment'; 2 | 3 | class Break extends EmbedBlot { 4 | static value() { 5 | return undefined; 6 | } 7 | 8 | optimize() { 9 | if (this.prev || this.next) { 10 | this.remove(); 11 | } 12 | } 13 | 14 | length() { 15 | return 0; 16 | } 17 | 18 | value() { 19 | return ''; 20 | } 21 | } 22 | Break.blotName = 'break'; 23 | Break.tagName = 'BR'; 24 | 25 | export default Break; 26 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/list-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/website/src/data/playground.tsx: -------------------------------------------------------------------------------- 1 | const playground = [ 2 | { 3 | title: 'Basic setup with snow theme', 4 | url: '/playground/snow', 5 | }, 6 | { 7 | title: 'Using Quill inside a form', 8 | url: '/playground/form', 9 | }, 10 | { 11 | title: 'Custom font and formats', 12 | url: '/playground/custom-formats', 13 | }, 14 | { 15 | title: 'Using Quill with React', 16 | url: '/playground/react', 17 | }, 18 | ]; 19 | 20 | export default playground; 21 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/miro.svg: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-insert-columns.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/quill/src/formats/align.ts: -------------------------------------------------------------------------------- 1 | import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment'; 2 | 3 | const config = { 4 | scope: Scope.BLOCK, 5 | whitelist: ['right', 'center', 'justify'], 6 | }; 7 | 8 | const AlignAttribute = new Attributor('align', 'align', config); 9 | const AlignClass = new ClassAttributor('align', 'ql-align', config); 10 | const AlignStyle = new StyleAttributor('align', 'text-align', config); 11 | 12 | export { AlignAttribute, AlignClass, AlignStyle }; 13 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/size.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/formats/direction.ts: -------------------------------------------------------------------------------- 1 | import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment'; 2 | 3 | const config = { 4 | scope: Scope.BLOCK, 5 | whitelist: ['rtl'], 6 | }; 7 | 8 | const DirectionAttribute = new Attributor('direction', 'dir', config); 9 | const DirectionClass = new ClassAttributor('direction', 'ql-direction', config); 10 | const DirectionStyle = new StyleAttributor('direction', 'direction', config); 11 | 12 | export { DirectionAttribute, DirectionClass, DirectionStyle }; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "module": "commonjs" 6 | } 7 | }, 8 | "compilerOptions": { 9 | "allowSyntheticDefaultImports": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "declaration": true, 13 | "module": "ES2020", 14 | "moduleResolution": "node", 15 | "noEmit": true, 16 | "strictNullChecks": true, 17 | "noImplicitAny": true, 18 | "noUnusedLocals": true 19 | }, 20 | "include": ["./**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/zoom.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/spacing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-insert-rows.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Generate Changelog 2 | 3 | on: 4 | release: 5 | types: [published, created] 6 | workflow_dispatch: {} 7 | 8 | jobs: 9 | changelog: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Git checkout 13 | uses: actions/checkout@v4 14 | - name: Use Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 20 18 | 19 | - run: npm ci 20 | - run: node ./scripts/changelog.mjs 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-delete-columns.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-delete-rows.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/quill/src/formats/font.ts: -------------------------------------------------------------------------------- 1 | import { ClassAttributor, Scope, StyleAttributor } from 'parchment'; 2 | 3 | const config = { 4 | scope: Scope.INLINE, 5 | whitelist: ['serif', 'monospace'], 6 | }; 7 | 8 | const FontClass = new ClassAttributor('font', 'ql-font', config); 9 | 10 | class FontStyleAttributor extends StyleAttributor { 11 | value(node: HTMLElement) { 12 | return super.value(node).replace(/["']/g, ''); 13 | } 14 | } 15 | 16 | const FontStyle = new FontStyleAttributor('font', 'font-family', config); 17 | 18 | export { FontStyle, FontClass }; 19 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/grammarly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/formats/bold.ts: -------------------------------------------------------------------------------- 1 | import Inline from '../blots/inline.js'; 2 | 3 | class Bold extends Inline { 4 | static blotName = 'bold'; 5 | static tagName = ['STRONG', 'B']; 6 | 7 | static create() { 8 | return super.create(); 9 | } 10 | 11 | static formats() { 12 | return true; 13 | } 14 | 15 | optimize(context: { [key: string]: any }) { 16 | super.optimize(context); 17 | if (this.domNode.tagName !== this.statics.tagName[0]) { 18 | this.replaceWith(this.statics.blotName); 19 | } 20 | } 21 | } 22 | 23 | export default Bold; 24 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/strike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "compilerOptions": { 4 | "esModuleInterop": true 5 | } 6 | }, 7 | "compilerOptions": { 8 | "outDir": "./dist", 9 | "allowSyntheticDefaultImports": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "declaration": false, 14 | "module": "ES2020", 15 | "moduleResolution": "bundler", 16 | "strictNullChecks": true, 17 | "noImplicitAny": true, 18 | "noUnusedLocals": true 19 | }, 20 | "include": ["src/**/*", "test/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/website/src/pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import { GoogleAnalytics } from '@next/third-parties/google'; 2 | import { Theme } from '@radix-ui/themes'; 3 | import '@radix-ui/themes/styles.css'; 4 | import './variables.scss'; 5 | import './base.css'; 6 | import './styles.scss'; 7 | 8 | // This default export is required in a new `pages/_app.js` file. 9 | export default function MyApp({ Component, pageProps }) { 10 | return ( 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/website/src/svg/octocat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/superscript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/apollo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /packages/website/content/blog/an-official-cdn-for-quill.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: An Offical CDN for Quill 3 | date: 2014-08-12 4 | --- 5 | 6 | Quill now has an offical Content Distribution Network so you can have access to a reliable, high-speed host for the library. To include a file: 7 | 8 | ```html 9 | 10 | ``` 11 | 12 | ```html 13 | 14 | ``` 15 | 16 | You can also use "latest" as the version: 17 | 18 | ```html 19 | 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/quill/scripts/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | DIST=dist 6 | 7 | TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') 8 | npx tsc --declaration --emitDeclarationOnly --outDir $TMPDIR 9 | 10 | rm -rf $DIST 11 | mkdir $DIST 12 | mv $TMPDIR/src/* $DIST 13 | rm -rf $TMPDIR 14 | npx babel src --out-dir $DIST --copy-files --no-copy-ignored --extensions .ts --source-maps 15 | npx webpack -- --mode $1 16 | # https://github.com/webpack-contrib/mini-css-extract-plugin/issues/151 17 | rm -rf $DIST/dist/*.css.js $DIST/dist/*.css.js.* 18 | cp package.json $DIST 19 | cp README.md $DIST 20 | cp LICENSE $DIST 21 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | name: Pull Requests 2 | 3 | on: 4 | pull_request: 5 | types: [opened, labeled, unlabeled, synchronize] 6 | 7 | jobs: 8 | label: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: mheap/github-action-required-labels@v5 15 | with: 16 | mode: exactly 17 | count: 1 18 | labels: | 19 | change:bugfix 20 | change:feature 21 | change:documentation 22 | change:chore 23 | change:refactor 24 | add_comment: false 25 | -------------------------------------------------------------------------------- /packages/website/src/pages/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-accent: #f2d123; 3 | --color-accent-emphasis: #e2b810; 4 | 5 | --color-bg-inset: #f0efea; 6 | --color-bg-inset-emphasis: #e1ded1; 7 | 8 | --docsearch-searchbox-background: var(--color-bg-inset); 9 | --docsearch-primary-color: var(--color-accent); 10 | --docsearch-key-shadow: none; 11 | --docsearch-key-gradient: transparent; 12 | 13 | --width-readable: 800px; 14 | } 15 | 16 | [data-accent-color='yellow'] { 17 | // Override Radix styles 18 | --accent-9: var(--color-accent) !important; 19 | } 20 | 21 | .radix-themes { 22 | --default-font-size: 18px; 23 | } 24 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/website/content/blog/the-state-of-quill-and-2-0.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: The State of Quill and 2.0 3 | date: 2017-09-21 4 | external: https://medium.com/@jhchen/the-state-of-quill-and-2-0-fb38db7a59b9 5 | --- 6 | 7 | The 2.0 branch of Quill has officially been opened and development 8 | commenced. One design principle Quill embraces is to first make it 9 | possible, then make it easy. This allows the technical challenges to 10 | be proved out and provides clarity around use cases so that the 11 | right audience is designed for. Quill 1.0 pushed the boundaries on 12 | the former, and now 2.0 will focus on the latter. 13 | 14 | Let’s take a look at how we got here and where Quill is going! 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please describe the a concise description and fill out the details below. It will help others efficiently understand your request and get to an answer instead of repeated back and forth. Providing a [minimal, complete and verifiable example](https://stackoverflow.com/help/mcve) will further increase your chances that someone can help. 2 | 3 | **Steps for Reproduction** 4 | 5 | 1. Visit [quilljs.com, jsfiddle.net, codepen.io] 6 | 2. Step Two 7 | 3. Step Three 8 | 9 | **Expected behavior**: 10 | 11 | **Actual behavior**: 12 | 13 | **Platforms**: 14 | 15 | Include browser, operating system and respective versions 16 | 17 | **Version**: 18 | 19 | Run `Quill.version` to find out 20 | -------------------------------------------------------------------------------- /packages/quill/test/e2e/utils/index.ts: -------------------------------------------------------------------------------- 1 | export const isMac = process.platform === 'darwin'; 2 | export const SHORTKEY = isMac ? 'Meta' : 'Control'; 3 | 4 | export function getSelectionInTextNode() { 5 | const selection = document.getSelection(); 6 | if (!selection) { 7 | throw new Error('Selection is null'); 8 | } 9 | const { anchorNode, anchorOffset, focusNode, focusOffset } = selection; 10 | return JSON.stringify([ 11 | (anchorNode as Text).data, 12 | anchorOffset, 13 | (focusNode as Text).data, 14 | focusOffset, 15 | ]); 16 | } 17 | 18 | export const sleep = (ms: number) => 19 | new Promise((r) => { 20 | setTimeout(() => { 21 | r(); 22 | }, ms); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/subscript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/formats/script.ts: -------------------------------------------------------------------------------- 1 | import Inline from '../blots/inline.js'; 2 | 3 | class Script extends Inline { 4 | static blotName = 'script'; 5 | static tagName = ['SUB', 'SUP']; 6 | 7 | static create(value: 'super' | 'sub' | (string & {})) { 8 | if (value === 'super') { 9 | return document.createElement('sup'); 10 | } 11 | if (value === 'sub') { 12 | return document.createElement('sub'); 13 | } 14 | return super.create(value); 15 | } 16 | 17 | static formats(domNode: HTMLElement) { 18 | if (domNode.tagName === 'SUB') return 'sub'; 19 | if (domNode.tagName === 'SUP') return 'super'; 20 | return undefined; 21 | } 22 | } 23 | 24 | export default Script; 25 | -------------------------------------------------------------------------------- /packages/quill/test/unit/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { resolve } from 'path'; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | extensions: ['.ts', '.js'], 7 | }, 8 | test: { 9 | include: [resolve(__dirname, '**/*.spec.ts')], 10 | typecheck: { 11 | enabled: true, 12 | include: [resolve(__dirname, '**/*.test-d.ts')], 13 | }, 14 | setupFiles: [ 15 | resolve(__dirname, '__helpers__/expect.ts'), 16 | resolve(__dirname, '__helpers__/cleanup.ts'), 17 | ], 18 | browser: { 19 | enabled: true, 20 | provider: 'playwright', 21 | name: process.env.BROWSER || 'chromium', 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/website/src/playground/custom-formats/index.css: -------------------------------------------------------------------------------- 1 | /* Set default font-family */ 2 | #editor { 3 | font-family: 'Aref Ruqaa'; 4 | font-size: 18px; 5 | height: 375px; 6 | } 7 | 8 | /* Set dropdown font-families */ 9 | #toolbar .ql-font span[data-label='Aref Ruqaa']::before { 10 | font-family: 'Aref Ruqaa'; 11 | } 12 | #toolbar .ql-font span[data-label='Mirza']::before { 13 | font-family: 'Mirza'; 14 | } 15 | #toolbar .ql-font span[data-label='Roboto']::before { 16 | font-family: 'Roboto'; 17 | } 18 | 19 | /* Set content font-families */ 20 | .ql-font-mirza { 21 | font-family: 'Mirza'; 22 | } 23 | .ql-font-roboto { 24 | font-family: 'Roboto'; 25 | } 26 | /* We do not set Aref Ruqaa since it is the default font */ 27 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /packages/quill/src/assets/snow/toolbar.styl: -------------------------------------------------------------------------------- 1 | .ql-toolbar.ql-snow 2 | border: 1px solid borderColor 3 | box-sizing: border-box 4 | font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif 5 | padding: 8px 6 | 7 | .ql-formats 8 | margin-right: 15px 9 | 10 | .ql-picker-label 11 | border: 1px solid transparent 12 | .ql-picker-options 13 | border: 1px solid transparent 14 | box-shadow: rgba(0,0,0,0.2) 0 2px 8px 15 | .ql-picker.ql-expanded 16 | .ql-picker-label 17 | border-color: borderColor 18 | .ql-picker-options 19 | border-color: borderColor 20 | 21 | .ql-color-picker 22 | .ql-picker-item.ql-selected, .ql-picker-item:hover 23 | border-color: #000 24 | 25 | .ql-toolbar.ql-snow + .ql-container.ql-snow 26 | border-top: 0px; 27 | -------------------------------------------------------------------------------- /packages/website/src/playground/form/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/list-ordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/quill/src/formats/color.ts: -------------------------------------------------------------------------------- 1 | import { ClassAttributor, Scope, StyleAttributor } from 'parchment'; 2 | 3 | class ColorAttributor extends StyleAttributor { 4 | value(domNode: HTMLElement) { 5 | let value = super.value(domNode) as string; 6 | if (!value.startsWith('rgb(')) return value; 7 | value = value.replace(/^[^\d]+/, '').replace(/[^\d]+$/, ''); 8 | const hex = value 9 | .split(',') 10 | .map((component) => `00${parseInt(component, 10).toString(16)}`.slice(-2)) 11 | .join(''); 12 | return `#${hex}`; 13 | } 14 | } 15 | 16 | const ColorClass = new ClassAttributor('color', 'ql-color', { 17 | scope: Scope.INLINE, 18 | }); 19 | const ColorStyle = new ColorAttributor('color', 'color', { 20 | scope: Scope.INLINE, 21 | }); 22 | 23 | export { ColorAttributor, ColorClass, ColorStyle }; 24 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/embed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-insert-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-delete-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/quill/test/unit/formats/bold.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { 3 | createRegistry, 4 | createScroll as baseCreateScroll, 5 | } from '../__helpers__/factory.js'; 6 | import Bold from '../../../src/formats/bold.js'; 7 | 8 | const createScroll = (html: string) => 9 | baseCreateScroll(html, createRegistry([Bold])); 10 | 11 | describe('Bold', () => { 12 | test('optimize and merge', () => { 13 | const scroll = createScroll('

abc

'); 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 | 25 | 26 | 27 | ); 28 | Comp.displayName = 'withoutSSR'; 29 | 30 | return Comp; 31 | }; 32 | 33 | export default NoSSR; 34 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/website/src/pages/standalone/snow.mdx: -------------------------------------------------------------------------------- 1 | Snow Theme 2 | 3 | import { StandaloneSandpack } from '../../components/Sandpack'; 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | `, 20 | 'index.js': ` 21 | const quill = new Quill('#editor', { 22 | placeholder: 'Compose an epic...', 23 | theme: 'snow' 24 | }); 25 | ` 26 | }} 27 | /> -------------------------------------------------------------------------------- /packages/website/src/pages/standalone/bubble.mdx: -------------------------------------------------------------------------------- 1 | Bubble Theme 2 | 3 | import { StandaloneSandpack } from '../../components/Sandpack'; 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | `, 20 | 'index.js': ` 21 | const quill = new Quill('#editor', { 22 | placeholder: 'Compose an epic...', 23 | theme: 'bubble' 24 | }); 25 | ` 26 | }} 27 | /> -------------------------------------------------------------------------------- /packages/website/src/svg/users/front.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /packages/quill/src/core/logger.ts: -------------------------------------------------------------------------------- 1 | const levels = ['error', 'warn', 'log', 'info'] as const; 2 | export type DebugLevel = (typeof levels)[number]; 3 | let level: DebugLevel | false = 'warn'; 4 | 5 | function debug(method: DebugLevel, ...args: unknown[]) { 6 | if (level) { 7 | if (levels.indexOf(method) <= levels.indexOf(level)) { 8 | console[method](...args); // eslint-disable-line no-console 9 | } 10 | } 11 | } 12 | 13 | function namespace( 14 | ns: string, 15 | ): Record void> { 16 | return levels.reduce( 17 | (logger, method) => { 18 | logger[method] = debug.bind(console, method, ns); 19 | return logger; 20 | }, 21 | {} as Record void>, 22 | ); 23 | } 24 | 25 | namespace.level = (newLevel: DebugLevel | false) => { 26 | level = newLevel; 27 | }; 28 | debug.level = namespace.level; 29 | 30 | export default namespace; 31 | -------------------------------------------------------------------------------- /packages/quill/test/e2e/__dev_server__/webpack.config.cjs: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const common = require('../../../webpack.common.cjs'); 6 | const { merge } = require('webpack-merge'); 7 | require('webpack-dev-server'); 8 | 9 | module.exports = (env) => 10 | merge(common, { 11 | plugins: [ 12 | new HtmlWebpackPlugin({ 13 | publicPath: '/', 14 | filename: 'index.html', 15 | template: path.resolve(__dirname, 'index.html'), 16 | chunks: ['quill'], 17 | inject: 'head', 18 | scriptLoading: 'blocking', 19 | }), 20 | ], 21 | devServer: { 22 | port: env.port, 23 | server: 'https', 24 | hot: false, 25 | liveReload: false, 26 | compress: true, 27 | client: { 28 | overlay: false, 29 | }, 30 | webSocketServer: false, 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /packages/website/src/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/website/env.js: -------------------------------------------------------------------------------- 1 | const { version, homepage } = require('./package.json'); 2 | 3 | const cdn = process.env.NEXT_PUBLIC_LOCAL_QUILL 4 | ? `http://localhost:${process.env.npm_package_config_ports_webpack}` 5 | : `https://cdn.jsdelivr.net/npm/quill@${version}/dist`; 6 | 7 | module.exports = { 8 | version, 9 | cdn, 10 | github: 'https://github.com/slab/quill/tree/main/packages/website/', 11 | highlightjs: 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0', 12 | katex: 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist', 13 | url: homepage, 14 | title: 'Quill - Your powerful rich text editor', 15 | shortTitle: 'Quill Rich Text Editor', 16 | description: 17 | 'Quill is a free, open source WYSIWYG editor built for the modern web. Completely customize it for any need with its modular architecture and expressive API.', 18 | shortDescription: 19 | 'Quill is a free, open source rich text editor built for the modern web.', 20 | }; 21 | -------------------------------------------------------------------------------- /packages/website/src/components/PostLayout.module.scss: -------------------------------------------------------------------------------- 1 | .breadcrumbRow { 2 | align-items: center; 3 | display: flex; 4 | margin-bottom: 32px; 5 | justify-content: space-between; 6 | 7 | &:after { 8 | content: none; 9 | } 10 | 11 | .breadcrumb { 12 | font-size: 1.25rem; 13 | display: flex; 14 | 15 | span:not(:last-child) { 16 | &::after { 17 | content: '>'; 18 | font-weight: normal; 19 | margin: 0 4px 0 6px; 20 | } 21 | } 22 | } 23 | 24 | .breadcrumb span:first-child { 25 | font-weight: bold; 26 | margin-right: 4px; 27 | } 28 | 29 | .editOnGitHub { 30 | font-size: 1.08rem; 31 | max-width: var(--width-readable); 32 | text-transform: uppercase; 33 | } 34 | } 35 | 36 | .content { 37 | code { 38 | background: #f1f1f1; 39 | } 40 | 41 | pre { 42 | border-radius: 2px; 43 | 44 | code { 45 | background: transparent; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/figma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /packages/website/public/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/quill/src/formats/formula.ts: -------------------------------------------------------------------------------- 1 | import Embed from '../blots/embed.js'; 2 | 3 | class Formula extends Embed { 4 | static blotName = 'formula'; 5 | static className = 'ql-formula'; 6 | static tagName = 'SPAN'; 7 | 8 | static create(value: string) { 9 | // @ts-expect-error 10 | if (window.katex == null) { 11 | throw new Error('Formula module requires KaTeX.'); 12 | } 13 | const node = super.create(value) as Element; 14 | if (typeof value === 'string') { 15 | // @ts-expect-error 16 | window.katex.render(value, node, { 17 | throwOnError: false, 18 | errorColor: '#f00', 19 | }); 20 | node.setAttribute('data-value', value); 21 | } 22 | return node; 23 | } 24 | 25 | static value(domNode: Element) { 26 | return domNode.getAttribute('data-value'); 27 | } 28 | 29 | html() { 30 | const { formula } = this.value(); 31 | return `${formula}`; 32 | } 33 | } 34 | 35 | export default Formula; 36 | -------------------------------------------------------------------------------- /packages/website/src/svg/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/quill/src/ui/icon-picker.ts: -------------------------------------------------------------------------------- 1 | import Picker from './picker.js'; 2 | 3 | class IconPicker extends Picker { 4 | defaultItem: HTMLElement | null; 5 | 6 | constructor(select: HTMLSelectElement, icons: Record) { 7 | super(select); 8 | this.container.classList.add('ql-icon-picker'); 9 | Array.from(this.container.querySelectorAll('.ql-picker-item')).forEach( 10 | (item) => { 11 | item.innerHTML = icons[item.getAttribute('data-value') || '']; 12 | }, 13 | ); 14 | this.defaultItem = this.container.querySelector('.ql-selected'); 15 | this.selectItem(this.defaultItem); 16 | } 17 | 18 | selectItem(target: HTMLElement | null, trigger?: boolean) { 19 | super.selectItem(target, trigger); 20 | const item = target || this.defaultItem; 21 | if (item != null) { 22 | if (this.label.innerHTML === item.innerHTML) return; 23 | this.label.innerHTML = item.innerHTML; 24 | } 25 | } 26 | } 27 | 28 | export default IconPicker; 29 | -------------------------------------------------------------------------------- /packages/quill/test/e2e/fixtures/utils/Locker.ts: -------------------------------------------------------------------------------- 1 | import { unlink, writeFile } from 'fs/promises'; 2 | import { unlinkSync } from 'fs'; 3 | import { tmpdir } from 'os'; 4 | import { join } from 'path'; 5 | import { globSync } from 'glob'; 6 | 7 | const sleep = (ms: number) => 8 | new Promise((resolve) => { 9 | setTimeout(resolve, ms); 10 | }); 11 | 12 | const PREFIX = 'playwright_locker_'; 13 | 14 | class Locker { 15 | public static clearAll() { 16 | globSync(join(tmpdir(), `${PREFIX}*.txt`)).forEach(unlinkSync); 17 | } 18 | 19 | constructor(private key: string) {} 20 | 21 | private get filePath() { 22 | return join(tmpdir(), `${PREFIX}${this.key}.txt`); 23 | } 24 | 25 | async lock() { 26 | try { 27 | await writeFile(this.filePath, '', { flag: 'wx' }); 28 | } catch { 29 | await sleep(50); 30 | await this.lock(); 31 | } 32 | } 33 | 34 | async release() { 35 | await unlink(this.filePath); 36 | } 37 | } 38 | 39 | export default Locker; 40 | -------------------------------------------------------------------------------- /packages/website/src/components/OpenSource.jsx: -------------------------------------------------------------------------------- 1 | import GitHub from './GitHub'; 2 | import OpenSourceIcon from '../svg/features/open-source.svg'; 3 | import classNames from 'classnames'; 4 | import styles from './OpenSource.module.scss'; 5 | import Link from './Link'; 6 | 7 | const OpenSource = () => ( 8 |
9 |
10 |

An Open Source Project

11 | 12 | Quill is developed and maintained by{' '} 13 | 14 | Slab 15 | 16 | . It is permissively licensed under BSD. Use it freely in personal or 17 | commercial projects! 18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | ); 28 | 29 | export default OpenSource; 30 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/slab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/website/src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import LogoIcon from '../svg/logo.svg'; 2 | import Link from 'next/link'; 3 | import SEO from './SEO'; 4 | import Header from './Header'; 5 | import playground from '../data/playground'; 6 | import docs from '../data/docs'; 7 | 8 | const Layout = ({ children, title }) => { 9 | return ( 10 | <> 11 | 12 |
13 | {children} 14 |
15 |
16 |
17 | 18 |
19 |

Your powerful rich text editor.

20 |
21 | 22 | Documentation 23 | 24 | 25 | Playground 26 | 27 |
28 |
29 |
30 | 31 | ); 32 | }; 33 | 34 | export default Layout; 35 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/airtable.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /packages/quill/test/unit/modules/normalizeExternalHTML/normalizers/googleDocs.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import normalize from '../../../../../src/modules/normalizeExternalHTML/normalizers/googleDocs.js'; 3 | 4 | describe('Google Docs', () => { 5 | test('remove unnecessary b tags', () => { 6 | const html = ` 7 | 11 | Item 1Item 2 12 | 13 | Item 3 16 | `; 17 | 18 | const doc = new DOMParser().parseFromString(html, 'text/html'); 19 | normalize(doc); 20 | expect(doc.body.children).toMatchInlineSnapshot(` 21 | HTMLCollection [ 22 | 23 | Item 1 24 | , 25 | 26 | Item 2 27 | , 28 | 31 | Item 3 32 | , 33 | ] 34 | `); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/quill/test/unit/core/composition.spec.ts: -------------------------------------------------------------------------------- 1 | import Emitter from '../../../src/core/emitter.js'; 2 | import Composition from '../../../src/core/composition.js'; 3 | import Scroll from '../../../src/blots/scroll.js'; 4 | import { describe, expect, test, vitest } from 'vitest'; 5 | import { createRegistry } from '../__helpers__/factory.js'; 6 | import Quill from '../../../src/core.js'; 7 | 8 | describe('Composition', () => { 9 | test('triggers events on compositionstart', async () => { 10 | const emitter = new Emitter(); 11 | const scroll = new Scroll(createRegistry(), document.createElement('div'), { 12 | emitter, 13 | }); 14 | new Composition(scroll, emitter); 15 | 16 | vitest.spyOn(emitter, 'emit'); 17 | 18 | const event = new CompositionEvent('compositionstart'); 19 | scroll.domNode.dispatchEvent(event); 20 | expect(emitter.emit).toHaveBeenCalledWith( 21 | Quill.events.COMPOSITION_BEFORE_START, 22 | event, 23 | ); 24 | expect(emitter.emit).toHaveBeenCalledWith( 25 | Quill.events.COMPOSITION_START, 26 | event, 27 | ); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /packages/quill/test/unit/core/emitter.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import Emitter from '../../../src/core/emitter.js'; 3 | import Quill from '../../../src/core.js'; 4 | 5 | describe('emitter', () => { 6 | test('emit and on', () => { 7 | const emitter = new Emitter(); 8 | 9 | let received: unknown; 10 | emitter.on('abc', (data) => { 11 | received = data; 12 | }); 13 | emitter.emit('abc', { hello: 'world' }); 14 | 15 | expect(received).toEqual({ hello: 'world' }); 16 | }); 17 | 18 | test('listenDOM', () => { 19 | const quill = new Quill(document.createElement('div')); 20 | document.body.appendChild(quill.container); 21 | 22 | let calls = 0; 23 | quill.emitter.listenDOM('click', document.body, () => { 24 | calls += 1; 25 | }); 26 | 27 | document.body.click(); 28 | expect(calls).toEqual(1); 29 | 30 | quill.container.remove(); 31 | document.body.click(); 32 | expect(calls).toEqual(1); 33 | 34 | document.body.appendChild(quill.container); 35 | document.body.click(); 36 | expect(calls).toEqual(2); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/quill/src/formats/indent.ts: -------------------------------------------------------------------------------- 1 | import { ClassAttributor, Scope } from 'parchment'; 2 | 3 | class IndentAttributor extends ClassAttributor { 4 | add(node: HTMLElement, value: string | number) { 5 | let normalizedValue = 0; 6 | if (value === '+1' || value === '-1') { 7 | const indent = this.value(node) || 0; 8 | normalizedValue = value === '+1' ? indent + 1 : indent - 1; 9 | } else if (typeof value === 'number') { 10 | normalizedValue = value; 11 | } 12 | if (normalizedValue === 0) { 13 | this.remove(node); 14 | return true; 15 | } 16 | return super.add(node, normalizedValue.toString()); 17 | } 18 | 19 | canAdd(node: HTMLElement, value: string) { 20 | return super.canAdd(node, value) || super.canAdd(node, parseInt(value, 10)); 21 | } 22 | 23 | value(node: HTMLElement) { 24 | return parseInt(super.value(node), 10) || undefined; // Don't return NaN 25 | } 26 | } 27 | 28 | const IndentClass = new IndentAttributor('indent', 'ql-indent', { 29 | scope: Scope.BLOCK, 30 | // @ts-expect-error 31 | whitelist: [1, 2, 3, 4, 5, 6, 7, 8], 32 | }); 33 | 34 | export default IndentClass; 35 | -------------------------------------------------------------------------------- /packages/quill/src/ui/color-picker.ts: -------------------------------------------------------------------------------- 1 | import Picker from './picker.js'; 2 | 3 | class ColorPicker extends Picker { 4 | constructor(select: HTMLSelectElement, label: string) { 5 | super(select); 6 | this.label.innerHTML = label; 7 | this.container.classList.add('ql-color-picker'); 8 | Array.from(this.container.querySelectorAll('.ql-picker-item')) 9 | .slice(0, 7) 10 | .forEach((item) => { 11 | item.classList.add('ql-primary'); 12 | }); 13 | } 14 | 15 | buildItem(option: HTMLOptionElement) { 16 | const item = super.buildItem(option); 17 | item.style.backgroundColor = option.getAttribute('value') || ''; 18 | return item; 19 | } 20 | 21 | selectItem(item: HTMLElement | null, trigger?: boolean) { 22 | super.selectItem(item, trigger); 23 | const colorLabel = this.label.querySelector('.ql-color-label'); 24 | const value = item ? item.getAttribute('data-value') || '' : ''; 25 | if (colorLabel) { 26 | if (colorLabel.tagName === 'line') { 27 | colorLabel.style.stroke = value; 28 | } else { 29 | colorLabel.style.fill = value; 30 | } 31 | } 32 | } 33 | } 34 | 35 | export default ColorPicker; 36 | -------------------------------------------------------------------------------- /packages/website/src/components/GitHub.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | background-color: #1d1e30; 3 | border: 3px solid #1d1e30; 4 | display: flex; 5 | flex-direction: row; 6 | font-family: 'Sofia Pro', sans-serif; 7 | font-weight: bold; 8 | letter-spacing: 0.15rem; 9 | text-transform: uppercase; 10 | width: min-content; 11 | } 12 | 13 | .action { 14 | color: #fff; 15 | display: flex; 16 | flex-direction: row; 17 | font-size: 1.33rem; 18 | line-height: 32px; 19 | padding: 10px 22px; 20 | } 21 | 22 | .action:hover { 23 | color: #fff; 24 | } 25 | 26 | .action svg { 27 | float: left; 28 | height: 32px; 29 | margin-right: 12px; 30 | width: 32px; 31 | } 32 | 33 | .action path { 34 | fill: #fff; 35 | } 36 | 37 | .count { 38 | color: inherit; 39 | background-color: #fff; 40 | font-size: 1.75rem; 41 | line-height: 32px; 42 | padding: 10px 30px; 43 | } 44 | 45 | .button.isDark { 46 | background-color: #fff; 47 | border: 3px solid #fff; 48 | } 49 | 50 | .button.isDark .action { 51 | color: #1d1e30; 52 | } 53 | 54 | .button.isDark .action path { 55 | fill: #1d1e30; 56 | } 57 | 58 | .button.isDark .count { 59 | background-color: #1d1e30; 60 | color: #fff; 61 | } 62 | -------------------------------------------------------------------------------- /packages/website/src/components/PlaygroundLayout.module.scss: -------------------------------------------------------------------------------- 1 | .panel { 2 | display: flex; 3 | background-color: var(--yellow-a1); 4 | padding: 10px 16px; 5 | border-top-left-radius: 4px; 6 | border-top-right-radius: 4px; 7 | border: 1px solid #ccc; 8 | border-bottom: 0; 9 | 10 | .panelMeta { 11 | margin-left: auto; 12 | } 13 | } 14 | 15 | .exampleLabel { 16 | font-weight: bold; 17 | font-size: 14px; 18 | color: #999; 19 | } 20 | 21 | .exampleSelector { 22 | display: flex; 23 | align-items: center; 24 | gap: 10px; 25 | font-size: 14px; 26 | text-align: left; 27 | } 28 | 29 | .copied { 30 | position: fixed; 31 | top: 0; 32 | left: 0; 33 | right: 0; 34 | bottom: 0; 35 | color: white; 36 | display: flex; 37 | align-items: center; 38 | place-content: center; 39 | font-size: 20px; 40 | opacity: 0; 41 | transition: opacity 0.2s; 42 | pointer-events: none; 43 | z-index: 99999; 44 | 45 | &.active { 46 | opacity: 1; 47 | } 48 | 49 | &::before { 50 | content: 'URL copied to clipboard'; 51 | background-color: rgba(0, 0, 0, 0.5); 52 | width: max-content; 53 | padding: 10px 20px; 54 | border-radius: 10px; 55 | line-height: 20px; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/website/content/blog/quill-1-0-beta-release.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quill 1.0 Beta Release 3 | date: 2016-05-03 4 | --- 5 | 6 | Today Quill is ready for its first beta preview of 1.0. This is the biggest rewrite to Quill since its inception and enables many new possibilities not available in previous versions of Quill, nor any other editor. The code is as always available on GitHub and through npm: 7 | 8 | ``` 9 | npm install quill@1.0.0-beta.0 10 | ``` 11 | 12 | The skeleton of a new documentation site is also being built out at [beta.quilljs.com](https://beta.quilljs.com). Whereas the current site focuses on being a referential resource, the new site will also be a guide to provide insight on approaching different customization goals. There is also an [interactive playground](https://beta.quilljs.com/playground/) to try out various configurations and explore the API. 13 | 14 | {/* more */} 15 | 16 | The goal now is of course an official 1.0 release. To get there, Quill will now enter a weekly cadence of beta releases, so you can expect rapid interations on stability and bug fixes each week. GitHub is still the center of all development so please do report [Issues](https://github.com/quilljs/quill/issues) as you encounter them in the beta preview. 17 | -------------------------------------------------------------------------------- /packages/website/src/components/SEO.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Head from 'next/head'; 3 | 4 | const SEO = ({ title, permalink }) => { 5 | const pageTitle = title 6 | ? `${title} - ${process.env.shortTitle}` 7 | : process.env.title; 8 | 9 | return ( 10 | 11 | 12 | 13 | 14 | 18 | 19 | 23 | 27 | 28 | 29 | {pageTitle} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default SEO; 36 | -------------------------------------------------------------------------------- /packages/website/src/components/Editor.jsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef } from 'react'; 2 | import { withoutSSR } from './NoSSR'; 3 | 4 | const Editor = ({ 5 | children, 6 | rootStyle, 7 | config, 8 | onSelectionChange, 9 | onLoad, 10 | ...props 11 | }) => { 12 | const ref = useRef(null); 13 | const rootStyleRef = useRef(rootStyle); 14 | const onSelectionChangeRef = useRef(onSelectionChange); 15 | const onLoadRef = useRef(onLoad); 16 | 17 | useLayoutEffect(() => { 18 | onSelectionChangeRef.current = onSelectionChange; 19 | }, [onSelectionChange]); 20 | 21 | useLayoutEffect(() => { 22 | onLoadRef.current = onLoad; 23 | }, [onLoad]); 24 | 25 | const configRef = useRef(config); 26 | 27 | useLayoutEffect(() => { 28 | const quill = new window.Quill(ref.current, configRef.current); 29 | if (rootStyleRef) { 30 | Object.assign(quill.root.style, rootStyleRef.current); 31 | } 32 | quill.on(window.Quill.events.SELECTION_CHANGE, () => { 33 | onSelectionChangeRef.current?.(); 34 | }); 35 | 36 | onLoadRef.current?.(quill); 37 | }, []); 38 | 39 | return ( 40 |
41 | {children} 42 |
43 | ); 44 | }; 45 | 46 | export default withoutSSR(Editor); 47 | -------------------------------------------------------------------------------- /packages/quill/src/modules/normalizeExternalHTML/normalizers/googleDocs.ts: -------------------------------------------------------------------------------- 1 | const normalWeightRegexp = /font-weight:\s*normal/; 2 | const blockTagNames = ['P', 'OL', 'UL']; 3 | 4 | const isBlockElement = (element: Element | null) => { 5 | return element && blockTagNames.includes(element.tagName); 6 | }; 7 | 8 | const normalizeEmptyLines = (doc: Document) => { 9 | Array.from(doc.querySelectorAll('br')) 10 | .filter( 11 | (br) => 12 | isBlockElement(br.previousElementSibling) && 13 | isBlockElement(br.nextElementSibling), 14 | ) 15 | .forEach((br) => { 16 | br.parentNode?.removeChild(br); 17 | }); 18 | }; 19 | 20 | const normalizeFontWeight = (doc: Document) => { 21 | Array.from(doc.querySelectorAll('b[style*="font-weight"]')) 22 | .filter((node) => node.getAttribute('style')?.match(normalWeightRegexp)) 23 | .forEach((node) => { 24 | const fragment = doc.createDocumentFragment(); 25 | fragment.append(...node.childNodes); 26 | node.parentNode?.replaceChild(fragment, node); 27 | }); 28 | }; 29 | 30 | export default function normalize(doc: Document) { 31 | if (doc.querySelector('[id^="docs-internal-guid-"]')) { 32 | normalizeFontWeight(doc); 33 | normalizeEmptyLines(doc); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/website/content/docs/quickstart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quickstart 3 | --- 4 | 5 | The best way to get started is to try a simple example. Quill is initialized with a DOM element to contain the editor. The contents of that element will become the initial contents of Quill. 6 | 7 | 10 | 11 | 12 | 13 |
14 |

Hello World!

15 |

Some initial bold text

16 |


17 |
18 | 19 | 20 | 21 | 22 | 23 | ` 28 | 29 | }}/ > 30 | 31 | And that's all there is to it! 32 | 33 | ## Next Steps 34 | 35 | The real magic of Quill comes in its flexibility and extensibility. You can get an idea of what is possible by playing around with the demos throughout this site or head straight to the [Interactive Playground](/playground/). For an in-depth walkthrough, take a look at [How to Customize Quill](/guides/how-to-customize-quill/). 36 | -------------------------------------------------------------------------------- /packages/quill/src/assets/bubble.styl: -------------------------------------------------------------------------------- 1 | themeName = 'bubble' 2 | activeColor = #fff 3 | borderColor = #777 4 | backgroundColor = #444 5 | inactiveColor = #ccc 6 | shadowColor = #ddd 7 | textColor = #fff 8 | 9 | @import './core' 10 | @import './base' 11 | @import './bubble/*' 12 | 13 | .ql-container.ql-bubble:not(.ql-disabled) 14 | a:not(.ql-close) 15 | position: relative 16 | white-space: nowrap 17 | a:not(.ql-close)::before 18 | background-color: #444 19 | border-radius: 15px 20 | top: -5px 21 | font-size: 12px 22 | color: #fff 23 | content: attr(href) 24 | font-weight: normal 25 | overflow: hidden 26 | padding: 5px 15px 27 | text-decoration: none 28 | z-index: 1 29 | a:not(.ql-close)::after 30 | border-top: 6px solid #444 31 | border-left: 6px solid transparent 32 | border-right: 6px solid transparent 33 | top: 0 34 | content: " " 35 | height: 0 36 | width: 0 37 | a:not(.ql-close)::before, a:not(.ql-close)::after 38 | left: 0 39 | margin-left: 50% 40 | position: absolute 41 | transform: translate(-50%, -100%) 42 | transition: visibility 0s ease 200ms 43 | visibility: hidden 44 | a:not(.ql-close):hover::before, a:not(.ql-close):hover::after 45 | visibility: visible 46 | -------------------------------------------------------------------------------- /packages/website/src/playground/form/index.js: -------------------------------------------------------------------------------- 1 | const initialData = { 2 | name: 'Wall-E', 3 | location: 'Earth', 4 | // `about` is a Delta object 5 | // Learn more at: https://quilljs.com/docs/delta 6 | about: [ 7 | { 8 | insert: 9 | 'A robot who has developed sentience, and is the only robot of his kind shown to be still functioning on Earth.\n', 10 | }, 11 | ], 12 | }; 13 | 14 | const quill = new Quill('#editor', { 15 | modules: { 16 | toolbar: [ 17 | ['bold', 'italic'], 18 | ['link', 'blockquote', 'code-block', 'image'], 19 | [{ list: 'ordered' }, { list: 'bullet' }], 20 | ], 21 | }, 22 | theme: 'snow', 23 | }); 24 | 25 | const resetForm = () => { 26 | document.querySelector('[name="name"]').value = initialData.name; 27 | document.querySelector('[name="location"]').value = initialData.location; 28 | quill.setContents(initialData.about); 29 | }; 30 | 31 | resetForm(); 32 | 33 | const form = document.querySelector('form'); 34 | form.addEventListener('formdata', (event) => { 35 | // Append Quill content before submitting 36 | event.formData.append('about', JSON.stringify(quill.getContents().ops)); 37 | }); 38 | 39 | document.querySelector('#resetForm').addEventListener('click', () => { 40 | resetForm(); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/quill/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:prettier/recommended", 5 | "plugin:import/recommended", 6 | "plugin:require-extensions/recommended" 7 | ], 8 | "env": { 9 | "browser": true, 10 | "commonjs": true, 11 | "es6": true 12 | }, 13 | "parser": "@typescript-eslint/parser", 14 | "settings": { 15 | "import/resolver": { 16 | "webpack": { 17 | "env": "development" 18 | }, 19 | "typescript": true 20 | } 21 | }, 22 | "ignorePatterns": ["*.js", "*.d.ts"], 23 | "overrides": [ 24 | { 25 | "files": ["**/*.ts"], 26 | "extends": [ 27 | "plugin:@typescript-eslint/recommended", 28 | "plugin:import/typescript" 29 | ], 30 | "excludedFiles": "*.d.ts", 31 | "plugins": ["@typescript-eslint", "require-extensions"], 32 | "rules": { 33 | "@typescript-eslint/consistent-type-imports": "error", 34 | "@typescript-eslint/ban-ts-comment": "off", 35 | "@typescript-eslint/no-empty-function": "off", 36 | "@typescript-eslint/ban-types": "off", 37 | "@typescript-eslint/no-explicit-any": "off", 38 | "import/no-named-as-default-member": "off", 39 | "prefer-arrow-callback": "error" 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /packages/quill/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | const port = 9001; 4 | 5 | export default defineConfig({ 6 | testDir: './test/e2e', 7 | testMatch: '*.spec.ts', 8 | timeout: 30 * 1000, 9 | expect: { 10 | timeout: 5000, 11 | }, 12 | fullyParallel: true, 13 | forbidOnly: !!process.env.CI, 14 | retries: process.env.CI ? 2 : 0, 15 | workers: process.env.CI ? 1 : undefined, 16 | reporter: 'list', 17 | use: { 18 | actionTimeout: 0, 19 | trace: 'on-first-retry', 20 | baseURL: `https://127.0.0.1:${port}`, 21 | ignoreHTTPSErrors: true, 22 | }, 23 | projects: [ 24 | { 25 | name: 'Chrome', 26 | use: { 27 | ...devices['Desktop Chrome'], 28 | contextOptions: { 29 | permissions: ['clipboard-read', 'clipboard-write'], 30 | }, 31 | }, 32 | }, 33 | { name: 'Firefox', use: { ...devices['Desktop Firefox'] } }, 34 | { name: 'Safari', use: { ...devices['Desktop Safari'] } }, 35 | ], 36 | webServer: { 37 | command: `npx webpack serve --config test/e2e/__dev_server__/webpack.config.cjs --env port=${port}`, 38 | port, 39 | ignoreHTTPSErrors: true, 40 | reuseExistingServer: !process.env.CI, 41 | stdout: 'ignore', 42 | stderr: 'pipe', 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /packages/quill/webpack.config.cjs: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | 3 | const { BannerPlugin, DefinePlugin } = require('webpack'); 4 | const common = require('./webpack.common.cjs'); 5 | const { merge } = require('webpack-merge'); 6 | require('webpack-dev-server'); 7 | const { readFileSync } = require('fs'); 8 | const { join, resolve } = require('path'); 9 | 10 | const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')); 11 | 12 | const bannerPack = new BannerPlugin({ 13 | banner: [ 14 | `Quill Editor v${pkg.version}`, 15 | pkg.homepage, 16 | `Copyright (c) 2017-${new Date().getFullYear()}, Slab`, 17 | 'Copyright (c) 2014, Jason Chen', 18 | 'Copyright (c) 2013, salesforce.com', 19 | ].join('\n'), 20 | entryOnly: true, 21 | }); 22 | const constantPack = new DefinePlugin({ 23 | QUILL_VERSION: JSON.stringify(pkg.version), 24 | }); 25 | 26 | module.exports = (env) => 27 | merge(common, { 28 | mode: env.production ? 'production' : 'development', 29 | devtool: 'source-map', 30 | plugins: [bannerPack, constantPack], 31 | devServer: { 32 | static: { 33 | directory: resolve(__dirname, './dist'), 34 | }, 35 | hot: false, 36 | allowedHosts: 'all', 37 | devMiddleware: { 38 | stats: 'minimal', 39 | }, 40 | }, 41 | stats: 'minimal', 42 | }); 43 | -------------------------------------------------------------------------------- /packages/website/src/pages/standalone/stress.mdx: -------------------------------------------------------------------------------- 1 | Stress 2 | 3 | import { StandaloneSandpack } from '../../components/Sandpack'; 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | `, 20 | 'index.js': ` 21 | 22 | const editor = document.getElementById('editor'); 23 | 24 | editor.innerHTML = new Array(200).fill(0).map((_, index) => { 25 | return \` 26 |

Heading \${index}

27 |

List items:

28 |
    29 | \${ 30 | new Array(20).fill(0).map((_, index) => { 31 | return \`
  • List item \${index}
  • \` 32 | }).join('') 33 | } 34 |
35 | \` 36 | }).join('') 37 | 38 | const quill = new Quill('#editor', { 39 | placeholder: 'Compose an epic...', 40 | theme: 'snow' 41 | }); 42 | ` 43 | }} 44 | /> -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quill-monorepo", 3 | "version": "2.0.3", 4 | "description": "Quill development environment", 5 | "private": true, 6 | "author": "Jason Chen ", 7 | "homepage": "https://quilljs.com", 8 | "config": { 9 | "ports": { 10 | "webpack": "9080", 11 | "website": "9000" 12 | } 13 | }, 14 | "workspaces": [ 15 | "packages/*" 16 | ], 17 | "license": "BSD-3-Clause", 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/slab/quill.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/slab/quill/issues" 24 | }, 25 | "scripts": { 26 | "build": "run-p build:*", 27 | "build:quill": "npm run build -w quill", 28 | "build:website": "npm run build -w website", 29 | "start": "run-p start:*", 30 | "start:quill": "npm start -w quill", 31 | "start:website": "NEXT_PUBLIC_LOCAL_QUILL=true npm start -w website", 32 | "lint": "npm run lint -ws" 33 | }, 34 | "keywords": [ 35 | "quill", 36 | "editor", 37 | "rich text", 38 | "wysiwyg", 39 | "operational transformation", 40 | "ot", 41 | "framework" 42 | ], 43 | "engines": { 44 | "npm": ">=8.2.3" 45 | }, 46 | "engineStrict": true, 47 | "devDependencies": { 48 | "execa": "^9.0.2", 49 | "npm-run-all": "^4.1.5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/quill/src/core/utils/createRegistryWithFormats.ts: -------------------------------------------------------------------------------- 1 | import { Registry } from 'parchment'; 2 | 3 | const MAX_REGISTER_ITERATIONS = 100; 4 | const CORE_FORMATS = ['block', 'break', 'cursor', 'inline', 'scroll', 'text']; 5 | 6 | const createRegistryWithFormats = ( 7 | formats: string[], 8 | sourceRegistry: Registry, 9 | debug: { error: (errorMessage: string) => void }, 10 | ) => { 11 | const registry = new Registry(); 12 | CORE_FORMATS.forEach((name) => { 13 | const coreBlot = sourceRegistry.query(name); 14 | if (coreBlot) registry.register(coreBlot); 15 | }); 16 | 17 | formats.forEach((name) => { 18 | let format = sourceRegistry.query(name); 19 | if (!format) { 20 | debug.error( 21 | `Cannot register "${name}" specified in "formats" config. Are you sure it was registered?`, 22 | ); 23 | } 24 | let iterations = 0; 25 | while (format) { 26 | registry.register(format); 27 | format = 'blotName' in format ? format.requiredContainer ?? null : null; 28 | 29 | iterations += 1; 30 | if (iterations > MAX_REGISTER_ITERATIONS) { 31 | debug.error( 32 | `Cycle detected in registering blot requiredContainer: "${name}"`, 33 | ); 34 | break; 35 | } 36 | } 37 | }); 38 | 39 | return registry; 40 | }; 41 | 42 | export default createRegistryWithFormats; 43 | -------------------------------------------------------------------------------- /packages/quill/src/assets/bubble/tooltip.styl: -------------------------------------------------------------------------------- 1 | arrowWidth = 6px 2 | 3 | .ql-bubble 4 | .ql-tooltip 5 | background-color: backgroundColor 6 | border-radius: 25px 7 | color: textColor 8 | .ql-tooltip-arrow 9 | border-left: arrowWidth solid transparent 10 | border-right: arrowWidth solid transparent 11 | content: " " 12 | display: block 13 | left: 50% 14 | margin-left: -1 * arrowWidth 15 | position: absolute 16 | .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow 17 | border-bottom: arrowWidth solid backgroundColor 18 | top: -1 * arrowWidth 19 | .ql-tooltip.ql-flip .ql-tooltip-arrow 20 | border-top: arrowWidth solid backgroundColor 21 | bottom: -1 * arrowWidth 22 | 23 | .ql-tooltip.ql-editing 24 | .ql-tooltip-editor 25 | display: block 26 | .ql-formats 27 | visibility: hidden 28 | 29 | .ql-tooltip-editor 30 | display: none 31 | input[type=text] 32 | background: transparent 33 | border: none 34 | color: textColor 35 | font-size: 13px 36 | height: 100% 37 | outline: none 38 | padding: 10px 20px 39 | position: absolute 40 | width: 100% 41 | a 42 | &:before 43 | color: inactiveColor 44 | content: "\00D7" 45 | font-size: 16px 46 | font-weight: bold 47 | top: 10px 48 | position: absolute 49 | right: 20px 50 | -------------------------------------------------------------------------------- /packages/website/src/components/ActiveLink.jsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import Link from 'next/link'; 3 | import React, { useState, useEffect, useCallback } from 'react'; 4 | 5 | const ActiveLink = ({ 6 | children, 7 | activeClassName, 8 | className = '', 9 | activePath, 10 | ...props 11 | }) => { 12 | const { asPath, isReady } = useRouter(); 13 | 14 | const getClassName = useCallback(() => { 15 | // Using URL().pathname to get rid of query and hash 16 | const activePathname = asPath; 17 | 18 | const isActive = activePath 19 | ? activePathname.startsWith(activePath) 20 | : linkPathname === activePathname; 21 | return isActive ? `${className} ${activeClassName}`.trim() : className; 22 | }, [asPath, activePath, className, activeClassName]); 23 | 24 | const [computedClassName, setComputedClassName] = useState(getClassName()); 25 | 26 | useEffect(() => { 27 | // Check if the router fields are updated client-side 28 | if (isReady) { 29 | const newClassName = getClassName(); 30 | 31 | if (newClassName !== computedClassName) { 32 | setComputedClassName(newClassName); 33 | } 34 | } 35 | }, [isReady, computedClassName, getClassName]); 36 | 37 | return ( 38 | 39 | {children} 40 | 41 | ); 42 | }; 43 | 44 | export default ActiveLink; 45 | -------------------------------------------------------------------------------- /packages/quill/test/unit/formats/script.spec.ts: -------------------------------------------------------------------------------- 1 | import Editor from '../../../src/core/editor.js'; 2 | import Script from '../../../src/formats/script.js'; 3 | import { 4 | createScroll as baseCreateScroll, 5 | createRegistry, 6 | } from '../__helpers__/factory.js'; 7 | import { describe, expect, test } from 'vitest'; 8 | 9 | const createScroll = (html: string) => 10 | baseCreateScroll(html, createRegistry([Script])); 11 | 12 | describe('Script', () => { 13 | test('add', () => { 14 | const editor = new Editor( 15 | createScroll('

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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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 | 43 | ); 44 | }; 45 | 46 | export default GitHub; 47 | -------------------------------------------------------------------------------- /packages/quill/test/unit/blots/inline.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { createRegistry, createScroll } from '../__helpers__/factory.js'; 3 | import Bold from '../../../src/formats/bold.js'; 4 | import Italic from '../../../src/formats/italic.js'; 5 | 6 | describe('Inline', () => { 7 | test('format order', () => { 8 | const scroll = createScroll( 9 | '

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: Record, attribute) => { 21 | if (domNode.hasAttribute(attribute)) { 22 | formats[attribute] = domNode.getAttribute(attribute); 23 | } 24 | return formats; 25 | }, 26 | {}, 27 | ); 28 | } 29 | 30 | static match(url: string) { 31 | return /\.(jpe?g|gif|png)$/.test(url) || /^data:image\/.+;base64/.test(url); 32 | } 33 | 34 | static sanitize(url: string) { 35 | return sanitize(url, ['http', 'https', 'data']) ? url : '//:0'; 36 | } 37 | 38 | static value(domNode: Element) { 39 | return domNode.getAttribute('src'); 40 | } 41 | 42 | domNode: HTMLImageElement; 43 | 44 | format(name: string, value: string) { 45 | if (ATTRIBUTES.indexOf(name) > -1) { 46 | if (value) { 47 | this.domNode.setAttribute(name, value); 48 | } else { 49 | this.domNode.removeAttribute(name); 50 | } 51 | } else { 52 | super.format(name, value); 53 | } 54 | } 55 | } 56 | 57 | export default Image; 58 | -------------------------------------------------------------------------------- /packages/quill/test/unit/modules/uiNode.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../../src/quill.js'; 2 | import { describe, expect, test } from 'vitest'; 3 | import UINode, { 4 | TTL_FOR_VALID_SELECTION_CHANGE, 5 | } from '../../../src/modules/uiNode.js'; 6 | import Quill, { Delta } from '../../../src/core.js'; 7 | 8 | // Fake timer is not supported in browser mode yet. 9 | const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); 10 | 11 | describe('uiNode', () => { 12 | test('extends deadline when multiple possible shortcuts are pressed', async () => { 13 | const quill = new Quill(document.createElement('div')); 14 | document.body.appendChild(quill.container); 15 | quill.setContents( 16 | new Delta().insert('item 1').insert('\n', { list: 'bullet' }), 17 | ); 18 | new UINode(quill, {}); 19 | 20 | for (let i = 0; i < 2; i += 1) { 21 | quill.root.dispatchEvent( 22 | new KeyboardEvent('keydown', { key: 'ArrowRight', metaKey: true }), 23 | ); 24 | await delay(TTL_FOR_VALID_SELECTION_CHANGE / 2); 25 | } 26 | 27 | quill.root.dispatchEvent( 28 | new KeyboardEvent('keydown', { key: 'ArrowLeft', metaKey: true }), 29 | ); 30 | const range = document.createRange(); 31 | range.setStart(quill.root.querySelector('li')!, 0); 32 | range.setEnd(quill.root.querySelector('li')!, 0); 33 | 34 | const selection = document.getSelection(); 35 | selection?.removeAllRanges(); 36 | selection?.addRange(range); 37 | 38 | await delay(TTL_FOR_VALID_SELECTION_CHANGE / 2); 39 | expect(selection?.getRangeAt(0).startOffset).toEqual(1); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-border-none.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/quill/src/formats/video.ts: -------------------------------------------------------------------------------- 1 | import { BlockEmbed } from '../blots/block.js'; 2 | import Link from './link.js'; 3 | 4 | const ATTRIBUTES = ['height', 'width']; 5 | 6 | class Video extends BlockEmbed { 7 | static blotName = 'video'; 8 | static className = 'ql-video'; 9 | static tagName = 'IFRAME'; 10 | 11 | static create(value: string) { 12 | const node = super.create(value) as Element; 13 | node.setAttribute('frameborder', '0'); 14 | node.setAttribute('allowfullscreen', 'true'); 15 | node.setAttribute('src', this.sanitize(value)); 16 | return node; 17 | } 18 | 19 | static formats(domNode: Element) { 20 | return ATTRIBUTES.reduce( 21 | (formats: Record, attribute) => { 22 | if (domNode.hasAttribute(attribute)) { 23 | formats[attribute] = domNode.getAttribute(attribute); 24 | } 25 | return formats; 26 | }, 27 | {}, 28 | ); 29 | } 30 | 31 | static sanitize(url: string) { 32 | return Link.sanitize(url); 33 | } 34 | 35 | static value(domNode: Element) { 36 | return domNode.getAttribute('src'); 37 | } 38 | 39 | domNode: HTMLVideoElement; 40 | 41 | format(name: string, value: string) { 42 | if (ATTRIBUTES.indexOf(name) > -1) { 43 | if (value) { 44 | this.domNode.setAttribute(name, value); 45 | } else { 46 | this.domNode.removeAttribute(name); 47 | } 48 | } else { 49 | super.format(name, value); 50 | } 51 | } 52 | 53 | html() { 54 | const { video } = this.value(); 55 | return `${video}`; 56 | } 57 | } 58 | 59 | export default Video; 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2024, Slab 2 | Copyright (c) 2014, Jason Chen 3 | Copyright (c) 2013, salesforce.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /packages/website/src/components/ClickOutsideHandler.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const TOUCH_EVENT = { react: 'onTouchStart', native: 'touchstart' }; 4 | const MOUSE_EVENT = { react: 'onMouseDown', native: 'mousedown' }; 5 | 6 | const setUpReactEventHandlers = (handler, props) => ({ 7 | ...props, 8 | [TOUCH_EVENT.react]: (e) => { 9 | handler(); 10 | props[TOUCH_EVENT.react]?.(e); 11 | }, 12 | [MOUSE_EVENT.react]: (e) => { 13 | handler(); 14 | props[MOUSE_EVENT.react]?.(e); 15 | }, 16 | }); 17 | 18 | const ClickOutsideHandler = ({ onClickOutside, ...props }) => { 19 | const isTargetInsideReactTreeRef = useRef(false); 20 | 21 | const onClickOutsideRef = useRef(onClickOutside); 22 | useEffect(() => { 23 | onClickOutsideRef.current = onClickOutside; 24 | }, [onClickOutside]); 25 | 26 | useEffect(() => { 27 | const handler = (e) => { 28 | if (!isTargetInsideReactTreeRef.current) { 29 | onClickOutsideRef.current?.(e); 30 | } 31 | 32 | isTargetInsideReactTreeRef.current = false; 33 | }; 34 | 35 | document.addEventListener(TOUCH_EVENT.native, handler, { passive: true }); 36 | document.addEventListener(MOUSE_EVENT.native, handler, { passive: true }); 37 | return () => { 38 | document.removeEventListener(TOUCH_EVENT.native, handler); 39 | document.removeEventListener(MOUSE_EVENT.native, handler); 40 | }; 41 | }, []); 42 | 43 | const handleReactEvent = () => { 44 | isTargetInsideReactTreeRef.current = true; 45 | }; 46 | 47 | return
; 48 | }; 49 | 50 | export default ClickOutsideHandler; 51 | -------------------------------------------------------------------------------- /packages/quill/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2024, Slab 2 | Copyright (c) 2014, Jason Chen 3 | Copyright (c) 2013, salesforce.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /packages/quill/test/unit/formats/header.spec.ts: -------------------------------------------------------------------------------- 1 | import Delta from 'quill-delta'; 2 | import { 3 | createScroll as baseCreateScroll, 4 | createRegistry, 5 | } from '../__helpers__/factory.js'; 6 | import Editor from '../../../src/core/editor.js'; 7 | import Header from '../../../src/formats/header.js'; 8 | import Italic from '../../../src/formats/italic.js'; 9 | import { describe, expect, test } from 'vitest'; 10 | 11 | const createScroll = (html: string) => 12 | baseCreateScroll(html, createRegistry([Header, Italic])); 13 | 14 | describe('Header', () => { 15 | test('add', () => { 16 | const editor = new Editor(createScroll('

0123

')); 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

'); 22 | }); 23 | 24 | test('remove', () => { 25 | const editor = new Editor(createScroll('

0123

')); 26 | editor.formatText(4, 1, { header: false }); 27 | expect(editor.getDelta()).toEqual( 28 | new Delta().insert('0123', { italic: true }).insert('\n'), 29 | ); 30 | expect(editor.scroll.domNode).toEqualHTML('

0123

'); 31 | }); 32 | 33 | test('change', () => { 34 | const editor = new Editor(createScroll('

0123

')); 35 | editor.formatText(4, 1, { header: 2 }); 36 | expect(editor.getDelta()).toEqual( 37 | new Delta().insert('0123', { italic: true }).insert('\n', { header: 2 }), 38 | ); 39 | expect(editor.scroll.domNode).toEqualHTML('

0123

'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/quill/src/core/theme.ts: -------------------------------------------------------------------------------- 1 | import type Quill from '../core.js'; 2 | import type Clipboard from '../modules/clipboard.js'; 3 | import type History from '../modules/history.js'; 4 | import type Keyboard from '../modules/keyboard.js'; 5 | import type { ToolbarProps } from '../modules/toolbar.js'; 6 | import type Uploader from '../modules/uploader.js'; 7 | 8 | export interface ThemeOptions { 9 | modules: Record & { 10 | toolbar?: null | ToolbarProps; 11 | }; 12 | } 13 | 14 | class Theme { 15 | static DEFAULTS: ThemeOptions = { 16 | modules: {}, 17 | }; 18 | 19 | static themes = { 20 | default: Theme, 21 | }; 22 | 23 | modules: ThemeOptions['modules'] = {}; 24 | 25 | constructor( 26 | protected quill: Quill, 27 | protected options: ThemeOptions, 28 | ) {} 29 | 30 | init() { 31 | Object.keys(this.options.modules).forEach((name) => { 32 | if (this.modules[name] == null) { 33 | this.addModule(name); 34 | } 35 | }); 36 | } 37 | 38 | addModule(name: 'clipboard'): Clipboard; 39 | addModule(name: 'keyboard'): Keyboard; 40 | addModule(name: 'uploader'): Uploader; 41 | addModule(name: 'history'): History; 42 | addModule(name: string): unknown; 43 | addModule(name: string) { 44 | // @ts-expect-error 45 | const ModuleClass = this.quill.constructor.import(`modules/${name}`); 46 | this.modules[name] = new ModuleClass( 47 | this.quill, 48 | this.options.modules[name] || {}, 49 | ); 50 | return this.modules[name]; 51 | } 52 | } 53 | 54 | export interface ThemeConstructor { 55 | new (quill: Quill, options: unknown): Theme; 56 | DEFAULTS: ThemeOptions; 57 | } 58 | 59 | export default Theme; 60 | -------------------------------------------------------------------------------- /packages/quill/webpack.common.cjs: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | 3 | const { resolve } = require('path'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | 6 | const tsRules = { 7 | test: /\.ts$/, 8 | include: [resolve(__dirname, 'src')], 9 | use: ['babel-loader'], 10 | }; 11 | 12 | const sourceMapRules = { 13 | test: /\.js$/, 14 | enforce: 'pre', 15 | use: ['source-map-loader'], 16 | }; 17 | 18 | const svgRules = { 19 | test: /\.svg$/, 20 | include: [resolve(__dirname, 'src/assets/icons')], 21 | use: [ 22 | { 23 | loader: 'html-loader', 24 | options: { 25 | minimize: true, 26 | }, 27 | }, 28 | ], 29 | }; 30 | 31 | const stylRules = { 32 | test: /\.styl$/, 33 | include: [resolve(__dirname, 'src/assets')], 34 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader'], 35 | }; 36 | 37 | module.exports = { 38 | entry: { 39 | quill: './src/quill.ts', 40 | 'quill.core': './src/core.ts', 41 | 'quill.core.css': './src/assets/core.styl', 42 | 'quill.bubble.css': './src/assets/bubble.styl', 43 | 'quill.snow.css': './src/assets/snow.styl', 44 | }, 45 | output: { 46 | filename: '[name].js', 47 | library: { 48 | name: 'Quill', 49 | type: 'umd', 50 | export: 'default', 51 | }, 52 | path: resolve(__dirname, 'dist/dist'), 53 | clean: true, 54 | }, 55 | resolve: { 56 | extensions: ['.js', '.styl', '.ts'], 57 | extensionAlias: { 58 | '.js': ['.ts', '.js'], 59 | }, 60 | }, 61 | module: { 62 | rules: [tsRules, stylRules, svgRules, sourceMapRules], 63 | }, 64 | plugins: [ 65 | new MiniCssExtractPlugin({ 66 | filename: '[name]', 67 | }), 68 | ], 69 | }; 70 | -------------------------------------------------------------------------------- /packages/website/src/playground/react/Editor.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useEffect, useLayoutEffect, useRef } from 'react'; 2 | 3 | // Editor is an uncontrolled React component 4 | const Editor = forwardRef( 5 | ({ readOnly, defaultValue, onTextChange, onSelectionChange }, ref) => { 6 | const containerRef = useRef(null); 7 | const defaultValueRef = useRef(defaultValue); 8 | const onTextChangeRef = useRef(onTextChange); 9 | const onSelectionChangeRef = useRef(onSelectionChange); 10 | 11 | useLayoutEffect(() => { 12 | onTextChangeRef.current = onTextChange; 13 | onSelectionChangeRef.current = onSelectionChange; 14 | }); 15 | 16 | useEffect(() => { 17 | ref.current?.enable(!readOnly); 18 | }, [ref, readOnly]); 19 | 20 | useEffect(() => { 21 | const container = containerRef.current; 22 | const editorContainer = container.appendChild( 23 | container.ownerDocument.createElement('div'), 24 | ); 25 | const quill = new Quill(editorContainer, { 26 | theme: 'snow', 27 | }); 28 | 29 | ref.current = quill; 30 | 31 | if (defaultValueRef.current) { 32 | quill.setContents(defaultValueRef.current); 33 | } 34 | 35 | quill.on(Quill.events.TEXT_CHANGE, (...args) => { 36 | onTextChangeRef.current?.(...args); 37 | }); 38 | 39 | quill.on(Quill.events.SELECTION_CHANGE, (...args) => { 40 | onSelectionChangeRef.current?.(...args); 41 | }); 42 | 43 | return () => { 44 | ref.current = null; 45 | container.innerHTML = ''; 46 | }; 47 | }, [ref]); 48 | 49 | return
; 50 | }, 51 | ); 52 | 53 | Editor.displayName = 'Editor'; 54 | 55 | export default Editor; 56 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-border-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/website/src/pages/docs/[...id].jsx: -------------------------------------------------------------------------------- 1 | import { serialize } from 'next-mdx-remote/serialize'; 2 | import docs from '../../data/docs'; 3 | import { readFile } from 'node:fs/promises'; 4 | import { join } from 'node:path'; 5 | import PostLayout from '../../components/PostLayout'; 6 | import env from '../../../env'; 7 | import MDX from '../../components/MDX'; 8 | import flattenData from '../../utils/flattenData'; 9 | 10 | export async function getStaticPaths() { 11 | return { 12 | paths: flattenData(docs).map((d) => d.url), 13 | fallback: false, 14 | }; 15 | } 16 | 17 | export async function getStaticProps({ params }) { 18 | const basePath = join('content', 'docs', `${params.id.join('/')}`); 19 | const filePath = `${basePath}.mdx`; 20 | const markdown = await readFile(join(process.cwd(), filePath), 'utf8'); 21 | let data = {}; 22 | try { 23 | const path = params.id.join('/'); 24 | if (path === 'guides/cloning-medium-with-parchment') { 25 | data = await import(`../../../content/docs/${path}`); 26 | } 27 | } catch {} 28 | const mdxSource = await serialize( 29 | markdown.replace(/\{\{site\.(\w+)\}\}/g, (...args) => { 30 | return env[args[1]]; 31 | }), 32 | { parseFrontmatter: true }, 33 | ); 34 | return { 35 | props: { 36 | mdxSource, 37 | filePath, 38 | permalink: `/docs/${params.id}`, 39 | data: JSON.parse(JSON.stringify(data)), 40 | }, 41 | }; 42 | } 43 | 44 | export default function Doc({ mdxSource, filePath, permalink, data }) { 45 | return ( 46 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /packages/quill/src/core.ts: -------------------------------------------------------------------------------- 1 | import Quill, { Parchment, Range } from './core/quill.js'; 2 | import type { 3 | Bounds, 4 | DebugLevel, 5 | EmitterSource, 6 | ExpandedQuillOptions, 7 | QuillOptions, 8 | } from './core/quill.js'; 9 | 10 | import Block, { BlockEmbed } from './blots/block.js'; 11 | import Break from './blots/break.js'; 12 | import Container from './blots/container.js'; 13 | import Cursor from './blots/cursor.js'; 14 | import Embed from './blots/embed.js'; 15 | import Inline from './blots/inline.js'; 16 | import Scroll from './blots/scroll.js'; 17 | import TextBlot from './blots/text.js'; 18 | 19 | import Clipboard from './modules/clipboard.js'; 20 | import History from './modules/history.js'; 21 | import Keyboard from './modules/keyboard.js'; 22 | import Uploader from './modules/uploader.js'; 23 | import Delta, { Op, OpIterator, AttributeMap } from 'quill-delta'; 24 | import Input from './modules/input.js'; 25 | import UINode from './modules/uiNode.js'; 26 | 27 | export { default as Module } from './core/module.js'; 28 | export { Delta, Op, OpIterator, AttributeMap, Parchment, Range }; 29 | export type { 30 | Bounds, 31 | DebugLevel, 32 | EmitterSource, 33 | ExpandedQuillOptions, 34 | QuillOptions, 35 | }; 36 | 37 | Quill.register({ 38 | 'blots/block': Block, 39 | 'blots/block/embed': BlockEmbed, 40 | 'blots/break': Break, 41 | 'blots/container': Container, 42 | 'blots/cursor': Cursor, 43 | 'blots/embed': Embed, 44 | 'blots/inline': Inline, 45 | 'blots/scroll': Scroll, 46 | 'blots/text': TextBlot, 47 | 48 | 'modules/clipboard': Clipboard, 49 | 'modules/history': History, 50 | 'modules/keyboard': Keyboard, 51 | 'modules/uploader': Uploader, 52 | 'modules/input': Input, 53 | 'modules/uiNode': UINode, 54 | }); 55 | 56 | export default Quill; 57 | -------------------------------------------------------------------------------- /packages/website/src/components/Heading.jsx: -------------------------------------------------------------------------------- 1 | import { createElement } from 'react'; 2 | import slug from '../utils/slug'; 3 | 4 | const EXPERIMENTAL_FLAG = ' #experimental'; 5 | 6 | const Heading = ({ level, children, anchor = 'on' }) => { 7 | const tag = `h${level}`; 8 | 9 | if (typeof children !== 'string') { 10 | return createElement(tag, null, children); 11 | } 12 | 13 | const isExperimental = children.endsWith(EXPERIMENTAL_FLAG); 14 | const title = isExperimental 15 | ? children.slice(0, -EXPERIMENTAL_FLAG.length) 16 | : children; 17 | const id = 18 | anchor === 'on' 19 | ? slug(title) + (isExperimental ? '-experimental' : '') 20 | : undefined; 21 | 22 | return createElement( 23 | tag, 24 | { id }, 25 | <> 26 | {id && } 27 | {title} 28 | {isExperimental && experimental} 29 | , 30 | ); 31 | }; 32 | 33 | export const Heading1 = ({ children, anchor }) => ( 34 | 35 | {children} 36 | 37 | ); 38 | export const Heading2 = ({ children, anchor }) => ( 39 | 40 | {children} 41 | 42 | ); 43 | export const Heading3 = ({ children, anchor }) => ( 44 | 45 | {children} 46 | 47 | ); 48 | export const Heading4 = ({ children, anchor }) => ( 49 | 50 | {children} 51 | 52 | ); 53 | export const Heading5 = ({ children, anchor }) => ( 54 | 55 | {children} 56 | 57 | ); 58 | export const Heading6 = ({ children, anchor }) => ( 59 | 60 | {children} 61 | 62 | ); 63 | -------------------------------------------------------------------------------- /.github/workflows/_test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | workflow_call: 4 | jobs: 5 | e2e: 6 | name: E2E Tests 7 | timeout-minutes: 60 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 20 14 | - name: Install dependencies 15 | run: npm ci 16 | - name: Install Playwright Browsers 17 | run: npx playwright install --with-deps 18 | working-directory: packages/quill 19 | - name: Run Playwright tests 20 | uses: coactions/setup-xvfb@v1 21 | with: 22 | run: npm run test:e2e -- --headed 23 | working-directory: packages/quill 24 | fuzz: 25 | name: Fuzz Tests 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - name: Git checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Use Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: 20 36 | 37 | - run: npm ci 38 | env: 39 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 40 | - run: npm run test:fuzz -w quill 41 | unit: 42 | name: Unit Tests 43 | runs-on: ubuntu-latest 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | browser: [chromium, webkit, firefox] 48 | 49 | steps: 50 | - name: Git checkout 51 | uses: actions/checkout@v3 52 | 53 | - name: Use Node.js 54 | uses: actions/setup-node@v3 55 | with: 56 | node-version: 20 57 | 58 | - run: npm ci 59 | - run: npx playwright install --with-deps 60 | - run: npm run lint 61 | - run: npm run test:unit -w quill || npm run test:unit -w quill || npm run test:unit -w quill 62 | env: 63 | BROWSER: ${{ matrix.browser }} 64 | -------------------------------------------------------------------------------- /packages/quill/src/core/composition.ts: -------------------------------------------------------------------------------- 1 | import Embed from '../blots/embed.js'; 2 | import type Scroll from '../blots/scroll.js'; 3 | import Emitter from './emitter.js'; 4 | 5 | class Composition { 6 | isComposing = false; 7 | 8 | constructor( 9 | private scroll: Scroll, 10 | private emitter: Emitter, 11 | ) { 12 | this.setupListeners(); 13 | } 14 | 15 | private setupListeners() { 16 | this.scroll.domNode.addEventListener('compositionstart', (event) => { 17 | if (!this.isComposing) { 18 | this.handleCompositionStart(event); 19 | } 20 | }); 21 | 22 | this.scroll.domNode.addEventListener('compositionend', (event) => { 23 | if (this.isComposing) { 24 | // Webkit makes DOM changes after compositionend, so we use microtask to 25 | // ensure the order. 26 | // https://bugs.webkit.org/show_bug.cgi?id=31902 27 | queueMicrotask(() => { 28 | this.handleCompositionEnd(event); 29 | }); 30 | } 31 | }); 32 | } 33 | 34 | private handleCompositionStart(event: CompositionEvent) { 35 | const blot = 36 | event.target instanceof Node 37 | ? this.scroll.find(event.target, true) 38 | : null; 39 | 40 | if (blot && !(blot instanceof Embed)) { 41 | this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_START, event); 42 | this.scroll.batchStart(); 43 | this.emitter.emit(Emitter.events.COMPOSITION_START, event); 44 | this.isComposing = true; 45 | } 46 | } 47 | 48 | private handleCompositionEnd(event: CompositionEvent) { 49 | this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_END, event); 50 | this.scroll.batchEnd(); 51 | this.emitter.emit(Emitter.events.COMPOSITION_END, event); 52 | this.isComposing = false; 53 | } 54 | } 55 | 56 | export default Composition; 57 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-border-bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/website/src/playground/react/App.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import Editor from './Editor'; 3 | 4 | const Delta = Quill.import('delta'); 5 | 6 | const App = () => { 7 | const [range, setRange] = useState(); 8 | const [lastChange, setLastChange] = useState(); 9 | const [readOnly, setReadOnly] = useState(false); 10 | 11 | // Use a ref to access the quill instance directly 12 | const quillRef = useRef(); 13 | 14 | return ( 15 |
16 | 30 |
31 | 39 | 48 |
49 |
50 |
Current Range:
51 | {range ? JSON.stringify(range) : 'Empty'} 52 |
53 |
54 |
Last Change:
55 | {lastChange ? JSON.stringify(lastChange.ops) : 'Empty'} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default App; 62 | -------------------------------------------------------------------------------- /packages/quill/test/unit/modules/normalizeExternalHTML/normalizers/msWord.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import normalize from '../../../../../src/modules/normalizeExternalHTML/normalizers/msWord.js'; 3 | 4 | describe('Microsoft Word', () => { 5 | test('keep the list style', () => { 6 | const html = ` 7 | 8 | 12 | 13 |

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 |
    27 |
  • 30 | item 1 31 |
  • 32 |
  • 36 | item 2 37 |
  • 38 |
, 39 |
    40 |
  • 44 | item 3 in another list 45 |
  • 46 |
, 47 |

48 | Plain paragraph 49 |

, 50 |
    51 |
  • 54 | the last item 55 |
  • 56 |
, 57 | ] 58 | `); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/calendly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /packages/quill/src/formats/list.ts: -------------------------------------------------------------------------------- 1 | import Block from '../blots/block.js'; 2 | import Container from '../blots/container.js'; 3 | import type Scroll from '../blots/scroll.js'; 4 | import Quill from '../core/quill.js'; 5 | 6 | class ListContainer extends Container {} 7 | ListContainer.blotName = 'list-container'; 8 | ListContainer.tagName = 'OL'; 9 | 10 | class ListItem extends Block { 11 | static create(value: string) { 12 | const node = super.create() as HTMLElement; 13 | node.setAttribute('data-list', value); 14 | return node; 15 | } 16 | 17 | static formats(domNode: HTMLElement) { 18 | return domNode.getAttribute('data-list') || undefined; 19 | } 20 | 21 | static register() { 22 | Quill.register(ListContainer); 23 | } 24 | 25 | constructor(scroll: Scroll, domNode: HTMLElement) { 26 | super(scroll, domNode); 27 | const ui = domNode.ownerDocument.createElement('span'); 28 | const listEventHandler = (e: Event) => { 29 | if (!scroll.isEnabled()) return; 30 | const format = this.statics.formats(domNode, scroll); 31 | if (format === 'checked') { 32 | this.format('list', 'unchecked'); 33 | e.preventDefault(); 34 | } else if (format === 'unchecked') { 35 | this.format('list', 'checked'); 36 | e.preventDefault(); 37 | } 38 | }; 39 | ui.addEventListener('mousedown', listEventHandler); 40 | ui.addEventListener('touchstart', listEventHandler); 41 | this.attachUI(ui); 42 | } 43 | 44 | format(name: string, value: string) { 45 | if (name === this.statics.blotName && value) { 46 | this.domNode.setAttribute('data-list', value); 47 | } else { 48 | super.format(name, value); 49 | } 50 | } 51 | } 52 | ListItem.blotName = 'list'; 53 | ListItem.tagName = 'LI'; 54 | 55 | ListContainer.allowedChildren = [ListItem]; 56 | ListItem.requiredContainer = ListContainer; 57 | 58 | export { ListContainer, ListItem as default }; 59 | -------------------------------------------------------------------------------- /packages/quill/src/formats/code.ts: -------------------------------------------------------------------------------- 1 | import Block from '../blots/block.js'; 2 | import Break from '../blots/break.js'; 3 | import Cursor from '../blots/cursor.js'; 4 | import Inline from '../blots/inline.js'; 5 | import TextBlot, { escapeText } from '../blots/text.js'; 6 | import Container from '../blots/container.js'; 7 | import Quill from '../core/quill.js'; 8 | 9 | class CodeBlockContainer extends Container { 10 | static create(value: string) { 11 | const domNode = super.create(value) as Element; 12 | domNode.setAttribute('spellcheck', 'false'); 13 | return domNode; 14 | } 15 | 16 | code(index: number, length: number) { 17 | return ( 18 | this.children 19 | // @ts-expect-error 20 | .map((child) => (child.length() <= 1 ? '' : child.domNode.innerText)) 21 | .join('\n') 22 | .slice(index, index + length) 23 | ); 24 | } 25 | 26 | html(index: number, length: number) { 27 | // `\n`s are needed in order to support empty lines at the beginning and the end. 28 | // https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions 29 | return `
\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 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 46 | `}} 47 | /> 48 | 49 | ### Use npm Package 50 | 51 | If you install highlight.js as an npm package and don't want to expose it to `window`, you need to pass it to syntax module as an option: 52 | 53 | ```js 54 | import Quill from 'quill'; 55 | import hljs from 'highlight.js'; 56 | 57 | const quill = new Quill('#editor', { 58 | modules: { 59 | syntax: { hljs }, 60 | }, 61 | }); 62 | ``` -------------------------------------------------------------------------------- /packages/quill/test/unit/formats/color.spec.ts: -------------------------------------------------------------------------------- 1 | import Delta from 'quill-delta'; 2 | import Editor from '../../../src/core/editor.js'; 3 | import { 4 | createScroll as baseCreateScroll, 5 | createRegistry, 6 | } from '../__helpers__/factory.js'; 7 | import { ColorStyle } from '../../../src/formats/color.js'; 8 | import { describe, expect, test } from 'vitest'; 9 | import Bold from '../../../src/formats/bold.js'; 10 | 11 | const createScroll = (html: string) => 12 | baseCreateScroll(html, createRegistry([ColorStyle, Bold])); 13 | 14 | describe('Color', () => { 15 | test('add', () => { 16 | const editor = new Editor(createScroll('

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 | --------------------------------------------------------------------------------