- Notifications
You must be signed in to change notification settings - Fork0
Generalized state model for rich-text editors to interface with browser DOM
License
signalapp/parchment
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Parchment isQuill's document model. It is a parallel tree structure to the DOM tree, and provides functionality useful for content editors, like Quill. A Parchment tree is made up ofBlots, which mirror a DOM node counterpart. Blots can provide structure, formatting, and/or content.Attributors can also provide lightweight formatting information.
Note: You should never instantiate a Blot yourself withnew
. This may prevent necessary lifecycle functionality of a Blot. Use theRegistry'screate()
method instead.
npm install parchment
SeeCloning Medium with Parchment for a guide on how Quill uses Parchment its document model.
Blots are the basic building blocks of a Parchment document. Several basic implementations such asBlock,Inline, andEmbed are provided. In general you will want to extend one of these, instead of building from scratch. After implementation, blots need to beregistered before usage.
At the very minimum a Blot must be named with a staticblotName
and associated with either atagName
orclassName
. If a Blot is defined with both a tag and class, the class takes precedence, but the tag may be used as a fallback. Blots must also have ascope, which determine if it is inline or block.
classBlot{staticblotName:string;staticclassName:string;statictagName:string|string[];staticscope:Scope;domNode:Node;prev:Blot|null;next:Blot|null;parent:Blot;// Creates corresponding DOM nodestaticcreate(value?:any):Node;constructor(domNode:Node,value?:any);// For leaves, length of blot's value()// For parents, sum of children's valueslength():Number;// Manipulate at given index and length, if applicable.// Will often pass call onto appropriate child.deleteAt(index:number,length:number);formatAt(index:number,length:number,format:string,value:any);insertAt(index:number,text:string);insertAt(index:number,embed:string,value:any);// Returns offset between this blot and an ancestor'soffset(ancestor:Blot=this.parent):number;// Called after update cycle completes. Cannot change the value or length// of the document, and any DOM operation must reduce complexity of the DOM// tree. A shared context object is passed through all blots.optimize(context:{[key:string]:any}):void;// Called when blot changes, with the mutation records of its change.// Internal records of the blot values can be updated, and modifications of// the blot itself is permitted. Can be trigger from user change or API call.// A shared context object is passed through all blots.update(mutations:MutationRecord[],context:{[key:string]:any});/** Leaf Blots only **/// Returns the value represented by domNode if it is this Blot's type// No checking that domNode can represent this Blot type is required so// applications needing it should check externally before calling.staticvalue(domNode):any;// Given location represented by node and offset from DOM Selection Range,// return index to that location.index(node:Node,offset:number):number;// Given index to location within blot, return node and offset representing// that location, consumable by DOM Selection Rangeposition(index:number,inclusive:boolean):[Node,number];// Return value represented by this blot// Should not change without interaction from API or// user change detectable by update()value():any;/** Parent blots only **/// Whitelist array of Blots that can be direct children.staticallowedChildren:Registry.BlotConstructor[];// Default child blot to be inserted if this blot becomes empty.staticdefaultChild:Registry.BlotConstructor;children:LinkedList<Blot>;// Called during construction, should fill its own children LinkedList.build();// Useful search functions for descendant(s), should not modifydescendant(type:BlotClass,index:number,inclusive):Blot;descendants(type:BlotClass,index:number,length:number):Blot[];/** Formattable blots only **/// Returns format values represented by domNode if it is this Blot's type// No checking that domNode is this Blot's type is required.staticformats(domNode:Node);// Apply format to blot. Should not pass onto child or other blot.format(format:name,value:any);// Return formats represented by blot, including from Attributors.formats():Object;}
Implementation for a Blot representing a link, which is a parent, inline scoped, and formattable.
import{InlineBlot,register}from'parchment';classLinkBlotextendsInlineBlot{staticblotName='link';statictagName='A';staticcreate(url){letnode=super.create();node.setAttribute('href',url);node.setAttribute('target','_blank');node.setAttribute('title',node.textContent);returnnode;}staticformats(domNode){returndomNode.getAttribute('href')||true;}format(name,value){if(name==='link'&&value){this.domNode.setAttribute('href',value);}else{super.format(name,value);}}formats(){letformats=super.formats();formats['link']=LinkBlot.formats(this.domNode);returnformats;}}register(LinkBlot);
Quill also provides many great example implementations in itssource code.
Basic implementation of a block scoped formattable parent Blot. Formatting a block blot by default will replace the appropriate subsection of the blot.
Basic implementation of an inline scoped formattable parent Blot. Formatting an inline blot by default either wraps itself with another blot or passes the call to the appropriate child.
Basic implementation of a non-text leaf blot, that is formattable. Its corresponding DOM node will often be aVoid Element, but can be aNormal Element. In these cases Parchment will not manipulate or generally be aware of the element's children, and it will be important to correctly implement the blot'sindex()
andposition()
functions to correctly work with cursors/selections.
The root parent blot of a Parchment document. It is not formattable.
Attributors are the alternative, more lightweight, way to represent formats. Their DOM counterpart is anAttribute. Like a DOM attribute's relationship to a node, Attributors are meant to belong to Blots. Callingformats()
on anInline orBlock blot will return both the format of the corresponding DOM node represents (if any) and the formats the DOM node's attributes represent (if any).
Attributors have the following interface:
classAttributor{attrName:string;keyName:string;scope:Scope;whitelist:string[];constructor(attrName:string,keyName:string,options:Object={});add(node:HTMLElement,value:string):boolean;canAdd(node:HTMLElement,value:string):boolean;remove(node:HTMLElement);value(node:HTMLElement);}
Note custom attributors are instances, rather than class definitions like Blots. Similar to Blots, instead of creating from scratch, you will probably want to use existing Attributor implementations, such as the baseAttributor,Class Attributor orStyle Attributor.
The implementation for Attributors is surprisingly simple, and itssource code may be another source of understanding.
Uses a plain attribute to represent formats.
import{Attributor,register}from'parchment';letWidth=newAttributor('width','width');register(Width);letimageNode=document.createElement('img');Width.add(imageNode,'10px');console.log(imageNode.outerHTML);// Will print <img width="10px">Width.value(imageNode);// Will return 10pxWidth.remove(imageNode);console.log(imageNode.outerHTML);// Will print <img>
Uses a class name pattern to represent formats.
import{ClassAttributor,register}from'parchment';letAlign=newClassAttributor('align','blot-align');register(Align);letnode=document.createElement('div');Align.add(node,'right');console.log(node.outerHTML);// Will print <div></div>
Uses inline styles to represent formats.
import{StyleAttributor,register}from'parchment';letAlign=newStyleAttributor('align','text-align',{whitelist:['right','center','justify'],// Having no value implies left align});register(Align);letnode=document.createElement('div');Align.add(node,'right');console.log(node.outerHTML);// Will print <div></div>
All methods are accessible from Parchment ex.Parchment.create('bold')
.
// Creates a blot given a name or DOM node.// When given just a scope, creates blot the same name as scopecreate(domNode:Node,value?: any):Blot;create(blotName: string,value?: any):Blot;create(scope:Scope):Blot;// Given DOM node, find corresponding Blot.// Bubbling is useful when searching for a Embed Blot with its corresponding// DOM node's descendant nodes.find(domNode:Node,bubble:boolean=false):Blot;// Search for a Blot or Attributor// When given just a scope, finds blot with same name as scopequery(tagName: string,scope:Scope=Scope.ANY):BlotClass;query(blotName: string,scope:Scope=Scope.ANY):BlotClass;query(domNode:Node,scope:Scope=Scope.ANY):BlotClass;query(scope:Scope):BlotClass;query(attributorName: string,scope:Scope=Scope.ANY):Attributor;// Register Blot class definition or Attributor instanceregister(BlotClass|Attributor);