Showing preview only (1,041K chars total). Download the full file or copy to clipboard to get everything.
Repository: slab/quill
Branch: main
Commit: 539cbffd0a13
Files: 260
Total size: 968.9 KB
Directory structure:
gitextract_t0yo5mmz/
├── .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.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/
│ │ │ │ │ ├── toolbar.styl
│ │ │ │ │ └── tooltip.styl
│ │ │ │ ├── bubble.styl
│ │ │ │ ├── core.styl
│ │ │ │ ├── snow/
│ │ │ │ │ ├── toolbar.styl
│ │ │ │ │ └── tooltip.styl
│ │ │ │ └── snow.styl
│ │ │ ├── blots/
│ │ │ │ ├── block.ts
│ │ │ │ ├── break.ts
│ │ │ │ ├── container.ts
│ │ │ │ ├── cursor.ts
│ │ │ │ ├── embed.ts
│ │ │ │ ├── inline.ts
│ │ │ │ ├── scroll.ts
│ │ │ │ └── text.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
│ │ │ ├── core.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/
│ │ │ ├── registries.mdx
│ │ │ └── themes.mdx
│ │ ├── customization.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/
│ │ │ ├── clipboard.mdx
│ │ │ ├── history.mdx
│ │ │ ├── keyboard.mdx
│ │ │ ├── syntax.mdx
│ │ │ └── toolbar.mdx
│ │ ├── modules.mdx
│ │ ├── quickstart.mdx
│ │ ├── upgrading-to-2-0.mdx
│ │ └── why-quill.mdx
│ ├── env.js
│ ├── next.config.mjs
│ ├── package.json
│ ├── public/
│ │ ├── CNAME
│ │ └── 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/
│ │ │ └── [...id].jsx
│ │ ├── docs.jsx
│ │ ├── index.jsx
│ │ ├── playground/
│ │ │ ├── [...id].jsx
│ │ │ └── [...id].module.scss
│ │ ├── playground.jsx
│ │ ├── 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
│ └── utils/
│ ├── flattenData.js
│ ├── replaceCDN.js
│ └── slug.js
├── scripts/
│ ├── changelog.mjs
│ ├── release.js
│ └── utils/
│ └── configGit.mjs
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
dist/
scripts/
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at conduct@quilljs.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
The best way to contribute is to help others in the Quill community. This includes:
- Reporting new [bugs](https://github.com/slab/quill/labels/bug) or adding details to existing ones
- Reproducing [unconfirmed bugs](https://github.com/slab/quill/labels/needs%20reproduction)
- Quick typo fix or documentation improvement [Pull Requests](#pull-requests)
- Participating in [discussions](https://github.com/slab/quill/discussions)
After becoming familiar with Quill and the codebase, likely through using Quill yourself and making some of the above contributions, you may choose to take on a bigger commitment by:
- Helping fix [bugs](https://github.com/slab/quill/labels/bug)
- Implementing new [features](https://github.com/slab/quill/labels/feature)
- Publishing guides, tutorials, and examples
- Supporting Quill in other ecosystems (Angular, React, etc)
Note that if you are going to be making significant contributions, you should first open
a [discussion](https://github.com/slab/quill/discussions) to ensure your work aligns with the project's goals and direction.
## Questions
If you have a question, it is best to ask on [Discussions](https://github.com/slab/quill/discussions) under the Q&A category.
## Bug Reports
Search through [Github Issues](https://github.com/slab/quill/issues) to see if the bug has already been reported. If so, please comment with any additional information.
New bug reports must include:
1. Detailed description of faulty behavior
2. Steps for reproduction or failing test case
3. Expected and actual behaviors
4. Platforms (OS **and** browser combination) affected
5. Version of Quill
Lacking reports it may be autoclosed with a link to these instructions.
## Feature Requests
Search through [Github Issues](https://github.com/slab/quill/labels/feature) to see if someone has already suggested the feature. If so, please provide support with a [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) and add your own use case.
To open a new feature request, please include:
1. A detailed description of the feature
2. Why this feature belongs in Quill core, instead of your own application logic
3. Background of where and how you are using Quill
4. The use case that would be enabled or improved for your product, if the feature was implemented
Features are prioritized based on real world users and use cases, not theoretically useful additions for other unknown users. Lacking feature requests may be autoclosed with a link to this section.
The more complete and compelling the request, the more likely it will ultimately be implemented. Garnering community support will help as well!
## Pull Requests
Please check to make sure your plans fall within Quill's scope. This often means opening up a [discussion](https://github.com/slab/quill/labels/discussion).
Non-code Pull Requests such as typo fixes or documentation improvements are highly encouraged and are often accepted immediately.
Pull Requests modifying public facing interfaces or APIs, including backwards compatible additions, will undergo the most scrutiny, and will almost certainly require a proper discussion of the motivation and merits beforehand. Simply increasing code complexity is a cost not to be taken lightly.
Pull requests must:
1. Be forked off the [main](https://github.com/slab/quill/tree/main) branch.
2. Pass the linter and conform to existing coding styles.
3. Commits are [squashed](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Squashing-Commits) to minimally coherent units of changes.
4. Are accompanied by tests covering the new feature or demonstrating the bug for fixes.
5. Serve a single atomic purpose (add one feature or fix one bug).
6. Introduce only changes that further the PR's singular purpose (ex. do not tweak an unrelated config along with adding your feature).
7. Not break any existing unit or end to end tests.
**Important:** By issuing a Pull Request you agree to allow the project owners to license your work under the terms of the [License](https://github.com/slab/quill/blob/master/LICENSE).
================================================
FILE: .github/DEVELOPMENT.md
================================================
# Development
This repo is a monorepo powered by npm's official [workspace feature](https://docs.npmjs.com/cli/v10/using-npm/workspaces). It contains the following packages:
### quill
This is the Quill library. It's written in [TypeScript](https://www.typescriptlang.org/), and use [Webpack](https://webpack.js.org/) as the bundler.
It uses [Vitest](https://vitest.dev) for unit testing, and [Playwright](https://playwright.dev/) for E2E testing.
### website
It's Quill's website (hosted at [quilljs.com](https://quilljs.com/)). It's built with [Next.js](https://nextjs.org/).
## Setup
To prepare your local environment for development, ensure you have Node.js installed. The repo uses npm, and doesn't support Yarn and pnpm.
Install the necessary dependencies with the command below:
```shell
npm install
```
Start the development environment using:
```shell
npm start
```
This command starts two services:
- Quill's webpack dev server
- Website's Next.js dev server
These servers dynamically build and serve the latest copy of the source code.
Access the running website at [localhost:9000](http://localhost:9000/). By default, the website will use your local Quill build, that includes all the examples in the website. This convenient setup allows for seamless development and ensures changes to Quill do not disrupt the website's content.
If you need to modify only the website's code, start the website with `npm start -w website``. This makes the website use the latest CDN version.
## Testing
To run the unit tests in watch mode, run:
npm run test:unit -w quill
To execute the E2E tests, run:
npm run test:e2e -w quill
## Workflow
A standard development workflow involves:
1. `npm start` - to run development services
2. [localhost:9000/standalone/snow](http://localhost:9000/standalone/snow) - to interactively develop and test an isolated example
3. `npm run test:unit -w quill` - to run unit tests
4. If everything is working, run the E2E tests
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
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.
**Steps for Reproduction**
1. Visit [quilljs.com, jsfiddle.net, codepen.io]
2. Step Two
3. Step Three
**Expected behavior**:
**Actual behavior**:
**Platforms**:
Include browser, operating system and respective versions
**Version**:
Run `Quill.version` to find out
================================================
FILE: .github/release.yml
================================================
changelog:
exclude:
authors:
- quill-bot
categories:
- title: Bug Fixes 🛠
labels:
- change:bugfix
- title: New Features 🎉
labels:
- change:feature
- title: Documentation 📚
labels:
- change:documentation
- title: Other Changes
labels:
- "*"
================================================
FILE: .github/workflows/_test.yml
================================================
name: Tests
on:
workflow_call:
jobs:
e2e:
name: E2E Tests
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
working-directory: packages/quill
- name: Run Playwright tests
uses: coactions/setup-xvfb@v1
with:
run: npm run test:e2e -- --headed
working-directory: packages/quill
fuzz:
name: Fuzz Tests
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
- run: npm run test:fuzz -w quill
unit:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
browser: [chromium, webkit, firefox]
steps:
- name: Git checkout
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run lint
- run: npm run test:unit -w quill || npm run test:unit -w quill || npm run test:unit -w quill
env:
BROWSER: ${{ matrix.browser }}
================================================
FILE: .github/workflows/changelog.yml
================================================
name: Generate Changelog
on:
release:
types: [published, created]
workflow_dispatch: {}
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: node ./scripts/changelog.mjs
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/label.yml
================================================
name: Pull Requests
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: mheap/github-action-required-labels@v5
with:
mode: exactly
count: 1
labels: |
change:bugfix
change:feature
change:documentation
change:chore
change:refactor
add_comment: false
================================================
FILE: .github/workflows/main.yml
================================================
name: Main
on:
push:
branches: [main]
concurrency:
group: main-build
cancel-in-progress: true
jobs:
test:
uses: ./.github/workflows/_test.yml
================================================
FILE: .github/workflows/pull-request.yml
================================================
name: Pull Requests
on:
pull_request:
branches: [main]
jobs:
test:
uses: ./.github/workflows/_test.yml
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'npm version. Examples: "2.0.0", "2.0.0-beta.0". To deploy an experimental version, type "experimental".'
default: "experimental"
required: true
dry-run:
description: "Only create a tarball, do not publish to npm or create a release on GitHub."
type: boolean
default: true
required: true
permissions:
contents: write
jobs:
test:
uses: ./.github/workflows/_test.yml
release:
runs-on: ubuntu-latest
needs: test
steps:
- name: Git checkout
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: ./scripts/release.js --version ${{ github.event.inputs.version }} ${{ github.event.inputs.dry-run == 'true' && '--dry-run' || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Archive npm package tarball
uses: actions/upload-artifact@v4
with:
name: npm
path: |
packages/quill/dist/*.tgz
================================================
FILE: .gitignore
================================================
.*
!.eslintrc.json
!.eslintignore
!.npmignore
!.gitignore
!.github
node_modules
test-results/
playwright-report/
================================================
FILE: .npmignore
================================================
*.ts
!*.d.ts
.*
.github
.vscode
docs
test
tsconfig.json
================================================
FILE: CHANGELOG.md
================================================
# v2.0.2 (2024-05-13)
<!-- Release notes generated using configuration in .github/release.yml at v2.0.2 -->
## What's Changed
### Bug Fixes 🛠
- Fix typing errors for Quill.register by [@hzgotb](https://github.com/hzgotb) in https://github.com/slab/quill/pull/4127
- Fix event source when deleting a link with shortcuts by [@luin](https://github.com/luin) in https://github.com/slab/quill/pull/4200
- Avoid side effects for Enter/Backspace when composing in Safari by [@luin](https://github.com/luin) in https://github.com/slab/quill/pull/4201
- Ignore pasting images when image format is disallowed by [@luin](https://github.com/luin) in https://github.com/slab/quill/pull/4202
[All changes](https://github.com/slab/quill/releases/tag/v2.0.2)
# v2.0.1 (2024-05-01)
## Bug Fixes
- Prevent overriding of theme's default toolbar settings mistakenly [#4120](https://github.com/slab/quill/pull/4120)
- Improve typings for methods that return a Delta [#4136](https://github.com/slab/quill/pull/4136)
- Fix toolbar icons for h3-h6 [#4131](https://github.com/slab/quill/pull/4131)
[All changes](https://github.com/slab/quill/releases/tag/v2.0.1)
# v2.0.0 (2024-04-17)
We are thrilled to announce the release of Quill 2.0! Please check out the [announcement post](https://slab.com/blog/announcing-quill-2-0/).
## Major Improvements
- Quill is now a valid ESM package for better ecosystem (e.g. bundlers) and tree-shaking support
- Nested Quill support [#3590](https://github.com/slab/quill/pull/3590)
- Improved IME and spell corrector support [#3807](https://github.com/slab/quill/pull/3807)
- Semantic cleanups for TEXT_CHANGE event [#3778](https://github.com/slab/quill/pull/3778)
- **History**: Record selection in history module [#3823](https://github.com/slab/quill/pull/3823)
- Auto detect scrolling container [#3840](https://github.com/slab/quill/pull/3840)
- **Clipboard**: Improve support for pasting from Google Docs and Microsoft Word
## Performance Improvements
Quill 2.0 includes many performance optimizations, the most important of which is the improved rendering speed for large content.
- Improve inserting performance [#3815](https://github.com/slab/quill/pull/3815)
- Avoid fetching selections when possible [#3538](https://github.com/slab/quill/pull/3538)
- No need to setContents when container is empty [#3539](https://github.com/slab/quill/pull/3539)
## Code Modernization
- Migrated to TypeScript
- Provided official TypeScript declarations
- Migrated to Vitest for unit testing
- Migrated to Playwright for E2E testing
- Migrated website to Gatsby
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0)
# v2.0.0-rc.5 (2024-04-04)
- **Clipboard** Add support for Quill v1 list attributes
- Fix overload declarations for `quill.formatText()` and other methods
- Expose Bounds type for getBounds()
- Expose Range type
- Allow ref for insertBefore to be null
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.5)
# v2.0.0-rc.4 (2024-03-24)
- Include source maps for Parchment
- **Clipboard** Support pasting links copied from iOS share sheets
- Fix config parsing where undefined values were kept
- Expose types for Quill options
- Remove empty .css.js files generated by bundlers
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.4)
# v2.0.0-rc.3 (2024-03-16)
- Fix `Quill#getSemanticHTML()` for list items
- Remove unnecessary Firefox workaround
- **Clipboard** Fix redundant newlines when pasting from external sources
- Add `formats` option for specifying allowed formats
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.3)
# v2.0.0-rc.2 (2024-02-15)
- Fix toolbar button state not updated in some cases
- Narrower `BubbleTheme.tooltip` type
- Fix `Selection#getBounds()` when starting range at end of text node
- Improve compatibility with esbuild
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.2)
# v2.0.0-rc.1 (2024-02-12)
- Remove unnecessary lodash usages.
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.1)
# v2.0.0-rc.0 (2024-02-03)
- **Clipboard** Convert newlines between inline elements to a space.
- **Clipboard** Avoid generating unsupported formats on paste.
- **Clipboard** Improve support for pasting from Google Docs and Microsoft Word.
- **Clipboard** Ignore whitespace between pasted empty paragraphs.
- **Syntax** Support highlight.js v10 and v11.
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-rc.0)
# v2.0.0-beta.2 (2024-01-30)
- Fix IME not working correctly in Safari.
- **Clipboard** Support paste as plain text.
- Fix `Quill.getText()` not respecting `length` parameter.
- **History** Fix redo shortcut not working on Linux and Windows.
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-beta.2)
# v2.0.0-beta.1 (2024-01-21)
- Fix syntax label from "Javascript" to "JavaScript".
- Fix typing errors for emitter.
- Inline SVG images for easier bundler setup.
- Improve typing for Registry.
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-beta.1)
# v2.0.0-beta.0 (2023-12-08)
In the upcoming 2.0 release, Quill has been significantly modernized. Leveraging the latest browser-supported APIs, Quill now delivers a more efficient and reliable editing experience.
## Major Improvements
- Nested Quill support [#3590](https://github.com/slab/quill/pull/3590)
- Improved IME and spell corrector support [#3807](https://github.com/slab/quill/pull/3807)
- Semantic cleanups for TEXT_CHANGE event [#3778](https://github.com/slab/quill/pull/3778)
- **History**: Record selection in history module [#3823](https://github.com/slab/quill/pull/3823)
- Auto detect scrolling container [#3840](https://github.com/slab/quill/pull/3840)
## Performance Improvements
Quill 2.0 includes many performance optimizations, the most important of which is the improved rendering speed for large content.
- Improve inserting performance [#3815](https://github.com/slab/quill/pull/3815)
- Avoid fetching selections when possible [#3538](https://github.com/slab/quill/pull/3538)
- No need to setContents when container is empty [#3539](https://github.com/slab/quill/pull/3539)
## Code Modernization
- Migrated to TypeScript
- Provided official TypeScript declarations
- Migrated to Vitest for unit testing
- Migrated to Playwright for E2E testing
- Migrated website to Gatsby
[All changes](https://github.com/slab/quill/releases/tag/v2.0.0-beta.0)
# v1.3.7 (2019-09-09)
Security related bug fixes.
- https://app.snyk.io/vuln/npm:extend:20180424
- https://www.npmjs.com/advisories/1039
Thank you [@danfuzz](https://github.com/danfuzz), [@danielw93](https://github.com/danielw93), [@jonathanlloyd](https://github.com/jonathanlloyd), and [@k-sai-kiranmayee](https://github.com/k-sai-kiranmayee) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.3.7)
# v1.3.6 (2018-03-12)
- Make picker accessible [#1999](https://github.com/slab/quill/pull/1999)
- Fix Japanese composition in Chrome 65 [#2009](https://github.com/slab/quill/issues/2009)
Thanks to [@berylw](https://github.com/berylw) and [@erinsinger93](https://github.com/erinsinger93) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.3.6)
# v1.3.5 (2018-01-22)
- Fix indent preservation of a checked checklist item [#1818](https://github.com/slab/quill/issues/1818)
- added as a shortcut to trigger bullet list formatting [#1819](https://github.com/slab/quill/pull/1819)
- Fix pasting text-align styles [#1873](https://github.com/slab/quill/issues/1873)
- Fix cursor position after dangerouslyPasteHTML [#1886](https://github.com/slab/quill/issues/1886)
- Fix value of history stack in text-change handler [#1906](https://github.com/slab/quill/pull/1906)
- Workaround for Webkit locking up when navigating around images using hotkeys [#1910](https://github.com/slab/quill/issues/1910)
Thank you [@araruna](https://github.com/araruna), [@bryanrsmith](https://github.com/bryanrsmith), [@haugstrup](https://github.com/haugstrup), [@icylace](https://github.com/icylace), [@leimig](https://github.com/leimig), [@LFDM](https://github.com/LFDM), [@nikparo](https://github.com/nikparo), [@rafpaf](https://github.com/rafpaf) and [@vk2sky](https://github.com/vk2sky) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.3.5)
# v1.3.4 (2017-11-06)
- Loosen dependency specification [#1748](https://github.com/slab/quill/issues/1748)
- Loosen list autofill constraint [#1749](https://github.com/slab/quill/issues/1749)
Thanks to [@danfuzz](https://github.com/danfuzz) and [@SoftVision-CarmenFat](https://github.com/SoftVision-CarmenFat) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.3.4)
# v1.3.3 (2017-10-09)
- Fix `getFormat` with no parameters while editor is not focused [#1548](https://github.com/slab/quill/issues/1548)
- Remove automatic highlighting across embeds [#1691](https://github.com/slab/quill/issues/1691)
- Support checking checklist on mobile [#1693](https://github.com/slab/quill/pull/1711)
- Fix list creation keyboard shortcuts [#1723](https://github.com/slab/quill/issues/1723)
- Show KaTex rendering errors [#1738](https://github.com/slab/quill/pull/1738)
Thank you [@altschuler](https://github.com/altschuler), [@arrocke](https://github.com/arrocke), [@guillaumepotier](https://github.com/guillaumepotier), [@sferoze](https://github.com/sferoze) and [@volser](https://github.com/volser) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.3.3)
# v1.3.2 (2017-09-04)
- Pasting into code block should always paste as code [#1624](https://github.com/slab/quill/issues/1624)
- Fix removing embed selection when arrow keys change selection [#1633](https://github.com/slab/quill/issues/1633)
- Fix selection restoration after image insertion [#1649](https://github.com/slab/quill/issues/1649)
- Fix selection-change firing after dragging off screen [#1654](https://github.com/slab/quill/issues/1654)
- Fix placeholder text spacing [#1677](https://github.com/slab/quill/issues/1677)
Thanks to [@abramz](https://github.com/abramz), [@amitm02](https://github.com/amitm02), [@eamodio](https://github.com/eamodio), [@HWliao](https://github.com/HWliao), [@mmitis](https://github.com/mmitis), [@nelsonpecora](https://github.com/nelsonpecora), [@nipunjain87](https://github.com/nipunjain87), and [@ValueBerry](https://github.com/ValueBerry) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.3.2)
# v1.3.1 (2017-08-07)
- Fix placeholder when emptying text [#1594](https://github.com/slab/quill/issues/1594)
- Fix inserting newline after header [#1616](https://github.com/slab/quill/issues/1616)
Thank you [@Natim](https://github.com/Natim) and [@stephenLYao](https://github.com/stephenLYao) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.3.1)
# v1.3.0 (2017-07-17)
Add `matchVisual` [configuration](https://quilljs.com/docs/modules/clipboard/#configuration) to Clipboard.
- Use DOM API to determine selected `<select>` option [#1576](https://github.com/slab/quill/pull/1576)
- Add `:focus` styles to toolbar [#1540](https://github.com/slab/quill/issues/1540)
- Allow users to undo automatic keyboard completions [#1538](https://github.com/slab/quill/issues/1538)
- Use github-pages gem to make development environment consistent [#1536](https://github.com/slab/quill/issues/1536) [#1544](https://github.com/slab/quill/pull/1544)
- Fix composing Chinese with preformatting [#1514](https://github.com/slab/quill/issues/1514)
- Fix example clipboard module in docs [#1502](https://github.com/slab/quill/issues/1502)
- Fix list layout in RTL mode [#1498](https://github.com/slab/quill/issues/1498)
- Clarify documentation for scrollingContainer [#1496](https://github.com/slab/quill/issues/1496)
- Add `tel` to default link whitelist [#1436](https://github.com/slab/quill/pull/1436)
- Fix cursor interaction with custom contenteditable=false embeds [#1172](https://github.com/slab/quill/issues/1172) [#1181](https://github.com/slab/quill/issues/1181)
- Fix rendered cursor in Chrome when interacting with scrollbar [#1114](https://github.com/slab/quill/issues/1114)
Thanks to [@alexkrolick](https://github.com/alexkrolick), [@amitm02](https://github.com/amitm02), [@Christilut](https://github.com/Christilut), [@danielschwartz](https://github.com/danielschwartz), [@emanuelbsilva](https://github.com/emanuelbsilva), [@ersommer](https://github.com/ersommer), [@fiurrr](https://github.com/fiurrr), [@jackmu95](https://github.com/jackmu95), [@jmzhang](https://github.com/jmzhang), [@mdpye](https://github.com/mdpye), [@ralrom](https://github.com/ralrom), [@sferoze](https://github.com/sferoze), [@simon-at-fugu](https://github.com/simon-at-fugu), and [@yogadzx](https://github.com/yogadzx) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.3.0)
# v1.2.6 (2017-06-05)
- Disable Grammarly by default [#574](https://github.com/slab/quill/issues/574)
- Fix RTL list spacing [#1485](https://github.com/slab/quill/pull/1485)
- Add support for mobile Youtube links [#1489](https://github.com/slab/quill/pull/1489)
Thank you [@amitm02](https://github.com/amitm02), [@benbro](https://github.com/benbro)
[@nickbaum](https://github.com/nickbaum), [@stalniy](https://github.com/stalniy) and [@ygrishajev](https://github.com/ygrishajev) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.2.6)
# v1.2.5 (2017-05-29)
- Fix cursor shifting to be exclusive of user cursor [#1367](https://github.com/slab/quill/issues/1367)
- Fix iOS hover state on toolbar [#1388](https://github.com/slab/quill/issues/1388)
- Fix `getText()` for Bangla [#1427](https://github.com/slab/quill/issues/1427)
- Fix Korean character composition in Safari [#1437](https://github.com/slab/quill/issues/1437)
- Fix pasting HTML handling special class names [#1445](https://github.com/slab/quill/issues/1445)
- Fix paste or initializing with font-weight [#1456](https://github.com/slab/quill/issues/1456)
- Fix updating active picker logic [#1468](https://github.com/slab/quill/issues/1468)
Thanks to [@aliciawood](https://github.com/aliciawood), [@benbro](https://github.com/benbro), [@denis-aes](https://github.com/denis-aes), [@despreju](https://github.com/despreju), [@GlenKPeterson](https://github.com/GlenKPeterson), [@haugstrup](https://github.com/haugstrup), [@jziggas](https://github.com/jziggas), [@RobAley](https://github.com/RobAley), [@sheley1998](https://github.com/sheley1998), [@silverprize](https://github.com/silverprize), and [@yairy](https://github.com/yairy) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.2.5)
# v1.2.4 (2017-04-18)
- Fix pasting nested list [#906](https://github.com/slab/quill/issues/906)
- Fix delete key interaction at end of list [#1277](https://github.com/slab/quill/issues/1277)
- Fix pasting whitespace prefix [#1244](https://github.com/slab/quill/issues/1244)
- Fix file dialog open speed [#1265](https://github.com/slab/quill/issues/1265)
- Fix backspace with at beginning of list interaction with meta keys [#1307](https://github.com/slab/quill/issues/1307)
- Fix pasting nested styles [#1333](https://github.com/slab/quill/issues/1333)
- Fix backspacing into an empty line should keep own formats [#1339](https://github.com/slab/quill/issues/1339)
- Fix IE11 autolinking interaction [#1390](https://github.com/slab/quill/issues/1390)
- Fix persistent focus interaction with tabbing away [#1404](https://github.com/slab/quill/issues/1404)
Thanks to [@bigggge](https://github.com/bigggge), [@CoenWarmer](https://github.com/CoenWarmer), [@cutteroid](https://github.com/cutteroid), [@jay-cox](https://github.com/jay-cox), [@kiewic](https://github.com/kiewic), [@kloots](https://github.com/kloots), [@MichaelTontchev](https://github.com/MichaelTontchev), [@montlebalm](https://github.com/montlebalm), [@RichardNeill](https://github.com/RichardNeill), and [@vasconita](https://github.com/vasconita) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.2.4)
# v1.2.3 (2017-03-29)
- Fix scrolling when appending new lines [#1276](https://github.com/slab/quill/issues/1276) [#1361](https://github.com/slab/quill/issues/1361)
- Fix binding to explicit shortcut key [#1365](https://github.com/slab/quill/issues/1365)
- Merge clone update [#1359](https://github.com/slab/quill/pull/1359)
Thank you [@artaommahe](https://github.com/artaommahe), [@c-w](https://github.com/c-w), [@EladBet](https://github.com/EladBet), [@emenoh](https://github.com/emenoh), and [@montlebalm](https://github.com/montlebalm) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.2.3)
# v1.2.2 (2017-02-27)
- Fix backspace/delete on Windows/Ubuntu [#1334](https://github.com/slab/quill/issues/1334)
Thanks to [@dinusuresh](https://github.com/dinusuresh) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.2.2)
# v1.2.1 (2017-02-27)
- Fix link removal on Snow theme [#1259](https://github.com/slab/quill/issues/1259)
- Fix CMD+backspace on empty editor [#1319](https://github.com/slab/quill/issues/1319)
- Fix disabled checklist behavior [#1312](https://github.com/slab/quill/issues/1312)
Thank you [@danielschwartz](https://github.com/@danielschwartz), [@JedWatson](https://github.com/@JedWatson), [@montlebalm](https://github.com/@montlebalm), and [@simi](https://github.com/@simi) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.2.1)
# v1.2.0 (2017-01-21)
Add concept of experimental APIs: they are APIs meant to try out support for use cases we would like to address, but gives flexibility to find the right API interface. As such they are not covered by Semantic Versioning. Several are added to start things off: `find`, `getIndex`, `getLeaf`, `getLine`, `getLines`.
- Merge disabling list keyboard shortcut when list format is disabled [#1257](https://github.com/slab/quill/pull/1257)
Thanks to [@haugstrup](https://github.com/haugstrup) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.2.0)
# v1.1.10 (2017-01-16)
- Preserve user selection on API changes [#1152](https://github.com/slab/quill/issues/1152)
- Fix backspacing into emojis [#1230](https://github.com/slab/quill/issues/1230)
- Fix ability to type after emptying line in IE/Firefox [#1254](https://github.com/slab/quill/issues/1254)
- Fix whitelisting block formats [#1256](https://github.com/slab/quill/issues/1256)
Thank you [@benbro](https://github.com/benbro), [@haugstrup](https://github.com/haugstrup), [@peterweck](https://github.com/peterweck) and [@sbevels](https://github.com/sbevels) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.1.10)
# v1.1.9 (2017-01-02)
- Support pasting italics from Google Docs [#1185](https://github.com/slab/quill/issues/1185)
- Fix setting dropdown picker back to default [#1191](https://github.com/slab/quill/issues/1191)
- Fix code-block formatting on empty first line in Firefox [#1195](https://github.com/slab/quill/issues/1195)
- Prevent formatting via keyboard shortcuts when not whitelisted [#1197](https://github.com/slab/quill/issues/1197)
- Fix select-all copy and overwrite paste in Firefox [#1202](https://github.com/slab/quill/issues/1202)
Thank you [@adfaure](https://github.com/adfaure), [@berndschimmer](https://github.com/berndschimmer), [@CoenWarmer](https://github.com/CoenWarmer), [@montlebalm](https://github.com/montlebalm), and [@TraceyYau](https://github.com/TraceyYau) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.1.9)
# v1.1.8 (2016-12-23)
- Support pasting italics from Google Docs [#1185](https://github.com/slab/quill/issues/1185)
- Fix setting dropdown picker back to default [#1191](https://github.com/slab/quill/issues/1191)
- Fix code-block formatting on empty first line in Firefox [#1195](https://github.com/slab/quill/issues/1195)
- Prevent formatting via keyboard shortcuts when not whitelisted [#1197](https://github.com/slab/quill/issues/1197)
- Fix select-all copy and overwrite paste in Firefox [#1202](https://github.com/slab/quill/issues/1202)
Thank you [@adfaure](https://github.com/adfaure), [@berndschimmer](https://github.com/berndschimmer), [@CoenWarmer](https://github.com/CoenWarmer), [@montlebalm](https://github.com/montlebalm), and [@TraceyYau](https://github.com/TraceyYau) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.1.8)
# v1.1.7 (2016-12-13)
- Fix dropdown values reverting to default [#901](https://github.com/slab/quill/issues/901)
- Add config to prevent scroll jumping on paste [#1082](https://github.com/slab/quill/issues/1082)
- Prevent scrolling on API source calls [#1152](https://github.com/slab/quill/issues/1152)
- Fix tsconfig build error [#1165](https://github.com/slab/quill/issues/1165)
- Fix delete and formatting interaction in Firefox [#1171](https://github.com/slab/quill/issues/1171)
- Fix cursor jump on formatting in middle of text [#1176](https://github.com/slab/quill/issues/1176)
Thanks to [@cutteroid](https://github.com/cutteroid), [@houxg](https://github.com/houxg), [@jasongisstl](https://github.com/jasongisstl), [@nikparo](https://github.com/nikparo), [@sbevels](https://github.com/sbevels), and [sferoze](https://github.com/sferoze) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.1.7)
# v1.1.6 (2016-12-06)
### Features
Checklists [#759](https://github.com/slab/quill/issues/759) support has been added to the API. UI and relevant interactions are still forthcoming.
### Bug Fixes
- Fix bug that allowed edits in readOnly mode [#1151](https://github.com/slab/quill/issues/1151)
- Fix max call stack bug on large paste [#1123](https://github.com/slab/quill/issues/1123)
Thank you [@jgmediadesign](https://github.com/jgmediadesign) and [@julienbmobile](https://github.com/julienbmobile) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.1.6)
# v1.1.5 (2016-11-07)
- Remove unnecessary type attribute in documentation [#1087](https://github.com/slab/quill/pull/1087)
- Fix chrome 52+ input file label open slow [#1090](https://github.com/slab/quill/pull/1090)
- Only query the last op's insertion string if it's actually an insert [#1095](https://github.com/slab/quill/pull/1095)
Thank you [@jleen](https://github.com/jleen), [@kaelig](https://github.com/kaelig), and [@YouHan26](https://github.com/YouHan26) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.1.5)
# v1.1.3 (2016-10-24)
- Update quill-delta [delta#2](https://github.com/quilljs/delta/issues/2)
- Fix link creation [#1073](https://github.com/slab/quill/issues/1073)
Thanks to [@eamodio](https://github.com/eamodio) and [@metsavir](https://github.com/metsavir) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.1.3)
# v1.1.2 (2016-10-24)
- Fix setContents on already formatted text [#1065](https://github.com/slab/quill/issues/1065)
- Fix regression [#1067](https://github.com/slab/quill/issues/1067)
- Improve documentation [#1069](https://github.com/slab/quill/pull/1069) [#1070](https://github.com/slab/quill/pull/1070)
Thank you [benbro](https://github.com/benbro), [derickruiz](https://github.com/derickruiz), [eamodio](https://github.com/eamodio), [hallaathrad](https://github.com/hallaathrad), and [philly385](https://github.com/philly385) for your contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.1.2)
# v1.1.1 (2016-10-21)
### Bug fixes
- TEXT_CHANGE event now use cursor position to inform change location [#746](https://github.com/slab/quill/issues/746)
- Fix inconsistent cursor reporting between browsers [#1007](https://github.com/slab/quill/issues/1007)
- Fix tooltip overflow in docs [#1060](https://github.com/slab/quill/issues/1060)
- Fix naming [#1063](https://github.com/slab/quill/pull/1063)
- Fix Medium example [#1064](https://github.com/slab/quill/issues/1064)
Thanks to [@artaommahe](https://github.com/artaommahe), [@benbro](https://github.com/benbro), [@fuffalist](https://github.com/fuffalist), [@sachinrekhi](https://github.com/sachinrekhi), [@sergop321](https://github.com/sergop321), and [@tlg](https://github.com/tlg) for contributions to this release!
Special thanks to [@DadaMonad](https://github.com/DadaMonad) for contributions on [fast-diff](https://github.com/jhchen/fast-diff) that enabled the [#746](https://github.com/slab/quill/issues/746) fix.
[All changes](https://github.com/slab/quill/releases/tag/v1.1.1)
# v1.1.0 (2016-10-17)
### Additions
Quill has always allowed API calls, even when the editor is in readOnly mode. All API calls also took a `source` parameter to indicate the origin of the change. For example, a click handler in the toolbar would call `formatText` with `source` set to `"user"`. When the editor is in readOnly mode, it would make sense for user initiated actions to be ignored. For example the user cannot focus or type into the editor. However because API calls are allowed, the user could still modify the editor contents [#909](https://github.com/slab/quill/issues/909). The natural fix is to ignore user initiated actions, even if it came through an API call, when the editor is in readOnly mode.
However, the documentation never stated API calls with `source` set to `"user"` would be ignored sometimes, so this would be a breaking change under semver. Some could argue this is a bug fix and would only warrant a patch version bump, but this seems disingenuous for this particular case. The fact that almost no one took advantage of the `source` beyond default values is irrelevant under the eyes of semver.
So a `strict` configuration option has been added. It is true by default so the above behavior is unchanged, and [#909](https://github.com/slab/quill/issues/909) is unfixed. Changing this to `false`, will use new behavior of ignoring user initiated changes on a disabled editor, even if through an API call.
### Fixes
- Fix undo when preformatted text inserted before plain text [#1019](https://github.com/slab/quill/issues/1019)
- Add focus indicator on toolbar buttons [#1020](https://github.com/slab/quill/issues/1020)
- Do not steal focus on API calls [#1029](https://github.com/slab/quill/issues/1029)
- Disable paste when Quill is disabled [#1038](https://github.com/slab/quill/issues/1038)
- Fix blank detection [#1043](https://github.com/slab/quill/issues/1043)
- Enable yarn [#1041](https://github.com/slab/quill/issues/1041)
- Documentation fixes [#1026](https://github.com/slab/quill/pull/1026), [#1027](https://github.com/slab/quill/pull/1027), [#1032](https://github.com/slab/quill/pull/1032)
Thank you [@benbro](https://github.com/benbro), [@cutteroid](https://github.com/cutteroid), [@evansolomon](https://github.com/evansolomon), [@felipeochoa](https://github.com/felipeochoa), [jackmu95](https://github.com/jackmu95), [@joedynamite](https://github.com/joedynamite), [@lance13c](https://github.com/lance13c), [@leebenson](https://github.com/leebenson), [@maartenvanvliet](https://github.com/maartenvanvliet), [@sarbbottam](https://github.com/sarbbottam), [@viljark](https://github.com/viljark), [@w00fz](https://github.com/w00fz) for their contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.1.0)
# v1.0.6 (2016-09-30)
Documentation clarifications and bug fixes.
- Fix attaching toolbar to `<select>` without themes [#997](https://github.com/slab/quill/issues/997)
- Link `code` icon to `code-block` [#998](https://github.com/slab/quill/issues/998)
- Fix undo stack when at size limit [#1001](https://github.com/slab/quill/pull/1001)
- Fix bug where `formatLine` did not ignore inline formats [8a7190](https://github.com/quilljs/parchment/commit/8a71905b2dd02d003edb02a15fdc727b26914e49)
Thanks to [@dropfen](https://github.com/dropfen), [@evansolomon](https://github.com/evansolomon), [@hallaathrad](https://github.com/hallaathrad), [@janyksteenbeek](https://github.com/janyksteenbeek), [@jackmu95](https://github.com/jackmu95), [@marktron](https://github.com/marktron), [@mcat-ee](https://github.com/mcat-ee), [@unhammer](https://github.com/unhammer), and [@zeke](https://github.com/zeke) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.0.6)
# v1.0.4 (2016-09-19)
- Fix bubble theme defaults [#963](https://github.com/slab/quill/issues/963)
- Fix browsers modifying inline nesting order [#971](https://github.com/slab/quill/issues/971)
- Do not fire selection-change event on paste [#974](https://github.com/slab/quill/issues/974)
- Support alt attribute in images [#975](https://github.com/slab/quill/issues/975)
- Deprecate `pasteHTML` for removal in Quill 2.0 [#981](https://github.com/slab/quill/issues/981)
Thank you [jackmu95](https://github.com/jackmu95), [kristeehan](https://github.com/kristeehan), [ruffle1986](https://github.com/ruffle1986), [sergop321](https://github.com/sergop321), [sferoze](https://github.com/sferoze), and [sijad](https://github.com/sijad) for contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.4)
# v1.0.3 (2016-09-07)
- Fix [#928](https://github.com/slab/quill/issues/928)
Thank you [@scottmessinger](https://github.com/scottmessinger) for the bug report.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.3)
# v1.0.2 (2016-09-07)
- Fix building quill.core.js [docs #11](https://github.com/quilljs/quilljs.github.io/issues/11)
- Fix regression of [#793](https://github.com/slab/quill/issues/793)
Thanks to [@eamodio](https://github.com/eamodio) and [@neandrake](https://github.com/neandrake) for their contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.2)
# v1.0.0 (2016-09-06)
Quill 1.0 is released! Read the [official announcement](https://quilljs.com/blog/announcing-quill-1-0/).
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0)
# v1.0.0-rc.4 (2016-08-31)
Fix one important bug [fdd920](https://github.com/slab/quill/commit/fdd920250c05403ed9e5d6d86826a00167ba0b09)
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.4)
# v1.0.0-rc.3 (2016-08-29)
A few bug fixes, one with with possibly significant implications. See the [issue #889](https://github.com/slab/quill/issues/889) and [commit fix](https://github.com/slab/quill/commit/be24c62a6234818548658fcb5e1935a0c07b4eb7) for more details.
### Bug Fixes
- Fix indenting beyond first level with toolbar [#882](https://github.com/slab/quill/issues/882)
- Fix toolbar font/size display on Safari [#884](https://github.com/slab/quill/issues/884)
- Fix pasting from Gmail from on different browser [#886](https://github.com/slab/quill/issues/886)
- Fix undo/redo consistency [#889](https://github.com/slab/quill/issues/889)
- Fix null error when selecting all on Firefox [#891](https://github.com/slab/quill/issues/891)
- Fix merging keyboard options twice [#897](https://github.com/slab/quill/issues/897)
Thank you [@benbro](https://github.com/benbro), [@cgilboy](https://github.com/cgilboy), [@cutteroid](https://github.com/cutteroid), and [@routman](https://github.com/routman) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.3)
# v1.0.0-rc.2 (2016-08-23)
A few bug fixes, including one significant [one](https://github.com/slab/quill/issues/883)
### Bug Fixes
- Fix icon picker rendering in MS Edge [#877](https://github.com/slab/quill/issues/877)
- Add back minified build to release [#881](https://github.com/slab/quill/issues/881)
- Fix optimized change calculation with preformatted text [#883](https://github.com/slab/quill/issues/883)
Thanks to [benbro](https://github.com/benbro), [cutteroid](https://github.com/cutteroid), and [CapTec](https://github.com/CapTec) for their contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.2)
# v1.0.0-rc.1 (2016-08-23)
A few bug fixes and performance improvements.
### Features
- Source maps now available from CDN for minified build
### Bug Fixes
- Fix scroll interaction between two Quill editors [#855](https://github.com/slab/quill/issues/855)
- Fix scroll on paste [#856](https://github.com/slab/quill/issues/856)
- Fix native iOS tooltip formatting [#862](https://github.com/slab/quill/issues/862)
- Remove comments from pasting from Word [#872](https://github.com/slab/quill/issues/872)
- Fix indent at all supported indent levels [#873](https://github.com/slab/quill/issues/873)
- Fix indent interaction with text direction [#874](https://github.com/slab/quill/issues/874)
Thank you [@benbro](https://github.com/benbro), [@fernandogmar](https://github.com/fernandogmar), [@sachinrekhi](https://github.com/sachinrekhi), [@sferoze](https://github.com/sferoze), and [@stalniy](https://github.com/stalniy) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.1)
# v1.0.0-rc.0 (2016-08-18)
Take a look at [Quill 1.0 Release Candidate](https://quilljs.com/blog/quill-1-0-release-candidate-released/) for more details.
### Updates
- Going forward the minimal stylesheet build will be named quill.core.css, instead of quill.css
### Bug Fixes
- Fix identifying ordered and bulletd lists [#846](https://github.com/slab/quill/issues/846) [#847](https://github.com/slab/quill/issues/847)
- Fix bullet interaction with text direction [#848](https://github.com/slab/quill/issues/848)
A huge thank you to all contributors to through the beta! Special thanks goes to [@benbro](https://github.com/benbro) and [@sachinrekhi](https://github.com/sachinrekhi) who together submitted submitted almost 50 Issues and Pull Requests!
- [@abejdaniels](https://github.com/abejdaniels)
- [@anovi](https://github.com/anovi)
- [@benbro](https://github.com/benbro)
- [@bram2w](https://github.com/bram2w)
- [@brynjagr](https://github.com/brynjagr)
- [@CapTec](https://github.com/CapTec)
- [@Cinamonas](https://github.com/Cinamonas)
- [@clemmy](https://github.com/clemmy)
- [@crisbeto](https://github.com/crisbeto)
- [@cutteroid](https://github.com/cutteroid)
- [@DadaMonad](https://github.com/DadaMonad)
- [@davelozier](https://github.com/davelozier)
- [@emanuelbsilva](https://github.com/emanuelbsilva)
- [@ersommer](https://github.com/ersommer)
- [@fernandogmar](https://github.com/fernandogmar)
- [@george-norris-salesforce](https://github.com/george-norris-salesforce)
- [@jackmu95](https://github.com/jackmu95)
- [@jasonmng](https://github.com/jasonmng)
- [@jbrowning](https://github.com/jbrowning)
- [@jonnolen](https://github.com/jonnolen)
- [@KameSama](https://github.com/KameSama)
- [@kei-ito](https://github.com/kei-ito)
- [@kylebragger](https://github.com/kylebragger)
- [@LucVanPelt](https://github.com/LucVanPelt)
- [@lukechapman](https://github.com/lukechapman)
- [@micimize](https://github.com/micimize)
- [@mmorearty](https://github.com/mmorearty)
- [@mshamaiev-intel471](https://github.com/mshamaiev-intel471)
- [@quentez](https://github.com/quentez)
- [@sachinrekhi](https://github.com/sachinrekhi)
- [@sagacitysite](https://github.com/sagacitysite)
- [@saw](https://github.com/saw)
- [@stalniy](https://github.com/stalniy)
- [@tOgg1](https://github.com/tOgg1)
- [@u9520107](https://github.com/u9520107)
- [@WriterStat](https://github.com/WriterStat)
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-rc.0)
# v1.0.0-beta.11 (2016-08-03)
Fixed some regressive bugs from previous release.
### Bug Fixes
- Fix activating more than one format before typing [#841](https://github.com/slab/quill/issues/841)
- Run default matchers before before user defined ones [#843](https://github.com/slab/quill/issues/843)
- Fix merging theme configurations [#844](https://github.com/slab/quill/issues/844), [#845](https://github.com/slab/quill/issues/845)
Thanks [benbro](https://github.com/benbro), [jackmu95](https://github.com/jackmu95), and [george-norris-salesforce](https://github.com/george-norris-salesforce) for the bug reports.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.11)
# v1.0.0-beta.10 (2016-08-03)
Lots of bug fixes and performance improvements.
### Breaking Changes
- Keyboard handler format in initial [configuration](beta.quilljs.com/docs/modules/keyboard/) has changed. `addBinding` is overloaded to be backwards compatible.
### Bug Fixes
- Preserve last bullet on paste [#696](https://github.com/slab/quill/issues/696)
- Fix getBounds calculation for lists [#765](https://github.com/slab/quill/issues/765)
- Escape quotes in font value [#769](https://github.com/slab/quill/issues/769)
- Fix spacing calculation on paste [#797](https://github.com/slab/quill/issues/797)
- Fix Snow tooltip label [#798](https://github.com/slab/quill/issues/798)
- Fix link tooltip showing up on long click [#799](https://github.com/slab/quill/issues/799)
- Fix entering code block in IE and Firefox [#803](https://github.com/slab/quill/issues/803)
- Fix opening image dialog on Firefox [#805](https://github.com/slab/quill/issues/805)
- Fix focus loss on updateContents [#809](https://github.com/slab/quill/issues/809)
- Reset toolbar of blur [#810](https://github.com/slab/quill/issues/810)
- Fix cursor position calculation on delete [#811](https://github.com/slab/quill/issues/811)
- Fix highlighting across different alignment values [#815](https://github.com/slab/quill/issues/815)
- Allow default active button [#816](https://github.com/slab/quill/issues/816)
- Fix deleting last character of formatted text on Firefox [#824](https://github.com/slab/quill/issues/824)
- Fix Youtube regex [#826](https://github.com/slab/quill/pull/826)
- Fix missing imports when Quill not global [#836](https://github.com/slab/quill/pull/836)
Thanks to [benbro](https://github.com/benbro), [clemmy](https://github.com/clemmy), [crisbeto](https://github.com/crisbeto), [cutteroid](https://github.com/cutteroid), [jackmu95](https://github.com/jackmu95), [kylebragger](https://github.com/kylebragger), [sachinrekhi](https://github.com/sachinrekhi), [stalniy](https://github.com/stalniy), and [tOgg1](https://github.com/tOgg1) for their contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.10)
# v1.0.0-beta.9 (2016-07-19)
Potentially the final beta before a release candidate, if no major issues are discovered.
### Breaking Changes
- No longer expose `ui/link-tooltip` through `import` as implementation is now Snow specific
- Significant refactoring of `ui/tooltip`
- Syntax module now autodetects language, instead of defaulting to Javascript
### Features
- Formula and video insertion UI added to Snow and Bubble themes
### Bug Fixes
- Fix toolbar active state after backspacing to previous line [#730](https://github.com/slab/quill/issues/730)
- User selection is now preserved various API calls [#731](https://github.com/slab/quill/issues/731)
- Fix long click on link-tooltip [#747](https://github.com/slab/quill/issues/747)
- Fix ordered list and text-align right interaction [#784](https://github.com/slab/quill/issues/784)
- Fix toggling code block off [#789](https://github.com/slab/quill/issues/789)
- Scroll position is now automatically preserved between editor blur and focus
Thank you [@benbro](https://github.com/benbro), [@KameSama](https://github.com/KameSama), and [@sachinrekhi](https://github.com/sachinrekhi) for contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.9)
# v1.0.0-beta.8 (2016-07-08)
Weekly beta preview release. The editor is almost ready for release candidacy but a couple cycles will be spent on the Snow and Bubble interfaces.
### Work in Progress
Image insertion is being reworked in the provided Snow and Bubble themes. The old image-tooltip has been removed in favor of a simpler and native interaction. By default clicking the image icon on the toolbar will open the OS file picker to convert and that into a base64 image. This will allow for a more natural hook to upload to a remote server instead. Some changes to the link tooltip is also being made to accommodate formula and video insertion, currently only available through the API.
### Breaking Changes
- Image tooltip UI has been removed, see above
- Code blocks now use a single `<pre>` tag, instead of one per line [#723](https://github.com/slab/quill/issues/723)
### Bug Fixes
- Fix multiline syntax highlighting [#723](https://github.com/slab/quill/issues/723)
- Keep pickers open on api text-change [#734](https://github.com/slab/quill/issues/734)
- Emit correct source for text-change [#760](https://github.com/slab/quill/issues/760)
- Emit correct parameters in selection-change [#762](https://github.com/slab/quill/issues/762)
- Fix error redoing line insertion [#767](https://github.com/slab/quill/issues/767)
- Better emitted Deltas for text-change [#768](https://github.com/slab/quill/issues/768)
- Better Array.prototype.find polyfill for IE11 [#776](https://github.com/slab/quill/issues/776)
- Fix Parchment errors in replacing text [#779](https://github.com/slab/quill/issues/779) [#783](https://github.com/slab/quill/issues/783)
- Fix align button active state [#780](https://github.com/slab/quill/issues/780)
- Fix format text on falsy value [#782](https://github.com/slab/quill/issues/782)
- Use native cut [#785](https://github.com/slab/quill/issues/785)
- Fix inializing document where last line is formatted [#786](https://github.com/slab/quill/issues/786)
Thanks to [benbro](https://github.com/benbro), [bram2w](https://github.com/bram2w), [clemmy](https://github.com/clemmy), [DadaMonad](https://github.com/DadaMonad), [ersommer](https://github.com/ersommer), [michaeljosephrosenthal](https://github.com/michaeljosephrosenthal), [mmorearty](https://github.com/mmorearty), [mshamaiev-intel471](https://github.com/mshamaiev-intel471), and [sachinrekhi](https://github.com/sachinrekhi) for their contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.8)
# v1.0.0-beta.6 (2016-06-21)
Weekly beta preview release.
### Features
- Pickers can now be used and is styled in Bubble theme
### Bug Fixes
- Fix editing within formula [#702](https://github.com/slab/quill/issues/702)
- Fix adding new line when deleting across lists [#741](https://github.com/slab/quill/issues/741)
- Fix placeholder when default block tag is changed [#743](https://github.com/slab/quill/issues/743)
- Keep Bubble tooltip open on format [#744](https://github.com/slab/quill/issues/744)
- Fix format loss when copying from Quill [#748](https://github.com/slab/quill/issues/748) [#750](https://github.com/slab/quill/issues/750)
- Break long lines in Firefox [#751](https://github.com/slab/quill/issues/751)
- Fix cursor position being off after formatting and typing quickly [#752](https://github.com/slab/quill/issues/752)
- Remove image resizing handles on Firefox [#753](https://github.com/slab/quill/issues/753)
- Fix removing blockquote on initialization [#754](https://github.com/slab/quill/issues/754)
- Fix adding blank lines on initialization [#756](https://github.com/slab/quill/issues/756)
Thank you [abejdaniels](https://github.com/abejdaniels), [benbro](https://github.com/benbro), [davelozier](https://github.com/davelozier), [fernandogmar](https://github.com/fernandogmar), [KameSama](https://github.com/KameSama), and [WriterStat](https://github.com/WriterStat) for contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.6)
# v1.0.0-beta.5 (2016-06-14)
Weekly beta preview release.
### Features
- Add blur() [#726](https://github.com/slab/quill/pull/726)
### Bug Fixes
- Fix null error [#728](https://github.com/slab/quill/issues/728)
- Fix building with Node v6 [#732](https://github.com/slab/quill/issues/732)
- Ensure button type for supplied buttons [#733](https://github.com/slab/quill/issues/733)
- Fix line break pasting on Firefox [#735](https://github.com/slab/quill/issues/735)
- Fix 'user' source on API calls [#739](https://github.com/slab/quill/issues/739)
Thanks to [benbro](https://github.com/benbro), [lukechapman](https://github.com/lukechapman), [sachinrekhi](https://github.com/sachinrekhi), and [saw](https://github.com/saw) for their contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.5)
# v1.0.0-beta.4 (2016-06-03)
Weekly beta preview release.
### Breaking Changes
- Headers no longer generates id attribute [#700](https://github.com/slab/quill/issues/700)
- Add Control+Y hotkey on Windows [#705](https://github.com/slab/quill/issues/705)
- BlockEmbed Blots are now length 1 and represented in a Delta the same as an inline embed
- value() used to return object and newline, newline is now removed
- formats used to be attributed on the newline character, it is now attributed on the object
### Features
- Enter on empty and indented list removes indent [#707](https://github.com/slab/quill/issues/707)
- Allow base64 images to be inserted via APIs [#721](https://github.com/slab/quill/issues/721)
### Bug Fixes
- Fix typing after clearing inline format [#703](https://github.com/slab/quill/issues/703)
- Correctly position Bubble tooltip when selecting multiple lines [#706](https://github.com/slab/quill/issues/706)
- Fix typing after link format [#708](https://github.com/slab/quill/issues/708)
- Fix loss of selection on using link tooltip [#709](https://github.com/slab/quill/issues/709)
- Fix `setSelection(null)` [#722](https://github.com/slab/quill/issues/722)
Thank you [@benbro](https://github.com/benbro), [@brynjagr](https://github.com/brynjagr), and [@sachinrekhi](https://github.com/sachinrekhi) for contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.4)
# v1.0.0-beta.3 (2016-05-25)
# 1.0.0-beta.3
Weekly beta preview release.
### Breaking Changes
- Keyboard was incorrectly using `metaKey` to refer to the control key on Windows. It now correctly refers to the Window key and `shortKey` has been added to refer the common platform specific modifier for hotkeys (metaKey for Mac, ctrlKey for Windows/Linux)
- Formula is now a module, since it uses KaTeX
### Features
- Picker now uses text from original `<option>` if available
- Tabbing inside code blocks inserts tab to each line
### Bug Fixes
- Enter preserves inline formats [#666](https://github.com/slab/quill/issues/666)
- Fix resetting format button with no selection [#667](https://github.com/slab/quill/issues/667)
- Fix paste interpretation from Word [#668](https://github.com/slab/quill/issues/668)
- Focus scrolls to correct cursor position [#669](https://github.com/slab/quill/issues/669)
- Fix deleting image on otherwise empty document [#670](https://github.com/slab/quill/issues/670)
- Fix bubble toolbar formatting [#679](https://github.com/slab/quill/issues/679)
- Fix pasting ql-indent lines [#681](https://github.com/slab/quill/issues/681)
- Fix getting into state with double underline tag [#695](https://github.com/slab/quill/issues/695)
- Fix source type on delete [#697](https://github.com/slab/quill/issues/697)
- Fix indent becoming NaN [#698](https://github.com/slab/quill/issues/698)
Thanks to [@benbro](https://github.com/benbro), [@Cinamonas](https://github.com/Cinamonas), [@emanuelbsilva](https://github.com/emanuelbsilva), [@jasonmng](https://github.com/jasonmng), [@jonnolen](https://github.com/jonnolen), [@LucVanPelt](https://github.com/LucVanPelt), [@sachinrekhi](https://github.com/sachinrekhi), [@sagacitysite](https://github.com/sagacitysite), [@WriterStat](https://github.com/WriterStat) for their contributions to this release.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.3)
# v1.0.0-beta.2 (2016-05-15)
Weekly beta preview release. Major emphasis on keyboard API and customization.
### Breaking Changes
- Rename code highlighter module to syntax
- Clipboard matchers specified in configuration appends to instead of replaces default matchers
- Change video embed to use `<iframe>` instead of `<video>` enabling Youtube/Vimeo links
### Features
- Add contextual keyboard listeners
- Allow indent format to take +1/-1 in addition to target indent level
- Shortcuts for creating ordered or bulleted lists
- Autofill mailto for email links [#278](https://github.com/slab/quill/issues/278)
- Enter does not continue header format [#540](https://github.com/slab/quill/issues/540)
### Bug Fixes
- Allow native handling of backspace [#473](https://github.com/slab/quill/issues/473) [#548](https://github.com/slab/quill/issues/548) [#565](https://github.com/slab/quill/issues/565)
- removeFormat() removes last line block formats [#649](https://github.com/slab/quill/issues/649)
- Fix text direction icon directon [#654](https://github.com/slab/quill/issues/654)
- Fix text insertion into root scroll [#655](https://github.com/slab/quill/issues/655)
- Fix focusing on placeholder text in FF [#656](https://github.com/slab/quill/issues/656)
- Hide placeholder on formatted line [#657](https://github.com/slab/quill/issues/657)
- Fix selection handling on focus and blur [#664](https://github.com/slab/quill/issues/664)
Thanks to [@anovi](https://github.com/anovi), [@benbro](https://github.com/benbro), [@jbrowning](https://github.com/jbrowning), [@kei-ito](https://github.com/kei-ito), [@quentez](https://github.com/quentez), [@u9520107](https://github.com/u9520107) for their contributions to this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.2)
# v1.0.0-beta.1 (2016-05-10)
Weekly beta preview release.
### Breaking Changes
- Toolbar only attaches to `<button>` and `<select>` elements
- Toolbar uses button `value` attribute, instead of `data-value`
- Toolbar handlers overwrite default handlers instead of possibly cascading
- Deprecate keyboard `removeBinding` and `removeAllBindings`
### Features
- Expose default keyboard bindings in configuration
- Add context listener to keyboard bindings
### Bug Fixes
- Error when cursor places next to video embed [#644](https://github.com/slab/quill/issues/644)
- Selection removed when clicking on a menu button in the toolbar [#645](https://github.com/slab/quill/issues/645)
- Editor looses focus in FF after typing two bold characters [#646](https://github.com/slab/quill/issues/646)
- Get rid of resize boxes in code in IE11 [0ad636](https://github.com/slab/quill/commit/0ad6363c9fcd70c52ca667d39a393760eeb646b5)
- Text direction icon should flip the arrow when pressed [#651](https://github.com/slab/quill/issues/651)
- Not possible to combine direction:rtl with text-align:left [#652](https://github.com/slab/quill/issues/652)
Thanks to [@benbro](https://github.com/benbro) for the bug reports for this release!
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.1)
# v1.0.0-beta.0 (2016-05-04)
Please see the [Upgrading to 1.0](http://beta.quilljs.com/guides/upgrading-to-1-0/) guide.
[All changes](https://github.com/slab/quill/releases/tag/v1.0.0-beta.0)
================================================
FILE: LICENSE
================================================
Copyright (c) 2017-2024, Slab
Copyright (c) 2014, Jason Chen
Copyright (c) 2013, salesforce.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
<h1 align="center">
<a href="https://quilljs.com/" title="Quill">Quill Rich Text Editor</a>
</h1>
<p align="center">
<a href="https://quilljs.com/" title="Quill"><img alt="Quill Logo" src="https://quilljs.com/assets/images/logo.svg" width="180"></a>
</p>
<p align="center">
<a title="Documentation" href="https://quilljs.com/docs/quickstart"><strong>Documentation</strong></a>
•
<a title="Development" href="https://github.com/slab/quill/blob/main/.github/DEVELOPMENT.md"><strong>Development</strong></a>
•
<a title="Contributing" href="https://github.com/slab/quill/blob/main/.github/CONTRIBUTING.md"><strong>Contributing</strong></a>
•
<a title="Interactive Playground" href="https://quilljs.com/playground/"><strong>Interactive Playground</strong></a>
</p>
<p align="center">
<a href="https://github.com/slab/quill/actions" title="Build Status"><img src="https://github.com/slab/quill/actions/workflows/main.yml/badge.svg" alt="Build Status"></a>
<a href="https://npmjs.com/package/quill" title="Version"><img src="https://img.shields.io/npm/v/quill.svg" alt="Version"></a>
<a href="https://npmjs.com/package/quill" title="Downloads"><img src="https://img.shields.io/npm/dm/quill.svg" alt="Downloads"></a>
</p>
<hr/>
[Quill](https://quilljs.com/) is a modern rich text editor built for compatibility and extensibility. It was created by [Jason Chen](https://twitter.com/jhchen) and [Byron Milligan](https://twitter.com/byronmilligan) and actively maintained by [Slab](https://slab.com).
To get started, check out [https://quilljs.com/](https://quilljs.com/) for documentation, guides, and live demos!
## Quickstart
Instantiate a new Quill object with a css selector for the div that should become the editor.
```html
<!-- Include Quill stylesheet -->
<link
href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css"
rel="stylesheet"
/>
<!-- Create the toolbar container -->
<div id="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- Create the editor container -->
<div id="editor">
<p>Hello World!</p>
<p>Some initial <strong>bold</strong> text</p>
<p><br /></p>
</div>
<!-- Include the Quill library -->
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
<!-- Initialize Quill editor -->
<script>
const quill = new Quill("#editor", {
theme: "snow",
});
</script>
```
Take a look at the [Quill](https://quilljs.com/) website for more documentation, guides and [live playground](https://quilljs.com/playground/)!
## Download
```shell
npm install quill
```
### CDN
```html
<!-- Main Quill library -->
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"></script>
<!-- Theme included stylesheets -->
<link
href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css"
rel="stylesheet"
/>
<link
href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.bubble.css"
rel="stylesheet"
/>
<!-- Core build with no theme, formatting, non-essential modules -->
<link
href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.core.css"
rel="stylesheet"
/>
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.core.js"></script>
```
## Community
Get help or stay up to date.
- [Contribute](https://github.com/slab/quill/blob/main/.github/CONTRIBUTING.md) on [Issues](https://github.com/slab/quill/issues)
- Ask questions on [Discussions](https://github.com/slab/quill/discussions)
## License
BSD 3-clause
================================================
FILE: package.json
================================================
{
"name": "quill-monorepo",
"version": "2.0.3",
"description": "Quill development environment",
"private": true,
"author": "Jason Chen <jhchen7@gmail.com>",
"homepage": "https://quilljs.com",
"config": {
"ports": {
"webpack": "9080",
"website": "9000"
}
},
"workspaces": [
"packages/*"
],
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git+https://github.com/slab/quill.git"
},
"bugs": {
"url": "https://github.com/slab/quill/issues"
},
"scripts": {
"build": "run-p build:*",
"build:quill": "npm run build -w quill",
"build:website": "npm run build -w website",
"start": "run-p start:*",
"start:quill": "npm start -w quill",
"start:website": "NEXT_PUBLIC_LOCAL_QUILL=true npm start -w website",
"lint": "npm run lint -ws"
},
"keywords": [
"quill",
"editor",
"rich text",
"wysiwyg",
"operational transformation",
"ot",
"framework"
],
"engines": {
"npm": ">=8.2.3"
},
"engineStrict": true,
"devDependencies": {
"execa": "^9.0.2",
"npm-run-all": "^4.1.5"
}
}
================================================
FILE: packages/quill/.eslintrc.json
================================================
{
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:import/recommended",
"plugin:require-extensions/recommended"
],
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"parser": "@typescript-eslint/parser",
"settings": {
"import/resolver": {
"webpack": {
"env": "development"
},
"typescript": true
}
},
"ignorePatterns": ["*.js", "*.d.ts"],
"overrides": [
{
"files": ["**/*.ts"],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript"
],
"excludedFiles": "*.d.ts",
"plugins": ["@typescript-eslint", "require-extensions"],
"rules": {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"import/no-named-as-default-member": "off",
"prefer-arrow-callback": "error"
}
}
]
}
================================================
FILE: packages/quill/.gitignore
================================================
dist
================================================
FILE: packages/quill/LICENSE
================================================
Copyright (c) 2017-2024, Slab
Copyright (c) 2014, Jason Chen
Copyright (c) 2013, salesforce.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: packages/quill/README.md
================================================
# Quill
This is the main package of Quill.
================================================
FILE: packages/quill/babel.config.cjs
================================================
const pkg = require('./package.json');
module.exports = {
presets: [
['@babel/preset-env', { modules: false }],
'@babel/preset-typescript',
],
plugins: [
['transform-define', { QUILL_VERSION: pkg.version }],
'./scripts/babel-svg-inline-import.cjs',
],
};
================================================
FILE: packages/quill/package.json
================================================
{
"name": "quill",
"version": "2.0.3",
"description": "Your powerful, rich text editor",
"author": "Jason Chen <jhchen7@gmail.com>",
"homepage": "https://quilljs.com",
"main": "quill.js",
"type": "module",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash-es": "^4.17.21",
"parchment": "^3.0.0",
"quill-delta": "^5.1.0"
},
"devDependencies": {
"@babel/cli": "^7.23.9",
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@babel/preset-typescript": "^7.23.3",
"@playwright/test": "^1.54.1",
"@types/highlight.js": "^9.12.4",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.10.0",
"@types/webpack": "^5.28.5",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitest/browser": "^3.2.4",
"babel-loader": "^9.1.3",
"babel-plugin-transform-define": "^2.1.4",
"css-loader": "^6.10.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-import-resolver-webpack": "^0.13.8",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-require-extensions": "^0.1.3",
"glob": "10.4.2",
"highlight.js": "^9.18.1",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.3",
"jsdom": "^22.1.0",
"mini-css-extract-plugin": "^2.7.6",
"prettier": "^3.0.3",
"source-map-loader": "^5.0.0",
"style-loader": "^3.3.3",
"stylus": "^0.62.0",
"stylus-loader": "^7.1.3",
"svgo": "^3.2.0",
"terser-webpack-plugin": "^5.3.9",
"transpile-webpack-plugin": "^1.1.3",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typescript": "^5.4.2",
"vitest": "^3.2.4",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-merge": "^5.10.0"
},
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git+https://github.com/slab/quill.git",
"directory": "packages/quill"
},
"bugs": {
"url": "https://github.com/slab/quill/issues"
},
"prettier": {
"singleQuote": true
},
"browserslist": [
"defaults"
],
"scripts": {
"build": "./scripts/build production",
"lint": "run-s lint:*",
"lint:eslint": "eslint .",
"lint:tsc": "tsc --noEmit --skipLibCheck",
"start": "[[ -z \"$npm_package_config_ports_webpack\" ]] && webpack-dev-server || webpack-dev-server --port $npm_package_config_ports_webpack",
"test": "run-s test:*",
"test:unit": "vitest --config test/unit/vitest.config.ts",
"test:fuzz": "vitest --config test/fuzz/vitest.config.ts",
"test:e2e": "playwright test"
},
"keywords": [
"quill",
"editor",
"rich text",
"wysiwyg",
"operational transformation",
"ot",
"framework"
],
"engines": {
"npm": ">=8.2.3"
},
"engineStrict": true
}
================================================
FILE: packages/quill/playwright.config.ts
================================================
import { defineConfig, devices } from '@playwright/test';
const port = 9001;
export default defineConfig({
testDir: './test/e2e',
testMatch: '*.spec.ts',
timeout: 30 * 1000,
expect: {
timeout: 5000,
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'list',
use: {
actionTimeout: 0,
trace: 'on-first-retry',
baseURL: `https://127.0.0.1:${port}`,
ignoreHTTPSErrors: true,
},
projects: [
{
name: 'Chrome',
use: {
...devices['Desktop Chrome'],
contextOptions: {
permissions: ['clipboard-read', 'clipboard-write'],
},
},
},
{ name: 'Firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'Safari', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: `npx webpack serve --config test/e2e/__dev_server__/webpack.config.cjs --env port=${port}`,
port,
ignoreHTTPSErrors: true,
reuseExistingServer: !process.env.CI,
stdout: 'ignore',
stderr: 'pipe',
},
});
================================================
FILE: packages/quill/scripts/babel-svg-inline-import.cjs
================================================
const fs = require('fs');
const { dirname, resolve } = require('path');
const { optimize } = require('svgo');
module.exports = ({ types: t }) => {
class BabelSVGInlineImport {
constructor() {
return {
visitor: {
ImportDeclaration: {
exit(path, state) {
const givenPath = path.node.source.value;
if (!givenPath.endsWith('.svg')) {
return;
}
const specifier = path.node.specifiers[0];
const id = specifier.local.name;
const reference = state && state.file && state.file.opts.filename;
const absolutePath = resolve(dirname(reference), givenPath);
const content = optimize(
fs.readFileSync(absolutePath).toString(),
{ plugins: [] },
).data;
const variableValue = t.stringLiteral(content);
const variable = t.variableDeclarator(
t.identifier(id),
variableValue,
);
path.replaceWith({
type: 'VariableDeclaration',
kind: 'const',
declarations: [variable],
});
},
},
},
};
}
}
return new BabelSVGInlineImport();
};
================================================
FILE: packages/quill/scripts/build
================================================
#!/usr/bin/env bash
set -e
DIST=dist
TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
npx tsc --declaration --emitDeclarationOnly --outDir $TMPDIR
rm -rf $DIST
mkdir $DIST
mv $TMPDIR/src/* $DIST
rm -rf $TMPDIR
npx babel src --out-dir $DIST --copy-files --no-copy-ignored --extensions .ts --source-maps
npx webpack -- --mode $1
# https://github.com/webpack-contrib/mini-css-extract-plugin/issues/151
rm -rf $DIST/dist/*.css.js $DIST/dist/*.css.js.*
cp package.json $DIST
cp README.md $DIST
cp LICENSE $DIST
================================================
FILE: packages/quill/src/assets/base.styl
================================================
// Styles shared between snow and bubble
controlHeight = 24px
inputPaddingWidth = 5px
inputPaddingHeight = 3px
colorItemMargin = 2px
colorItemSize = 16px
colorItemsPerRow = 7
.ql-{themeName}.ql-toolbar, .ql-{themeName} .ql-toolbar
&:after
clear: both
content: ''
display: table
button
background: none
border: none
cursor: pointer
display: inline-block
float: left
height: controlHeight
padding: inputPaddingHeight inputPaddingWidth
width: controlHeight + (inputPaddingWidth - inputPaddingHeight)*2
svg
float: left
height: 100%
&:active:hover
outline: none
input.ql-image[type=file]
display: none
button:hover, button:focus, button.ql-active,
.ql-picker-label:hover, .ql-picker-label.ql-active,
.ql-picker-item:hover, .ql-picker-item.ql-selected
color: activeColor
.ql-fill, .ql-stroke.ql-fill
fill: activeColor
.ql-stroke, .ql-stroke-miter
stroke: activeColor
// Fix for iOS not losing hover on touch
@media (pointer: coarse)
.ql-{themeName}.ql-toolbar, .ql-{themeName} .ql-toolbar
button:hover:not(.ql-active)
color: inactiveColor
.ql-fill, .ql-stroke.ql-fill
fill: inactiveColor
.ql-stroke, .ql-stroke-miter
stroke: inactiveColor
.ql-{themeName}
box-sizing: border-box
*
box-sizing: border-box
.ql-hidden
display: none
.ql-out-bottom, .ql-out-top
visibility: hidden
.ql-tooltip
position: absolute
transform: translateY(10px)
a
cursor: pointer
text-decoration: none
.ql-tooltip.ql-flip
transform: translateY(-10px)
.ql-formats
&:after
clear: both
content: ''
display: table
display: inline-block
vertical-align: middle
.ql-stroke
fill: none
stroke: inactiveColor
stroke-linecap: round
stroke-linejoin: round
stroke-width: 2
.ql-stroke-miter
fill: none
stroke: inactiveColor
stroke-miterlimit: 10
stroke-width: 2
.ql-fill, .ql-stroke.ql-fill
fill: inactiveColor
.ql-empty
fill: none
.ql-even
fill-rule: evenodd
.ql-thin, .ql-stroke.ql-thin
stroke-width: 1
.ql-transparent
opacity: 0.4
.ql-direction
svg:last-child
display: none
.ql-direction.ql-active
svg:last-child
display: inline
svg:first-child
display: none
.ql-editor
h1
font-size: 2em
h2
font-size: 1.5em
h3
font-size: 1.17em
h4
font-size: 1em
h5
font-size: 0.83em
h6
font-size: 0.67em
a
text-decoration: underline
blockquote
border-left: 4px solid #ccc
margin-bottom: 5px
margin-top: 5px
padding-left: 16px
code, .ql-code-block-container
background-color: #f0f0f0
border-radius: 3px
.ql-code-block-container
margin-bottom: 5px
margin-top: 5px
padding: 5px 10px
code
font-size: 85%
padding: 2px 4px
.ql-code-block-container
background-color: #23241f
color: #f8f8f2
overflow: visible
img
max-width: 100%
.ql-picker
color: inactiveColor
display: inline-block
float: left
font-size: 14px
font-weight: 500
height: controlHeight
position: relative
vertical-align: middle
.ql-picker-label
cursor: pointer
display: inline-block
height: 100%
padding-left: 8px
padding-right: 2px
position: relative
width: 100%
&::before
display: inline-block
line-height: 22px
.ql-picker-options
background-color: backgroundColor
display: none
min-width: 100%
padding: 4px 8px
position: absolute
white-space: nowrap
.ql-picker-item
cursor: pointer
display: block
padding-bottom: 5px
padding-top: 5px
.ql-picker.ql-expanded
.ql-picker-label
color: borderColor
z-index: 2
.ql-fill
fill: borderColor
.ql-stroke
stroke: borderColor
.ql-picker-options
display: block
margin-top: -1px
top: 100%
z-index: 1
.ql-color-picker, .ql-icon-picker
width: controlHeight + 4
.ql-picker-label
padding: 2px 4px
svg
right: 4px
.ql-icon-picker
.ql-picker-options
padding: 4px 0px
.ql-picker-item
height: controlHeight
width: controlHeight
padding: 2px 4px
.ql-color-picker
.ql-picker-options
padding: inputPaddingHeight inputPaddingWidth
width: (colorItemSize + 2*colorItemMargin) * colorItemsPerRow + 2*inputPaddingWidth + 2 // +2 for the border
.ql-picker-item
border: 1px solid transparent
float: left
height: colorItemSize
margin: colorItemMargin
padding: 0px
width: colorItemSize
.ql-picker:not(.ql-color-picker):not(.ql-icon-picker)
svg
position: absolute
margin-top: -9px
right: 0
top: 50%
width: 18px
.ql-picker.ql-header, .ql-picker.ql-font, .ql-picker.ql-size
.ql-picker-label[data-label]:not([data-label='']),
.ql-picker-item[data-label]:not([data-label=''])
&::before
content: attr(data-label)
.ql-picker.ql-header
width: 98px
.ql-picker-label::before,
.ql-picker-item::before
content: 'Normal'
for num in (1..6)
.ql-picker-label[data-value=\"{num}\"]::before,
.ql-picker-item[data-value=\"{num}\"]::before
content: 'Heading ' + num
.ql-picker-item[data-value="1"]::before
font-size: 2em
.ql-picker-item[data-value="2"]::before
font-size: 1.5em
.ql-picker-item[data-value="3"]::before
font-size: 1.17em
.ql-picker-item[data-value="4"]::before
font-size: 1em
.ql-picker-item[data-value="5"]::before
font-size: 0.83em
.ql-picker-item[data-value="6"]::before
font-size: 0.67em
.ql-picker.ql-font
width: 108px
.ql-picker-label::before,
.ql-picker-item::before
content: 'Sans Serif'
.ql-picker-label[data-value=serif]::before,
.ql-picker-item[data-value=serif]::before
content: 'Serif'
.ql-picker-label[data-value=monospace]::before,
.ql-picker-item[data-value=monospace]::before
content: 'Monospace'
.ql-picker-item[data-value=serif]::before
font-family: Georgia, Times New Roman, serif
.ql-picker-item[data-value=monospace]::before
font-family: Monaco, Courier New, monospace
.ql-picker.ql-size
width: 98px
.ql-picker-label::before,
.ql-picker-item::before
content: 'Normal'
.ql-picker-label[data-value=small]::before,
.ql-picker-item[data-value=small]::before
content: 'Small'
.ql-picker-label[data-value=large]::before,
.ql-picker-item[data-value=large]::before
content: 'Large'
.ql-picker-label[data-value=huge]::before,
.ql-picker-item[data-value=huge]::before
content: 'Huge'
.ql-picker-item[data-value=small]::before
font-size: 10px
.ql-picker-item[data-value=large]::before
font-size: 18px
.ql-picker-item[data-value=huge]::before
font-size: 32px
.ql-color-picker.ql-background
.ql-picker-item
background-color: #fff
.ql-color-picker.ql-color
.ql-picker-item
background-color: #000
.ql-code-block-container
position: relative
.ql-ui
right: 5px
top: 5px
================================================
FILE: packages/quill/src/assets/bubble/toolbar.styl
================================================
arrowWidth = 6px
.ql-bubble
.ql-toolbar
.ql-formats
margin: 8px 12px 8px 0px
.ql-formats:first-child
margin-left: 12px
.ql-color-picker
svg
margin: 1px
.ql-picker-item.ql-selected, .ql-picker-item:hover
border-color: activeColor
================================================
FILE: packages/quill/src/assets/bubble/tooltip.styl
================================================
arrowWidth = 6px
.ql-bubble
.ql-tooltip
background-color: backgroundColor
border-radius: 25px
color: textColor
.ql-tooltip-arrow
border-left: arrowWidth solid transparent
border-right: arrowWidth solid transparent
content: " "
display: block
left: 50%
margin-left: -1 * arrowWidth
position: absolute
.ql-tooltip:not(.ql-flip) .ql-tooltip-arrow
border-bottom: arrowWidth solid backgroundColor
top: -1 * arrowWidth
.ql-tooltip.ql-flip .ql-tooltip-arrow
border-top: arrowWidth solid backgroundColor
bottom: -1 * arrowWidth
.ql-tooltip.ql-editing
.ql-tooltip-editor
display: block
.ql-formats
visibility: hidden
.ql-tooltip-editor
display: none
input[type=text]
background: transparent
border: none
color: textColor
font-size: 13px
height: 100%
outline: none
padding: 10px 20px
position: absolute
width: 100%
a
&:before
color: inactiveColor
content: "\00D7"
font-size: 16px
font-weight: bold
top: 10px
position: absolute
right: 20px
================================================
FILE: packages/quill/src/assets/bubble.styl
================================================
themeName = 'bubble'
activeColor = #fff
borderColor = #777
backgroundColor = #444
inactiveColor = #ccc
shadowColor = #ddd
textColor = #fff
@import './core'
@import './base'
@import './bubble/*'
.ql-container.ql-bubble:not(.ql-disabled)
a:not(.ql-close)
position: relative
white-space: nowrap
a:not(.ql-close)::before
background-color: #444
border-radius: 15px
top: -5px
font-size: 12px
color: #fff
content: attr(href)
font-weight: normal
overflow: hidden
padding: 5px 15px
text-decoration: none
z-index: 1
a:not(.ql-close)::after
border-top: 6px solid #444
border-left: 6px solid transparent
border-right: 6px solid transparent
top: 0
content: " "
height: 0
width: 0
a:not(.ql-close)::before, a:not(.ql-close)::after
left: 0
margin-left: 50%
position: absolute
transform: translate(-50%, -100%)
transition: visibility 0s ease 200ms
visibility: hidden
a:not(.ql-close):hover::before, a:not(.ql-close):hover::after
visibility: visible
================================================
FILE: packages/quill/src/assets/core.styl
================================================
// Styles necessary for Quill
LIST_STYLE = decimal lower-alpha lower-roman
LIST_STYLE_WIDTH = 1.2em
LIST_STYLE_MARGIN = 0.3em
LIST_STYLE_OUTER_WIDTH = LIST_STYLE_MARGIN + LIST_STYLE_WIDTH
MAX_INDENT = 9
resets(arr)
unquote('list-' + join(' list-', arr))
.ql-container
box-sizing: border-box
font-family: Helvetica, Arial, sans-serif
font-size: 13px
height: 100%
margin: 0px
position: relative
.ql-container.ql-disabled
.ql-tooltip
visibility: hidden
.ql-container:not(.ql-disabled)
li[data-list=checked],
li[data-list=unchecked]
> .ql-ui
cursor: pointer
.ql-clipboard
left: -100000px
height: 1px
overflow-y: hidden
position: absolute
top: 50%
p
margin: 0
padding: 0
.ql-editor
box-sizing: border-box
counter-reset: resets(0..MAX_INDENT)
line-height: 1.42
height: 100%
outline: none
overflow-y: auto
padding: 12px 15px
tab-size: 4
-moz-tab-size: 4
text-align: left
white-space: pre-wrap
word-wrap: break-word
> *
cursor: text
p, ol, pre, blockquote, h1, h2, h3, h4, h5, h6
margin: 0
padding: 0
p, h1, h2, h3, h4, h5, h6
@supports (counter-set: none)
counter-set: resets(0..MAX_INDENT)
@supports not (counter-set: none)
counter-reset: resets(0..MAX_INDENT)
table
border-collapse: collapse
td
border: 1px solid #000
padding: 2px 5px
ol
padding-left: 1.5em
li
list-style-type: none
padding-left: LIST_STYLE_OUTER_WIDTH
position: relative
> .ql-ui:before
display: inline-block
margin-left: -1*LIST_STYLE_OUTER_WIDTH
margin-right: LIST_STYLE_MARGIN
text-align: right
white-space: nowrap
width: LIST_STYLE_WIDTH
li[data-list=checked],
li[data-list=unchecked]
> .ql-ui
color: #777
li[data-list=bullet] > .ql-ui:before
content: '\2022'
li[data-list=checked] > .ql-ui:before
content: '\2611'
li[data-list=unchecked] > .ql-ui:before
content: '\2610'
li[data-list]
@supports (counter-set: none)
counter-set: resets(1..MAX_INDENT)
@supports not (counter-set: none)
counter-reset: resets(1..MAX_INDENT)
li[data-list=ordered]
counter-increment: list-0
> .ql-ui:before
content: unquote('counter(list-0, ' + LIST_STYLE[0] + ')') '. '
for num in (1..MAX_INDENT)
li[data-list=ordered].ql-indent-{num}
counter-increment: unquote('list-' + num)
> .ql-ui:before
content: unquote('counter(list-' + num + ', ' + LIST_STYLE[num%3] + ')') '. '
if (num < MAX_INDENT)
li[data-list].ql-indent-{num}
@supports (counter-set: none)
counter-set: resets((num+1)..MAX_INDENT)
@supports not (counter-set: none)
counter-reset: resets((num+1)..MAX_INDENT)
for num in (1..MAX_INDENT)
.ql-indent-{num}:not(.ql-direction-rtl)
padding-left: (3*num)em
li.ql-indent-{num}:not(.ql-direction-rtl)
padding-left: (3*num + LIST_STYLE_OUTER_WIDTH)em
.ql-indent-{num}.ql-direction-rtl.ql-align-right
padding-right: (3*num)em
li.ql-indent-{num}.ql-direction-rtl.ql-align-right
padding-right: (3*num + LIST_STYLE_OUTER_WIDTH)em
li.ql-direction-rtl
padding-right: LIST_STYLE_OUTER_WIDTH
> .ql-ui:before
margin-left: LIST_STYLE_MARGIN
margin-right: -1*LIST_STYLE_OUTER_WIDTH
text-align: left
table
table-layout: fixed
width: 100%
td
outline: none
.ql-code-block-container
font-family: monospace
.ql-video
display: block
max-width: 100%
.ql-video.ql-align-center
margin: 0 auto
.ql-video.ql-align-right
margin: 0 0 0 auto
.ql-bg-black
background-color: rgb(0,0,0)
.ql-bg-red
background-color: rgb(230,0,0)
.ql-bg-orange
background-color: rgb(255,153,0)
.ql-bg-yellow
background-color: rgb(255,255,0)
.ql-bg-green
background-color: rgb(0,138,0)
.ql-bg-blue
background-color: rgb(0,102,204)
.ql-bg-purple
background-color: rgb(153,51,255)
.ql-color-white
color: rgb(255,255,255)
.ql-color-red
color: rgb(230,0,0)
.ql-color-orange
color: rgb(255,153,0)
.ql-color-yellow
color: rgb(255,255,0)
.ql-color-green
color: rgb(0,138,0)
.ql-color-blue
color: rgb(0,102,204)
.ql-color-purple
color: rgb(153,51,255)
.ql-font-serif
font-family: Georgia, Times New Roman, serif
.ql-font-monospace
font-family: Monaco, Courier New, monospace
.ql-size-small
font-size: 0.75em
.ql-size-large
font-size: 1.5em
.ql-size-huge
font-size: 2.5em
.ql-direction-rtl
direction: rtl
text-align: inherit
.ql-align-center
text-align: center
.ql-align-justify
text-align: justify
.ql-align-right
text-align: right
.ql-ui
position: absolute
.ql-editor.ql-blank::before
color: rgba(0,0,0,0.6)
content: attr(data-placeholder)
font-style: italic
left: 15px
pointer-events: none
position: absolute
right: 15px
================================================
FILE: packages/quill/src/assets/snow/toolbar.styl
================================================
.ql-toolbar.ql-snow
border: 1px solid borderColor
box-sizing: border-box
font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif
padding: 8px
.ql-formats
margin-right: 15px
.ql-picker-label
border: 1px solid transparent
.ql-picker-options
border: 1px solid transparent
box-shadow: rgba(0,0,0,0.2) 0 2px 8px
.ql-picker.ql-expanded
.ql-picker-label
border-color: borderColor
.ql-picker-options
border-color: borderColor
.ql-color-picker
.ql-picker-item.ql-selected, .ql-picker-item:hover
border-color: #000
.ql-toolbar.ql-snow + .ql-container.ql-snow
border-top: 0px;
================================================
FILE: packages/quill/src/assets/snow/tooltip.styl
================================================
tooltipMargin = 8px
.ql-snow
.ql-tooltip
background-color: #fff
border: 1px solid borderColor
box-shadow: 0px 0px 5px shadowColor
color: textColor
padding: 5px 12px
white-space: nowrap
&::before
content: "Visit URL:"
line-height: 26px
margin-right: tooltipMargin
input[type=text]
display: none
border: 1px solid borderColor
font-size: 13px
height: 26px
margin: 0px
padding: 3px 5px
width: 170px
a.ql-preview
display: inline-block
max-width: 200px
overflow-x: hidden
text-overflow: ellipsis
vertical-align: top
a.ql-action::after
border-right: 1px solid borderColor
content: 'Edit'
margin-left: tooltipMargin*2
padding-right: tooltipMargin
a.ql-remove::before
content: 'Remove'
margin-left: tooltipMargin
a
line-height: 26px
.ql-tooltip.ql-editing
a.ql-preview, a.ql-remove
display: none
input[type=text]
display: inline-block
a.ql-action::after
border-right: 0px
content: 'Save'
padding-right: 0px
.ql-tooltip[data-mode=link]::before
content: "Enter link:"
.ql-tooltip[data-mode=formula]::before
content: "Enter formula:"
.ql-tooltip[data-mode=video]::before
content: "Enter video:"
================================================
FILE: packages/quill/src/assets/snow.styl
================================================
themeName = 'snow'
activeColor = #06c
borderColor = #ccc
backgroundColor = #fff
inactiveColor = #444
shadowColor = #ddd
textColor = #444
@import './core'
@import './base'
@import './snow/*'
.ql-snow
a
color: activeColor
.ql-container.ql-snow
border: 1px solid borderColor
================================================
FILE: packages/quill/src/blots/block.ts
================================================
import {
AttributorStore,
BlockBlot,
EmbedBlot,
LeafBlot,
Scope,
} from 'parchment';
import type { Blot, Parent } from 'parchment';
import Delta from 'quill-delta';
import Break from './break.js';
import Inline from './inline.js';
import TextBlot from './text.js';
const NEWLINE_LENGTH = 1;
class Block extends BlockBlot {
cache: { delta?: Delta | null; length?: number } = {};
delta(): Delta {
if (this.cache.delta == null) {
this.cache.delta = blockDelta(this);
}
return this.cache.delta;
}
deleteAt(index: number, length: number) {
super.deleteAt(index, length);
this.cache = {};
}
formatAt(index: number, length: number, name: string, value: unknown) {
if (length <= 0) return;
if (this.scroll.query(name, Scope.BLOCK)) {
if (index + length === this.length()) {
this.format(name, value);
}
} else {
super.formatAt(
index,
Math.min(length, this.length() - index - 1),
name,
value,
);
}
this.cache = {};
}
insertAt(index: number, value: string, def?: unknown) {
if (def != null) {
super.insertAt(index, value, def);
this.cache = {};
return;
}
if (value.length === 0) return;
const lines = value.split('\n');
const text = lines.shift() as string;
if (text.length > 0) {
if (index < this.length() - 1 || this.children.tail == null) {
super.insertAt(Math.min(index, this.length() - 1), text);
} else {
this.children.tail.insertAt(this.children.tail.length(), text);
}
this.cache = {};
}
// TODO: Fix this next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-this-alias
let block: Blot | this = this;
lines.reduce((lineIndex, line) => {
// @ts-expect-error Fix me later
block = block.split(lineIndex, true);
block.insertAt(0, line);
return line.length;
}, index + text.length);
}
insertBefore(blot: Blot, ref?: Blot | null) {
const { head } = this.children;
super.insertBefore(blot, ref);
if (head instanceof Break) {
head.remove();
}
this.cache = {};
}
length() {
if (this.cache.length == null) {
this.cache.length = super.length() + NEWLINE_LENGTH;
}
return this.cache.length;
}
moveChildren(target: Parent, ref?: Blot | null) {
super.moveChildren(target, ref);
this.cache = {};
}
optimize(context: { [key: string]: any }) {
super.optimize(context);
this.cache = {};
}
path(index: number) {
return super.path(index, true);
}
removeChild(child: Blot) {
super.removeChild(child);
this.cache = {};
}
split(index: number, force: boolean | undefined = false): Blot | null {
if (force && (index === 0 || index >= this.length() - NEWLINE_LENGTH)) {
const clone = this.clone();
if (index === 0) {
this.parent.insertBefore(clone, this);
return this;
}
this.parent.insertBefore(clone, this.next);
return clone;
}
const next = super.split(index, force);
this.cache = {};
return next;
}
}
Block.blotName = 'block';
Block.tagName = 'P';
Block.defaultChild = Break;
Block.allowedChildren = [Break, Inline, EmbedBlot, TextBlot];
class BlockEmbed extends EmbedBlot {
attributes: AttributorStore;
domNode: HTMLElement;
attach() {
super.attach();
this.attributes = new AttributorStore(this.domNode);
}
delta() {
return new Delta().insert(this.value(), {
...this.formats(),
...this.attributes.values(),
});
}
format(name: string, value: unknown) {
const attribute = this.scroll.query(name, Scope.BLOCK_ATTRIBUTE);
if (attribute != null) {
// @ts-expect-error TODO: Scroll#query() should return Attributor when scope is attribute
this.attributes.attribute(attribute, value);
}
}
formatAt(index: number, length: number, name: string, value: unknown) {
this.format(name, value);
}
insertAt(index: number, value: string, def?: unknown) {
if (def != null) {
super.insertAt(index, value, def);
return;
}
const lines = value.split('\n');
const text = lines.pop();
const blocks = lines.map((line) => {
const block = this.scroll.create(Block.blotName);
block.insertAt(0, line);
return block;
});
const ref = this.split(index);
blocks.forEach((block) => {
this.parent.insertBefore(block, ref);
});
if (text) {
this.parent.insertBefore(this.scroll.create('text', text), ref);
}
}
}
BlockEmbed.scope = Scope.BLOCK_BLOT;
// It is important for cursor behavior BlockEmbeds use tags that are block level elements
function blockDelta(blot: BlockBlot, filter = true) {
return blot
.descendants(LeafBlot)
.reduce((delta, leaf) => {
if (leaf.length() === 0) {
return delta;
}
return delta.insert(leaf.value(), bubbleFormats(leaf, {}, filter));
}, new Delta())
.insert('\n', bubbleFormats(blot));
}
function bubbleFormats(
blot: Blot | null,
formats: Record<string, unknown> = {},
filter = true,
): Record<string, unknown> {
if (blot == null) return formats;
if ('formats' in blot && typeof blot.formats === 'function') {
formats = {
...formats,
...blot.formats(),
};
if (filter) {
// exclude syntax highlighting from deltas and getFormat()
delete formats['code-token'];
}
}
if (
blot.parent == null ||
blot.parent.statics.blotName === 'scroll' ||
blot.parent.statics.scope !== blot.statics.scope
) {
return formats;
}
return bubbleFormats(blot.parent, formats, filter);
}
export { blockDelta, bubbleFormats, BlockEmbed, Block as default };
================================================
FILE: packages/quill/src/blots/break.ts
================================================
import { EmbedBlot } from 'parchment';
class Break extends EmbedBlot {
static value() {
return undefined;
}
optimize() {
if (this.prev || this.next) {
this.remove();
}
}
length() {
return 0;
}
value() {
return '';
}
}
Break.blotName = 'break';
Break.tagName = 'BR';
export default Break;
================================================
FILE: packages/quill/src/blots/container.ts
================================================
import { ContainerBlot } from 'parchment';
class Container extends ContainerBlot {}
export default Container;
================================================
FILE: packages/quill/src/blots/cursor.ts
================================================
import { EmbedBlot, Scope } from 'parchment';
import type { Parent, ScrollBlot } from 'parchment';
import type Selection from '../core/selection.js';
import TextBlot from './text.js';
import type { EmbedContextRange } from './embed.js';
class Cursor extends EmbedBlot {
static blotName = 'cursor';
static className = 'ql-cursor';
static tagName = 'span';
static CONTENTS = '\uFEFF'; // Zero width no break space
static value() {
return undefined;
}
selection: Selection;
textNode: Text;
savedLength: number;
constructor(scroll: ScrollBlot, domNode: HTMLElement, selection: Selection) {
super(scroll, domNode);
this.selection = selection;
this.textNode = document.createTextNode(Cursor.CONTENTS);
this.domNode.appendChild(this.textNode);
this.savedLength = 0;
}
detach() {
// super.detach() will also clear domNode.__blot
if (this.parent != null) this.parent.removeChild(this);
}
format(name: string, value: unknown) {
if (this.savedLength !== 0) {
super.format(name, value);
return;
}
// TODO: Fix this next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-this-alias
let target: Parent | this = this;
let index = 0;
while (target != null && target.statics.scope !== Scope.BLOCK_BLOT) {
index += target.offset(target.parent);
target = target.parent;
}
if (target != null) {
this.savedLength = Cursor.CONTENTS.length;
// @ts-expect-error TODO: allow empty context in Parchment
target.optimize();
target.formatAt(index, Cursor.CONTENTS.length, name, value);
this.savedLength = 0;
}
}
index(node: Node, offset: number) {
if (node === this.textNode) return 0;
return super.index(node, offset);
}
length() {
return this.savedLength;
}
position(): [Text, number] {
return [this.textNode, this.textNode.data.length];
}
remove() {
super.remove();
// @ts-expect-error Fix me later
this.parent = null;
}
restore(): EmbedContextRange | null {
if (this.selection.composing || this.parent == null) return null;
const range = this.selection.getNativeRange();
// Browser may push down styles/nodes inside the cursor blot.
// https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#push-down-values
while (
this.domNode.lastChild != null &&
this.domNode.lastChild !== this.textNode
) {
// @ts-expect-error Fix me later
this.domNode.parentNode.insertBefore(
this.domNode.lastChild,
this.domNode,
);
}
const prevTextBlot = this.prev instanceof TextBlot ? this.prev : null;
const prevTextLength = prevTextBlot ? prevTextBlot.length() : 0;
const nextTextBlot = this.next instanceof TextBlot ? this.next : null;
// @ts-expect-error TODO: make TextBlot.text public
const nextText = nextTextBlot ? nextTextBlot.text : '';
const { textNode } = this;
// take text from inside this blot and reset it
const newText = textNode.data.split(Cursor.CONTENTS).join('');
textNode.data = Cursor.CONTENTS;
// proactively merge TextBlots around cursor so that optimization
// doesn't lose the cursor. the reason we are here in cursor.restore
// could be that the user clicked in prevTextBlot or nextTextBlot, or
// the user typed something.
let mergedTextBlot;
if (prevTextBlot) {
mergedTextBlot = prevTextBlot;
if (newText || nextTextBlot) {
prevTextBlot.insertAt(prevTextBlot.length(), newText + nextText);
if (nextTextBlot) {
nextTextBlot.remove();
}
}
} else if (nextTextBlot) {
mergedTextBlot = nextTextBlot;
nextTextBlot.insertAt(0, newText);
} else {
const newTextNode = document.createTextNode(newText);
mergedTextBlot = this.scroll.create(newTextNode);
this.parent.insertBefore(mergedTextBlot, this);
}
this.remove();
if (range) {
// calculate selection to restore
const remapOffset = (node: Node, offset: number) => {
if (prevTextBlot && node === prevTextBlot.domNode) {
return offset;
}
if (node === textNode) {
return prevTextLength + offset - 1;
}
if (nextTextBlot && node === nextTextBlot.domNode) {
return prevTextLength + newText.length + offset;
}
return null;
};
const start = remapOffset(range.start.node, range.start.offset);
const end = remapOffset(range.end.node, range.end.offset);
if (start !== null && end !== null) {
return {
startNode: mergedTextBlot.domNode,
startOffset: start,
endNode: mergedTextBlot.domNode,
endOffset: end,
};
}
}
return null;
}
update(mutations: MutationRecord[], context: Record<string, unknown>) {
if (
mutations.some((mutation) => {
return (
mutation.type === 'characterData' && mutation.target === this.textNode
);
})
) {
const range = this.restore();
if (range) context.range = range;
}
}
// Avoid .ql-cursor being a descendant of `<a/>`.
// The reason is Safari pushes down `<a/>` on text insertion.
// That will cause DOM nodes not sync with the model.
//
// For example ({I} is the caret), given the markup:
// <a><span class="ql-cursor">\uFEFF{I}</span></a>
// When typing a char "x", `<a/>` will be pushed down inside the `<span>` first:
// <span class="ql-cursor"><a>\uFEFF{I}</a></span>
// And then "x" will be inserted after `<a/>`:
// <span class="ql-cursor"><a>\uFEFF</a>d{I}</span>
optimize(context?: unknown) {
// @ts-expect-error Fix me later
super.optimize(context);
let { parent } = this;
while (parent) {
if (parent.domNode.tagName === 'A') {
this.savedLength = Cursor.CONTENTS.length;
// @ts-expect-error TODO: make isolate generic
parent.isolate(this.offset(parent), this.length()).unwrap();
this.savedLength = 0;
break;
}
parent = parent.parent;
}
}
value() {
return '';
}
}
export default Cursor;
================================================
FILE: packages/quill/src/blots/embed.ts
================================================
import type { ScrollBlot } from 'parchment';
import { EmbedBlot } from 'parchment';
import TextBlot from './text.js';
const GUARD_TEXT = '\uFEFF';
export interface EmbedContextRange {
startNode: Node | Text;
startOffset: number;
endNode?: Node | Text;
endOffset?: number;
}
class Embed extends EmbedBlot {
contentNode: HTMLSpanElement;
leftGuard: Text;
rightGuard: Text;
constructor(scroll: ScrollBlot, node: Node) {
super(scroll, node);
this.contentNode = document.createElement('span');
this.contentNode.setAttribute('contenteditable', 'false');
Array.from(this.domNode.childNodes).forEach((childNode) => {
this.contentNode.appendChild(childNode);
});
this.leftGuard = document.createTextNode(GUARD_TEXT);
this.rightGuard = document.createTextNode(GUARD_TEXT);
this.domNode.appendChild(this.leftGuard);
this.domNode.appendChild(this.contentNode);
this.domNode.appendChild(this.rightGuard);
}
index(node: Node, offset: number) {
if (node === this.leftGuard) return 0;
if (node === this.rightGuard) return 1;
return super.index(node, offset);
}
restore(node: Text): EmbedContextRange | null {
let range: EmbedContextRange | null = null;
let textNode: Text;
const text = node.data.split(GUARD_TEXT).join('');
if (node === this.leftGuard) {
if (this.prev instanceof TextBlot) {
const prevLength = this.prev.length();
this.prev.insertAt(prevLength, text);
range = {
startNode: this.prev.domNode,
startOffset: prevLength + text.length,
};
} else {
textNode = document.createTextNode(text);
this.parent.insertBefore(this.scroll.create(textNode), this);
range = {
startNode: textNode,
startOffset: text.length,
};
}
} else if (node === this.rightGuard) {
if (this.next instanceof TextBlot) {
this.next.insertAt(0, text);
range = {
startNode: this.next.domNode,
startOffset: text.length,
};
} else {
textNode = document.createTextNode(text);
this.parent.insertBefore(this.scroll.create(textNode), this.next);
range = {
startNode: textNode,
startOffset: text.length,
};
}
}
node.data = GUARD_TEXT;
return range;
}
update(mutations: MutationRecord[], context: Record<string, unknown>) {
mutations.forEach((mutation) => {
if (
mutation.type === 'characterData' &&
(mutation.target === this.leftGuard ||
mutation.target === this.rightGuard)
) {
const range = this.restore(mutation.target as Text);
if (range) context.range = range;
}
});
}
}
export default Embed;
================================================
FILE: packages/quill/src/blots/inline.ts
================================================
import { EmbedBlot, InlineBlot, Scope } from 'parchment';
import type { BlotConstructor } from 'parchment';
import Break from './break.js';
import Text from './text.js';
class Inline extends InlineBlot {
static allowedChildren: BlotConstructor[] = [Inline, Break, EmbedBlot, Text];
// Lower index means deeper in the DOM tree, since not found (-1) is for embeds
static order = [
'cursor',
'inline', // Must be lower
'link', // Chrome wants <a> to be lower
'underline',
'strike',
'italic',
'bold',
'script',
'code', // Must be higher
];
static compare(self: string, other: string) {
const selfIndex = Inline.order.indexOf(self);
const otherIndex = Inline.order.indexOf(other);
if (selfIndex >= 0 || otherIndex >= 0) {
return selfIndex - otherIndex;
}
if (self === other) {
return 0;
}
if (self < other) {
return -1;
}
return 1;
}
formatAt(index: number, length: number, name: string, value: unknown) {
if (
Inline.compare(this.statics.blotName, name) < 0 &&
this.scroll.query(name, Scope.BLOT)
) {
const blot = this.isolate(index, length);
if (value) {
blot.wrap(name, value);
}
} else {
super.formatAt(index, length, name, value);
}
}
optimize(context: { [key: string]: any }) {
super.optimize(context);
if (
this.parent instanceof Inline &&
Inline.compare(this.statics.blotName, this.parent.statics.blotName) > 0
) {
const parent = this.parent.isolate(this.offset(), this.length());
// @ts-expect-error TODO: make isolate generic
this.moveChildren(parent);
parent.wrap(this);
}
}
}
export default Inline;
================================================
FILE: packages/quill/src/blots/scroll.ts
================================================
import { ContainerBlot, LeafBlot, Scope, ScrollBlot } from 'parchment';
import type { Blot, Parent, EmbedBlot, ParentBlot, Registry } from 'parchment';
import Delta, { AttributeMap, Op } from 'quill-delta';
import Emitter from '../core/emitter.js';
import type { EmitterSource } from '../core/emitter.js';
import Block, { BlockEmbed, bubbleFormats } from './block.js';
import Break from './break.js';
import Container from './container.js';
type RenderBlock =
| {
type: 'blockEmbed';
attributes: AttributeMap;
key: string;
value: unknown;
}
| { type: 'block'; attributes: AttributeMap; delta: Delta };
function isLine(blot: unknown): blot is Block | BlockEmbed {
return blot instanceof Block || blot instanceof BlockEmbed;
}
interface UpdatableEmbed {
updateContent(change: unknown): void;
}
function isUpdatable(blot: Blot): blot is Blot & UpdatableEmbed {
return typeof (blot as unknown as any).updateContent === 'function';
}
class Scroll extends ScrollBlot {
static blotName = 'scroll';
static className = 'ql-editor';
static tagName = 'DIV';
static defaultChild = Block;
static allowedChildren = [Block, BlockEmbed, Container];
emitter: Emitter;
batch: false | MutationRecord[];
constructor(
registry: Registry,
domNode: HTMLDivElement,
{ emitter }: { emitter: Emitter },
) {
super(registry, domNode);
this.emitter = emitter;
this.batch = false;
this.optimize();
this.enable();
this.domNode.addEventListener('dragstart', (e) => this.handleDragStart(e));
}
batchStart() {
if (!Array.isArray(this.batch)) {
this.batch = [];
}
}
batchEnd() {
if (!this.batch) return;
const mutations = this.batch;
this.batch = false;
this.update(mutations);
}
emitMount(blot: Blot) {
this.emitter.emit(Emitter.events.SCROLL_BLOT_MOUNT, blot);
}
emitUnmount(blot: Blot) {
this.emitter.emit(Emitter.events.SCROLL_BLOT_UNMOUNT, blot);
}
emitEmbedUpdate(blot: Blot, change: unknown) {
this.emitter.emit(Emitter.events.SCROLL_EMBED_UPDATE, blot, change);
}
deleteAt(index: number, length: number) {
const [first, offset] = this.line(index);
const [last] = this.line(index + length);
super.deleteAt(index, length);
if (last != null && first !== last && offset > 0) {
if (first instanceof BlockEmbed || last instanceof BlockEmbed) {
this.optimize();
return;
}
const ref =
last.children.head instanceof Break ? null : last.children.head;
// @ts-expect-error
first.moveChildren(last, ref);
// @ts-expect-error
first.remove();
}
this.optimize();
}
enable(enabled = true) {
this.domNode.setAttribute('contenteditable', enabled ? 'true' : 'false');
}
formatAt(index: number, length: number, format: string, value: unknown) {
super.formatAt(index, length, format, value);
this.optimize();
}
insertAt(index: number, value: string, def?: unknown) {
if (index >= this.length()) {
if (def == null || this.scroll.query(value, Scope.BLOCK) == null) {
const blot = this.scroll.create(this.statics.defaultChild.blotName);
this.appendChild(blot);
if (def == null && value.endsWith('\n')) {
blot.insertAt(0, value.slice(0, -1), def);
} else {
blot.insertAt(0, value, def);
}
} else {
const embed = this.scroll.create(value, def);
this.appendChild(embed);
}
} else {
super.insertAt(index, value, def);
}
this.optimize();
}
insertBefore(blot: Blot, ref?: Blot | null) {
if (blot.statics.scope === Scope.INLINE_BLOT) {
const wrapper = this.scroll.create(
this.statics.defaultChild.blotName,
) as Parent;
wrapper.appendChild(blot);
super.insertBefore(wrapper, ref);
} else {
super.insertBefore(blot, ref);
}
}
insertContents(index: number, delta: Delta) {
const renderBlocks = this.deltaToRenderBlocks(
delta.concat(new Delta().insert('\n')),
);
const last = renderBlocks.pop();
if (last == null) return;
this.batchStart();
const first = renderBlocks.shift();
if (first) {
const shouldInsertNewlineChar =
first.type === 'block' &&
(first.delta.length() === 0 ||
(!this.descendant(BlockEmbed, index)[0] && index < this.length()));
const delta =
first.type === 'block'
? first.delta
: new Delta().insert({ [first.key]: first.value });
insertInlineContents(this, index, delta);
const newlineCharLength = first.type === 'block' ? 1 : 0;
const lineEndIndex = index + delta.length() + newlineCharLength;
if (shouldInsertNewlineChar) {
this.insertAt(lineEndIndex - 1, '\n');
}
const formats = bubbleFormats(this.line(index)[0]);
const attributes = AttributeMap.diff(formats, first.attributes) || {};
Object.keys(attributes).forEach((name) => {
this.formatAt(lineEndIndex - 1, 1, name, attributes[name]);
});
index = lineEndIndex;
}
let [refBlot, refBlotOffset] = this.children.find(index);
if (renderBlocks.length) {
if (refBlot) {
refBlot = refBlot.split(refBlotOffset);
refBlotOffset = 0;
}
renderBlocks.forEach((renderBlock) => {
if (renderBlock.type === 'block') {
const block = this.createBlock(
renderBlock.attributes,
refBlot || undefined,
);
insertInlineContents(block, 0, renderBlock.delta);
} else {
const blockEmbed = this.create(
renderBlock.key,
renderBlock.value,
) as EmbedBlot;
this.insertBefore(blockEmbed, refBlot || undefined);
Object.keys(renderBlock.attributes).forEach((name) => {
blockEmbed.format(name, renderBlock.attributes[name]);
});
}
});
}
if (last.type === 'block' && last.delta.length()) {
const offset = refBlot
? refBlot.offset(refBlot.scroll) + refBlotOffset
: this.length();
insertInlineContents(this, offset, last.delta);
}
this.batchEnd();
this.optimize();
}
isEnabled() {
return this.domNode.getAttribute('contenteditable') === 'true';
}
leaf(index: number): [LeafBlot | null, number] {
const last = this.path(index).pop();
if (!last) {
return [null, -1];
}
const [blot, offset] = last;
return blot instanceof LeafBlot ? [blot, offset] : [null, -1];
}
line(index: number): [Block | BlockEmbed | null, number] {
if (index === this.length()) {
return this.line(index - 1);
}
// @ts-expect-error TODO: make descendant() generic
return this.descendant(isLine, index);
}
lines(index = 0, length = Number.MAX_VALUE): (Block | BlockEmbed)[] {
const getLines = (
blot: ParentBlot,
blotIndex: number,
blotLength: number,
) => {
let lines: (Block | BlockEmbed)[] = [];
let lengthLeft = blotLength;
blot.children.forEachAt(
blotIndex,
blotLength,
(child, childIndex, childLength) => {
if (isLine(child)) {
lines.push(child);
} else if (child instanceof ContainerBlot) {
lines = lines.concat(getLines(child, childIndex, lengthLeft));
}
lengthLeft -= childLength;
},
);
return lines;
};
return getLines(this, index, length);
}
optimize(context?: { [key: string]: any }): void;
optimize(
mutations?: MutationRecord[],
context?: { [key: string]: any },
): void;
optimize(mutations = [], context = {}) {
if (this.batch) return;
super.optimize(mutations, context);
if (mutations.length > 0) {
this.emitter.emit(Emitter.events.SCROLL_OPTIMIZE, mutations, context);
}
}
path(index: number) {
return super.path(index).slice(1); // Exclude self
}
remove() {
// Never remove self
}
update(source?: EmitterSource): void;
update(mutations?: MutationRecord[]): void;
update(mutations?: MutationRecord[] | EmitterSource): void {
if (this.batch) {
if (Array.isArray(mutations)) {
this.batch = this.batch.concat(mutations);
}
return;
}
let source: EmitterSource = Emitter.sources.USER;
if (typeof mutations === 'string') {
source = mutations;
}
if (!Array.isArray(mutations)) {
mutations = this.observer.takeRecords();
}
mutations = mutations.filter(({ target }) => {
const blot = this.find(target, true);
return blot && !isUpdatable(blot);
});
if (mutations.length > 0) {
this.emitter.emit(Emitter.events.SCROLL_BEFORE_UPDATE, source, mutations);
}
super.update(mutations.concat([])); // pass copy
if (mutations.length > 0) {
this.emitter.emit(Emitter.events.SCROLL_UPDATE, source, mutations);
}
}
updateEmbedAt(index: number, key: string, change: unknown) {
// Currently it only supports top-level embeds (BlockEmbed).
// We can update `ParentBlot` in parchment to support inline embeds.
const [blot] = this.descendant((b: Blot) => b instanceof BlockEmbed, index);
if (blot && blot.statics.blotName === key && isUpdatable(blot)) {
blot.updateContent(change);
}
}
protected handleDragStart(event: DragEvent) {
event.preventDefault();
}
private deltaToRenderBlocks(delta: Delta) {
const renderBlocks: RenderBlock[] = [];
let currentBlockDelta = new Delta();
delta.forEach((op) => {
const insert = op?.insert;
if (!insert) return;
if (typeof insert === 'string') {
const splitted = insert.split('\n');
splitted.slice(0, -1).forEach((text) => {
currentBlockDelta.insert(text, op.attributes);
renderBlocks.push({
type: 'block',
delta: currentBlockDelta,
attributes: op.attributes ?? {},
});
currentBlockDelta = new Delta();
});
const last = splitted[splitted.length - 1];
if (last) {
currentBlockDelta.insert(last, op.attributes);
}
} else {
const key = Object.keys(insert)[0];
if (!key) return;
if (this.query(key, Scope.INLINE)) {
currentBlockDelta.push(op);
} else {
if (currentBlockDelta.length()) {
renderBlocks.push({
type: 'block',
delta: currentBlockDelta,
attributes: {},
});
}
currentBlockDelta = new Delta();
renderBlocks.push({
type: 'blockEmbed',
key,
value: insert[key],
attributes: op.attributes ?? {},
});
}
}
});
if (currentBlockDelta.length()) {
renderBlocks.push({
type: 'block',
delta: currentBlockDelta,
attributes: {},
});
}
return renderBlocks;
}
private createBlock(attributes: AttributeMap, refBlot?: Blot) {
let blotName: string | undefined;
const formats: AttributeMap = {};
Object.entries(attributes).forEach(([key, value]) => {
const isBlockBlot = this.query(key, Scope.BLOCK & Scope.BLOT) != null;
if (isBlockBlot) {
blotName = key;
} else {
formats[key] = value;
}
});
const block = this.create(
blotName || this.statics.defaultChild.blotName,
blotName ? attributes[blotName] : undefined,
) as ParentBlot;
this.insertBefore(block, refBlot || undefined);
const length = block.length();
Object.entries(formats).forEach(([key, value]) => {
block.formatAt(0, length, key, value);
});
return block;
}
}
function insertInlineContents(
parent: ParentBlot,
index: number,
inlineContents: Delta,
) {
inlineContents.reduce((index, op) => {
const length = Op.length(op);
let attributes = op.attributes || {};
if (op.insert != null) {
if (typeof op.insert === 'string') {
const text = op.insert;
parent.insertAt(index, text);
const [leaf] = parent.descendant(LeafBlot, index);
const formats = bubbleFormats(leaf);
attributes = AttributeMap.diff(formats, attributes) || {};
} else if (typeof op.insert === 'object') {
const key = Object.keys(op.insert)[0]; // There should only be one key
if (key == null) return index;
parent.insertAt(index, key, op.insert[key]);
const isInlineEmbed = parent.scroll.query(key, Scope.INLINE) != null;
if (isInlineEmbed) {
const [leaf] = parent.descendant(LeafBlot, index);
const formats = bubbleFormats(leaf);
attributes = AttributeMap.diff(formats, attributes) || {};
}
}
}
Object.keys(attributes).forEach((key) => {
parent.formatAt(index, length, key, attributes[key]);
});
return index + length;
}, index);
}
export default Scroll;
================================================
FILE: packages/quill/src/blots/text.ts
================================================
import { TextBlot } from 'parchment';
class Text extends TextBlot {}
// https://lodash.com/docs#escape
const entityMap: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
function escapeText(text: string) {
return text.replace(/[&<>"']/g, (s) => entityMap[s]);
}
export { Text as default, escapeText };
================================================
FILE: packages/quill/src/core/composition.ts
================================================
import Embed from '../blots/embed.js';
import type Scroll from '../blots/scroll.js';
import Emitter from './emitter.js';
class Composition {
isComposing = false;
constructor(
private scroll: Scroll,
private emitter: Emitter,
) {
this.setupListeners();
}
private setupListeners() {
this.scroll.domNode.addEventListener('compositionstart', (event) => {
if (!this.isComposing) {
this.handleCompositionStart(event);
}
});
this.scroll.domNode.addEventListener('compositionend', (event) => {
if (this.isComposing) {
// Webkit makes DOM changes after compositionend, so we use microtask to
// ensure the order.
// https://bugs.webkit.org/show_bug.cgi?id=31902
queueMicrotask(() => {
this.handleCompositionEnd(event);
});
}
});
}
private handleCompositionStart(event: CompositionEvent) {
const blot =
event.target instanceof Node
? this.scroll.find(event.target, true)
: null;
if (blot && !(blot instanceof Embed)) {
this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_START, event);
this.scroll.batchStart();
this.emitter.emit(Emitter.events.COMPOSITION_START, event);
this.isComposing = true;
}
}
private handleCompositionEnd(event: CompositionEvent) {
this.emitter.emit(Emitter.events.COMPOSITION_BEFORE_END, event);
this.scroll.batchEnd();
this.emitter.emit(Emitter.events.COMPOSITION_END, event);
this.isComposing = false;
}
}
export default Composition;
================================================
FILE: packages/quill/src/core/editor.ts
================================================
import { cloneDeep, isEqual, merge } from 'lodash-es';
import { LeafBlot, EmbedBlot, Scope, ParentBlot } from 'parchment';
import type { Blot } from 'parchment';
import Delta, { AttributeMap, Op } from 'quill-delta';
import Block, { BlockEmbed, bubbleFormats } from '../blots/block.js';
import Break from '../blots/break.js';
import CursorBlot from '../blots/cursor.js';
import type Scroll from '../blots/scroll.js';
import TextBlot, { escapeText } from '../blots/text.js';
import { Range } from './selection.js';
const ASCII = /^[ -~]*$/;
type SelectionInfo = {
newRange: Range;
oldRange: Range;
};
class Editor {
scroll: Scroll;
delta: Delta;
constructor(scroll: Scroll) {
this.scroll = scroll;
this.delta = this.getDelta();
}
applyDelta(delta: Delta): Delta {
this.scroll.update();
let scrollLength = this.scroll.length();
this.scroll.batchStart();
const normalizedDelta = normalizeDelta(delta);
const deleteDelta = new Delta();
const normalizedOps = splitOpLines(normalizedDelta.ops.slice());
normalizedOps.reduce((index, op) => {
const length = Op.length(op);
let attributes = op.attributes || {};
let isImplicitNewlinePrepended = false;
let isImplicitNewlineAppended = false;
if (op.insert != null) {
deleteDelta.retain(length);
if (typeof op.insert === 'string') {
const text = op.insert;
isImplicitNewlineAppended =
!text.endsWith('\n') &&
(scrollLength <= index ||
!!this.scroll.descendant(BlockEmbed, index)[0]);
this.scroll.insertAt(index, text);
const [line, offset] = this.scroll.line(index);
let formats = merge({}, bubbleFormats(line));
if (line instanceof Block) {
const [leaf] = line.descendant(LeafBlot, offset);
if (leaf) {
formats = merge(formats, bubbleFormats(leaf));
}
}
attributes = AttributeMap.diff(formats, attributes) || {};
} else if (typeof op.insert === 'object') {
const key = Object.keys(op.insert)[0]; // There should only be one key
if (key == null) return index;
const isInlineEmbed = this.scroll.query(key, Scope.INLINE) != null;
if (isInlineEmbed) {
if (
scrollLength <= index ||
!!this.scroll.descendant(BlockEmbed, index)[0]
) {
isImplicitNewlineAppended = true;
}
} else if (index > 0) {
const [leaf, offset] = this.scroll.descendant(LeafBlot, index - 1);
if (leaf instanceof TextBlot) {
const text = leaf.value();
if (text[offset] !== '\n') {
isImplicitNewlinePrepended = true;
}
} else if (
leaf instanceof EmbedBlot &&
leaf.statics.scope === Scope.INLINE_BLOT
) {
isImplicitNewlinePrepended = true;
}
}
this.scroll.insertAt(index, key, op.insert[key]);
if (isInlineEmbed) {
const [leaf] = this.scroll.descendant(LeafBlot, index);
if (leaf) {
const formats = merge({}, bubbleFormats(leaf));
attributes = AttributeMap.diff(formats, attributes) || {};
}
}
}
scrollLength += length;
} else {
deleteDelta.push(op);
if (op.retain !== null && typeof op.retain === 'object') {
const key = Object.keys(op.retain)[0];
if (key == null) return index;
this.scroll.updateEmbedAt(index, key, op.retain[key]);
}
}
Object.keys(attributes).forEach((name) => {
this.scroll.formatAt(index, length, name, attributes[name]);
});
const prependedLength = isImplicitNewlinePrepended ? 1 : 0;
const addedLength = isImplicitNewlineAppended ? 1 : 0;
scrollLength += prependedLength + addedLength;
deleteDelta.retain(prependedLength);
deleteDelta.delete(addedLength);
return index + length + prependedLength + addedLength;
}, 0);
deleteDelta.reduce((index, op) => {
if (typeof op.delete === 'number') {
this.scroll.deleteAt(index, op.delete);
return index;
}
return index + Op.length(op);
}, 0);
this.scroll.batchEnd();
this.scroll.optimize();
return this.update(normalizedDelta);
}
deleteText(index: number, length: number): Delta {
this.scroll.deleteAt(index, length);
return this.update(new Delta().retain(index).delete(length));
}
formatLine(
index: number,
length: number,
formats: Record<string, unknown> = {},
): Delta {
this.scroll.update();
Object.keys(formats).forEach((format) => {
this.scroll.lines(index, Math.max(length, 1)).forEach((line) => {
line.format(format, formats[format]);
});
});
this.scroll.optimize();
const delta = new Delta().retain(index).retain(length, cloneDeep(formats));
return this.update(delta);
}
formatText(
index: number,
length: number,
formats: Record<string, unknown> = {},
): Delta {
Object.keys(formats).forEach((format) => {
this.scroll.formatAt(index, length, format, formats[format]);
});
const delta = new Delta().retain(index).retain(length, cloneDeep(formats));
return this.update(delta);
}
getContents(index: number, length: number): Delta {
return this.delta.slice(index, index + length);
}
getDelta(): Delta {
return this.scroll.lines().reduce((delta, line) => {
return delta.concat(line.delta());
}, new Delta());
}
getFormat(index: number, length = 0): Record<string, unknown> {
let lines: (Block | BlockEmbed)[] = [];
let leaves: LeafBlot[] = [];
if (length === 0) {
this.scroll.path(index).forEach((path) => {
const [blot] = path;
if (blot instanceof Block) {
lines.push(blot);
} else if (blot instanceof LeafBlot) {
leaves.push(blot);
}
});
} else {
lines = this.scroll.lines(index, length);
leaves = this.scroll.descendants(LeafBlot, index, length);
}
const [lineFormats, leafFormats] = [lines, leaves].map((blots) => {
const blot = blots.shift();
if (blot == null) return {};
let formats = bubbleFormats(blot);
while (Object.keys(formats).length > 0) {
const blot = blots.shift();
if (blot == null) return formats;
formats = combineFormats(bubbleFormats(blot), formats);
}
return formats;
});
return { ...lineFormats, ...leafFormats };
}
getHTML(index: number, length: number): string {
const [line, lineOffset] = this.scroll.line(index);
if (line) {
const lineLength = line.length();
const isWithinLine = line.length() >= lineOffset + length;
if (isWithinLine && !(lineOffset === 0 && length === lineLength)) {
return convertHTML(line, lineOffset, length, true);
}
return convertHTML(this.scroll, index, length, true);
}
return '';
}
getText(index: number, length: number): string {
return this.getContents(index, length)
.filter((op) => typeof op.insert === 'string')
.map((op) => op.insert)
.join('');
}
insertContents(index: number, contents: Delta): Delta {
const normalizedDelta = normalizeDelta(contents);
const change = new Delta().retain(index).concat(normalizedDelta);
this.scroll.insertContents(index, normalizedDelta);
return this.update(change);
}
insertEmbed(index: number, embed: string, value: unknown): Delta {
this.scroll.insertAt(index, embed, value);
return this.update(new Delta().retain(index).insert({ [embed]: value }));
}
insertText(
index: number,
text: string,
formats: Record<string, unknown> = {},
): Delta {
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
this.scroll.insertAt(index, text);
Object.keys(formats).forEach((format) => {
this.scroll.formatAt(index, text.length, format, formats[format]);
});
return this.update(
new Delta().retain(index).insert(text, cloneDeep(formats)),
);
}
isBlank(): boolean {
if (this.scroll.children.length === 0) return true;
if (this.scroll.children.length > 1) return false;
const blot = this.scroll.children.head;
if (blot?.statics.blotName !== Block.blotName) return false;
const block = blot as Block;
if (block.children.length > 1) return false;
return block.children.head instanceof Break;
}
removeFormat(index: number, length: number): Delta {
const text = this.getText(index, length);
const [line, offset] = this.scroll.line(index + length);
let suffixLength = 0;
let suffix = new Delta();
if (line != null) {
suffixLength = line.length() - offset;
suffix = line
.delta()
.slice(offset, offset + suffixLength - 1)
.insert('\n');
}
const contents = this.getContents(index, length + suffixLength);
const diff = contents.diff(new Delta().insert(text).concat(suffix));
const delta = new Delta().retain(index).concat(diff);
return this.applyDelta(delta);
}
update(
change: Delta | null,
mutations: MutationRecord[] = [],
selectionInfo: SelectionInfo | undefined = undefined,
): Delta {
const oldDelta = this.delta;
if (
mutations.length === 1 &&
mutations[0].type === 'characterData' &&
// @ts-expect-error Fix me later
mutations[0].target.data.match(ASCII) &&
this.scroll.find(mutations[0].target)
) {
// Optimization for character changes
const textBlot = this.scroll.find(mutations[0].target) as Blot;
const formats = bubbleFormats(textBlot);
const index = textBlot.offset(this.scroll);
// @ts-expect-error Fix me later
const oldValue = mutations[0].oldValue.replace(CursorBlot.CONTENTS, '');
const oldText = new Delta().insert(oldValue);
// @ts-expect-error
const newText = new Delta().insert(textBlot.value());
const relativeSelectionInfo = selectionInfo && {
oldRange: shiftRange(selectionInfo.oldRange, -index),
newRange: shiftRange(selectionInfo.newRange, -index),
};
const diffDelta = new Delta()
.retain(index)
.concat(oldText.diff(newText, relativeSelectionInfo));
change = diffDelta.reduce((delta, op) => {
if (op.insert) {
return delta.insert(op.insert, formats);
}
return delta.push(op);
}, new Delta());
this.delta = oldDelta.compose(change);
} else {
this.delta = this.getDelta();
if (!change || !isEqual(oldDelta.compose(change), this.delta)) {
change = oldDelta.diff(this.delta, selectionInfo);
}
}
return change;
}
}
interface ListItem {
child: Blot;
offset: number;
length: number;
indent: number;
type: string;
}
function convertListHTML(
items: ListItem[],
lastIndent: number,
types: string[],
): string {
if (items.length === 0) {
const [endTag] = getListType(types.pop());
if (lastIndent <= 0) {
return `</li></${endTag}>`;
}
return `</li></${endTag}>${convertListHTML([], lastIndent - 1, types)}`;
}
const [{ child, offset, length, indent, type }, ...rest] = items;
const [tag, attribute] = getListType(type);
if (indent > lastIndent) {
types.push(type);
if (indent === lastIndent + 1) {
return `<${tag}><li${attribute}>${convertHTML(
child,
offset,
length,
)}${convertListHTML(rest, indent, types)}`;
}
return `<${tag}><li>${convertListHTML(items, lastIndent + 1, types)}`;
}
const previousType = types[types.length - 1];
if (indent === lastIndent && type === previousType) {
return `</li><li${attribute}>${convertHTML(
child,
offset,
length,
)}${convertListHTML(rest, indent, types)}`;
}
const [endTag] = getListType(types.pop());
return `</li></${endTag}>${convertListHTML(items, lastIndent - 1, types)}`;
}
function convertHTML(
blot: Blot,
index: number,
length: number,
isRoot = false,
): string {
if ('html' in blot && typeof blot.html === 'function') {
return blot.html(index, length);
}
if (blot instanceof TextBlot) {
const escapedText = escapeText(blot.value().slice(index, index + length));
return escapedText.replaceAll(' ', ' ');
}
if (blot instanceof ParentBlot) {
// TODO fix API
if (blot.statics.blotName === 'list-container') {
const items: any[] = [];
blot.children.forEachAt(index, length, (child, offset, childLength) => {
const formats =
'formats' in child && typeof child.formats === 'function'
? child.formats()
: {};
items.push({
child,
offset,
length: childLength,
indent: formats.indent || 0,
type: formats.list,
});
});
return convertListHTML(items, -1, []);
}
const parts: string[] = [];
blot.children.forEachAt(index, length, (child, offset, childLength) => {
parts.push(convertHTML(child, offset, childLength));
});
if (isRoot || blot.statics.blotName === 'list') {
return parts.join('');
}
const { outerHTML, innerHTML } = blot.domNode as Element;
const [start, end] = outerHTML.split(`>${innerHTML}<`);
// TODO cleanup
if (start === '<table') {
return `<table style="border: 1px solid #000;">${parts.join('')}<${end}`;
}
return `${start}>${parts.join('')}<${end}`;
}
return blot.domNode instanceof Element ? blot.domNode.outerHTML : '';
}
function combineFormats(
formats: Record<string, unknown>,
combined: Record<string, unknown>,
): Record<string, unknown> {
return Object.keys(combined).reduce(
(merged, name) => {
if (formats[name] == null) return merged;
const combinedValue = combined[name];
if (combinedValue === formats[name]) {
merged[name] = combinedValue;
} else if (Array.isArray(combinedValue)) {
if (combinedValue.indexOf(formats[name]) < 0) {
merged[name] = combinedValue.concat([formats[name]]);
} else {
// If style already exists, don't add to an array, but don't lose other styles
merged[name] = combinedValue;
}
} else {
merged[name] = [combinedValue, formats[name]];
}
return merged;
},
{} as Record<string, unknown>,
);
}
function getListType(type: string | undefined) {
const tag = type === 'ordered' ? 'ol' : 'ul';
switch (type) {
case 'checked':
return [tag, ' data-list="checked"'];
case 'unchecked':
return [tag, ' data-list="unchecked"'];
default:
return [tag, ''];
}
}
function normalizeDelta(delta: Delta) {
return delta.reduce((normalizedDelta, op) => {
if (typeof op.insert === 'string') {
const text = op.insert.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
return normalizedDelta.insert(text, op.attributes);
}
return normalizedDelta.push(op);
}, new Delta());
}
function shiftRange({ index, length }: Range, amount: number) {
return new Range(index + amount, length);
}
function splitOpLines(ops: Op[]) {
const split: Op[] = [];
ops.forEach((op) => {
if (typeof op.insert === 'string') {
const lines = op.insert.split('\n');
lines.forEach((line, index) => {
if (index) split.push({ insert: '\n', attributes: op.attributes });
if (line) split.push({ insert: line, attributes: op.attributes });
});
} else {
split.push(op);
}
});
return split;
}
export default Editor;
================================================
FILE: packages/quill/src/core/emitter.ts
================================================
import { EventEmitter } from 'eventemitter3';
import instances from './instances.js';
import logger from './logger.js';
const debug = logger('quill:events');
const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click'];
EVENTS.forEach((eventName) => {
document.addEventListener(eventName, (...args) => {
Array.from(document.querySelectorAll('.ql-container')).forEach((node) => {
const quill = instances.get(node);
if (quill && quill.emitter) {
quill.emitter.handleDOM(...args);
}
});
});
});
class Emitter extends EventEmitter<string> {
static events = {
EDITOR_CHANGE: 'editor-change',
SCROLL_BEFORE_UPDATE: 'scroll-before-update',
SCROLL_BLOT_MOUNT: 'scroll-blot-mount',
SCROLL_BLOT_UNMOUNT: 'scroll-blot-unmount',
SCROLL_OPTIMIZE: 'scroll-optimize',
SCROLL_UPDATE: 'scroll-update',
SCROLL_EMBED_UPDATE: 'scroll-embed-update',
SELECTION_CHANGE: 'selection-change',
TEXT_CHANGE: 'text-change',
COMPOSITION_BEFORE_START: 'composition-before-start',
COMPOSITION_START: 'composition-start',
COMPOSITION_BEFORE_END: 'composition-before-end',
COMPOSITION_END: 'composition-end',
} as const;
static sources = {
API: 'api',
SILENT: 'silent',
USER: 'user',
} as const;
protected domListeners: Record<string, { node: Node; handler: Function }[]>;
constructor() {
super();
this.domListeners = {};
this.on('error', debug.error);
}
emit(...args: unknown[]): boolean {
debug.log.call(debug, ...args);
// @ts-expect-error
return super.emit(...args);
}
handleDOM(event: Event, ...args: unknown[]) {
(this.domListeners[event.type] || []).forEach(({ node, handler }) => {
if (event.target === node || node.contains(event.target as Node)) {
handler(event, ...args);
}
});
}
listenDOM(eventName: string, node: Node, handler: EventListener) {
if (!this.domListeners[eventName]) {
this.domListeners[eventName] = [];
}
this.domListeners[eventName].push({ node, handler });
}
}
export type EmitterSource =
(typeof Emitter.sources)[keyof typeof Emitter.sources];
export default Emitter;
================================================
FILE: packages/quill/src/core/instances.ts
================================================
import type Quill from '../core.js';
export default new WeakMap<Node, Quill>();
================================================
FILE: packages/quill/src/core/logger.ts
================================================
const levels = ['error', 'warn', 'log', 'info'] as const;
export type DebugLevel = (typeof levels)[number];
let level: DebugLevel | false = 'warn';
function debug(method: DebugLevel, ...args: unknown[]) {
if (level) {
if (levels.indexOf(method) <= levels.indexOf(level)) {
console[method](...args); // eslint-disable-line no-console
}
}
}
function namespace(
ns: string,
): Record<DebugLevel, (...args: unknown[]) => void> {
return levels.reduce(
(logger, method) => {
logger[method] = debug.bind(console, method, ns);
return logger;
},
{} as Record<DebugLevel, (...args: unknown[]) => void>,
);
}
namespace.level = (newLevel: DebugLevel | false) => {
level = newLevel;
};
debug.level = namespace.level;
export default namespace;
================================================
FILE: packages/quill/src/core/module.ts
================================================
import type Quill from './quill.js';
abstract class Module<T extends {} = {}> {
static DEFAULTS = {};
constructor(
public quill: Quill,
protected options: Partial<T> = {},
) {}
}
export default Module;
================================================
FILE: packages/quill/src/core/quill.ts
================================================
import { merge } from 'lodash-es';
import * as Parchment from 'parchment';
import type { Op } from 'quill-delta';
import Delta from 'quill-delta';
import type { BlockEmbed } from '../blots/block.js';
import type Block from '../blots/block.js';
import type Scroll from '../blots/scroll.js';
import type Clipboard from '../modules/clipboard.js';
import type History from '../modules/history.js';
import type Keyboard from '../modules/keyboard.js';
import type Uploader from '../modules/uploader.js';
import Editor from './editor.js';
import Emitter from './emitter.js';
import type { EmitterSource } from './emitter.js';
import instances from './instances.js';
import logger from './logger.js';
import type { DebugLevel } from './logger.js';
import Module from './module.js';
import Selection, { Range } from './selection.js';
import type { Bounds } from './selection.js';
import Composition from './composition.js';
import Theme from './theme.js';
import type { ThemeConstructor } from './theme.js';
import scrollRectIntoView from './utils/scrollRectIntoView.js';
import type {
Rect,
ScrollRectIntoViewOptions,
} from './utils/scrollRectIntoView.js';
import createRegistryWithFormats from './utils/createRegistryWithFormats.js';
const debug = logger('quill');
const globalRegistry = new Parchment.Registry();
Parchment.ParentBlot.uiClass = 'ql-ui';
/**
* Options for initializing a Quill instance
*/
export interface QuillOptions {
theme?: string;
debug?: DebugLevel | boolean;
registry?: Parchment.Registry;
/**
* Whether to disable the editing
* @default false
*/
readOnly?: boolean;
/**
* Placeholder text to display when the editor is empty
* @default ""
*/
placeholder?: string;
bounds?: HTMLElement | string | null;
modules?: Record<string, unknown>;
/**
* A list of formats that are recognized and can exist within the editor contents.
* `null` means all formats are allowed.
* @default null
*/
formats?: string[] | null;
}
/**
* Similar to QuillOptions, but with all properties expanded to their default values,
* and all selectors resolved to HTMLElements.
*/
export interface ExpandedQuillOptions
extends Omit<QuillOptions, 'theme' | 'formats'> {
theme: ThemeConstructor;
registry: Parchment.Registry;
container: HTMLElement;
modules: Record<string, unknown>;
bounds?: HTMLElement | null;
readOnly: boolean;
}
class Quill {
static DEFAULTS = {
bounds: null,
modules: {
clipboard: true,
keyboard: true,
history: true,
uploader: true,
},
placeholder: '',
readOnly: false,
registry: globalRegistry,
theme: 'default',
} satisfies Partial<QuillOptions>;
static events = Emitter.events;
static sources = Emitter.sources;
static version = typeof QUILL_VERSION === 'undefined' ? 'dev' : QUILL_VERSION;
static imports: Record<string, unknown> = {
delta: Delta,
parchment: Parchment,
'core/module': Module,
'core/theme': Theme,
};
static debug(limit: DebugLevel | boolean) {
if (limit === true) {
limit = 'log';
}
logger.level(limit);
}
static find(node: Node, bubble = false) {
return instances.get(node) || globalRegistry.find(node, bubble);
}
static import(name: 'core/module'): typeof Module;
static import(name: `themes/${string}`): typeof Theme;
static import(name: 'parchment'): typeof Parchment;
static import(name: 'delta'): typeof Delta;
static import(name: string): unknown;
static import(name: string) {
if (this.imports[name] == null) {
debug.error(`Cannot import ${name}. Are you sure it was registered?`);
}
return this.imports[name];
}
static register(
targets: Record<
string,
| Parchment.RegistryDefinition
| Record<string, unknown> // any objects
| Theme
| Module
| Function // ES5 constructors
>,
overwrite?: boolean,
): void;
static register(
target: Parchment.RegistryDefinition,
overwrite?: boolean,
): void;
static register(path: string, target: any, overwrite?: boolean): void;
static register(...args: any[]): void {
if (typeof args[0] !== 'string') {
const target = args[0];
const overwrite = !!args[1];
const name = 'attrName' in target ? target.attrName : target.blotName;
if (typeof name === 'string') {
// Shortcut for formats:
// register(Blot | Attributor, overwrite)
this.register(`formats/${name}`, target, overwrite);
} else {
Object.keys(target).forEach((key) => {
this.register(key, target[key], overwrite);
});
}
} else {
const path = args[0];
const target = args[1];
const overwrite = !!args[2];
if (this.imports[path] != null && !overwrite) {
debug.warn(`Overwriting ${path} with`, target);
}
this.imports[path] = target;
if (
(path.startsWith('blots/') || path.startsWith('formats/')) &&
target &&
typeof target !== 'boolean' &&
target.blotName !== 'abstract'
) {
globalRegistry.register(target);
}
if (typeof target.register === 'function') {
target.register(globalRegistry);
}
}
}
container: HTMLElement;
root: HTMLDivElement;
scroll: Scroll;
emitter: Emitter;
protected allowReadOnlyEdits: boolean;
editor: Editor;
composition: Composition;
selection: Selection;
theme: Theme;
keyboard: Keyboard;
clipboard: Clipboard;
history: History;
uploader: Uploader;
options: ExpandedQuillOptions;
constructor(container: HTMLElement | string, options: QuillOptions = {}) {
this.options = expandConfig(container, options);
this.container = this.options.container;
if (this.container == null) {
debug.error('Invalid Quill container', container);
return;
}
if (this.options.debug) {
Quill.debug(this.options.debug);
}
const html = this.container.innerHTML.trim();
this.container.classList.add('ql-container');
this.container.innerHTML = '';
instances.set(this.container, this);
this.root = this.addContainer('ql-editor');
this.root.classList.add('ql-blank');
this.emitter = new Emitter();
const scrollBlotName = Parchment.ScrollBlot.blotName;
const ScrollBlot = this.options.registry.query(scrollBlotName);
if (!ScrollBlot || !('blotName' in ScrollBlot)) {
throw new Error(
`Cannot initialize Quill without "${scrollBlotName}" blot`,
);
}
this.scroll = new ScrollBlot(this.options.registry, this.root, {
emitter: this.emitter,
}) as Scroll;
this.editor = new Editor(this.scroll);
this.selection = new Selection(this.scroll, this.emitter);
this.composition = new Composition(this.scroll, this.emitter);
this.theme = new this.options.theme(this, this.options); // eslint-disable-line new-cap
this.keyboard = this.theme.addModule('keyboard');
this.clipboard = this.theme.addModule('clipboard');
this.history = this.theme.addModule('history');
this.uploader = this.theme.addModule('uploader');
this.theme.addModule('input');
this.theme.addModule('uiNode');
this.theme.init();
this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => {
if (type === Emitter.events.TEXT_CHANGE) {
this.root.classList.toggle('ql-blank', this.editor.isBlank());
}
});
this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {
const oldRange = this.selection.lastRange;
const [newRange] = this.selection.getRange();
const selectionInfo =
oldRange && newRange ? { oldRange, newRange } : undefined;
modify.call(
this,
() => this.editor.update(null, mutations, selectionInfo),
source,
);
});
this.emitter.on(Emitter.events.SCROLL_EMBED_UPDATE, (blot, delta) => {
const oldRange = this.selection.lastRange;
const [newRange] = this.selection.getRange();
const selectionInfo =
oldRange && newRange ? { oldRange, newRange } : undefined;
modify.call(
this,
() => {
const change = new Delta()
.retain(blot.offset(this))
.retain({ [blot.statics.blotName]: delta });
return this.editor.update(change, [], selectionInfo);
},
Quill.sources.USER,
);
});
if (html) {
const contents = this.clipboard.convert({
html: `${html}<p><br></p>`,
text: '\n',
});
this.setContents(contents);
}
this.history.clear();
if (this.options.placeholder) {
this.root.setAttribute('data-placeholder', this.options.placeholder);
}
if (this.options.readOnly) {
this.disable();
}
this.allowReadOnlyEdits = false;
}
addContainer(container: string, refNode?: Node | null): HTMLDivElement;
addContainer(container: HTMLElement, refNode?: Node | null): HTMLElement;
addContainer(
container: string | HTMLElement,
refNode: Node | null = null,
): HTMLDivElement | HTMLElement {
if (typeof container === 'string') {
const className = container;
container = document.createElement('div');
container.classList.add(className);
}
this.container.insertBefore(container, refNode);
return container;
}
blur() {
this.selection.setRange(null);
}
deleteText(range: Range, source?: EmitterSource): Delta;
deleteText(index: number, length: number, source?: EmitterSource): Delta;
deleteText(
index: number | Range,
length?: number | EmitterSource,
source?: EmitterSource,
): Delta {
// @ts-expect-error
[index, length, , source] = overload(index, length, source);
return modify.call(
this,
() => {
return this.editor.deleteText(index, length);
},
source,
index,
-1 * length,
);
}
disable() {
this.enable(false);
}
editReadOnly<T>(modifier: () => T): T {
this.allowReadOnlyEdits = true;
const value = modifier();
this.allowReadOnlyEdits = false;
return value;
}
enable(enabled = true) {
this.scroll.enable(enabled);
this.container.classList.toggle('ql-disabled', !enabled);
}
focus(options: { preventScroll?: boolean } = {}) {
this.selection.focus();
if (!options.preventScroll) {
this.scrollSelectionIntoView();
}
}
format(
name: string,
value: unknown,
source: EmitterSource = Emitter.sources.API,
): Delta {
return modify.call(
this,
() => {
const range = this.getSelection(true);
let change = new Delta();
if (range == null) return change;
if (this.scroll.query(name, Parchment.Scope.BLOCK)) {
change = this.editor.formatLine(range.index, range.length, {
[name]: value,
});
} else if (range.length === 0) {
this.selection.format(name, value);
return change;
} else {
change = this.editor.formatText(range.index, range.length, {
[name]: value,
});
}
this.setSelection(range, Emitter.sources.SILENT);
return change;
},
source,
);
}
formatLine(
index: number,
length: number,
formats: Record<string, unknown>,
source?: EmitterSource,
): Delta;
formatLine(
index: number,
length: number,
name: string,
value?: unknown,
source?: EmitterSource,
): Delta;
formatLine(
index: number,
length: number,
name: string | Record<string, unknown>,
value?: unknown | EmitterSource,
source?: EmitterSource,
): Delta {
let formats: Record<string, unknown>;
// eslint-disable-next-line prefer-const
[index, length, formats, source] = overload(
index,
length,
// @ts-expect-error
name,
value,
source,
);
return modify.call(
this,
() => {
return this.editor.formatLine(index, length, formats);
},
source,
index,
0,
);
}
formatText(
range: Range,
name: string,
value: unknown,
source?: EmitterSource,
): Delta;
formatText(
index: number,
length: number,
name: string,
value: unknown,
source?: EmitterSource,
): Delta;
formatText(
index: number,
length: number,
formats: Record<string, unknown>,
source?: EmitterSource,
): Delta;
formatText(
index: number | Range,
length: number | string,
name: string | unknown,
value?: unknown | EmitterSource,
source?: EmitterSource,
): Delta {
let formats: Record<string, unknown>;
// eslint-disable-next-line prefer-const
[index, length, formats, source] = overload(
// @ts-expect-error
index,
length,
name,
value,
source,
);
return modify.call(
this,
() => {
return this.editor.formatText(index, length, formats);
},
source,
index,
0,
);
}
getBounds(index: number | Range, length = 0): Bounds | null {
let bounds: Bounds | null = null;
if (typeof index === 'number') {
bounds = this.selection.getBounds(index, length);
} else {
bounds = this.selection.getBounds(index.index, index.length);
}
if (!bounds) return null;
const containerBounds = this.container.getBoundingClientRect();
return {
bottom: bounds.bottom - containerBounds.top,
height: bounds.height,
left: bounds.left - containerBounds.left,
right: bounds.right - containerBounds.left,
top: bounds.top - containerBounds.top,
width: bounds.width,
};
}
getContents(index = 0, length = this.getLength() - index) {
[index, length] = overload(index, length);
return this.editor.getContents(index, length);
}
getFormat(index?: number, length?: number): { [format: string]: unknown };
getFormat(range?: Range): {
[format: string]: unknown;
};
getFormat(
index: Range | number = this.getSelection(true),
length = 0,
): { [format: string]: unknown } {
if (typeof index === 'number') {
return this.editor.getFormat(index, length);
}
return this.editor.getFormat(index.index, index.length);
}
getIndex(blot: Parchment.Blot) {
return blot.offset(this.scroll);
}
getLength() {
return this.scroll.length();
}
getLeaf(index: number) {
return this.scroll.leaf(index);
}
getLine(index: number) {
return this.scroll.line(index);
}
getLines(range: Range): (Block | BlockEmbed)[];
getLines(index?: number, length?: number): (Block | BlockEmbed)[];
getLines(
index: Range | number = 0,
length = Number.MAX_VALUE,
): (Block | BlockEmbed)[] {
if (typeof index !== 'number') {
return this.scroll.lines(index.index, index.length);
}
return this.scroll.lines(index, length);
}
getModule(name: string) {
return this.theme.modules[name];
}
getSelection(focus: true): Range;
getSelection(focus?: boolean): Range | null;
getSelection(focus = false): Range | null {
if (focus) this.focus();
this.update(); // Make sure we access getRange with editor in consistent state
return this.selection.getRange()[0];
}
getSemanticHTML(range: Range): string;
getSemanticHTML(index?: number, length?: number): string;
getSemanticHTML(index: Range | number = 0, length?: number) {
if (typeof index === 'number') {
length = length ?? this.getLength() - index;
}
// @ts-expect-error
[index, length] = overload(index, length);
return this.editor.getHTML(index, length);
}
getText(range?: Range): string;
getText(index?: number, length?: number): string;
getText(index: Range | number = 0, length?: number): string {
if (typeof index === 'number') {
length = length ?? this.getLength() - index;
}
// @ts-expect-error
[index, length] = overload(index, length);
return this.editor.getText(index, length);
}
hasFocus() {
return this.selection.hasFocus();
}
insertEmbed(
index: number,
embed: string,
value: unknown,
source: EmitterSource = Quill.sources.API,
): Delta {
return modify.call(
this,
() => {
return this.editor.insertEmbed(index, embed, value);
},
source,
index,
);
}
insertText(index: number, text: string, source?: EmitterSource): Delta;
insertText(
index: number,
text: string,
formats: Record<string, unknown>,
source?: EmitterSource,
): Delta;
insertText(
index: number,
text: string,
name: string,
value: unknown,
source?: EmitterSource,
): Delta;
insertText(
index: number,
text: string,
name?: string | Record<string, unknown> | EmitterSource,
value?: unknown,
source?: EmitterSource,
): Delta {
let formats: Record<string, unknown>;
// eslint-disable-next-line prefer-const
// @ts-expect-error
[index, , formats, source] = overload(index, 0, name, value, source);
return modify.call(
this,
() => {
return this.editor.insertText(index, text, formats);
},
source,
index,
text.length,
);
}
isEnabled() {
return this.scroll.isEnabled();
}
off(...args: Parameters<(typeof Emitter)['prototype']['off']>) {
return this.emitter.off(...args);
}
on(
event: (typeof Emitter)['events']['TEXT_CHANGE'],
handler: (delta: Delta, oldContent: Delta, source: EmitterSource) => void,
): Emitter;
on(
event: (typeof Emitter)['events']['SELECTION_CHANGE'],
handler: (range: Range, oldRange: Range, source: EmitterSource) => void,
): Emitter;
on(
event: (typeof Emitter)['events']['EDITOR_CHANGE'],
handler: (
...args:
| [
(typeof Emitter)['events']['TEXT_CHANGE'],
Delta,
Delta,
EmitterSource,
]
| [
(typeof Emitter)['events']['SELECTION_CHANGE'],
Range,
Range,
EmitterSource,
]
) => void,
): Emitter;
on(event: string, ...args: unknown[]): Emitter;
on(...args: Parameters<(typeof Emitter)['prototype']['on']>): Emitter {
return this.emitter.on(...args);
}
once(...args: Parameters<(typeof Emitter)['prototype']['once']>) {
return this.emitter.once(...args);
}
removeFormat(index: number, length: number, source?: EmitterSource): Delta {
[index, length, , source] = overload(index, length, source);
return modify.call(
this,
() => {
return this.editor.removeFormat(index, length);
},
source,
index,
);
}
scrollRectIntoView(rect: Rect, options: ScrollRectIntoViewOptions = {}) {
scrollRectIntoView(this.root, rect, options);
}
/**
* @deprecated Use Quill#scrollSelectionIntoView() instead.
*/
scrollIntoView() {
console.warn(
'Quill#scrollIntoView() has been deprecated and will be removed in the near future. Please use Quill#scrollSelectionIntoView() instead.',
);
this.scrollSelectionIntoView();
}
/**
* Scroll the current selection into the visible area.
* If the selection is already visible, no scrolling will occur.
*/
scrollSelectionIntoView(options: ScrollRectIntoViewOptions = {}) {
const range = this.selection.lastRange;
const bounds = range && this.selection.getBounds(range.index, range.length);
if (bounds) {
this.scrollRectIntoView(bounds, options);
}
}
setContents(
delta: Delta | Op[],
source: EmitterSource = Emitter.sources.API,
): Delta {
return modify.call(
this,
() => {
delta = new Delta(delta);
const length = this.getLength();
// Quill will set empty editor to \n
const delete1 = this.editor.deleteText(0, length);
const applied = this.editor.insertContents(0, delta);
// Remove extra \n from empty editor initialization
const delete2 = this.editor.deleteText(this.getLength() - 1, 1);
return delete1.compose(applied).compose(delete2);
},
source,
);
}
setSelection(range: Range | null, source?: EmitterSource): void;
setSelection(index: number, source?: EmitterSource): void;
setSelection(index: number, length?: number, source?: EmitterSource): void;
setSelection(index: number, source?: EmitterSource): void;
setSelection(
index: Range | null | number,
length?: EmitterSource | number,
source?: EmitterSource,
): void {
if (index == null) {
// @ts-expect-error https://github.com/microsoft/TypeScript/issues/22609
this.selection.setRange(null, length || Quill.sources.API);
} else {
// @ts-expect-error
[index, length, , source] = overload(index, length, source);
this.selection.setRange(new Range(Math.max(0, index), length), source);
if (source !== Emitter.sources.SILENT) {
this.scrollSelectionIntoView();
}
}
}
setText(text: string, source: EmitterSource = Emitter.sources.API) {
const delta = new Delta().insert(text);
return this.setContents(delta, source);
}
update(source: EmitterSource = Emitter.sources.USER) {
const change = this.scroll.update(source); // Will update selection before selection.update() does if text changes
this.selection.update(source);
// TODO this is usually undefined
return change;
}
updateContents(
delta: Delta | Op[],
source: EmitterSource = Emitter.sources.API,
): Delta {
return modify.call(
this,
() => {
delta = new Delta(delta);
return this.editor.applyDelta(delta);
},
source,
true,
);
}
}
function resolveSelector(selector: string | HTMLElement | null | undefined) {
return typeof selector === 'string'
? document.querySelector<HTMLElement>(selector)
: selector;
}
function expandModuleConfig(config: Record<string, unknown> | undefined) {
return Object.entries(config ?? {}).reduce(
(expanded, [key, value]) => ({
...expanded,
[key]: value === true ? {} : value,
}),
{} as Record<string, unknown>,
);
}
function omitUndefinedValuesFromOptions(obj: QuillOptions) {
return Object.fromEntries(
Object.entries(obj).filter((entry) => entry[1] !== undefined),
);
}
function expandConfig(
containerOrSelector: HTMLElement | string,
options: QuillOptions,
): ExpandedQuillOptions {
const container = resolveSelector(containerOrSelector);
if (!container) {
throw new Error('Invalid Quill container');
}
const shouldUseDefaultTheme =
!options.theme || options.theme === Quill.DEFAULTS.theme;
const theme = shouldUseDefaultTheme
? Theme
: Quill.import(`themes/${options.theme}`);
if (!theme) {
throw new Error(`Invalid theme ${options.theme}. Did you register it?`);
}
const { modules: quillModuleDefaults, ...quillDefaults } = Quill.DEFAULTS;
const { modules: themeModuleDefaults, ...themeDefaults } = theme.DEFAULTS;
let userModuleOptions = expandModuleConfig(options.modules);
// Special case toolbar shorthand
if (
userModuleOptions != null &&
userModuleOptions.toolbar &&
userModuleOptions.toolbar.constructor !== Object
) {
userModuleOptions = {
...userModuleOptions,
toolbar: { container: userModuleOptions.toolbar },
};
}
const modules: ExpandedQuillOptions['modules'] = merge(
{},
expandModuleConfig(quillModuleDefaults),
expandModuleConfig(themeModuleDefaults),
userModuleOptions,
);
const config = {
...quillDefaults,
...omitUndefinedValuesFromOptions(themeDefaults),
...omitUndefinedValuesFromOptions(options),
};
let registry = options.registry;
if (registry) {
if (options.formats) {
debug.warn('Ignoring "formats" option because "registry" is specified');
}
} else {
registry = options.formats
? createRegistryWithFormats(options.formats, config.registry, debug)
: config.registry;
}
return {
...config,
registry,
container,
theme,
modules: Object.entries(modules).reduce(
(modulesWithDefaults, [name, value]) => {
if (!value) return modulesWithDefaults;
const moduleClass = Quill.import(`modules/${name}`);
if (moduleClass == null) {
debug.error(
`Cannot load ${name} module. Are you sure you registered it?`,
);
return modulesWithDefaults;
}
return {
...modulesWithDefaults,
// @ts-expect-error
[name]: merge({}, moduleClass.DEFAULTS || {}, value),
};
},
{},
),
bounds: resolveSelector(config.bounds),
};
}
// Handle selection preservation and TEXT_CHANGE emission
// common to modification APIs
function modify(
modifier: () => Delta,
source: EmitterSource,
index: number | boolean,
shift: number | null,
) {
if (
!this.isEnabled() &&
source === Emitter.sources.USER &&
!this.allowReadOnlyEdits
) {
return new Delta();
}
let range = index == null ? null : this.getSelection();
const oldDelta = this.editor.delta;
const change = modifier();
if (range != null) {
if (index === true) {
index = range.index; // eslint-disable-line prefer-destructuring
}
if (shift == null) {
range = shiftRange(range, change, source);
} else if (shift !== 0) {
// @ts-expect-error index should always be number
range = shiftRange(range, index, shift, source);
}
this.setSelection(range, Emitter.sources.SILENT);
}
if (change.length() > 0) {
const args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source];
this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
if (source !== Emitter.sources.SILENT) {
this.emitter.emit(...args);
}
}
return change;
}
type NormalizedIndexLength = [
number,
number,
Record<string, unknown>,
EmitterSource,
];
function overload(index: number, source?: EmitterSource): NormalizedIndexLength;
function overload(
index: number,
length: number,
source?: EmitterSource,
): NormalizedIndexLength;
function overload(
index: number,
length: number,
format: string,
value: unknown,
source?: EmitterSource,
): NormalizedIndexLength;
function overload(
index: number,
length: number,
format: Record<string, unknown>,
source?: EmitterSource,
): NormalizedIndexLength;
function overload(range: Range, source?: EmitterSource): NormalizedIndexLength;
function overload(
range: Range,
format: string,
value: unknown,
source?: EmitterSource,
): NormalizedIndexLength;
function overload(
range: Range,
format: Record<string, unknown>,
source?: EmitterSource,
): NormalizedIndexLength;
function overload(
index: Range | number,
length?: number | string | Record<string, unknown> | EmitterSource,
name?: string | unknown | Record<string, unknown> | EmitterSource,
value?: unknown | EmitterSource,
source?: EmitterSource,
): NormalizedIndexLength {
let formats: Record<string, unknown> = {};
// @ts-expect-error
if (typeof index.index === 'number' && typeof index.length === 'number') {
// Allow for throwaway end (used by insertText/insertEmbed)
if (typeof length !== 'number') {
// @ts-expect-error
source = value;
value = name;
name = length;
// @ts-expect-error
length = index.length; // eslint-disable-line prefer-destructuring
// @ts-expect-error
index = index.index; // eslint-disable-line prefer-destructuring
} else {
// @ts-expect-error
length = index.length; // eslint-disable-line prefer-destructuring
// @ts-expect-error
index = index.index; // eslint-disable-line prefer-destructuring
}
} else if (typeof length !== 'number') {
// @ts-expect-error
source = value;
value = name;
name = length;
length = 0;
}
// Handle format being object, two format name/value strings or excluded
if (typeof name === 'object') {
// @ts-expect-error Fix me later
formats = name;
// @ts-expect-error
source = value;
} else if (typeof name === 'string') {
if (value != null) {
formats[name] = value;
} else {
// @ts-expect-error
source = name;
}
}
// Handle optional source
source = source || Emitter.sources.API;
// @ts-expect-error
return [index, length, formats, source];
}
function shiftRange(range: Range, change: Delta, source?: EmitterSource): Range;
function shiftRange(
range: Range,
index: number,
length?: number,
source?: EmitterSource,
): Range;
function shiftRange(
range: Range,
index: number | Delta,
lengthOrSource?: number | EmitterSource,
source?: EmitterSource,
) {
const length = typeof lengthOrSource === 'number' ? lengthOrSource : 0;
if (range == null) return null;
let start;
let end;
// @ts-expect-error -- TODO: add a better type guard around `index`
if (index && typeof index.transformPosition === 'function') {
[start, end] = [range.index, range.index + range.length].map((pos) =>
// @ts-expect-error -- TODO: add a better type guard around `index`
index.transformPosition(pos, source !== Emitter.sources.USER),
);
} else {
[start, end] = [range.index, range.index + range.length].map((pos) => {
// @ts-expect-error -- TODO: add a better type guard around `index`
if (pos < index || (pos === index && source === Emitter.sources.USER))
return pos;
if (length >= 0) {
return pos + length;
}
// @ts-expect-error -- TODO: add a better type guard around `index`
return Math.max(index, pos + length);
});
}
return new Range(start, end - start);
}
export type { Bounds, DebugLevel, EmitterSource };
export { Parchment, Range };
export { globalRegistry, expandConfig, overload, Quill as default };
================================================
FILE: packages/quill/src/core/selection.ts
================================================
import { LeafBlot, Scope } from 'parchment';
import { cloneDeep, isEqual } from 'lodash-es';
import Emitter from './emitter.js';
import type { EmitterSource } from './emitter.js';
import logger from './logger.js';
import type Cursor from '../blots/cursor.js';
import type Scroll from '../blots/scroll.js';
const debug = logger('quill:selection');
type NativeRange = AbstractRange;
interface NormalizedRange {
start: {
node: NativeRange['startContainer'];
offset: NativeRange['startOffset'];
};
end: { node: NativeRange['endContainer']; offset: NativeRange['endOffset'] };
native: NativeRange;
}
export interface Bounds {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
}
export class Range {
constructor(
public index: number,
public length = 0,
) {}
}
class Selection {
scroll: Scroll;
emitter: Emitter;
composing: boolean;
mouseDown: boolean;
root: HTMLElement;
cursor: Cursor;
savedRange: Range;
lastRange: Range | null;
lastNative: NormalizedRange | null;
constructor(scroll: Scroll, emitter: Emitter) {
this.emitter = emitter;
this.scroll = scroll;
this.composing = false;
this.mouseDown = false;
this.root = this.scroll.domNode;
// @ts-expect-error
this.cursor = this.scroll.create('cursor', this);
// savedRange is last non-null range
this.savedRange = new Range(0, 0);
this.lastRange = this.savedRange;
this.lastNative = null;
this.handleComposition();
this.handleDragging();
this.emitter.listenDOM('selectionchange', document, () => {
if (!this.mouseDown && !this.composing) {
setTimeout(this.update.bind(this, Emitter.sources.USER), 1);
}
});
this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, () => {
if (!this.hasFocus()) return;
const native = this.getNativeRange();
if (native == null) return;
if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle
this.emitter.once(
Emitter.events.SCROLL_UPDATE,
(source, mutations: MutationRecord[]) => {
try {
if (
this.root.contains(native.start.node) &&
this.root.contains(native.end.node)
) {
this.setNativeRange(
native.start.node,
native.start.offset,
native.end.node,
native.end.offset,
);
}
const triggeredByTyping = mutations.some(
(mutation) =>
mutation.type === 'characterData' ||
mutation.type === 'childList' ||
(mutation.type === 'attributes' &&
mutation.target === this.root),
);
this.update(triggeredByTyping ? Emitter.sources.SILENT : source);
} catch (ignored) {
// ignore
}
},
);
});
this.emitter.on(Emitter.events.SCROLL_OPTIMIZE, (mutations, context) => {
if (context.range) {
const { startNode, startOffset, endNode, endOffset } = context.range;
this.setNativeRange(startNode, startOffset, endNode, endOffset);
this.update(Emitter.sources.SILENT);
}
});
this.update(Emitter.sources.SILENT);
}
handleComposition() {
this.emitter.on(Emitter.events.COMPOSITION_BEFORE_START, () => {
this.composing = true;
});
this.emitter.on(Emitter.events.COMPOSITION_END, () => {
this.composing = false;
if (this.cursor.parent) {
const range = this.cursor.restore();
if (!range) return;
setTimeout(() => {
this.setNativeRange(
range.startNode,
range.startOffset,
range.endNode,
range.endOffset,
);
}, 1);
}
});
}
handleDragging() {
this.emitter.listenDOM('mousedown', document.body, () => {
this.mouseDown = true;
});
this.emitter.listenDOM('mouseup', document.body, () => {
this.mouseDown = false;
this.update(Emitter.sources.USER);
});
}
focus() {
if (this.hasFocus()) return;
this.root.focus({ preventScroll: true });
this.setRange(this.savedRange);
}
format(format: string, value: unknown) {
this.scroll.update();
const nativeRange = this.getNativeRange();
if (
nativeRange == null ||
!nativeRange.native.collapsed ||
this.scroll.query(format, Scope.BLOCK)
)
return;
if (nativeRange.start.node !== this.cursor.textNode) {
const blot = this.scroll.find(nativeRange.start.node, false);
if (blot == null) return;
// TODO Give blot ability to not split
if (blot instanceof LeafBlot) {
const after = blot.split(nativeRange.start.offset);
blot.parent.insertBefore(this.cursor, after);
} else {
// @ts-expect-error TODO: nativeRange.start.node doesn't seem to match function signature
blot.insertBefore(this.cursor, nativeRange.start.node); // Should never happen
}
this.cursor.attach();
}
this.cursor.format(format, value);
this.scroll.optimize();
this.setNativeRange(this.cursor.textNode, this.cursor.textNode.data.length);
this.update();
}
getBounds(index: number, length = 0) {
const scrollLength = this.scroll.length();
index = Math.min(index, scrollLength - 1);
length = Math.min(index + length, scrollLength - 1) - index;
let node: Node;
let [leaf, offset] = this.scroll.leaf(index);
if (leaf == null) return null;
if (length > 0 && offset === leaf.length()) {
const [next] = this.scroll.leaf(index + 1);
if (next) {
const [line] = this.scroll.line(index);
const [nextLine] = this.scroll.line(index + 1);
if (line === nextLine) {
leaf = next;
offset = 0;
}
}
}
[node, offset] = leaf.position(offset, true);
const range = document.createRange();
if (length > 0) {
range.setStart(node, offset);
[leaf, offset] = this.scroll.leaf(index + length);
if (leaf == null) return null;
[node, offset] = leaf.position(offset, true);
range.setEnd(node, offset);
return range.getBoundingClientRect();
}
let side: 'left' | 'right' = 'left';
let rect: DOMRect;
if (node instanceof Text) {
// Return null if the text node is empty because it is
// not able to get a useful client rect:
// https://github.com/w3c/csswg-drafts/issues/2514.
// Empty text nodes are most likely caused by TextBlot#optimize()
// not getting called when editor content changes.
if (!node.data.length) {
return null;
}
if (offset < node.data.length) {
range.setStart(node, offset);
range.setEnd(node, offset + 1);
} else {
range.setStart(node, offset - 1);
range.setEnd(node, offset);
side = 'right';
}
rect = range.getBoundingClientRect();
} else {
if (!(leaf.domNode instanceof Element)) return null;
rect = leaf.domNode.getBoundingClientRect();
if (offset > 0) side = 'right';
}
return {
bottom: rect.top + rect.height,
height: rect.height,
left: rect[side],
right: rect[side],
top: rect.top,
width: 0,
};
}
getNativeRange(): NormalizedRange | null {
const selection = document.getSelection();
if (selection == null || selection.rangeCount <= 0) return null;
const nativeRange = selection.getRangeAt(0);
if (nativeRange == null) return null;
const range = this.normalizeNative(nativeRange);
debug.info('getNativeRange', range);
return range;
}
getRange(): [Range, NormalizedRange] | [null, null] {
const root = this.scroll.domNode;
if ('isConnected' in root && !root.isConnected) {
// document.getSelection() forces layout on Blink, so we trend to
// not calling it.
return [null, null];
}
const normalized = this.getNativeRange();
if (normalized == null) return [null, null];
const range = this.normalizedToRange(normalized);
return [range, normalized];
}
hasFocus(): boolean {
return (
document.activeElement === this.root ||
(document.activeElement != null &&
contains(this.root, document.activeElement))
);
}
normalizedToRange(range: NormalizedRange) {
const positions: [Node, number][] = [
[range.start.node, range.start.offset],
];
if (!range.native.collapsed) {
positions.push([range.end.node, range.end.offset]);
}
const indexes = positions.map((position) => {
const [node, offset] = position;
const blot = this.scroll.find(node, true);
// @ts-expect-error Fix me later
const index = blot.offset(this.scroll);
if (offset === 0) {
return index;
}
if (blot instanceof LeafBlot) {
return index + blot.index(node, offset);
}
// @ts-expect-error Fix me later
return index + blot.length();
});
const end = Math.min(Math.max(...indexes), this.scroll.length() - 1);
const start = Math.min(end, ...indexes);
return new Range(start, end - start);
}
normalizeNative(nativeRange: NativeRange) {
if (
!contains(this.root, nativeRange.startContainer) ||
(!nativeRange.collapsed && !contains(this.root, nativeRange.endContainer))
) {
return null;
}
const range = {
start: {
node: nativeRange.startContainer,
offset: nativeRange.startOffset,
},
end: { node: nativeRange.endContainer, offset: nativeRange.endOffset },
native: nativeRange,
};
[range.start, range.end].forEach((position) => {
let { node, offset } = position;
while (!(node instanceof Text) && node.childNodes.length > 0) {
if (node.childNodes.length > offset) {
node = node.childNodes[offset];
offset = 0;
} else if (node.childNodes.length === offset) {
// @ts-expect-error Fix me later
node = node.lastChild;
if (node instanceof Text) {
offset = node.data.length;
} else if (node.childNodes.length > 0) {
// Container case
offset = node.childNodes.length;
} else {
// Embed case
offset = node.childNodes.length + 1;
}
} else {
break;
}
}
position.node = node;
position.offset = offset;
});
return range;
}
rangeToNative(range: Range): [Node | null, number, Node | null, number] {
const scrollLength = this.scroll.length();
const getPosition = (
index: number,
inclusive: boolean,
): [Node | null, number] => {
index = Math.min(scrollLength - 1, index);
const [leaf, leafOffset] = this.scroll.leaf(index);
return leaf ? leaf.position(leafOffset, inclusive) : [null, -1];
};
return [
...getPosition(range.index, false),
...getPosition(range.index + range.length, true),
];
}
setNativeRange(
startNode: Node | null,
startOffset?: number,
endNode = startNode,
endOffset = startOffset,
force = false,
) {
debug.info('setNativeRange', startNode, startOffset, endNode, endOffset);
if (
startNode != null &&
(this.root.parentNode == null ||
startNode.parentNode == null ||
// @ts-expect-error Fix me later
endNode.parentNode == null)
) {
return;
}
const selection = document.getSelection();
if (selection == null) return;
if (startNode != null) {
if (!this.hasFocus()) this.root.focus({ preventScroll: true });
const { native } = this.getNativeRange() || {};
if (
native == null ||
force ||
startNode !== native.startContainer ||
startOffset !== native.startOffset ||
endNode !== native.endContainer ||
endOffset !== native.endOffset
) {
if (startNode instanceof Element && startNode.tagName === 'BR') {
// @ts-expect-error Fix me later
startOffset = Array.from(startNode.parentNode.childNodes).indexOf(
startNode,
);
startNode = startNode.parentNode;
}
if (endNode instanceof Element && endNode.tagName === 'BR') {
// @ts-expect-error Fix me later
endOffset = Array.from(endNode.parentNode.childNodes).indexOf(
endNode,
);
endNode = endNode.parentNode;
}
const range = document.createRange();
// @ts-expect-error Fix me later
range.setStart(startNode, startOffset);
// @ts-expect-error Fix me later
range.setEnd(endNode, endOffset);
selection.removeAllRanges();
selection.addRange(range);
}
} else {
selection.removeAllRanges();
this.root.blur();
}
}
setRange(range: Range | null, force: boolean, source?: EmitterSource): void;
setRange(range: Range | null, source?: EmitterSource): void;
setRange(
range: Range | null,
force: boolean | EmitterSource = false,
source: EmitterSource = Emitter.sources.API,
): void {
if (typeof force === 'string') {
source = force;
force = false;
}
debug.info('setRange', range);
if (range != null) {
const args = this.rangeToNative(range);
this.setNativeRange(...args, force);
} else {
this.setNativeRange(null);
}
this.update(source);
}
update(source: EmitterSource = Emitter.sources.USER) {
const oldRange = this.lastRange;
const [lastRange, nativeRange] = this.getRange();
this.lastRange = lastRange;
this.lastNative = nativeRange;
if (this.lastRange != null) {
this.savedRange = this.lastRange;
}
gitextract_t0yo5mmz/ ├── .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.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/ │ │ │ │ │ ├── toolbar.styl │ │ │ │ │ └── tooltip.styl │ │ │ │ ├── bubble.styl │ │ │ │ ├── core.styl │ │ │ │ ├── snow/ │ │ │ │ │ ├── toolbar.styl │ │ │ │ │ └── tooltip.styl │ │ │ │ └── snow.styl │ │ │ ├── blots/ │ │ │ │ ├── block.ts │ │ │ │ ├── break.ts │ │ │ │ ├── container.ts │ │ │ │ ├── cursor.ts │ │ │ │ ├── embed.ts │ │ │ │ ├── inline.ts │ │ │ │ ├── scroll.ts │ │ │ │ └── text.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 │ │ │ ├── core.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/ │ │ │ ├── registries.mdx │ │ │ └── themes.mdx │ │ ├── customization.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/ │ │ │ ├── clipboard.mdx │ │ │ ├── history.mdx │ │ │ ├── keyboard.mdx │ │ │ ├── syntax.mdx │ │ │ └── toolbar.mdx │ │ ├── modules.mdx │ │ ├── quickstart.mdx │ │ ├── upgrading-to-2-0.mdx │ │ └── why-quill.mdx │ ├── env.js │ ├── next.config.mjs │ ├── package.json │ ├── public/ │ │ ├── CNAME │ │ └── 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/ │ │ │ └── [...id].jsx │ │ ├── docs.jsx │ │ ├── index.jsx │ │ ├── playground/ │ │ │ ├── [...id].jsx │ │ │ └── [...id].module.scss │ │ ├── playground.jsx │ │ ├── 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 │ └── utils/ │ ├── flattenData.js │ ├── replaceCDN.js │ └── slug.js ├── scripts/ │ ├── changelog.mjs │ ├── release.js │ └── utils/ │ └── configGit.mjs └── tsconfig.json
SYMBOL INDEX (631 symbols across 84 files)
FILE: packages/quill/scripts/babel-svg-inline-import.cjs
class BabelSVGInlineImport (line 6) | class BabelSVGInlineImport {
method constructor (line 7) | constructor() {
FILE: packages/quill/src/blots/block.ts
constant NEWLINE_LENGTH (line 14) | const NEWLINE_LENGTH = 1;
class Block (line 16) | class Block extends BlockBlot {
method delta (line 19) | delta(): Delta {
method deleteAt (line 26) | deleteAt(index: number, length: number) {
method formatAt (line 31) | formatAt(index: number, length: number, name: string, value: unknown) {
method insertAt (line 48) | insertAt(index: number, value: string, def?: unknown) {
method insertBefore (line 76) | insertBefore(blot: Blot, ref?: Blot | null) {
method length (line 85) | length() {
method moveChildren (line 92) | moveChildren(target: Parent, ref?: Blot | null) {
method optimize (line 97) | optimize(context: { [key: string]: any }) {
method path (line 102) | path(index: number) {
method removeChild (line 106) | removeChild(child: Blot) {
method split (line 111) | split(index: number, force: boolean | undefined = false): Blot | null {
class BlockEmbed (line 131) | class BlockEmbed extends EmbedBlot {
method attach (line 135) | attach() {
method delta (line 140) | delta() {
method format (line 147) | format(name: string, value: unknown) {
method formatAt (line 155) | formatAt(index: number, length: number, name: string, value: unknown) {
method insertAt (line 159) | insertAt(index: number, value: string, def?: unknown) {
function blockDelta (line 183) | function blockDelta(blot: BlockBlot, filter = true) {
function bubbleFormats (line 195) | function bubbleFormats(
FILE: packages/quill/src/blots/break.ts
class Break (line 3) | class Break extends EmbedBlot {
method value (line 4) | static value() {
method optimize (line 8) | optimize() {
method length (line 14) | length() {
method value (line 18) | value() {
FILE: packages/quill/src/blots/container.ts
class Container (line 3) | class Container extends ContainerBlot {}
FILE: packages/quill/src/blots/cursor.ts
class Cursor (line 7) | class Cursor extends EmbedBlot {
method value (line 13) | static value() {
method constructor (line 21) | constructor(scroll: ScrollBlot, domNode: HTMLElement, selection: Selec...
method detach (line 29) | detach() {
method format (line 34) | format(name: string, value: unknown) {
method index (line 56) | index(node: Node, offset: number) {
method length (line 61) | length() {
method position (line 65) | position(): [Text, number] {
method remove (line 69) | remove() {
method restore (line 75) | restore(): EmbedContextRange | null {
method update (line 153) | update(mutations: MutationRecord[], context: Record<string, unknown>) {
method optimize (line 176) | optimize(context?: unknown) {
method value (line 193) | value() {
FILE: packages/quill/src/blots/embed.ts
constant GUARD_TEXT (line 5) | const GUARD_TEXT = '\uFEFF';
type EmbedContextRange (line 7) | interface EmbedContextRange {
class Embed (line 14) | class Embed extends EmbedBlot {
method constructor (line 19) | constructor(scroll: ScrollBlot, node: Node) {
method index (line 33) | index(node: Node, offset: number) {
method restore (line 39) | restore(node: Text): EmbedContextRange | null {
method update (line 79) | update(mutations: MutationRecord[], context: Record<string, unknown>) {
FILE: packages/quill/src/blots/inline.ts
class Inline (line 6) | class Inline extends InlineBlot {
method compare (line 21) | static compare(self: string, other: string) {
method formatAt (line 36) | formatAt(index: number, length: number, name: string, value: unknown) {
method optimize (line 50) | optimize(context: { [key: string]: any }) {
FILE: packages/quill/src/blots/scroll.ts
type RenderBlock (line 10) | type RenderBlock =
function isLine (line 19) | function isLine(blot: unknown): blot is Block | BlockEmbed {
type UpdatableEmbed (line 23) | interface UpdatableEmbed {
function isUpdatable (line 27) | function isUpdatable(blot: Blot): blot is Blot & UpdatableEmbed {
class Scroll (line 31) | class Scroll extends ScrollBlot {
method constructor (line 41) | constructor(
method batchStart (line 54) | batchStart() {
method batchEnd (line 60) | batchEnd() {
method emitMount (line 67) | emitMount(blot: Blot) {
method emitUnmount (line 71) | emitUnmount(blot: Blot) {
method emitEmbedUpdate (line 75) | emitEmbedUpdate(blot: Blot, change: unknown) {
method deleteAt (line 79) | deleteAt(index: number, length: number) {
method enable (line 98) | enable(enabled = true) {
method formatAt (line 102) | formatAt(index: number, length: number, format: string, value: unknown) {
method insertAt (line 107) | insertAt(index: number, value: string, def?: unknown) {
method insertBefore (line 127) | insertBefore(blot: Blot, ref?: Blot | null) {
method insertContents (line 139) | insertContents(index: number, delta: Delta) {
method isEnabled (line 212) | isEnabled() {
method leaf (line 216) | leaf(index: number): [LeafBlot | null, number] {
method line (line 226) | line(index: number): [Block | BlockEmbed | null, number] {
method lines (line 234) | lines(index = 0, length = Number.MAX_VALUE): (Block | BlockEmbed)[] {
method optimize (line 264) | optimize(mutations = [], context = {}) {
method path (line 272) | path(index: number) {
method remove (line 276) | remove() {
method update (line 282) | update(mutations?: MutationRecord[] | EmitterSource): void {
method updateEmbedAt (line 309) | updateEmbedAt(index: number, key: string, change: unknown) {
method handleDragStart (line 318) | protected handleDragStart(event: DragEvent) {
method deltaToRenderBlocks (line 322) | private deltaToRenderBlocks(delta: Delta) {
method createBlock (line 379) | private createBlock(attributes: AttributeMap, refBlot?: Blot) {
function insertInlineContents (line 408) | function insertInlineContents(
FILE: packages/quill/src/blots/text.ts
class Text (line 3) | class Text extends TextBlot {}
function escapeText (line 14) | function escapeText(text: string) {
FILE: packages/quill/src/core/composition.ts
class Composition (line 5) | class Composition {
method constructor (line 8) | constructor(
method setupListeners (line 15) | private setupListeners() {
method handleCompositionStart (line 34) | private handleCompositionStart(event: CompositionEvent) {
method handleCompositionEnd (line 48) | private handleCompositionEnd(event: CompositionEvent) {
FILE: packages/quill/src/core/editor.ts
constant ASCII (line 12) | const ASCII = /^[ -~]*$/;
type SelectionInfo (line 14) | type SelectionInfo = {
class Editor (line 19) | class Editor {
method constructor (line 23) | constructor(scroll: Scroll) {
method applyDelta (line 28) | applyDelta(delta: Delta): Delta {
method deleteText (line 125) | deleteText(index: number, length: number): Delta {
method formatLine (line 130) | formatLine(
method formatText (line 146) | formatText(
method getContents (line 158) | getContents(index: number, length: number): Delta {
method getDelta (line 162) | getDelta(): Delta {
method getFormat (line 168) | getFormat(index: number, length = 0): Record<string, unknown> {
method getHTML (line 198) | getHTML(index: number, length: number): string {
method getText (line 211) | getText(index: number, length: number): string {
method insertContents (line 218) | insertContents(index: number, contents: Delta): Delta {
method insertEmbed (line 225) | insertEmbed(index: number, embed: string, value: unknown): Delta {
method insertText (line 230) | insertText(
method isBlank (line 245) | isBlank(): boolean {
method removeFormat (line 255) | removeFormat(index: number, length: number): Delta {
method update (line 273) | update(
type ListItem (line 319) | interface ListItem {
function convertListHTML (line 326) | function convertListHTML(
function convertHTML (line 363) | function convertHTML(
function combineFormats (line 413) | function combineFormats(
function getListType (line 439) | function getListType(type: string | undefined) {
function normalizeDelta (line 451) | function normalizeDelta(delta: Delta) {
function shiftRange (line 461) | function shiftRange({ index, length }: Range, amount: number) {
function splitOpLines (line 465) | function splitOpLines(ops: Op[]) {
FILE: packages/quill/src/core/emitter.ts
constant EVENTS (line 6) | const EVENTS = ['selectionchange', 'mousedown', 'mouseup', 'click'];
class Emitter (line 19) | class Emitter extends EventEmitter<string> {
method constructor (line 44) | constructor() {
method emit (line 50) | emit(...args: unknown[]): boolean {
method handleDOM (line 56) | handleDOM(event: Event, ...args: unknown[]) {
method listenDOM (line 64) | listenDOM(eventName: string, node: Node, handler: EventListener) {
type EmitterSource (line 72) | type EmitterSource =
FILE: packages/quill/src/core/logger.ts
type DebugLevel (line 2) | type DebugLevel = (typeof levels)[number];
function debug (line 5) | function debug(method: DebugLevel, ...args: unknown[]) {
function namespace (line 13) | function namespace(
FILE: packages/quill/src/core/module.ts
method constructor (line 6) | constructor(
FILE: packages/quill/src/core/quill.ts
type QuillOptions (line 39) | interface QuillOptions {
type ExpandedQuillOptions (line 69) | interface ExpandedQuillOptions
class Quill (line 79) | class Quill {
method debug (line 104) | static debug(limit: DebugLevel | boolean) {
method find (line 111) | static find(node: Node, bubble = false) {
method import (line 120) | static import(name: string) {
method register (line 143) | static register(...args: any[]): void {
method constructor (line 198) | constructor(container: HTMLElement | string, options: QuillOptions = {...
method addContainer (line 287) | addContainer(
method blur (line 300) | blur() {
method deleteText (line 306) | deleteText(
method disable (line 324) | disable() {
method editReadOnly (line 328) | editReadOnly<T>(modifier: () => T): T {
method enable (line 335) | enable(enabled = true) {
method focus (line 340) | focus(options: { preventScroll?: boolean } = {}) {
method format (line 347) | format(
method formatLine (line 390) | formatLine(
method formatText (line 437) | formatText(
method getBounds (line 465) | getBounds(index: number | Range, length = 0): Bounds | null {
method getContents (line 484) | getContents(index = 0, length = this.getLength() - index) {
method getFormat (line 493) | getFormat(
method getIndex (line 503) | getIndex(blot: Parchment.Blot) {
method getLength (line 507) | getLength() {
method getLeaf (line 511) | getLeaf(index: number) {
method getLine (line 515) | getLine(index: number) {
method getLines (line 521) | getLines(
method getModule (line 531) | getModule(name: string) {
method getSelection (line 537) | getSelection(focus = false): Range | null {
method getSemanticHTML (line 545) | getSemanticHTML(index: Range | number = 0, length?: number) {
method getText (line 556) | getText(index: Range | number = 0, length?: number): string {
method hasFocus (line 565) | hasFocus() {
method insertEmbed (line 569) | insertEmbed(
method insertText (line 599) | insertText(
method isEnabled (line 621) | isEnabled() {
method off (line 625) | off(...args: Parameters<(typeof Emitter)['prototype']['off']>) {
method on (line 656) | on(...args: Parameters<(typeof Emitter)['prototype']['on']>): Emitter {
method once (line 660) | once(...args: Parameters<(typeof Emitter)['prototype']['once']>) {
method removeFormat (line 664) | removeFormat(index: number, length: number, source?: EmitterSource): D...
method scrollRectIntoView (line 676) | scrollRectIntoView(rect: Rect, options: ScrollRectIntoViewOptions = {}) {
method scrollIntoView (line 683) | scrollIntoView() {
method scrollSelectionIntoView (line 694) | scrollSelectionIntoView(options: ScrollRectIntoViewOptions = {}) {
method setContents (line 702) | setContents(
method setSelection (line 725) | setSelection(
method setText (line 743) | setText(text: string, source: EmitterSource = Emitter.sources.API) {
method update (line 748) | update(source: EmitterSource = Emitter.sources.USER) {
method updateContents (line 755) | updateContents(
function resolveSelector (line 771) | function resolveSelector(selector: string | HTMLElement | null | undefin...
function expandModuleConfig (line 777) | function expandModuleConfig(config: Record<string, unknown> | undefined) {
function omitUndefinedValuesFromOptions (line 787) | function omitUndefinedValuesFromOptions(obj: QuillOptions) {
function expandConfig (line 793) | function expandConfig(
function modify (line 881) | function modify(
type NormalizedIndexLength (line 919) | type NormalizedIndexLength = [
function overload (line 956) | function overload(
function shiftRange (line 1016) | function shiftRange(
FILE: packages/quill/src/core/selection.ts
type NativeRange (line 11) | type NativeRange = AbstractRange;
type NormalizedRange (line 13) | interface NormalizedRange {
type Bounds (line 22) | interface Bounds {
class Range (line 31) | class Range {
method constructor (line 32) | constructor(
class Selection (line 38) | class Selection {
method constructor (line 50) | constructor(scroll: Scroll, emitter: Emitter) {
method handleComposition (line 113) | handleComposition() {
method handleDragging (line 134) | handleDragging() {
method focus (line 144) | focus() {
method format (line 150) | format(format: string, value: unknown) {
method getBounds (line 178) | getBounds(index: number, length = 0) {
method getNativeRange (line 241) | getNativeRange(): NormalizedRange | null {
method getRange (line 251) | getRange(): [Range, NormalizedRange] | [null, null] {
method hasFocus (line 264) | hasFocus(): boolean {
method normalizedToRange (line 272) | normalizedToRange(range: NormalizedRange) {
method normalizeNative (line 298) | normalizeNative(nativeRange: NativeRange) {
method rangeToNative (line 341) | rangeToNative(range: Range): [Node | null, number, Node | null, number] {
method setNativeRange (line 358) | setNativeRange(
method setRange (line 418) | setRange(
method update (line 437) | update(source: EmitterSource = Emitter.sources.USER) {
function contains (line 476) | function contains(parent: Node, descendant: Node) {
FILE: packages/quill/src/core/theme.ts
type ThemeOptions (line 8) | interface ThemeOptions {
class Theme (line 14) | class Theme {
method constructor (line 25) | constructor(
method init (line 30) | init() {
method addModule (line 43) | addModule(name: string) {
type ThemeConstructor (line 54) | interface ThemeConstructor {
FILE: packages/quill/src/core/utils/createRegistryWithFormats.ts
constant MAX_REGISTER_ITERATIONS (line 3) | const MAX_REGISTER_ITERATIONS = 100;
constant CORE_FORMATS (line 4) | const CORE_FORMATS = ['block', 'break', 'cursor', 'inline', 'scroll', 't...
FILE: packages/quill/src/core/utils/scrollRectIntoView.ts
type Rect (line 1) | type Rect = {
type ScrollOffsetRecord (line 60) | interface ScrollOffsetRecord {
type ScrollRectIntoViewOptions (line 66) | interface ScrollRectIntoViewOptions {
FILE: packages/quill/src/formats/blockquote.ts
class Blockquote (line 3) | class Blockquote extends Block {
FILE: packages/quill/src/formats/bold.ts
class Bold (line 3) | class Bold extends Inline {
method create (line 7) | static create() {
method formats (line 11) | static formats() {
method optimize (line 15) | optimize(context: { [key: string]: any }) {
FILE: packages/quill/src/formats/code.ts
class CodeBlockContainer (line 9) | class CodeBlockContainer extends Container {
method create (line 10) | static create(value: string) {
method code (line 16) | code(index: number, length: number) {
method html (line 26) | html(index: number, length: number) {
class CodeBlock (line 33) | class CodeBlock extends Block {
method register (line 36) | static register() {
class Code (line 41) | class Code extends Inline {}
FILE: packages/quill/src/formats/color.ts
class ColorAttributor (line 3) | class ColorAttributor extends StyleAttributor {
method value (line 4) | value(domNode: HTMLElement) {
FILE: packages/quill/src/formats/font.ts
class FontStyleAttributor (line 10) | class FontStyleAttributor extends StyleAttributor {
method value (line 11) | value(node: HTMLElement) {
FILE: packages/quill/src/formats/formula.ts
class Formula (line 3) | class Formula extends Embed {
method create (line 8) | static create(value: string) {
method value (line 25) | static value(domNode: Element) {
method html (line 29) | html() {
FILE: packages/quill/src/formats/header.ts
class Header (line 3) | class Header extends Block {
method formats (line 7) | static formats(domNode: Element) {
FILE: packages/quill/src/formats/image.ts
constant ATTRIBUTES (line 4) | const ATTRIBUTES = ['alt', 'height', 'width'];
class Image (line 6) | class Image extends EmbedBlot {
method create (line 10) | static create(value: string) {
method formats (line 18) | static formats(domNode: Element) {
method match (line 30) | static match(url: string) {
method sanitize (line 34) | static sanitize(url: string) {
method value (line 38) | static value(domNode: Element) {
method format (line 44) | format(name: string, value: string) {
FILE: packages/quill/src/formats/indent.ts
class IndentAttributor (line 3) | class IndentAttributor extends ClassAttributor {
method add (line 4) | add(node: HTMLElement, value: string | number) {
method canAdd (line 19) | canAdd(node: HTMLElement, value: string) {
method value (line 23) | value(node: HTMLElement) {
FILE: packages/quill/src/formats/italic.ts
class Italic (line 3) | class Italic extends Bold {
FILE: packages/quill/src/formats/link.ts
class Link (line 3) | class Link extends Inline {
method create (line 9) | static create(value: string) {
method formats (line 17) | static formats(domNode: HTMLElement) {
method sanitize (line 21) | static sanitize(url: string) {
method format (line 25) | format(name: string, value: unknown) {
function sanitize (line 35) | function sanitize(url: string, protocols: string[]) {
FILE: packages/quill/src/formats/list.ts
class ListContainer (line 6) | class ListContainer extends Container {}
class ListItem (line 10) | class ListItem extends Block {
method create (line 11) | static create(value: string) {
method formats (line 17) | static formats(domNode: HTMLElement) {
method register (line 21) | static register() {
method constructor (line 25) | constructor(scroll: Scroll, domNode: HTMLElement) {
method format (line 44) | format(name: string, value: string) {
FILE: packages/quill/src/formats/script.ts
class Script (line 3) | class Script extends Inline {
method create (line 7) | static create(value: 'super' | 'sub' | (string & {})) {
method formats (line 17) | static formats(domNode: HTMLElement) {
FILE: packages/quill/src/formats/strike.ts
class Strike (line 3) | class Strike extends Bold {
FILE: packages/quill/src/formats/table.ts
class TableCell (line 5) | class TableCell extends Block {
method create (line 9) | static create(value: string) {
method formats (line 19) | static formats(domNode: HTMLElement) {
method cellOffset (line 28) | cellOffset() {
method format (line 35) | format(name: string, value: string) {
method row (line 43) | row(): TableRow {
method rowOffset (line 47) | rowOffset() {
method table (line 54) | table() {
class TableRow (line 59) | class TableRow extends Container {
method checkMerge (line 66) | checkMerge() {
method optimize (line 86) | optimize(context: { [key: string]: any }) {
method rowOffset (line 107) | rowOffset() {
method table (line 114) | table() {
class TableBody (line 119) | class TableBody extends Container {
class TableContainer (line 126) | class TableContainer extends Container {
method balanceCells (line 132) | balanceCells() {
method cells (line 151) | cells(column: number) {
method deleteColumn (line 155) | deleteColumn(index: number) {
method insertColumn (line 167) | insertColumn(index: number) {
method insertRow (line 180) | insertRow(index: number) {
method rows (line 194) | rows() {
function tableId (line 210) | function tableId() {
FILE: packages/quill/src/formats/underline.ts
class Underline (line 3) | class Underline extends Inline {
FILE: packages/quill/src/formats/video.ts
constant ATTRIBUTES (line 4) | const ATTRIBUTES = ['height', 'width'];
class Video (line 6) | class Video extends BlockEmbed {
method create (line 11) | static create(value: string) {
method formats (line 19) | static formats(domNode: Element) {
method sanitize (line 31) | static sanitize(url: string) {
method value (line 35) | static value(domNode: Element) {
method format (line 41) | format(name: string, value: string) {
method html (line 53) | html() {
FILE: packages/quill/src/modules/clipboard.ts
type Selector (line 29) | type Selector = string | Node['TEXT_NODE'] | Node['ELEMENT_NODE'];
type Matcher (line 30) | type Matcher = (node: Node, delta: Delta, scroll: ScrollBlot) => Delta;
constant CLIPBOARD_CONFIG (line 32) | const CLIPBOARD_CONFIG: [Selector, Matcher][] = [
constant ATTRIBUTE_ATTRIBUTORS (line 50) | const ATTRIBUTE_ATTRIBUTORS = [AlignAttribute, DirectionAttribute].reduce(
constant STYLE_ATTRIBUTORS (line 58) | const STYLE_ATTRIBUTORS = [
type ClipboardOptions (line 70) | interface ClipboardOptions {
class Clipboard (line 74) | class Clipboard extends Module<ClipboardOptions> {
method constructor (line 81) | constructor(quill: Quill, options: Partial<ClipboardOptions>) {
method addMatcher (line 96) | addMatcher(selector: Selector, matcher: Matcher) {
method convert (line 100) | convert(
method normalizeHTML (line 123) | protected normalizeHTML(doc: Document) {
method convertHTML (line 127) | protected convertHTML(html: string) {
method dangerouslyPasteHTML (line 151) | dangerouslyPasteHTML(
method onCaptureCopy (line 171) | onCaptureCopy(e: ClipboardEvent, isCut = false) {
method normalizeURIList (line 187) | private normalizeURIList(urlList: string) {
method onCapturePaste (line 197) | onCapturePaste(e: ClipboardEvent) {
method onCopy (line 229) | onCopy(range: Range) {
method onPaste (line 235) | onPaste(range: Range, { text, html }: { text?: string; html?: string }) {
method prepareMatching (line 252) | prepareMatching(container: Element, nodeMatches: WeakMap<Node, Matcher...
function applyFormat (line 280) | function applyFormat(
function deltaEndsWith (line 300) | function deltaEndsWith(delta: Delta, text: string) {
function isLine (line 314) | function isLine(node: Node, scroll: ScrollBlot) {
function isBetweenInlineElements (line 358) | function isBetweenInlineElements(node: HTMLElement, scroll: ScrollBlot) {
function isPre (line 368) | function isPre(node: Node | null) {
function traverse (line 381) | function traverse(
function createMatchAlias (line 420) | function createMatchAlias(format: string) {
function matchAttributor (line 426) | function matchAttributor(node: HTMLElement, delta: Delta, scroll: Scroll...
function matchBlot (line 457) | function matchBlot(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchBreak (line 492) | function matchBreak(node: Node, delta: Delta) {
function matchCodeBlock (line 499) | function matchCodeBlock(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchIgnore (line 508) | function matchIgnore() {
function matchIndent (line 512) | function matchIndent(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchList (line 541) | function matchList(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchNewline (line 553) | function matchNewline(node: Node, delta: Delta, scroll: ScrollBlot) {
function matchStyles (line 579) | function matchStyles(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
function matchTable (line 610) | function matchTable(
function matchText (line 627) | function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
FILE: packages/quill/src/modules/history.ts
type HistoryOptions (line 8) | interface HistoryOptions {
type StackItem (line 14) | interface StackItem {
type Stack (line 19) | interface Stack {
class History (line 24) | class History extends Module<HistoryOptions> {
method constructor (line 36) | constructor(quill: Quill, options: Partial<HistoryOptions>) {
method change (line 85) | change(source: 'undo' | 'redo', dest: 'redo' | 'undo') {
method clear (line 103) | clear() {
method cutoff (line 107) | cutoff() {
method record (line 111) | record(changeDelta: Delta, oldDelta: Delta) {
method redo (line 138) | redo() {
method transform (line 142) | transform(delta: Delta) {
method undo (line 147) | undo() {
method restoreSelection (line 151) | protected restoreSelection(stackItem: StackItem) {
function transformStack (line 161) | function transformStack(stack: StackItem[], delta: Delta) {
function endsWithNewlineChange (line 176) | function endsWithNewlineChange(scroll: Scroll, delta: Delta) {
function getLastChangeIndex (line 190) | function getLastChangeIndex(scroll: Scroll, delta: Delta) {
function transformRange (line 201) | function transformRange(range: Range | null, delta: Delta) {
FILE: packages/quill/src/modules/input.ts
constant INSERT_TYPES (line 7) | const INSERT_TYPES = ['insertText', 'insertReplacementText'];
class Input (line 9) | class Input extends Module {
method constructor (line 10) | constructor(quill: Quill, options: Record<string, never>) {
method deleteRange (line 26) | private deleteRange(range: Range) {
method replaceText (line 30) | private replaceText(range: Range, text = '') {
method handleBeforeInput (line 49) | private handleBeforeInput(event: InputEvent) {
method handleCompositionStart (line 78) | private handleCompositionStart() {
function getPlainTextFromInputEvent (line 86) | function getPlainTextFromInputEvent(event: InputEvent) {
FILE: packages/quill/src/modules/keyboard.ts
constant SHORTKEY (line 13) | const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
type Context (line 15) | interface Context {
type BindingObject (line 26) | interface BindingObject
type Binding (line 46) | type Binding = BindingObject | string | number;
type NormalizedBinding (line 48) | interface NormalizedBinding extends Omit<BindingObject, 'key' | 'shortKe...
type KeyboardOptions (line 52) | interface KeyboardOptions {
type KeyboardOptions (line 56) | interface KeyboardOptions {
class Keyboard (line 60) | class Keyboard extends Module<KeyboardOptions> {
method match (line 63) | static match(evt: KeyboardEvent, binding: BindingObject) {
method constructor (line 76) | constructor(quill: Quill, options: Partial<KeyboardOptions>) {
method addBinding (line 140) | addBinding(
method listen (line 173) | listen() {
method handleBackspace (line 268) | handleBackspace(range: Range, context: Context) {
method handleDelete (line 303) | handleDelete(range: Range, context: Context) {
method handleDeleteRange (line 329) | handleDeleteRange(range: Range) {
method handleEnter (line 334) | handleEnter(range: Range, context: Context) {
method handler (line 366) | handler(range, context) {
method handler (line 377) | handler(range, context) {
method handler (line 392) | handler(range, context) {
method handler (line 407) | handler(range) {
method handler (line 413) | handler(range, context) {
method handler (line 431) | handler() {
method handler (line 440) | handler(range, context) {
method handler (line 457) | handler(range) {
method handler (line 480) | handler(range, context) {
method handler (line 498) | handler() {}
method handler (line 505) | handler() {}
method handler (line 511) | handler(range) {
method handler (line 540) | handler(range, context) {
method handler (line 560) | handler(range, context) {
method handler (line 601) | handler(range) {
function makeCodeBlockHandler (line 639) | function makeCodeBlockHandler(indent: boolean): BindingObject {
function makeEmbedArrowHandler (line 683) | function makeEmbedArrowHandler(
function makeFormatHandler (line 727) | function makeFormatHandler(format: string): BindingObject {
function makeTableArrowHandler (line 737) | function makeTableArrowHandler(up: boolean): BindingObject {
function normalize (line 786) | function normalize(binding: Binding): BindingObject | null {
function deleteRange (line 802) | function deleteRange({ quill, range }: { quill: Quill; range: Range }) {
function tableSide (line 817) | function tableSide(_table: unknown, row: Blot, cell: Blot, offset: numbe...
FILE: packages/quill/src/modules/normalizeExternalHTML/index.ts
constant NORMALIZERS (line 4) | const NORMALIZERS = [msWord, googleDocs];
FILE: packages/quill/src/modules/normalizeExternalHTML/normalizers/googleDocs.ts
function normalize (line 30) | function normalize(doc: Document) {
FILE: packages/quill/src/modules/normalizeExternalHTML/normalizers/msWord.ts
function normalize (line 90) | function normalize(doc: Document) {
FILE: packages/quill/src/modules/syntax.ts
class CodeToken (line 17) | class CodeToken extends Inline {
method formats (line 18) | static formats(node: Element, scroll: ScrollBlot) {
method constructor (line 30) | constructor(scroll: ScrollBlot, domNode: Node, value: unknown) {
method format (line 36) | format(format: string, value: unknown) {
method optimize (line 47) | optimize(...args: unknown[]) {
class SyntaxCodeBlock (line 58) | class SyntaxCodeBlock extends CodeBlock {
method create (line 59) | static create(value: unknown) {
method formats (line 67) | static formats(domNode: Node) {
method register (line 72) | static register() {} // Syntax module will register
method format (line 74) | format(name: string, value: unknown) {
method replaceWith (line 83) | replaceWith(name: string | Blot, value?: any) {
class SyntaxCodeBlockContainer (line 89) | class SyntaxCodeBlockContainer extends CodeBlockContainer {
method attach (line 93) | attach() {
method format (line 100) | format(name: string, value: unknown) {
method formatAt (line 110) | formatAt(index: number, length: number, name: string, value: unknown) {
method highlight (line 117) | highlight(
method html (line 156) | html(index: number, length: number) {
method optimize (line 167) | optimize(context: Record<string, any>) {
type SyntaxOptions (line 188) | interface SyntaxOptions {
class Syntax (line 204) | class Syntax extends Module<SyntaxOptions> {
method register (line 207) | static register() {
method constructor (line 215) | constructor(quill: Quill, options: Partial<SyntaxOptions>) {
method initListener (line 235) | initListener() {
method initTimer (line 260) | initTimer() {
method highlight (line 273) | highlight(blot: SyntaxCodeBlockContainer | null = null, force = false) {
method highlightBlot (line 290) | highlightBlot(text: string, language = 'plain') {
FILE: packages/quill/src/modules/table.ts
class Table (line 12) | class Table extends Module {
method register (line 13) | static register() {
method constructor (line 20) | constructor(...args: ConstructorParameters<typeof Module>) {
method balanceTables (line 25) | balanceTables() {
method deleteColumn (line 31) | deleteColumn() {
method deleteRow (line 39) | deleteRow() {
method deleteTable (line 46) | deleteTable() {
method getTable (line 57) | getTable(
method insertColumn (line 71) | insertColumn(offset: number) {
method insertColumnLeft (line 90) | insertColumnLeft() {
method insertColumnRight (line 94) | insertColumnRight() {
method insertRow (line 98) | insertRow(offset: number) {
method insertRowAbove (line 117) | insertRowAbove() {
method insertRowBelow (line 121) | insertRowBelow() {
method insertTable (line 125) | insertTable(rows: number, columns: number) {
method listenBalanceCells (line 137) | listenBalanceCells() {
FILE: packages/quill/src/modules/tableEmbed.ts
type CellData (line 5) | type CellData = {
type TableRowColumnOp (line 10) | type TableRowColumnOp = Omit<Op, 'insert'> & {
type TableData (line 14) | interface TableData {
method compose (line 119) | compose(a: TableData, b: TableData, keepNull?: boolean) {
method transform (line 155) | transform(a: TableData, b: TableData, priority: boolean) {
method invert (line 211) | invert(change: TableData, base: TableData) {
class TableEmbed (line 257) | class TableEmbed extends Module {
method register (line 258) | static register() {
FILE: packages/quill/src/modules/toolbar.ts
type Handler (line 10) | type Handler = (this: Toolbar, value: any) => void;
type ToolbarConfig (line 12) | type ToolbarConfig = Array<
type ToolbarProps (line 15) | interface ToolbarProps {
class Toolbar (line 23) | class Toolbar extends Module<ToolbarProps> {
method constructor (line 30) | constructor(quill: Quill, options: Partial<ToolbarProps>) {
method addHandler (line 70) | addHandler(format: string, handler: Handler) {
method attach (line 74) | attach(input: HTMLElement) {
method update (line 139) | update(range: Range | null) {
function addButton (line 187) | function addButton(container: HTMLElement, format: string, value?: strin...
function addControls (line 201) | function addControls(
function addSelect (line 231) | function addSelect(
method clean (line 253) | clean() {
method direction (line 268) | direction(value) {
method indent (line 277) | indent(value) {
method link (line 289) | link(value) {
method list (line 295) | list(value) {
FILE: packages/quill/src/modules/uiNode.ts
constant TTL_FOR_VALID_SELECTION_CHANGE (line 8) | const TTL_FOR_VALID_SELECTION_CHANGE = 100;
class UINode (line 30) | class UINode extends Module {
method constructor (line 34) | constructor(quill: Quill, options: Record<string, never>) {
method handleArrowKeys (line 41) | private handleArrowKeys() {
method handleNavigationShortcuts (line 69) | private handleNavigationShortcuts() {
method ensureListeningToSelectionChange (line 83) | private ensureListeningToSelectionChange() {
method handleSelectionChange (line 102) | private handleSelectionChange() {
FILE: packages/quill/src/modules/uploader.ts
type UploaderOptions (line 7) | interface UploaderOptions {
class Uploader (line 12) | class Uploader extends Module<UploaderOptions> {
method constructor (line 15) | constructor(quill: Quill, options: Partial<UploaderOptions>) {
method upload (line 41) | upload(range: Range, files: FileList | File[]) {
method handler (line 57) | handler(range: Range, files: File[]) {
FILE: packages/quill/src/themes/base.ts
constant ALIGNS (line 17) | const ALIGNS = [false, 'center', 'right', 'justify'];
constant COLORS (line 19) | const COLORS = [
constant FONTS (line 57) | const FONTS = [false, 'serif', 'monospace'];
constant HEADERS (line 59) | const HEADERS = ['1', '2', '3', false];
constant SIZES (line 61) | const SIZES = ['small', false, 'large', 'huge'];
class BaseTheme (line 63) | class BaseTheme extends Theme {
method constructor (line 67) | constructor(quill: Quill, options: ThemeOptions) {
method addModule (line 102) | addModule(name: string) {
method buildButtons (line 111) | buildButtons(
method buildPickers (line 140) | buildPickers(
method formula (line 192) | formula() {
method image (line 195) | image() {
method video (line 216) | video() {
class BaseTooltip (line 224) | class BaseTooltip extends Tooltip {
method constructor (line 228) | constructor(quill: Quill, boundsContainer?: HTMLElement) {
method listen (line 234) | listen() {
method cancel (line 247) | cancel() {
method edit (line 252) | edit(mode = 'link', preview: string | null = null) {
method restoreFocus (line 274) | restoreFocus() {
method save (line 278) | save() {
function extractVideoUrl (line 329) | function extractVideoUrl(url: string) {
function fillSelect (line 347) | function fillSelect(
FILE: packages/quill/src/themes/bubble.ts
constant TOOLBAR_CONFIG (line 12) | const TOOLBAR_CONFIG: ToolbarConfig = [
class BubbleTooltip (line 17) | class BubbleTooltip extends BaseTooltip {
method constructor (line 26) | constructor(quill: Quill, bounds?: HTMLElement) {
method listen (line 70) | listen() {
method cancel (line 91) | cancel() {
method position (line 95) | position(reference: Bounds) {
class BubbleTheme (line 108) | class BubbleTheme extends BaseTheme {
method constructor (line 111) | constructor(quill: Quill, options: ThemeOptions) {
method extendToolbar (line 122) | extendToolbar(toolbar: Toolbar) {
method link (line 136) | link(value: string) {
FILE: packages/quill/src/themes/snow.ts
constant TOOLBAR_CONFIG (line 13) | const TOOLBAR_CONFIG: ToolbarConfig = [
class SnowTooltip (line 20) | class SnowTooltip extends BaseTooltip {
method listen (line 30) | listen() {
method show (line 88) | show() {
class SnowTheme (line 94) | class SnowTheme extends BaseTheme {
method constructor (line 95) | constructor(quill: Quill, options: ThemeOptions) {
method extendToolbar (line 106) | extendToolbar(toolbar: Toolbar) {
method link (line 128) | link(value: string) {
FILE: packages/quill/src/ui/color-picker.ts
class ColorPicker (line 3) | class ColorPicker extends Picker {
method constructor (line 4) | constructor(select: HTMLSelectElement, label: string) {
method buildItem (line 15) | buildItem(option: HTMLOptionElement) {
method selectItem (line 21) | selectItem(item: HTMLElement | null, trigger?: boolean) {
FILE: packages/quill/src/ui/icon-picker.ts
class IconPicker (line 3) | class IconPicker extends Picker {
method constructor (line 6) | constructor(select: HTMLSelectElement, icons: Record<string, string>) {
method selectItem (line 18) | selectItem(target: HTMLElement | null, trigger?: boolean) {
FILE: packages/quill/src/ui/picker.ts
function toggleAriaAttribute (line 5) | function toggleAriaAttribute(element: HTMLElement, attribute: string) {
class Picker (line 12) | class Picker {
method constructor (line 17) | constructor(select: HTMLSelectElement) {
method togglePicker (line 43) | togglePicker() {
method buildItem (line 51) | buildItem(option: HTMLOptionElement) {
method buildLabel (line 84) | buildLabel() {
method buildOptions (line 96) | buildOptions() {
method buildPicker (line 123) | buildPicker() {
method escape (line 132) | escape() {
method close (line 140) | close() {
method selectItem (line 147) | selectItem(item: HTMLElement | null, trigger = false) {
method update (line 177) | update() {
FILE: packages/quill/src/ui/tooltip.ts
class Tooltip (line 9) | class Tooltip {
method constructor (line 14) | constructor(quill: Quill, boundsContainer?: HTMLElement) {
method hide (line 28) | hide() {
method position (line 32) | position(reference: Bounds) {
method show (line 60) | show() {
FILE: packages/quill/test/e2e/fixtures/Clipboard.ts
class Clipboard (line 4) | class Clipboard {
method constructor (line 5) | constructor(private page: Page) {}
method copy (line 7) | async copy() {
method cut (line 11) | async cut() {
method paste (line 15) | async paste() {
method writeText (line 19) | async writeText(value: string) {
method writeHTML (line 45) | async writeHTML(value: string) {
method readText (line 49) | async readText() {
method readHTML (line 53) | async readHTML() {
method read (line 58) | private async read(type: string) {
method write (line 75) | private async write(data: string, type: string) {
FILE: packages/quill/test/e2e/fixtures/Composition.ts
method constructor (line 13) | constructor(protected page: Page) {}
method withKeyboardEvents (line 15) | protected async withKeyboardEvents(
class ChromiumCompositionSession (line 27) | class ChromiumCompositionSession extends CompositionSession {
method constructor (line 28) | constructor(
method update (line 35) | async update(key: string) {
method commit (line 47) | async commit(committedText: string) {
class Composition (line 56) | class Composition {
method constructor (line 57) | constructor(
method start (line 62) | async start() {
FILE: packages/quill/test/e2e/fixtures/index.ts
constant CHAPTER (line 34) | const CHAPTER = 'Chapter 1. Loomings.';
FILE: packages/quill/test/e2e/fixtures/utils/Locker.ts
constant PREFIX (line 12) | const PREFIX = 'playwright_locker_';
class Locker (line 14) | class Locker {
method clearAll (line 15) | public static clearAll() {
method constructor (line 19) | constructor(private key: string) {}
method filePath (line 21) | private get filePath() {
method lock (line 25) | async lock() {
method release (line 34) | async release() {
FILE: packages/quill/test/e2e/pageobjects/EditorPage.ts
type Op (line 4) | interface Op {
class EditorPage (line 57) | class EditorPage {
method constructor (line 58) | constructor(protected readonly page: Page) {}
method root (line 60) | get root() {
method open (line 64) | async open() {
method html (line 69) | async html(content: string, title = '') {
method getSelection (line 78) | getSelection() {
method setSelection (line 87) | async setSelection(
method typeWordWithIME (line 98) | async typeWordWithIME(composition: Composition, composedWord: string) {
method cutoffHistory (line 105) | async cutoffHistory() {
method updateContents (line 112) | async updateContents(delta: Op[], source: 'api' | 'user' = 'api') {
method setContents (line 122) | async setContents(delta: Op[]) {
method getContents (line 129) | getContents(): Promise<Op[]> {
method moveCursorTo (line 140) | async moveCursorTo(query: string) {
method moveCursorAfterText (line 163) | moveCursorAfterText(text: string) {
method moveCursorBeforeText (line 167) | moveCursorBeforeText(text: string) {
method selectText (line 171) | async selectText(start: string, end?: string) {
method waitForText (line 199) | private async waitForText(text: string) {
FILE: packages/quill/test/e2e/utils/index.ts
constant SHORTKEY (line 2) | const SHORTKEY = isMac ? 'Meta' : 'Control';
function getSelectionInTextNode (line 4) | function getSelectionInTextNode() {
FILE: packages/quill/test/fuzz/__helpers__/utils.ts
function randomInt (line 1) | function randomInt(max: number) {
function choose (line 5) | function choose<T>(choices: T[]): T {
function runFuzz (line 9) | function runFuzz(testCase: () => void) {
FILE: packages/quill/test/fuzz/editor.spec.ts
type AttributeDef (line 10) | type AttributeDef = { name: string; values: (number | string | boolean)[...
constant BLOCK_EMBED_NAME (line 11) | const BLOCK_EMBED_NAME = 'video';
constant INLINE_EMBED_NAME (line 12) | const INLINE_EMBED_NAME = 'image';
type SingleInsertValue (line 84) | type SingleInsertValue =
FILE: packages/quill/test/types/quill.test-d.ts
class MyBlot (line 16) | class MyBlot extends LeafBlot {}
FILE: packages/quill/test/unit/__helpers__/expect.ts
method toEqualHTML (line 26) | toEqualHTML(received, expected, options: { ignoreAttrs?: string[] } = {}) {
FILE: packages/quill/test/unit/__helpers__/vitest.d.ts
type CustomMatchers (line 3) | interface CustomMatchers<R = unknown> {
type Assertion (line 8) | interface Assertion<T = any> extends CustomMatchers<T> {}
type AsymmetricMatchersContaining (line 9) | interface AsymmetricMatchersContaining extends CustomMatchers {}
FILE: packages/quill/test/unit/core/editor.spec.ts
class MyBlot (line 874) | class MyBlot extends Block {
method formatAt (line 878) | formatAt(index: number, length: number, name: string, value: string) {
FILE: packages/quill/test/unit/core/quill.spec.ts
class Counter (line 47) | class Counter {}
class MyCounterBlot (line 55) | class MyCounterBlot extends LeafBlot {
class ABlot (line 66) | class ABlot extends LeafBlot {
class AModule (line 70) | class AModule {}
FILE: packages/quill/test/unit/core/utils/createRegistryWithFormats.spec.ts
class RequiredContainer (line 28) | class RequiredContainer extends Container {
class Child (line 31) | class Child extends Inline {
class InfiniteBlot (line 51) | class InfiniteBlot extends Inline {
FILE: packages/quill/test/unit/modules/syntax.spec.ts
constant HIGHLIGHT_INTERVAL (line 10) | const HIGHLIGHT_INTERVAL = 10;
FILE: packages/quill/test/unit/theme/base/tooltip.spec.ts
class Tooltip (line 7) | class Tooltip extends BaseTooltip {
FILE: packages/website/next.config.mjs
method webpack (line 54) | webpack(config) {
FILE: packages/website/src/components/ClickOutsideHandler.jsx
constant TOUCH_EVENT (line 3) | const TOUCH_EVENT = { react: 'onTouchStart', native: 'touchstart' };
constant MOUSE_EVENT (line 4) | const MOUSE_EVENT = { react: 'onMouseDown', native: 'mousedown' };
FILE: packages/website/src/components/Heading.jsx
constant EXPERIMENTAL_FLAG (line 4) | const EXPERIMENTAL_FLAG = ' #experimental';
FILE: packages/website/src/components/MDX.jsx
function MDX (line 72) | function MDX({ mdxSource, data }) {
FILE: packages/website/src/pages/_app.jsx
function MyApp (line 9) | function MyApp({ Component, pageProps }) {
FILE: packages/website/src/pages/_document.jsx
function Document (line 5) | function Document() {
FILE: packages/website/src/pages/docs/[...id].jsx
function getStaticPaths (line 10) | async function getStaticPaths() {
function getStaticProps (line 17) | async function getStaticProps({ params }) {
function Doc (line 44) | function Doc({ mdxSource, filePath, permalink, data }) {
FILE: packages/website/src/pages/index.jsx
function loadFonts (line 132) | function loadFonts() {
FILE: packages/website/src/pages/playground/[...id].jsx
function getStaticPaths (line 19) | async function getStaticPaths() {
function getStaticProps (line 26) | async function getStaticProps({ params }) {
function Playground (line 47) | function Playground({ pack, permalink, title }) {
FILE: packages/website/src/utils/flattenData.js
function flattenData (line 1) | function flattenData(root) {
FILE: scripts/release.js
function main (line 33) | async function main() {
FILE: scripts/utils/configGit.mjs
function configGit (line 3) | async function configGit() {
Condensed preview — 260 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,049K chars).
[
{
"path": ".eslintignore",
"chars": 15,
"preview": "dist/\nscripts/\n"
},
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 3227,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": ".github/CONTRIBUTING.md",
"chars": 4130,
"preview": "# Contributing\n\nThe best way to contribute is to help others in the Quill community. This includes:\n\n- Reporting new [bu"
},
{
"path": ".github/DEVELOPMENT.md",
"chars": 1987,
"preview": "# Development\n\nThis repo is a monorepo powered by npm's official [workspace feature](https://docs.npmjs.com/cli/v10/usin"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 609,
"preview": "Please describe the a concise description and fill out the details below. It will help others efficiently understand you"
},
{
"path": ".github/release.yml",
"chars": 326,
"preview": "changelog:\n exclude:\n authors:\n - quill-bot\n categories:\n - title: Bug Fixes 🛠\n labels:\n - chan"
},
{
"path": ".github/workflows/_test.yml",
"chars": 1532,
"preview": "name: Tests\non:\n workflow_call:\njobs:\n e2e:\n name: E2E Tests\n timeout-minutes: 60\n runs-on: ubuntu-latest\n "
},
{
"path": ".github/workflows/changelog.yml",
"chars": 447,
"preview": "name: Generate Changelog\n\non:\n release:\n types: [published, created]\n workflow_dispatch: {}\n\njobs:\n changelog:\n "
},
{
"path": ".github/workflows/label.yml",
"chars": 510,
"preview": "name: Pull Requests\n\non:\n pull_request:\n types: [opened, labeled, unlabeled, synchronize]\n\njobs:\n label:\n runs-o"
},
{
"path": ".github/workflows/main.yml",
"chars": 161,
"preview": "name: Main\n\non:\n push:\n branches: [main]\n\nconcurrency:\n group: main-build\n cancel-in-progress: true\n\njobs:\n test:"
},
{
"path": ".github/workflows/pull-request.yml",
"chars": 117,
"preview": "name: Pull Requests\n\non:\n pull_request:\n branches: [main]\n\njobs:\n test:\n uses: ./.github/workflows/_test.yml\n"
},
{
"path": ".github/workflows/release.yml",
"chars": 1200,
"preview": "name: Release\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: 'npm version. Examples: \"2.0.0\","
},
{
"path": ".gitignore",
"chars": 115,
"preview": ".*\n!.eslintrc.json\n!.eslintignore\n!.npmignore\n!.gitignore\n!.github\n\nnode_modules\n\ntest-results/\nplaywright-report/\n"
},
{
"path": ".npmignore",
"chars": 56,
"preview": "*.ts\n!*.d.ts\n.*\n.github\n.vscode\ndocs\ntest\ntsconfig.json\n"
},
{
"path": "CHANGELOG.md",
"chars": 51741,
"preview": "# v2.0.2 (2024-05-13)\n\n<!-- Release notes generated using configuration in .github/release.yml at v2.0.2 -->\n\n## What's "
},
{
"path": "LICENSE",
"chars": 1561,
"preview": "Copyright (c) 2017-2024, Slab\nCopyright (c) 2014, Jason Chen\nCopyright (c) 2013, salesforce.com\nAll rights reserved.\n\nRe"
},
{
"path": "README.md",
"chars": 3493,
"preview": "<h1 align=\"center\">\n <a href=\"https://quilljs.com/\" title=\"Quill\">Quill Rich Text Editor</a>\n</h1>\n<p align=\"center\">\n "
},
{
"path": "package.json",
"chars": 1127,
"preview": "{\n \"name\": \"quill-monorepo\",\n \"version\": \"2.0.3\",\n \"description\": \"Quill development environment\",\n \"private\": true,"
},
{
"path": "packages/quill/.eslintrc.json",
"chars": 1109,
"preview": "{\n \"extends\": [\n \"eslint:recommended\",\n \"plugin:prettier/recommended\",\n \"plugin:import/recommended\",\n \"plug"
},
{
"path": "packages/quill/.gitignore",
"chars": 5,
"preview": "dist\n"
},
{
"path": "packages/quill/LICENSE",
"chars": 1561,
"preview": "Copyright (c) 2017-2024, Slab\nCopyright (c) 2014, Jason Chen\nCopyright (c) 2013, salesforce.com\nAll rights reserved.\n\nRe"
},
{
"path": "packages/quill/README.md",
"chars": 44,
"preview": "# Quill\n\nThis is the main package of Quill.\n"
},
{
"path": "packages/quill/babel.config.cjs",
"chars": 280,
"preview": "const pkg = require('./package.json');\n\nmodule.exports = {\n presets: [\n ['@babel/preset-env', { modules: false }],\n "
},
{
"path": "packages/quill/package.json",
"chars": 2947,
"preview": "{\n \"name\": \"quill\",\n \"version\": \"2.0.3\",\n \"description\": \"Your powerful, rich text editor\",\n \"author\": \"Jason Chen <"
},
{
"path": "packages/quill/playwright.config.ts",
"chars": 1100,
"preview": "import { defineConfig, devices } from '@playwright/test';\n\nconst port = 9001;\n\nexport default defineConfig({\n testDir: "
},
{
"path": "packages/quill/scripts/babel-svg-inline-import.cjs",
"chars": 1321,
"preview": "const fs = require('fs');\nconst { dirname, resolve } = require('path');\nconst { optimize } = require('svgo');\n\nmodule.ex"
},
{
"path": "packages/quill/scripts/build",
"chars": 520,
"preview": "#!/usr/bin/env bash\n\nset -e\n\nDIST=dist\n\nTMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')\nnpx tsc --declaration"
},
{
"path": "packages/quill/src/assets/base.styl",
"chars": 7325,
"preview": "// Styles shared between snow and bubble\n\ncontrolHeight = 24px\ninputPaddingWidth = 5px\ninputPaddingHeight = 3px\n\ncolorIt"
},
{
"path": "packages/quill/src/assets/bubble/toolbar.styl",
"chars": 275,
"preview": "arrowWidth = 6px\n\n.ql-bubble\n .ql-toolbar\n .ql-formats\n margin: 8px 12px 8px 0px\n .ql-formats:first-child\n "
},
{
"path": "packages/quill/src/assets/bubble/tooltip.styl",
"chars": 1142,
"preview": "arrowWidth = 6px\n\n.ql-bubble\n .ql-tooltip\n background-color: backgroundColor\n border-radius: 25px\n color: text"
},
{
"path": "packages/quill/src/assets/bubble.styl",
"chars": 1053,
"preview": "themeName = 'bubble'\nactiveColor = #fff\nborderColor = #777\nbackgroundColor = #444\ninactiveColor = #ccc\nshadowColor = #dd"
},
{
"path": "packages/quill/src/assets/core.styl",
"chars": 4957,
"preview": "// Styles necessary for Quill\n\nLIST_STYLE = decimal lower-alpha lower-roman\nLIST_STYLE_WIDTH = 1.2em\nLIST_STYLE_MARGIN ="
},
{
"path": "packages/quill/src/assets/snow/toolbar.styl",
"chars": 644,
"preview": ".ql-toolbar.ql-snow\n border: 1px solid borderColor\n box-sizing: border-box\n font-family: 'Helvetica Neue', 'Helvetica"
},
{
"path": "packages/quill/src/assets/snow/tooltip.styl",
"chars": 1328,
"preview": "tooltipMargin = 8px\n\n.ql-snow\n .ql-tooltip\n background-color: #fff\n border: 1px solid borderColor\n box-shadow:"
},
{
"path": "packages/quill/src/assets/snow.styl",
"chars": 283,
"preview": "themeName = 'snow'\nactiveColor = #06c\nborderColor = #ccc\nbackgroundColor = #fff\ninactiveColor = #444\nshadowColor = #ddd\n"
},
{
"path": "packages/quill/src/blots/block.ts",
"chars": 5771,
"preview": "import {\n AttributorStore,\n BlockBlot,\n EmbedBlot,\n LeafBlot,\n Scope,\n} from 'parchment';\nimport type { Blot, Paren"
},
{
"path": "packages/quill/src/blots/break.ts",
"chars": 335,
"preview": "import { EmbedBlot } from 'parchment';\n\nclass Break extends EmbedBlot {\n static value() {\n return undefined;\n }\n\n "
},
{
"path": "packages/quill/src/blots/container.ts",
"chars": 112,
"preview": "import { ContainerBlot } from 'parchment';\n\nclass Container extends ContainerBlot {}\n\nexport default Container;\n"
},
{
"path": "packages/quill/src/blots/cursor.ts",
"chars": 6200,
"preview": "import { EmbedBlot, Scope } from 'parchment';\nimport type { Parent, ScrollBlot } from 'parchment';\nimport type Selection"
},
{
"path": "packages/quill/src/blots/embed.ts",
"chars": 2785,
"preview": "import type { ScrollBlot } from 'parchment';\nimport { EmbedBlot } from 'parchment';\nimport TextBlot from './text.js';\n\nc"
},
{
"path": "packages/quill/src/blots/inline.ts",
"chars": 1733,
"preview": "import { EmbedBlot, InlineBlot, Scope } from 'parchment';\nimport type { BlotConstructor } from 'parchment';\nimport Break"
},
{
"path": "packages/quill/src/blots/scroll.ts",
"chars": 13063,
"preview": "import { ContainerBlot, LeafBlot, Scope, ScrollBlot } from 'parchment';\nimport type { Blot, Parent, EmbedBlot, ParentBlo"
},
{
"path": "packages/quill/src/blots/text.ts",
"chars": 367,
"preview": "import { TextBlot } from 'parchment';\n\nclass Text extends TextBlot {}\n\n// https://lodash.com/docs#escape\nconst entityMap"
},
{
"path": "packages/quill/src/core/composition.ts",
"chars": 1564,
"preview": "import Embed from '../blots/embed.js';\nimport type Scroll from '../blots/scroll.js';\nimport Emitter from './emitter.js';"
},
{
"path": "packages/quill/src/core/editor.ts",
"chars": 15796,
"preview": "import { cloneDeep, isEqual, merge } from 'lodash-es';\nimport { LeafBlot, EmbedBlot, Scope, ParentBlot } from 'parchment"
},
{
"path": "packages/quill/src/core/emitter.ts",
"chars": 2182,
"preview": "import { EventEmitter } from 'eventemitter3';\nimport instances from './instances.js';\nimport logger from './logger.js';\n"
},
{
"path": "packages/quill/src/core/instances.ts",
"chars": 81,
"preview": "import type Quill from '../core.js';\n\nexport default new WeakMap<Node, Quill>();\n"
},
{
"path": "packages/quill/src/core/logger.ts",
"chars": 784,
"preview": "const levels = ['error', 'warn', 'log', 'info'] as const;\nexport type DebugLevel = (typeof levels)[number];\nlet level: D"
},
{
"path": "packages/quill/src/core/module.ts",
"chars": 219,
"preview": "import type Quill from './quill.js';\n\nabstract class Module<T extends {} = {}> {\n static DEFAULTS = {};\n\n constructor("
},
{
"path": "packages/quill/src/core/quill.ts",
"chars": 29923,
"preview": "import { merge } from 'lodash-es';\nimport * as Parchment from 'parchment';\nimport type { Op } from 'quill-delta';\nimport"
},
{
"path": "packages/quill/src/core/selection.ts",
"chars": 14990,
"preview": "import { LeafBlot, Scope } from 'parchment';\nimport { cloneDeep, isEqual } from 'lodash-es';\nimport Emitter from './emit"
},
{
"path": "packages/quill/src/core/theme.ts",
"chars": 1433,
"preview": "import type Quill from '../core.js';\nimport type Clipboard from '../modules/clipboard.js';\nimport type History from '../"
},
{
"path": "packages/quill/src/core/utils/createRegistryWithFormats.ts",
"chars": 1142,
"preview": "import { Registry } from 'parchment';\n\nconst MAX_REGISTER_ITERATIONS = 100;\nconst CORE_FORMATS = ['block', 'break', 'cur"
},
{
"path": "packages/quill/src/core/utils/scrollRectIntoView.ts",
"chars": 4552,
"preview": "export type Rect = {\n top: number;\n right: number;\n bottom: number;\n left: number;\n};\n\nconst getParentElement = (ele"
},
{
"path": "packages/quill/src/core.ts",
"chars": 1541,
"preview": "import Quill, { Parchment, Range } from './core/quill.js';\nimport type {\n Bounds,\n DebugLevel,\n EmitterSource,\n Expa"
},
{
"path": "packages/quill/src/formats/align.ts",
"chars": 427,
"preview": "import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst config = {\n scope: Scope.BLOCK,"
},
{
"path": "packages/quill/src/formats/background.ts",
"chars": 348,
"preview": "import { ClassAttributor, Scope } from 'parchment';\nimport { ColorAttributor } from './color.js';\n\nconst BackgroundClass"
},
{
"path": "packages/quill/src/formats/blockquote.ts",
"chars": 170,
"preview": "import Block from '../blots/block.js';\n\nclass Blockquote extends Block {\n static blotName = 'blockquote';\n static tagN"
},
{
"path": "packages/quill/src/formats/bold.ts",
"chars": 446,
"preview": "import Inline from '../blots/inline.js';\n\nclass Bold extends Inline {\n static blotName = 'bold';\n static tagName = ['S"
},
{
"path": "packages/quill/src/formats/code.ts",
"chars": 1703,
"preview": "import Block from '../blots/block.js';\nimport Break from '../blots/break.js';\nimport Cursor from '../blots/cursor.js';\ni"
},
{
"path": "packages/quill/src/formats/color.ts",
"chars": 707,
"preview": "import { ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nclass ColorAttributor extends StyleAttributor {\n "
},
{
"path": "packages/quill/src/formats/direction.ts",
"chars": 441,
"preview": "import { Attributor, ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst config = {\n scope: Scope.BLOCK,"
},
{
"path": "packages/quill/src/formats/font.ts",
"chars": 465,
"preview": "import { ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst config = {\n scope: Scope.INLINE,\n whitelis"
},
{
"path": "packages/quill/src/formats/formula.ts",
"chars": 815,
"preview": "import Embed from '../blots/embed.js';\n\nclass Formula extends Embed {\n static blotName = 'formula';\n static className "
},
{
"path": "packages/quill/src/formats/header.ts",
"chars": 278,
"preview": "import Block from '../blots/block.js';\n\nclass Header extends Block {\n static blotName = 'header';\n static tagName = ['"
},
{
"path": "packages/quill/src/formats/image.ts",
"chars": 1349,
"preview": "import { EmbedBlot } from 'parchment';\nimport { sanitize } from './link.js';\n\nconst ATTRIBUTES = ['alt', 'height', 'widt"
},
{
"path": "packages/quill/src/formats/indent.ts",
"chars": 987,
"preview": "import { ClassAttributor, Scope } from 'parchment';\n\nclass IndentAttributor extends ClassAttributor {\n add(node: HTMLEl"
},
{
"path": "packages/quill/src/formats/italic.ts",
"chars": 147,
"preview": "import Bold from './bold.js';\n\nclass Italic extends Bold {\n static blotName = 'italic';\n static tagName = ['EM', 'I'];"
},
{
"path": "packages/quill/src/formats/link.ts",
"chars": 1221,
"preview": "import Inline from '../blots/inline.js';\n\nclass Link extends Inline {\n static blotName = 'link';\n static tagName = 'A'"
},
{
"path": "packages/quill/src/formats/list.ts",
"chars": 1679,
"preview": "import Block from '../blots/block.js';\nimport Container from '../blots/container.js';\nimport type Scroll from '../blots/"
},
{
"path": "packages/quill/src/formats/script.ts",
"chars": 582,
"preview": "import Inline from '../blots/inline.js';\n\nclass Script extends Inline {\n static blotName = 'script';\n static tagName ="
},
{
"path": "packages/quill/src/formats/size.ts",
"chars": 358,
"preview": "import { ClassAttributor, Scope, StyleAttributor } from 'parchment';\n\nconst SizeClass = new ClassAttributor('size', 'ql-"
},
{
"path": "packages/quill/src/formats/strike.ts",
"chars": 151,
"preview": "import Bold from './bold.js';\n\nclass Strike extends Bold {\n static blotName = 'strike';\n static tagName = ['S', 'STRIK"
},
{
"path": "packages/quill/src/formats/table.ts",
"chars": 5695,
"preview": "import type { LinkedList } from 'parchment';\nimport Block from '../blots/block.js';\nimport Container from '../blots/cont"
},
{
"path": "packages/quill/src/formats/underline.ts",
"chars": 161,
"preview": "import Inline from '../blots/inline.js';\n\nclass Underline extends Inline {\n static blotName = 'underline';\n static tag"
},
{
"path": "packages/quill/src/formats/video.ts",
"chars": 1373,
"preview": "import { BlockEmbed } from '../blots/block.js';\nimport Link from './link.js';\n\nconst ATTRIBUTES = ['height', 'width'];\n\n"
},
{
"path": "packages/quill/src/modules/clipboard.ts",
"chars": 19077,
"preview": "import type { ScrollBlot } from 'parchment';\nimport {\n Attributor,\n BlockBlot,\n ClassAttributor,\n EmbedBlot,\n Scope"
},
{
"path": "packages/quill/src/modules/history.ts",
"chars": 5774,
"preview": "import { Scope } from 'parchment';\nimport type Delta from 'quill-delta';\nimport Module from '../core/module.js';\nimport "
},
{
"path": "packages/quill/src/modules/input.ts",
"chars": 2933,
"preview": "import Delta from 'quill-delta';\nimport Module from '../core/module.js';\nimport Quill from '../core/quill.js';\nimport ty"
},
{
"path": "packages/quill/src/modules/keyboard.ts",
"chars": 25766,
"preview": "import { cloneDeep, isEqual } from 'lodash-es';\nimport Delta, { AttributeMap } from 'quill-delta';\nimport { EmbedBlot, S"
},
{
"path": "packages/quill/src/modules/normalizeExternalHTML/index.ts",
"chars": 341,
"preview": "import googleDocs from './normalizers/googleDocs.js';\nimport msWord from './normalizers/msWord.js';\n\nconst NORMALIZERS ="
},
{
"path": "packages/quill/src/modules/normalizeExternalHTML/normalizers/googleDocs.ts",
"chars": 1058,
"preview": "const normalWeightRegexp = /font-weight:\\s*normal/;\nconst blockTagNames = ['P', 'OL', 'UL'];\n\nconst isBlockElement = (el"
},
{
"path": "packages/quill/src/modules/normalizeExternalHTML/normalizers/msWord.ts",
"chars": 3144,
"preview": "const ignoreRegexp = /\\bmso-list:[^;]*ignore/i;\nconst idRegexp = /\\bmso-list:[^;]*\\bl(\\d+)/i;\nconst indentRegexp = /\\bms"
},
{
"path": "packages/quill/src/modules/syntax.ts",
"chars": 10803,
"preview": "import Delta from 'quill-delta';\nimport { ClassAttributor, Scope } from 'parchment';\nimport type { Blot, ScrollBlot } fr"
},
{
"path": "packages/quill/src/modules/table.ts",
"chars": 4148,
"preview": "import Delta from 'quill-delta';\nimport Quill from '../core/quill.js';\nimport Module from '../core/module.js';\nimport {\n"
},
{
"path": "packages/quill/src/modules/tableEmbed.ts",
"chars": 7155,
"preview": "import Delta, { OpIterator } from 'quill-delta';\nimport type { Op, AttributeMap } from 'quill-delta';\nimport Module from"
},
{
"path": "packages/quill/src/modules/toolbar.ts",
"chars": 10291,
"preview": "import Delta from 'quill-delta';\nimport { EmbedBlot, Scope } from 'parchment';\nimport Quill from '../core/quill.js';\nimp"
},
{
"path": "packages/quill/src/modules/uiNode.ts",
"chars": 3337,
"preview": "import { ParentBlot } from 'parchment';\nimport Module from '../core/module.js';\nimport Quill from '../core/quill.js';\n\nc"
},
{
"path": "packages/quill/src/modules/uploader.ts",
"chars": 2683,
"preview": "import Delta from 'quill-delta';\nimport type Quill from '../core/quill.js';\nimport Emitter from '../core/emitter.js';\nim"
},
{
"path": "packages/quill/src/quill.ts",
"chars": 3485,
"preview": "import Quill from './core.js';\nimport type {\n Bounds,\n DebugLevel,\n EmitterSource,\n ExpandedQuillOptions,\n QuillOpt"
},
{
"path": "packages/quill/src/themes/base.ts",
"chars": 10185,
"preview": "import { merge } from 'lodash-es';\nimport type Quill from '../core/quill.js';\nimport Emitter from '../core/emitter.js';\n"
},
{
"path": "packages/quill/src/themes/bubble.ts",
"chars": 4660,
"preview": "import { merge } from 'lodash-es';\nimport Emitter from '../core/emitter.js';\nimport BaseTheme, { BaseTooltip } from './b"
},
{
"path": "packages/quill/src/themes/snow.ts",
"chars": 4837,
"preview": "import { merge } from 'lodash-es';\nimport Emitter from '../core/emitter.js';\nimport BaseTheme, { BaseTooltip } from './b"
},
{
"path": "packages/quill/src/types.d.ts",
"chars": 128,
"preview": "declare module '*.svg' {\n const content: string;\n export default content;\n}\n\ndeclare const QUILL_VERSION: string | und"
},
{
"path": "packages/quill/src/ui/color-picker.ts",
"chars": 1034,
"preview": "import Picker from './picker.js';\n\nclass ColorPicker extends Picker {\n constructor(select: HTMLSelectElement, label: st"
},
{
"path": "packages/quill/src/ui/icon-picker.ts",
"chars": 858,
"preview": "import Picker from './picker.js';\n\nclass IconPicker extends Picker {\n defaultItem: HTMLElement | null;\n\n constructor(s"
},
{
"path": "packages/quill/src/ui/icons.ts",
"chars": 2843,
"preview": "import alignLeftIcon from '../assets/icons/align-left.svg';\nimport alignCenterIcon from '../assets/icons/align-center.sv"
},
{
"path": "packages/quill/src/ui/picker.ts",
"chars": 5736,
"preview": "import DropdownIcon from '../assets/icons/dropdown.svg';\n\nlet optionsCounter = 0;\n\nfunction toggleAriaAttribute(element:"
},
{
"path": "packages/quill/src/ui/tooltip.ts",
"chars": 2158,
"preview": "import type Quill from '../core.js';\nimport type { Bounds } from '../core/selection.js';\n\nconst isScrollable = (el: Elem"
},
{
"path": "packages/quill/test/e2e/__dev_server__/index.html",
"chars": 2612,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edg"
},
{
"path": "packages/quill/test/e2e/__dev_server__/webpack.config.cjs",
"chars": 765,
"preview": "/*eslint-env node*/\n\nconst path = require('path');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst commo"
},
{
"path": "packages/quill/test/e2e/fixtures/Clipboard.ts",
"chars": 2568,
"preview": "import type { Page } from '@playwright/test';\nimport { SHORTKEY } from '../utils/index.js';\n\nclass Clipboard {\n constru"
},
{
"path": "packages/quill/test/e2e/fixtures/Composition.ts",
"chars": 1776,
"preview": "import type {\n CDPSession,\n Page,\n PlaywrightWorkerOptions,\n} from '@playwright/test';\n\nabstract class CompositionSes"
},
{
"path": "packages/quill/test/e2e/fixtures/index.ts",
"chars": 1235,
"preview": "import { test as base } from '@playwright/test';\nimport EditorPage from '../pageobjects/EditorPage.js';\nimport Compositi"
},
{
"path": "packages/quill/test/e2e/fixtures/utils/Locker.ts",
"chars": 791,
"preview": "import { unlink, writeFile } from 'fs/promises';\nimport { unlinkSync } from 'fs';\nimport { tmpdir } from 'os';\nimport { "
},
{
"path": "packages/quill/test/e2e/full.spec.ts",
"chars": 6432,
"preview": "import { expect } from '@playwright/test';\nimport { getSelectionInTextNode, SHORTKEY } from './utils/index.js';\nimport {"
},
{
"path": "packages/quill/test/e2e/history.spec.ts",
"chars": 5473,
"preview": "import { expect } from '@playwright/test';\nimport type { Page } from '@playwright/test';\nimport { test } from './fixture"
},
{
"path": "packages/quill/test/e2e/list.spec.ts",
"chars": 6067,
"preview": "import { expect } from '@playwright/test';\nimport { test } from './fixtures/index.js';\nimport { isMac, sleep } from './u"
},
{
"path": "packages/quill/test/e2e/pageobjects/EditorPage.ts",
"chars": 5914,
"preview": "import type { Page } from '@playwright/test';\nimport type Composition from '../fixtures/Composition.js';\n\ninterface Op {"
},
{
"path": "packages/quill/test/e2e/replaceSelection.spec.ts",
"chars": 3131,
"preview": "import { expect } from '@playwright/test';\nimport { test } from './fixtures/index.js';\n\ntest.describe('replace selection"
},
{
"path": "packages/quill/test/e2e/utils/index.ts",
"chars": 582,
"preview": "export const isMac = process.platform === 'darwin';\nexport const SHORTKEY = isMac ? 'Meta' : 'Control';\n\nexport function"
},
{
"path": "packages/quill/test/fuzz/__helpers__/utils.ts",
"chars": 338,
"preview": "export function randomInt(max: number) {\n return Math.floor(Math.random() * max);\n}\n\nexport function choose<T>(choices:"
},
{
"path": "packages/quill/test/fuzz/editor.spec.ts",
"chars": 7927,
"preview": "import type { Op } from 'quill-delta';\nimport Delta, { AttributeMap } from 'quill-delta';\nimport { choose, randomInt, ru"
},
{
"path": "packages/quill/test/fuzz/tableEmbed.spec.ts",
"chars": 5021,
"preview": "import type { AttributeMap } from 'quill-delta';\nimport Delta from 'quill-delta';\nimport TableEmbed from '../../src/modu"
},
{
"path": "packages/quill/test/fuzz/vitest.config.ts",
"chars": 258,
"preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n resolve: {\n extensions: ['.ts', '.js']"
},
{
"path": "packages/quill/test/types/quill.test-d.ts",
"chars": 5319,
"preview": "import { assertType, expectTypeOf } from 'vitest';\nimport Quill, { Delta } from '../../src/quill.js';\nimport type { Emit"
},
{
"path": "packages/quill/test/unit/__helpers__/cleanup.ts",
"chars": 93,
"preview": "import { beforeEach } from 'vitest';\n\nbeforeEach(() => {\n document.body.innerHTML = '';\n});\n"
},
{
"path": "packages/quill/test/unit/__helpers__/expect.ts",
"chars": 1758,
"preview": "import { expect } from 'vitest';\nimport { normalizeHTML } from './utils.js';\n\nconst sortAttributes = (element: HTMLEleme"
},
{
"path": "packages/quill/test/unit/__helpers__/factory.ts",
"chars": 1359,
"preview": "import { Registry } from 'parchment';\nimport type { Attributor } from 'parchment';\n\nimport Block from '../../../src/blot"
},
{
"path": "packages/quill/test/unit/__helpers__/utils.ts",
"chars": 254,
"preview": "export const sleep = (ms: number) =>\n new Promise<void>((r) => {\n setTimeout(() => {\n r();\n }, ms);\n });\n\ne"
},
{
"path": "packages/quill/test/unit/__helpers__/vitest.d.ts",
"chars": 340,
"preview": "import type { Assertion, AsymmetricMatchersContaining } from 'vitest';\n\ninterface CustomMatchers<R = unknown> {\n toEqua"
},
{
"path": "packages/quill/test/unit/blots/block-embed.spec.ts",
"chars": 5296,
"preview": "import { describe, expect, test } from 'vitest';\nimport {\n createScroll as baseCreateScroll,\n createRegistry,\n} from '"
},
{
"path": "packages/quill/test/unit/blots/block.spec.ts",
"chars": 3128,
"preview": "import { describe, expect, test } from 'vitest';\nimport {\n createScroll as baseCreateScroll,\n createRegistry,\n} from '"
},
{
"path": "packages/quill/test/unit/blots/inline.spec.ts",
"chars": 1278,
"preview": "import { describe, expect, test } from 'vitest';\nimport { createRegistry, createScroll } from '../__helpers__/factory.js"
},
{
"path": "packages/quill/test/unit/blots/scroll.spec.ts",
"chars": 3838,
"preview": "import { describe, expect, test, vitest } from 'vitest';\nimport Emitter from '../../../src/core/emitter.js';\nimport Sele"
},
{
"path": "packages/quill/test/unit/core/composition.spec.ts",
"chars": 957,
"preview": "import Emitter from '../../../src/core/emitter.js';\nimport Composition from '../../../src/core/composition.js';\nimport S"
},
{
"path": "packages/quill/test/unit/core/editor.spec.ts",
"chars": 47918,
"preview": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport Block from '../../../src/blots"
},
{
"path": "packages/quill/test/unit/core/emitter.spec.ts",
"chars": 937,
"preview": "import { describe, expect, test } from 'vitest';\nimport Emitter from '../../../src/core/emitter.js';\nimport Quill from '"
},
{
"path": "packages/quill/test/unit/core/quill.spec.ts",
"chars": 42996,
"preview": "import '../../../src/quill.js';\nimport Delta from 'quill-delta';\nimport { LeafBlot, Registry } from 'parchment';\nimport "
},
{
"path": "packages/quill/test/unit/core/selection.spec.ts",
"chars": 22073,
"preview": "import Selection, { Range } from '../../../src/core/selection.js';\nimport Cursor from '../../../src/blots/cursor.js';\nim"
},
{
"path": "packages/quill/test/unit/core/utils/createRegistryWithFormats.spec.ts",
"chars": 2517,
"preview": "import '../../../../src/quill.js';\nimport { describe, expect, test, vitest } from 'vitest';\nimport createRegistryWithFor"
},
{
"path": "packages/quill/test/unit/formats/align.spec.ts",
"chars": 1751,
"preview": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport { describe, test, expect } fro"
},
{
"path": "packages/quill/test/unit/formats/bold.spec.ts",
"chars": 758,
"preview": "import { describe, expect, test } from 'vitest';\nimport {\n createRegistry,\n createScroll as baseCreateScroll,\n} from '"
},
{
"path": "packages/quill/test/unit/formats/code.spec.ts",
"chars": 10990,
"preview": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport {\n createScroll as baseCreate"
},
{
"path": "packages/quill/test/unit/formats/color.spec.ts",
"chars": 1859,
"preview": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport {\n createScroll as baseCreate"
},
{
"path": "packages/quill/test/unit/formats/header.spec.ts",
"chars": 1488,
"preview": "import Delta from 'quill-delta';\nimport {\n createScroll as baseCreateScroll,\n createRegistry,\n} from '../__helpers__/f"
},
{
"path": "packages/quill/test/unit/formats/indent.spec.ts",
"chars": 1714,
"preview": "import Delta from 'quill-delta';\nimport {\n createScroll as baseCreateScroll,\n createRegistry,\n} from '../__helpers__/f"
},
{
"path": "packages/quill/test/unit/formats/link.spec.ts",
"chars": 2879,
"preview": "import Delta from 'quill-delta';\nimport {\n createScroll as baseCreateScroll,\n createRegistry,\n} from '../__helpers__/f"
},
{
"path": "packages/quill/test/unit/formats/list.spec.ts",
"chars": 10212,
"preview": "import Delta from 'quill-delta';\nimport {\n createScroll as baseCreateScroll,\n createRegistry,\n} from '../__helpers__/f"
},
{
"path": "packages/quill/test/unit/formats/script.spec.ts",
"chars": 1205,
"preview": "import Editor from '../../../src/core/editor.js';\nimport Script from '../../../src/formats/script.js';\nimport {\n create"
},
{
"path": "packages/quill/test/unit/formats/table.spec.ts",
"chars": 8837,
"preview": "import Delta from 'quill-delta';\nimport Editor from '../../../src/core/editor.js';\nimport {\n createScroll as baseCreate"
},
{
"path": "packages/quill/test/unit/modules/clipboard.spec.ts",
"chars": 23540,
"preview": "import Delta from 'quill-delta';\nimport { describe, expect, test, vitest } from 'vitest';\nimport Quill from '../../../sr"
},
{
"path": "packages/quill/test/unit/modules/history.spec.ts",
"chars": 9911,
"preview": "import Delta from 'quill-delta';\nimport { describe, expect, test, vitest } from 'vitest';\nimport Quill from '../../../sr"
},
{
"path": "packages/quill/test/unit/modules/keyboard.spec.ts",
"chars": 2461,
"preview": "import { describe, expect, test } from 'vitest';\nimport Keyboard, {\n SHORTKEY,\n normalize,\n} from '../../../src/module"
},
{
"path": "packages/quill/test/unit/modules/normalizeExternalHTML/normalizers/googleDocs.spec.ts",
"chars": 864,
"preview": "import { describe, expect, test } from 'vitest';\nimport normalize from '../../../../../src/modules/normalizeExternalHTML"
},
{
"path": "packages/quill/test/unit/modules/normalizeExternalHTML/normalizers/msWord.spec.ts",
"chars": 1621,
"preview": "import { describe, expect, test } from 'vitest';\nimport normalize from '../../../../../src/modules/normalizeExternalHTML"
},
{
"path": "packages/quill/test/unit/modules/syntax.spec.ts",
"chars": 11615,
"preview": "import hljs from 'highlight.js';\nimport Delta from 'quill-delta';\nimport { afterAll, beforeAll, describe, expect, test }"
},
{
"path": "packages/quill/test/unit/modules/table.spec.ts",
"chars": 5860,
"preview": "import Delta from 'quill-delta';\nimport Quill from '../../../src/core.js';\nimport { describe, expect, test } from 'vites"
},
{
"path": "packages/quill/test/unit/modules/tableEmbed.spec.ts",
"chars": 15927,
"preview": "import Delta from 'quill-delta';\nimport { tableHandler } from '../../../src/modules/tableEmbed.js';\nimport { afterEach, "
},
{
"path": "packages/quill/test/unit/modules/toolbar.spec.ts",
"chars": 9889,
"preview": "import { describe, expect, test } from 'vitest';\nimport Quill from '../../../src/core/quill.js';\nimport Toolbar, { addCo"
},
{
"path": "packages/quill/test/unit/modules/uiNode.spec.ts",
"chars": 1426,
"preview": "import '../../../src/quill.js';\nimport { describe, expect, test } from 'vitest';\nimport UINode, {\n TTL_FOR_VALID_SELECT"
},
{
"path": "packages/quill/test/unit/theme/base/tooltip.spec.ts",
"chars": 3848,
"preview": "import { describe, expect, test } from 'vitest';\nimport Quill from '../../../../src/core.js';\nimport Video from '../../."
},
{
"path": "packages/quill/test/unit/ui/picker.spec.ts",
"chars": 6197,
"preview": "import { describe, expect, test } from 'vitest';\nimport Picker from '../../../src/ui/picker.js';\n\ndescribe('Picker', () "
},
{
"path": "packages/quill/test/unit/vitest.config.ts",
"chars": 577,
"preview": "import { defineConfig } from 'vitest/config';\nimport { resolve } from 'path';\n\nexport default defineConfig({\n resolve: "
},
{
"path": "packages/quill/tsconfig.json",
"chars": 465,
"preview": "{\n \"ts-node\": {\n \"compilerOptions\": {\n \"esModuleInterop\": true\n }\n },\n \"compilerOptions\": {\n \"outDir\": "
},
{
"path": "packages/quill/webpack.common.cjs",
"chars": 1398,
"preview": "/*eslint-env node*/\n\nconst { resolve } = require('path');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin'"
},
{
"path": "packages/quill/webpack.config.cjs",
"chars": 1139,
"preview": "/*eslint-env node*/\n\nconst { BannerPlugin, DefinePlugin } = require('webpack');\nconst common = require('./webpack.common"
},
{
"path": "packages/website/.eslintrc.json",
"chars": 24,
"preview": "{\n \"extends\": \"next\"\n}\n"
},
{
"path": "packages/website/.gitignore",
"chars": 36,
"preview": "node_modules\n\n.next\nout\ncertificates"
},
{
"path": "packages/website/README.md",
"chars": 69,
"preview": "# Quill Official Website\n\n[https://quilljs.com](https://quilljs.com)\n"
},
{
"path": "packages/website/content/blog/a-new-delta.mdx",
"chars": 3439,
"preview": "---\ntitle: A New Delta\ndate: 2014-09-29\n---\n\nPart of providing a complete API in Quill is providing events for when and "
},
{
"path": "packages/website/content/blog/an-official-cdn-for-quill.mdx",
"chars": 481,
"preview": "---\ntitle: An Offical CDN for Quill\ndate: 2014-08-12\n---\n\nQuill now has an offical Content Distribution Network so you c"
},
{
"path": "packages/website/content/blog/announcing-quill-1-0.mdx",
"chars": 4509,
"preview": "---\ntitle: Announcing Quill 1.0\ndate: 2016-09-06\n---\n\nQuill 1.0 has arrived. It was just two years ago that Quill made i"
},
{
"path": "packages/website/content/blog/are-we-there-yet-to-1-0.mdx",
"chars": 1993,
"preview": "---\ntitle: Are We There Yet (to 1.0)?\ndate: 2016-03-14\n---\n\nWhen Quill laid out its [1.0 roadmap](/blog/the-road-to-1-0/"
},
{
"path": "packages/website/content/blog/quill-1-0-beta-release.mdx",
"chars": 1154,
"preview": "---\ntitle: Quill 1.0 Beta Release\ndate: 2016-05-03\n---\n\nToday Quill is ready for its first beta preview of 1.0. This is "
},
{
"path": "packages/website/content/blog/quill-1-0-release-candidate-released.mdx",
"chars": 3927,
"preview": "---\ntitle: Quill 1.0 Release Candidate... Released!\ndate: 2016-08-18\n---\n\nToday Quill enters its highly anticipated 1.0 "
},
{
"path": "packages/website/content/blog/quill-v0-19-no-more-iframes.mdx",
"chars": 3099,
"preview": "---\ntitle: Quill v0.19 - No More Iframes\ndate: 2014-11-06\n---\n\nCustomizability is core to Quill's ethos and the new [v0."
},
{
"path": "packages/website/content/blog/the-road-to-1-0.mdx",
"chars": 4588,
"preview": "---\ntitle: The Road to 1.0\ndate: 2015-09-15\n---\n\nQuill launched with the ambitious goal of becoming _the_ rich text edit"
},
{
"path": "packages/website/content/blog/the-state-of-quill-and-2-0.mdx",
"chars": 585,
"preview": "---\ntitle: The State of Quill and 2.0\ndate: 2017-09-21\nexternal: https://medium.com/@jhchen/the-state-of-quill-and-2-0-f"
},
{
"path": "packages/website/content/blog/upgrading-to-rich-text-deltas.mdx",
"chars": 3528,
"preview": "---\ntitle: Upgrading to Rich Text Deltas\ndate: 2014-10-19\n---\n\nThe new rich text type is now live and being used in Quil"
},
{
"path": "packages/website/content/docs/api.mdx",
"chars": 26177,
"preview": "---\ntitle: API\n---\n\n<div className=\"table-of-contents\">\n {data.api.map(({ hashes, title }) => (\n <nav key={title}>\n "
},
{
"path": "packages/website/content/docs/configuration.mdx",
"chars": 3954,
"preview": "---\ntitle: Configuration\n---\n\nQuill allows several ways to customize it to suit your needs. This section is dedicated to"
},
{
"path": "packages/website/content/docs/customization/registries.mdx",
"chars": 2468,
"preview": "---\ntitle: Registries\n---\n\nRegistries allow multiple editors with different formats to coexist on the same page.\n\nIf you"
},
{
"path": "packages/website/content/docs/customization/themes.mdx",
"chars": 2031,
"preview": "---\ntitle: Themes\n---\n\nThemes allow you to easily make your editor look good with minimal effort. Quill features two off"
},
{
"path": "packages/website/content/docs/customization.mdx",
"chars": 8303,
"preview": "---\ntitle: Customization\n---\n\nQuill was designed with customization and extension in mind. This is achieved by implement"
},
{
"path": "packages/website/content/docs/delta.mdx",
"chars": 6217,
"preview": "---\ntitle: Delta\n---\n\nDeltas are a simple, yet expressive format that can be used to describe Quill's contents and chang"
},
{
"path": "packages/website/content/docs/formats.mdx",
"chars": 3316,
"preview": "---\ntitle: Formats\n---\n\nQuill supports a number of formats, both in UI controls and API calls.\n\nBy default, all formats "
},
{
"path": "packages/website/content/docs/guides/building-a-custom-module.mdx",
"chars": 6601,
"preview": "---\ntitle: Building a Custom Module\n---\n\nQuill's core strength as an editor is its rich API and powerful customization c"
},
{
"path": "packages/website/content/docs/guides/cloning-medium-with-parchment.js",
"chars": 5609,
"preview": "export const externalResources = [\n 'https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css',\n 'h"
},
{
"path": "packages/website/content/docs/guides/cloning-medium-with-parchment.mdx",
"chars": 27699,
"preview": "---\ntitle: Cloning Medium with Parchment\n---\n\nTo provide a consistent editing experience, you need both consistent data "
},
{
"path": "packages/website/content/docs/guides/designing-the-delta-format.mdx",
"chars": 14999,
"preview": "---\ntitle: Designing the Delta Format\n---\n\nRich text editors lack a specification to express its own contents. Until rec"
},
{
"path": "packages/website/content/docs/installation.mdx",
"chars": 3482,
"preview": "---\ntitle: Installation\n---\n\nQuill comes ready to use in several convenient forms.\n\n## CDN\n\nA globally distributed and a"
},
{
"path": "packages/website/content/docs/modules/clipboard.mdx",
"chars": 2993,
"preview": "---\ntitle: Clipboard Module\n---\n\nThe Clipboard handles copy, cut and paste between Quill and external applications. A se"
},
{
"path": "packages/website/content/docs/modules/history.mdx",
"chars": 1916,
"preview": "---\ntitle: History Module\n---\n\nThe History module is responsible for handling undo and redo for Quill. It can be configu"
},
{
"path": "packages/website/content/docs/modules/keyboard.mdx",
"chars": 7461,
"preview": "---\ntitle: Keyboard Module\n---\n\nThe Keyboard module enables custom behavior for keyboard events in particular contexts. "
},
{
"path": "packages/website/content/docs/modules/syntax.mdx",
"chars": 1817,
"preview": "---\ntitle: Syntax Highlighter Module\n---\n\nThe Syntax Module enhances the Code Block format by automatically detecting an"
},
{
"path": "packages/website/content/docs/modules/toolbar.mdx",
"chars": 7502,
"preview": "---\ntitle: Toolbar Module\n---\n\nThe Toolbar module allow users to easily format Quill's contents.\n\n<div className=\"quill-"
},
{
"path": "packages/website/content/docs/modules.mdx",
"chars": 1818,
"preview": "---\ntitle: Modules\n---\n\nModules allow Quill's behavior and functionality to be customized. Several officially supported "
},
{
"path": "packages/website/content/docs/quickstart.mdx",
"chars": 1084,
"preview": "---\ntitle: Quickstart\n---\n\nThe best way to get started is to try a simple example. Quill is initialized with a DOM eleme"
},
{
"path": "packages/website/content/docs/upgrading-to-2-0.mdx",
"chars": 3283,
"preview": "---\ntitle: Upgrading to 2.0\n---\n\nQuill has been significantly modernized. Leveraging the latest browser-supported APIs, "
},
{
"path": "packages/website/content/docs/why-quill.mdx",
"chars": 3741,
"preview": "---\ntitle: Why Quill\n---\n\nContent creation has been at the core to the web since its beginning. The `<textarea>` provide"
},
{
"path": "packages/website/env.js",
"chars": 869,
"preview": "const { version, homepage } = require('./package.json');\n\nconst cdn = process.env.NEXT_PUBLIC_LOCAL_QUILL\n ? `http://lo"
},
{
"path": "packages/website/next.config.mjs",
"chars": 2354,
"preview": "import withMDX from '@next/mdx';\nimport env from './env.js';\n\n/** @type {import('next').NextConfig} */\nexport default wi"
},
{
"path": "packages/website/package.json",
"chars": 1221,
"preview": "{\n \"name\": \"website\",\n \"version\": \"2.0.3\",\n \"description\": \"Quill official website\",\n \"private\": true,\n \"homepage\":"
},
{
"path": "packages/website/public/CNAME",
"chars": 11,
"preview": "quilljs.com"
},
{
"path": "packages/website/public/robots.txt",
"chars": 23,
"preview": "User-agent: *\nAllow: /\n"
},
{
"path": "packages/website/src/components/ActiveLink.jsx",
"chars": 1178,
"preview": "import { useRouter } from 'next/router';\nimport Link from 'next/link';\nimport React, { useState, useEffect, useCallback "
},
{
"path": "packages/website/src/components/ClickOutsideHandler.jsx",
"chars": 1419,
"preview": "import { useEffect, useRef } from 'react';\n\nconst TOUCH_EVENT = { react: 'onTouchStart', native: 'touchstart' };\nconst M"
},
{
"path": "packages/website/src/components/Editor.jsx",
"chars": 1026,
"preview": "import { useLayoutEffect, useRef } from 'react';\nimport { withoutSSR } from './NoSSR';\n\nconst Editor = ({\n children,\n "
},
{
"path": "packages/website/src/components/GitHub.jsx",
"chars": 1227,
"preview": "import classNames from 'classnames';\nimport { useEffect, useState } from 'react';\nimport OctocatIcon from '../svg/octoca"
}
]
// ... and 60 more files (download for full content)
About this extraction
This page contains the full source code of the slab/quill GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 260 files (968.9 KB), approximately 262.8k tokens, and a symbol index with 631 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.