Skip to content

Instantly share code, notes, and snippets.

@felipeochoa
Created January 13, 2018 17:13
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save felipeochoa/353aa2fba9a30762382323efea5e843f to your computer and use it in GitHub Desktop.
Save felipeochoa/353aa2fba9a30762382323efea5e843f to your computer and use it in GitHub Desktop.
Notes to self on blots in quill

Quill Blots

There are four options for creating a custom blot:

——-——–+——-+

InlineBlock

——-——–+——-+

Embed

——-——–+——-+

Text

——-——–+——-+

Inline vs block

The major difference between inline and block blots is that blocks cannot be nested. “Instead of wrapping, Block blots replace one another when applied to the same text range.”

Embed vs text

How Quill/Parchment work

Quill’s user-facing interface is based on the Delta format: documents are represented as text strings with properties applied to certain ranges (this format strongly resembles the Emacs propertized strings format). Block-level formatting is handled by special-casing the formatting operations on newlines in the text string.

Parchment’s interface is tree-based, though it maps into a linear content model similar to how the DOM does (in the case of the DOM, the mapping is called document order). In Parchment’s tree, nodes are called Blots, which are instances of a ShadowBlot subclass. As with any tree, there are two main types of blots: parent blots inherit from ContainerBlot and leaf blots inherit from LeafBlot.

There’s another categorization of blots that’s specific to Parchment however:

Inline blots
These represent either run-level formatting (e.g., bold, italics, etc.) or run-level content (e.g., text nodes). Formatting blots are nestable, just like HTML tags.
Block blots
These represent paragraph-level content or formatting (e.g., block quote) that are not nestable. E.g., paragraphs can be of type “unordered list” or “ordered list”, but not both. Block-level content is unspecified in Parchment, but can be e.g., video or floated pictures.

How Quill uses Parchment

Each Quill instance has an associated Scroll instance attached to the main <div contenteditable> (viz. quill.root). Operations using the quill API like quill.applyDelta or quill.format will typically reduce to calls to one of the following core methods on the scroll instance:

formatAt(index, length, name, value)
apply the format identified by the string name with value to the user range [index, index + length)
insertAt(index, value: string, def?)
Insert a new string into the document at index (in user coordinates).
deleteAt(index, length)
Delete the user range [index, index + length)
update(mutations, context)
After any outside modifications to the DOM, this method synchronizes the Blot’s internal data with its DOM node
optimize()
This method “canonicalizes” a Blot tree to ensure that the Delta -> Blot map remains well-defined.

The Parchment blot hierarchy

ShadowBlot

This is the root blot type, defining core operations on the blot tree. There are also a number of essentially internal operations (attach, clone, …, wrap) made to ease the implementation of other blots that need to manipulate the blot tree

ContainerBlot

Defines a children property and extends the core methods to forward the call to the appropriate child (modifying the index and length parameters accordingly)

ScrollBlot

This blot defines a MutationObserver to ensure that update and optimize are called on descendant nodes after any DOM modification. This is used as the root blot in quill.

FormatBlot

In addition to managing child nodes, this blot handles Attributors (cf. README). It defines an additional API for its descendant nodes to handle formatAt calls. The idea is to associate with each blot a {key: value} format map representing the sum of the blot and its attributors. This map is used in the following methods:

formats()
Return the format map for this blot. The default implementation starts with the Attributor attributes and simply adds this.statics.blotName to the map with value static formats(this.domNode)
static formats(domNode)
Returns the value to use in the format map for the overal blot
format(name, value)
This method is in charge of modifying the blot tree to mirror the effect of setting name to value in the format map.
BlockBlot

BlockBlot handles formatAt calls in two ways:

attributor calls
These calls use formats registered to Attributors and simply delegate to FormatBlot
all-or-nothing
These calls replace this blot with another block-scoped blot.

insertAt is modified slightly to ensure that inserting block-scoped blots splits the blot instead of adding a child. The other core methods are essentially unchanged

InlineBlot

InlineBlot handles formatAt calls that cause the blot to vanish from the tree (e.g., removing bolding). It forwards Attributor calls into the FormatBlot API. InlineBlot.optimize performs two optimizations:

  1. If the blot has no children, it is removed from the tree
  2. If the blot and its next sibling have identical formats, they are merged together

LeafBlot

Leaf blots are independent units which have a value associated with them. Their static value(domNode) and value() methods retrieve this value from a dom node and a blot respectively. They also have index and position nodes that I don’t really understand

TextBlot

Implements deleteAt and insertAt as expected. optimize removes empty text nodes and merges consecutive text nodes.

EmbedBlot

Adds a format method for subclasses to override wholesale formatting of the embed. Similar to FormatBlot, adds static formats and formats methods as well, though instead of a key-value map, the two formats methods return any value representing the embed value. (Default undefined)

Quill blots

Quill extends and overrides Parchment’s blots as follows:

Container

Only allows quill-specific Blocks, BlockEmbeds, and itself as children.

ScrollBlot

Special-cases various things for code blocks and breaks. Adds a whitelist attribute to restrict which formats may be used. Ensures that inline blots are always descended from block blots.

FormatBlot

No change

Block

Adds a translation layer to the Delta format using newlines to indicate blocks. Thus, it ignores all-or-nothing formatAt calls unless they include the final newline and modifies insertAt calls to add blocks where needed

BlockEmbed

This new element ignores all formatAt calls except Attributors. insertAt adds new nodes as siblings rather than children.

Inline

Ensures nested inlines are always nested in the same order (to ensure a canonical representation as DOM nodes)

Leaf

Text

No change from Parchment

Embed

Wraps the embed in a span with leftGuard and rightGuard nodes on either side of the real dom node. Disables contenteditable on the wrapping span.

Specific methods

Basic blot methods

Every Blot has these structure editing methods, which don’t generally have to be overriden

  • attach
  • clone
  • deleteAt
  • detach
  • formatAt
  • insertAt
  • insertInto
  • isolate
  • offset
  • optimize
  • remove
  • replace
  • replaceWith
  • split
  • update
  • wrap

static create(value: any) : Node

This method should create a new `Node` and modify it such that value is later recoverable. In other words, this should be an invertible function. Often this involves using the `dataset` API. E.g., a `LinkBlot` which takes a url as `value` sets the `href` attribute, which can then be used to recover the url.

This method must also set non-format related attributes as needed (e.g., `target=_blank` on links), since the returned node will probably be added to the page.

The super method creates a `DOMNode` using the `tagName` and `className` properties.

When is this method called?

This method is only ever called by `Registry.create`, which is in turn called whenever Parchment tree manipulations (and their associated DOM node manipulations) are taking place

@bdovh
Copy link

bdovh commented Apr 7, 2021

I have
const Embed = Quill.import("blots/embed");
class CustomBlot extends Embed {

    I want to change this to ShadowBlot.
    When I use 
    
    const Shadow  = Quill.import("blots/shadow");
    class CustomBlot extends Shadow {
    
    it raises an error

Class extends value undefined is not a constructor or null

How do I overcome this error?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment