I spent a rainy Saturday afternoon going from having nothing to having a working implementation of the Quill editor. It doesn't do much, but it's a start, and it's a smart start. Before we get there, though, I thought I'd document the infrastructure I use for a project like this. It's plain old Javascript (well, Typescript) for the most part, and I have a big ol' gnarly toolchain.

I was reading through Quill's documentation and it looks like the writers of Quill took their inspiration from a post from Nick Santos at Medium Engineering about how the Medium editor works called Why ContentEditable is Terrible, and ran with it, and the results are quite impressive. Quill looks like the right editor for some of my projects. One thing I have to investigate is how well the typeahead lookup engine works, and what if anything interferes with it.

The final result of this project, including this post as the README, is available on my Github account at elfsternberg/quill-study.

Let's document the steps I took to get to the point where I could run it locally. I wanted my standard environment. Getting there was a chore.

I started with the basics. I made a folder and initialized an empty git repository.

mkdir quill-study
cd quill-study
git init
git commit --allow-empty -m "Initial commit for the Quill experiment."
yarn init
git commit -am "Initializing Yarn."

I edited the package.json file to remove the "main": field and to mark this as "private": true, as I don't want to be able to publish this by accident.

Next, it's time to install webpack:

yarn add -D webpack webpack-cli http-server rimraf

I also edited package.json, adding a 'build' and a 'serve' script:

"scripts": {
    "build": "rimraf dist && mkdir -p dist && cp index.html dist/ && webpack",
    "serve": "http-server dist/"
},

Most of this so far has been the webpack out-of-the-box tutorial, only with a few flourishes of my own. For the next step, I converted to Typescript, which is not in the Webpack tutorial.

yarn add -D typescript awesome-typescript-loader
yarn add -D ts-node '@types/node' '@types/webpack' '@types/lodash'

For the most part, this was simple: I renamed webpack.config.js to webpack.config.ts and added the Configuration type to the file. I renamed index.js to index.ts, added typing and null checks where the compiler told me to.

Next, we're going to add high-quality linting and prettying to the code:

yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
yarn add -D eslint-config-airbnb eslint-plugin-import eslint-plugin-prettier eslint-config-prettier prettier
yarn add -D eslint-plugin-jsx-a11y eslint-plugin-react

Using the AirBNB Eslint configuration feels like cheating, but it's always served me well. I do not appreciate needing all the react plugins, but they don't really get in the way of a non-React project. My Eslint configuration file is pretty standard:

{
    "parser": "@typescript-eslint/parser",
    "plugins": ["@typescript-eslint", "eslint-plugin-import", "prettier"],
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": ["airbnb", "plugin:@typescript-eslint/recommended", "prettier"],
    "parserOptions": {
        "project": ["tsconfig.json"],
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "rules": {
        "no-nested-ternary": "off",
        "@typescript-eslint/no-use-before-define": ["error"],
        "@typescript-eslint/explicit-function-return-type": "off",
        "@typescript-eslint/no-unused-vars": "off",
        "import/extensions": "off",
        "import/prefer-default-export": "off"
    },
    "settings": {
        "import/resolver": {
            "node": {
                "extensions": [".js", ".ts"]
            },
            "typescript": {
                "project": "./tsconfig.json"
            }
        }
    }
}

About the only weirdness here is that I've turned off export default as a requirement. I like being able to export multiple items from one file once in a while, especially for icon collections. I'm usually a one-product-per-file developer, but not always, and not since I've started having a lot of SVG-in-HTML developments in my code.

The other thing I did was install pre-commit. Pre-commit is my way of stopping myself from doing anything embarrassing in a repository. Syntax checks and bad formats are rejected before they're allowed to be committed. For my pre-commit I only use prettier, as eslint can be somewhat slow. My .pre-commit-config.yaml file is pretty basic:

repos:
    - repo: https://github.com/pre-commit/mirrors-prettier
      rev: "" # Use the sha / tag you want to point at
      hooks:
          - id: prettier

And now, after all that, I finally get to the part I was interested in: getting an instance of Quill up and running. The one thing I needed most to figure out was how to import the default stylesheet Quill uses for its demos, which ship with the editor.

There are two ways to do this. The first is to copy the CSS from its location in node_modules, but that's kinda lame. The second way is to use the (obscure) tilde syntax. Quill ships with two different styles, named "Snow" and "Blossom". I'll use Snow. You can write this in your CSS:

@import "~quill/dist/quill.snow.css

But you can't write this in your typescript:

import "~quill/dist/quill.snow.css

So you have to have an intermediate file, index.css which has the @import line above, in your /src folder, and then you can import that css into your typescript, an CSS Loader and Style Loader can handle that just fine.

When all is said and done, the entirety of my typescript looks just plain small:

import Quill from "quill";
import "./index.css";

(function _iffe() {
    if (document && document.body) {
        const content = document.body.querySelector("#content");
        if (content) {
            console.log("running...");
            const editor = new Quill(content, {
                placeholder: "Compose your masterpiece....",
                theme: "bubble",
            });
        }
    }
})();

But that's all you need. The #content div is just an empty div, 12rem high and 40rem wide, centered on an otherwise boring page.