├── .eslintignore ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── ISSUE_TEMPLATE.md ├── release.yml └── workflows │ ├── _test.yml │ ├── changelog.yml │ ├── label.yml │ ├── main.yml │ ├── pull-request.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── packages ├── quill │ ├── .eslintrc.json │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── babel.config.cjs │ ├── package.json │ ├── playwright.config.ts │ ├── scripts │ │ ├── babel-svg-inline-import.cjs │ │ └── build │ ├── src │ │ ├── assets │ │ │ ├── base.styl │ │ │ ├── bubble.styl │ │ │ ├── bubble │ │ │ │ ├── toolbar.styl │ │ │ │ └── tooltip.styl │ │ │ ├── core.styl │ │ │ ├── favicon.png │ │ │ ├── icons │ │ │ │ ├── align-center.svg │ │ │ │ ├── align-justify.svg │ │ │ │ ├── align-left.svg │ │ │ │ ├── align-right.svg │ │ │ │ ├── attachment.svg │ │ │ │ ├── audio.svg │ │ │ │ ├── authorship.svg │ │ │ │ ├── background.svg │ │ │ │ ├── blockquote.svg │ │ │ │ ├── bold.svg │ │ │ │ ├── clean.svg │ │ │ │ ├── code.svg │ │ │ │ ├── color.svg │ │ │ │ ├── comment.svg │ │ │ │ ├── direction-ltr.svg │ │ │ │ ├── direction-rtl.svg │ │ │ │ ├── dropdown.svg │ │ │ │ ├── embed.svg │ │ │ │ ├── emoji.svg │ │ │ │ ├── float-center.svg │ │ │ │ ├── float-full.svg │ │ │ │ ├── float-left.svg │ │ │ │ ├── float-right.svg │ │ │ │ ├── font.svg │ │ │ │ ├── formula.svg │ │ │ │ ├── hashtag.svg │ │ │ │ ├── header-2.svg │ │ │ │ ├── header-3.svg │ │ │ │ ├── header-4.svg │ │ │ │ ├── header-5.svg │ │ │ │ ├── header-6.svg │ │ │ │ ├── header.svg │ │ │ │ ├── horizontal-rule.svg │ │ │ │ ├── image.svg │ │ │ │ ├── indent.svg │ │ │ │ ├── italic.svg │ │ │ │ ├── link.svg │ │ │ │ ├── list-bullet.svg │ │ │ │ ├── list-check.svg │ │ │ │ ├── list-ordered.svg │ │ │ │ ├── map.svg │ │ │ │ ├── mention.svg │ │ │ │ ├── more.svg │ │ │ │ ├── outdent.svg │ │ │ │ ├── redo.svg │ │ │ │ ├── size-decrease.svg │ │ │ │ ├── size-increase.svg │ │ │ │ ├── size.svg │ │ │ │ ├── spacing.svg │ │ │ │ ├── speech.svg │ │ │ │ ├── strike.svg │ │ │ │ ├── subscript.svg │ │ │ │ ├── superscript.svg │ │ │ │ ├── table-border-all.svg │ │ │ │ ├── table-border-bottom.svg │ │ │ │ ├── table-border-left.svg │ │ │ │ ├── table-border-none.svg │ │ │ │ ├── table-border-outside.svg │ │ │ │ ├── table-border-right.svg │ │ │ │ ├── table-border-top.svg │ │ │ │ ├── table-delete-cells.svg │ │ │ │ ├── table-delete-columns.svg │ │ │ │ ├── table-delete-rows.svg │ │ │ │ ├── table-insert-cells.svg │ │ │ │ ├── table-insert-columns.svg │ │ │ │ ├── table-insert-rows.svg │ │ │ │ ├── table-merge-cells.svg │ │ │ │ ├── table-unmerge-cells.svg │ │ │ │ ├── table.svg │ │ │ │ ├── underline.svg │ │ │ │ ├── undo.svg │ │ │ │ └── video.svg │ │ │ ├── snow.styl │ │ │ └── snow │ │ │ │ ├── toolbar.styl │ │ │ │ └── tooltip.styl │ │ ├── blots │ │ │ ├── block.ts │ │ │ ├── break.ts │ │ │ ├── container.ts │ │ │ ├── cursor.ts │ │ │ ├── embed.ts │ │ │ ├── inline.ts │ │ │ ├── scroll.ts │ │ │ └── text.ts │ │ ├── core.ts │ │ ├── core │ │ │ ├── composition.ts │ │ │ ├── editor.ts │ │ │ ├── emitter.ts │ │ │ ├── instances.ts │ │ │ ├── logger.ts │ │ │ ├── module.ts │ │ │ ├── quill.ts │ │ │ ├── selection.ts │ │ │ ├── theme.ts │ │ │ └── utils │ │ │ │ ├── createRegistryWithFormats.ts │ │ │ │ └── scrollRectIntoView.ts │ │ ├── formats │ │ │ ├── align.ts │ │ │ ├── background.ts │ │ │ ├── blockquote.ts │ │ │ ├── bold.ts │ │ │ ├── code.ts │ │ │ ├── color.ts │ │ │ ├── direction.ts │ │ │ ├── font.ts │ │ │ ├── formula.ts │ │ │ ├── header.ts │ │ │ ├── image.ts │ │ │ ├── indent.ts │ │ │ ├── italic.ts │ │ │ ├── link.ts │ │ │ ├── list.ts │ │ │ ├── script.ts │ │ │ ├── size.ts │ │ │ ├── strike.ts │ │ │ ├── table.ts │ │ │ ├── underline.ts │ │ │ └── video.ts │ │ ├── modules │ │ │ ├── clipboard.ts │ │ │ ├── history.ts │ │ │ ├── input.ts │ │ │ ├── keyboard.ts │ │ │ ├── normalizeExternalHTML │ │ │ │ ├── index.ts │ │ │ │ └── normalizers │ │ │ │ │ ├── googleDocs.ts │ │ │ │ │ └── msWord.ts │ │ │ ├── syntax.ts │ │ │ ├── table.ts │ │ │ ├── tableEmbed.ts │ │ │ ├── toolbar.ts │ │ │ ├── uiNode.ts │ │ │ └── uploader.ts │ │ ├── quill.ts │ │ ├── themes │ │ │ ├── base.ts │ │ │ ├── bubble.ts │ │ │ └── snow.ts │ │ ├── types.d.ts │ │ └── ui │ │ │ ├── color-picker.ts │ │ │ ├── icon-picker.ts │ │ │ ├── icons.ts │ │ │ ├── picker.ts │ │ │ └── tooltip.ts │ ├── test │ │ ├── e2e │ │ │ ├── __dev_server__ │ │ │ │ ├── index.html │ │ │ │ └── webpack.config.cjs │ │ │ ├── fixtures │ │ │ │ ├── Clipboard.ts │ │ │ │ ├── Composition.ts │ │ │ │ ├── index.ts │ │ │ │ └── utils │ │ │ │ │ └── Locker.ts │ │ │ ├── full.spec.ts │ │ │ ├── history.spec.ts │ │ │ ├── list.spec.ts │ │ │ ├── pageobjects │ │ │ │ └── EditorPage.ts │ │ │ ├── replaceSelection.spec.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ ├── fuzz │ │ │ ├── __helpers__ │ │ │ │ └── utils.ts │ │ │ ├── editor.spec.ts │ │ │ ├── tableEmbed.spec.ts │ │ │ └── vitest.config.ts │ │ ├── types │ │ │ └── quill.test-d.ts │ │ └── unit │ │ │ ├── __helpers__ │ │ │ ├── cleanup.ts │ │ │ ├── expect.ts │ │ │ ├── factory.ts │ │ │ ├── utils.ts │ │ │ └── vitest.d.ts │ │ │ ├── blots │ │ │ ├── block-embed.spec.ts │ │ │ ├── block.spec.ts │ │ │ ├── inline.spec.ts │ │ │ └── scroll.spec.ts │ │ │ ├── core │ │ │ ├── composition.spec.ts │ │ │ ├── editor.spec.ts │ │ │ ├── emitter.spec.ts │ │ │ ├── quill.spec.ts │ │ │ ├── selection.spec.ts │ │ │ └── utils │ │ │ │ └── createRegistryWithFormats.spec.ts │ │ │ ├── formats │ │ │ ├── align.spec.ts │ │ │ ├── bold.spec.ts │ │ │ ├── code.spec.ts │ │ │ ├── color.spec.ts │ │ │ ├── header.spec.ts │ │ │ ├── indent.spec.ts │ │ │ ├── link.spec.ts │ │ │ ├── list.spec.ts │ │ │ ├── script.spec.ts │ │ │ └── table.spec.ts │ │ │ ├── modules │ │ │ ├── clipboard.spec.ts │ │ │ ├── history.spec.ts │ │ │ ├── keyboard.spec.ts │ │ │ ├── normalizeExternalHTML │ │ │ │ └── normalizers │ │ │ │ │ ├── googleDocs.spec.ts │ │ │ │ │ └── msWord.spec.ts │ │ │ ├── syntax.spec.ts │ │ │ ├── table.spec.ts │ │ │ ├── tableEmbed.spec.ts │ │ │ ├── toolbar.spec.ts │ │ │ └── uiNode.spec.ts │ │ │ ├── theme │ │ │ └── base │ │ │ │ └── tooltip.spec.ts │ │ │ ├── ui │ │ │ └── picker.spec.ts │ │ │ └── vitest.config.ts │ ├── tsconfig.json │ ├── webpack.common.cjs │ └── webpack.config.cjs └── website │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── content │ ├── blog │ │ ├── a-new-delta.mdx │ │ ├── an-official-cdn-for-quill.mdx │ │ ├── announcing-quill-1-0.mdx │ │ ├── are-we-there-yet-to-1-0.mdx │ │ ├── quill-1-0-beta-release.mdx │ │ ├── quill-1-0-release-candidate-released.mdx │ │ ├── quill-v0-19-no-more-iframes.mdx │ │ ├── the-road-to-1-0.mdx │ │ ├── the-state-of-quill-and-2-0.mdx │ │ └── upgrading-to-rich-text-deltas.mdx │ └── docs │ │ ├── api.mdx │ │ ├── configuration.mdx │ │ ├── customization.mdx │ │ ├── customization │ │ ├── registries.mdx │ │ └── themes.mdx │ │ ├── delta.mdx │ │ ├── formats.mdx │ │ ├── guides │ │ ├── building-a-custom-module.mdx │ │ ├── cloning-medium-with-parchment.js │ │ ├── cloning-medium-with-parchment.mdx │ │ └── designing-the-delta-format.mdx │ │ ├── installation.mdx │ │ ├── modules.mdx │ │ ├── modules │ │ ├── clipboard.mdx │ │ ├── history.mdx │ │ ├── keyboard.mdx │ │ ├── syntax.mdx │ │ └── toolbar.mdx │ │ ├── quickstart.mdx │ │ ├── upgrading-to-2-0.mdx │ │ └── why-quill.mdx │ ├── env.js │ ├── next.config.mjs │ ├── package.json │ ├── public │ ├── CNAME │ ├── assets │ │ ├── fonts │ │ │ ├── sailec-bold.woff2 │ │ │ ├── sailec-light.woff2 │ │ │ ├── sailec.woff2 │ │ │ ├── sofia-pro-bold.woff2 │ │ │ └── sofia-pro.woff2 │ │ └── images │ │ │ ├── blog │ │ │ ├── bubble.png │ │ │ ├── color.png │ │ │ ├── formula.png │ │ │ ├── syntax.png │ │ │ ├── theme-1.png │ │ │ └── theme-2.png │ │ │ ├── brand-asset.png │ │ │ ├── favicon.ico │ │ │ ├── footer.png │ │ │ ├── logo.svg │ │ │ └── users.png │ └── robots.txt │ └── src │ ├── components │ ├── ActiveLink.jsx │ ├── ClickOutsideHandler.jsx │ ├── Editor.jsx │ ├── GitHub.jsx │ ├── GitHub.module.scss │ ├── Header.jsx │ ├── Header.module.scss │ ├── Heading.jsx │ ├── Hint.jsx │ ├── Hint.module.scss │ ├── Layout.jsx │ ├── Link.jsx │ ├── MDX.jsx │ ├── NoSSR.jsx │ ├── OpenSource.jsx │ ├── OpenSource.module.scss │ ├── PlaygroundLayout.jsx │ ├── PlaygroundLayout.module.scss │ ├── PostLayout.jsx │ ├── PostLayout.module.scss │ ├── SEO.jsx │ ├── Sandpack.jsx │ └── Sandpack.module.scss │ ├── data │ ├── api.tsx │ ├── docs.tsx │ └── playground.tsx │ ├── pages │ ├── 404.jsx │ ├── _app.jsx │ ├── _document.jsx │ ├── base.css │ ├── docs.jsx │ ├── docs │ │ └── [...id].jsx │ ├── index.jsx │ ├── playground.jsx │ ├── playground │ │ ├── [...id].jsx │ │ └── [...id].module.scss │ ├── standalone │ │ ├── bubble.mdx │ │ ├── full.mdx │ │ ├── snow.mdx │ │ └── stress.mdx │ ├── styles.scss │ └── variables.scss │ ├── playground │ ├── custom-formats │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── playground.json │ ├── form │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── playground.json │ ├── react │ │ ├── App.js │ │ ├── Editor.js │ │ ├── playground.json │ │ └── styles.css │ └── snow │ │ ├── index.html │ │ ├── index.js │ │ └── playground.json │ ├── svg │ ├── breadcrumb-arrow.svg │ ├── dropdown.svg │ ├── external-link.svg │ ├── features │ │ ├── cross-platform.svg │ │ ├── developers.svg │ │ ├── open-source.svg │ │ └── scale.svg │ ├── logo.svg │ ├── octocat.svg │ ├── users │ │ ├── airtable.svg │ │ ├── apollo.svg │ │ ├── calendly.svg │ │ ├── figma.svg │ │ ├── front.svg │ │ ├── gem.svg │ │ ├── grammarly.svg │ │ ├── linkedin.svg │ │ ├── microsoft.svg │ │ ├── miro.svg │ │ ├── mode.svg │ │ ├── salesforce.svg │ │ ├── slab.svg │ │ ├── slack.svg │ │ ├── typeform.svg │ │ ├── vox-media.svg │ │ └── zoom.svg │ └── x.svg │ └── utils │ ├── flattenData.js │ ├── replaceCDN.js │ └── slug.js ├── scripts ├── changelog.mjs ├── release.js └── utils │ └── configGit.mjs └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | scripts/ 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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@v3 44 | with: 45 | name: npm 46 | path: | 47 | packages/quill/dist/*.tgz 48 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | .* 4 | .github 5 | .vscode 6 | docs 7 | test 8 | tsconfig.json 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/.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/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /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/README.md: -------------------------------------------------------------------------------- 1 | # Quill 2 | 3 | This is the main package of Quill. 4 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/quill/src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/quill/src/assets/favicon.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/attachment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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/quill/src/assets/icons/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/clean.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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/dropdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/embed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/emoji.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-center.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/float-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/formula.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/hashtag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/header.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/horizontal-rule.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/indent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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/assets/icons/list-check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/list-ordered.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/mention.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/outdent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/redo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/size-decrease.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/size-increase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/size.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/spacing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/speech.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/strike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/subscript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/superscript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-border-all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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/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/assets/icons/table-border-outside.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /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/quill/src/assets/icons/table-delete-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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/assets/icons/table-insert-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-insert-columns.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-insert-rows.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-merge-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table-unmerge-cells.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/underline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/quill/src/assets/icons/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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/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/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/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/blots/container.ts: -------------------------------------------------------------------------------- 1 | import { ContainerBlot } from 'parchment'; 2 | 3 | class Container extends ContainerBlot {} 4 | 5 | export default Container; 6 | -------------------------------------------------------------------------------- /packages/quill/src/blots/inline.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBlot, InlineBlot, Scope } from 'parchment'; 2 | import type { BlotConstructor } from 'parchment'; 3 | import Break from './break.js'; 4 | import Text from './text.js'; 5 | 6 | class Inline extends InlineBlot { 7 | static allowedChildren: BlotConstructor[] = [Inline, Break, EmbedBlot, Text]; 8 | // Lower index means deeper in the DOM tree, since not found (-1) is for embeds 9 | static order = [ 10 | 'cursor', 11 | 'inline', // Must be lower 12 | 'link', // Chrome wants to be lower 13 | 'underline', 14 | 'strike', 15 | 'italic', 16 | 'bold', 17 | 'script', 18 | 'code', // Must be higher 19 | ]; 20 | 21 | static compare(self: string, other: string) { 22 | const selfIndex = Inline.order.indexOf(self); 23 | const otherIndex = Inline.order.indexOf(other); 24 | if (selfIndex >= 0 || otherIndex >= 0) { 25 | return selfIndex - otherIndex; 26 | } 27 | if (self === other) { 28 | return 0; 29 | } 30 | if (self < other) { 31 | return -1; 32 | } 33 | return 1; 34 | } 35 | 36 | formatAt(index: number, length: number, name: string, value: unknown) { 37 | if ( 38 | Inline.compare(this.statics.blotName, name) < 0 && 39 | this.scroll.query(name, Scope.BLOT) 40 | ) { 41 | const blot = this.isolate(index, length); 42 | if (value) { 43 | blot.wrap(name, value); 44 | } 45 | } else { 46 | super.formatAt(index, length, name, value); 47 | } 48 | } 49 | 50 | optimize(context: { [key: string]: any }) { 51 | super.optimize(context); 52 | if ( 53 | this.parent instanceof Inline && 54 | Inline.compare(this.statics.blotName, this.parent.statics.blotName) > 0 55 | ) { 56 | const parent = this.parent.isolate(this.offset(), this.length()); 57 | // @ts-expect-error TODO: make isolate generic 58 | this.moveChildren(parent); 59 | parent.wrap(this); 60 | } 61 | } 62 | } 63 | 64 | export default Inline; 65 | -------------------------------------------------------------------------------- /packages/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/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/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/core/instances.ts: -------------------------------------------------------------------------------- 1 | import type Quill from '../core.js'; 2 | 3 | export default new WeakMap(); 4 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/__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/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/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 | -------------------------------------------------------------------------------- /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/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 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/quill/test/unit/__helpers__/cleanup.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach } from 'vitest'; 2 | 3 | beforeEach(() => { 4 | document.body.innerHTML = ''; 5 | }); 6 | -------------------------------------------------------------------------------- /packages/quill/test/unit/__helpers__/expect.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest'; 2 | import { normalizeHTML } from './utils.js'; 3 | 4 | const sortAttributes = (element: HTMLElement) => { 5 | const attributes = Array.from(element.attributes); 6 | const sortedAttributes = attributes.sort((a, b) => 7 | a.name.localeCompare(b.name), 8 | ); 9 | 10 | while (element.attributes.length > 0) { 11 | element.removeAttribute(element.attributes[0].name); 12 | } 13 | 14 | for (const attr of sortedAttributes) { 15 | element.setAttribute(attr.name, attr.value); 16 | } 17 | 18 | element.childNodes.forEach((child) => { 19 | if (child instanceof HTMLElement) { 20 | sortAttributes(child); 21 | } 22 | }); 23 | }; 24 | 25 | expect.extend({ 26 | toEqualHTML(received, expected, options: { ignoreAttrs?: string[] } = {}) { 27 | const ignoreAttrs = options?.ignoreAttrs ?? []; 28 | const receivedDOM = document.createElement('div'); 29 | const expectedDOM = document.createElement('div'); 30 | receivedDOM.innerHTML = normalizeHTML( 31 | typeof received === 'string' ? received : received.innerHTML, 32 | ); 33 | expectedDOM.innerHTML = normalizeHTML(expected); 34 | 35 | const doms = [receivedDOM, expectedDOM]; 36 | 37 | doms.forEach((dom) => { 38 | Array.from(dom.querySelectorAll('.ql-ui')).forEach((node) => { 39 | node.remove(); 40 | }); 41 | 42 | ignoreAttrs.forEach((attr) => { 43 | Array.from(dom.querySelectorAll(`[${attr}]`)).forEach((node) => { 44 | node.removeAttribute(attr); 45 | }); 46 | }); 47 | 48 | sortAttributes(dom); 49 | }); 50 | 51 | if (this.equals(receivedDOM.innerHTML, expectedDOM.innerHTML)) { 52 | return { pass: true, message: () => '' }; 53 | } 54 | return { 55 | pass: false, 56 | message: () => 57 | `HTMLs don't match.\n${this.utils.diff( 58 | this.utils.stringify(receivedDOM), 59 | this.utils.stringify(expectedDOM), 60 | )}`, 61 | }; 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/test/unit/formats/align.spec.ts: -------------------------------------------------------------------------------- 1 | import Delta from 'quill-delta'; 2 | import Editor from '../../../src/core/editor.js'; 3 | import { describe, test, expect } from 'vitest'; 4 | import { 5 | createRegistry, 6 | createScroll as baseCreateScroll, 7 | } from '../__helpers__/factory.js'; 8 | import { AlignClass } from '../../../src/formats/align.js'; 9 | 10 | const createScroll = (html: string) => 11 | baseCreateScroll(html, createRegistry([AlignClass])); 12 | 13 | describe('Align', () => { 14 | test('add', () => { 15 | const editor = new Editor(createScroll('

0123

')); 16 | editor.formatText(4, 1, { align: 'center' }); 17 | expect(editor.getDelta()).toEqual( 18 | new Delta().insert('0123').insert('\n', { align: 'center' }), 19 | ); 20 | expect(editor.scroll.domNode).toEqualHTML( 21 | '

0123

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

0123

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

0123

'); 32 | }); 33 | 34 | test('whitelist', () => { 35 | const editor = new Editor( 36 | createScroll('

0123

'), 37 | ); 38 | editor.formatText(4, 1, { align: 'middle' }); 39 | expect(editor.getDelta()).toEqual( 40 | new Delta().insert('0123').insert('\n', { align: 'center' }), 41 | ); 42 | expect(editor.scroll.domNode).toEqualHTML( 43 | '

0123

', 44 | ); 45 | }); 46 | 47 | test('invalid scope', () => { 48 | const editor = new Editor(createScroll('

0123

')); 49 | editor.formatText(1, 2, { align: 'center' }); 50 | expect(editor.getDelta()).toEqual(new Delta().insert('0123\n')); 51 | expect(editor.scroll.domNode).toEqualHTML('

0123

'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/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/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 | -------------------------------------------------------------------------------- /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/test/unit/formats/indent.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 List, { ListContainer } from '../../../src/formats/list.js'; 8 | import IndentClass from '../../../src/formats/indent.js'; 9 | import { describe, expect, test } from 'vitest'; 10 | 11 | const createScroll = (html: string) => 12 | baseCreateScroll(html, createRegistry([ListContainer, List, IndentClass])); 13 | 14 | describe('Indent', () => { 15 | test('+1', () => { 16 | const editor = new Editor( 17 | createScroll('
  1. 0123
'), 18 | ); 19 | editor.formatText(4, 1, { indent: '+1' }); 20 | expect(editor.getDelta()).toEqual( 21 | new Delta().insert('0123').insert('\n', { list: 'bullet', indent: 1 }), 22 | ); 23 | expect(editor.scroll.domNode).toEqualHTML(` 24 |
    25 |
  1. 0123
  2. 26 |
27 | `); 28 | }); 29 | 30 | test('-1', () => { 31 | const editor = new Editor( 32 | createScroll( 33 | '
  1. 0123
', 34 | ), 35 | ); 36 | editor.formatText(4, 1, { indent: '-1' }); 37 | expect(editor.getDelta()).toEqual( 38 | new Delta().insert('0123').insert('\n', { list: 'bullet' }), 39 | ); 40 | expect(editor.scroll.domNode).toEqualHTML(` 41 |
    42 |
  1. 0123
  2. 43 |
44 | `); 45 | }); 46 | 47 | test('1', () => { 48 | const editor = new Editor(createScroll('

abc

')); 49 | editor.formatText(3, 1, { indent: 1 }); 50 | expect(editor.getDelta()).toEqual( 51 | new Delta().insert('abc').insert('\n', { indent: 1 }), 52 | ); 53 | expect(editor.scroll.domNode).toEqualHTML(`

abc

`); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/quill/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 | -------------------------------------------------------------------------------- /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/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/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/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 | slowHijackESM: false, 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /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/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/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/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next" 3 | } 4 | -------------------------------------------------------------------------------- /packages/website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .next 4 | out 5 | certificates -------------------------------------------------------------------------------- /packages/website/README.md: -------------------------------------------------------------------------------- 1 | # Quill Official Website 2 | 3 | [https://quilljs.com](https://quilljs.com) 4 | -------------------------------------------------------------------------------- /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/website/content/blog/are-we-there-yet-to-1-0.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Are We There Yet (to 1.0)? 3 | date: 2016-03-14 4 | --- 5 | 6 | When Quill laid out its [1.0 roadmap](/blog/the-road-to-1-0/), core to its journey was the development of a new document model called Parchment. Today a beta release of Parchment is being made available on [GitHub](https://github.com/quilljs/parchment) and [NPM](https://www.npmjs.com/package/parchment). 7 | 8 | What this means is its design and API is reasonably stable and the adventurous can now take an early look. The latest Quill source is already using Parchment to implement its formatting and content capabilities, and its [integration](https://github.com/quilljs/quill/tree/develop/formats) would be a helpful example of Parchment in action. Of course, this is in addition to Parchment’s own [documentation](https://github.com/quilljs/parchment/blob/master/README.md). 9 | 10 | {/* more */} 11 | 12 | ### New Formats 13 | 14 | Parchment enables Quill to scalably support many formats and many are being added in 1.0. The list includes headers, blockquotes, code, superscript, subscript, text direction, nested lists, and video embeds. Syntax highlighted code and formulas will also be available through externally supported modules. In addition, formats that previously relied on style attributes are reimplemented to optionally use classes instead. By default, fonts, sizes, and text alignment will use classes, while foreground and background colors will still use style attributes, since there are so many possible color values. 15 | 16 | ### Quill 1.0 Beta 17 | 18 | With Parchment out of the way, Quill is nearing its own 1.0 release. This will also be prefaced with a beta period, optimistically planned for early April. You can also set up the [local development](https://github.com/quilljs/quill/blob/develop/.github/CONTRIBUTING.md#local-development) environment to follow along with the latest changes and progress. 19 | 20 | If you are currently using Quill at your company or project, we'd love to hear about your use case [hello@quilljs.com](mailto:hello@quilljs.com)! 21 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /packages/website/content/docs/modules.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Modules 3 | --- 4 | 5 | Modules allow Quill's behavior and functionality to be customized. Several officially supported modules are available to pick and choose from, some with additional configuration options and APIs. Refer to their respective documentation pages for more details. 6 | 7 | To enable a module, simply include it in Quill's configuration. 8 | 9 | ```javascript 10 | const quill = new Quill('#editor', { 11 | modules: { 12 | history: { // Enable with custom configurations 13 | delay: 2500, 14 | userOnly: true 15 | }, 16 | syntax: true // Enable with default configuration 17 | } 18 | }); 19 | ``` 20 | 21 | The [Clipboard](/docs/modules/clipboard/), [Keyboard](/docs/modules/keyboard/), and [History](/docs/modules/history/) modules are required by Quill and do not need to be included explictly, but may be configured like any other module. 22 | 23 | 24 | ## Extending 25 | 26 | Modules may also be extended and re-registered, replacing the original module. Even required modules may be re-registered and replaced. 27 | 28 | ```javascript 29 | const Clipboard = Quill.import('modules/clipboard'); 30 | const Delta = Quill.import('delta'); 31 | 32 | class PlainClipboard extends Clipboard { 33 | convert(html = null) { 34 | if (typeof html === 'string') { 35 | this.container.innerHTML = html; 36 | } 37 | let text = this.container.innerText; 38 | this.container.innerHTML = ''; 39 | return new Delta().insert(text); 40 | } 41 | } 42 | 43 | Quill.register('modules/clipboard', PlainClipboard, true); 44 | 45 | // Will be created with instance of PlainClipboard 46 | const quill = new Quill('#editor'); 47 | ``` 48 | 49 | 50 | This particular example was selected to show what is possible. It is often easier to just use an API or configuration the existing module exposes. In this example, the existing Clipboard's [addMatcher](/docs/modules/clipboard/#addmatcher) API is suitable for most paste customization scenarios. 51 | 52 | -------------------------------------------------------------------------------- /packages/website/content/docs/modules/syntax.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Syntax Highlighter Module 3 | --- 4 | 5 | The Syntax Module enhances the Code Block format by automatically detecting and applying syntax highlighting. The excellent [highlight.js](https://highlightjs.org/) library is used as a dependency to parse and tokenize code blocks. 6 | 7 | In general, you may [configure](https://highlightjs.readthedocs.io/en/latest/api.html#configure-options) highlight.js as needed. However, Quill expects and requires the `useBR` option to be `false` if you are using highlight.js < v11. 8 | 9 | Quill supports highlight.js v9.12.0 and later. 10 | 11 | ### Usage 12 | 13 | 18 | 19 | 20 | 21 | 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/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/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/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/public/CNAME: -------------------------------------------------------------------------------- 1 | quilljs.com -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sailec-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/fonts/sailec-bold.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sailec-light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/fonts/sailec-light.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sailec.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/fonts/sailec.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sofia-pro-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/fonts/sofia-pro-bold.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/fonts/sofia-pro.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/fonts/sofia-pro.woff2 -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/blog/bubble.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/blog/color.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/formula.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/blog/formula.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/blog/syntax.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/theme-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/blog/theme-1.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/blog/theme-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/blog/theme-2.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/brand-asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/brand-asset.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/favicon.ico -------------------------------------------------------------------------------- /packages/website/public/assets/images/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/footer.png -------------------------------------------------------------------------------- /packages/website/public/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/public/assets/images/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slab/quill/ebe16ca24724ac4f52505628ac2c4934f0a98b85/packages/website/public/assets/images/users.png -------------------------------------------------------------------------------- /packages/website/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/components/OpenSource.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .about { 3 | line-height: 1.2; 4 | } 5 | } 6 | 7 | .github { 8 | margin-top: 20px; 9 | } 10 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | 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/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/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 | /> -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/snow/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /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/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/svg/breadcrumb-arrow.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /packages/website/src/svg/dropdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/src/svg/external-link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/src/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/website/src/svg/octocat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/airtable.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/apollo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/calendly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/figma.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/front.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 13 | 15 | 16 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/gem.svg: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/grammarly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/miro.svg: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/mode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /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/svg/users/slack.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/typeform.svg: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/vox-media.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/website/src/svg/users/zoom.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /packages/website/src/svg/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Artboard 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------