Text alignment

I would like to implement text alignment in PM, but I’m unsure how. @marijn gave the tip to add it as an attribute to Paragraphs, which indeed was my first thought as well (though it might be good to have for headings to, and that’s when I got a little unsure).

@ericandrewlewis if you would share some tips on how you did it I, and I’m sure others, would be grateful!

First import the classes you need to modify

import { Textblock, Heading, Paragraph, Attribute, Block } from "prosemirror/dist/model"

Add the new attribute to Textblock. We only wanted alignment for text, if you want it for everything probably best to do this on Block

Textblock.updateAttrs({
	align: new Attribute({default: "left"})
})

Because heading has it’s own get attrs() we need to add it there also

Heading.updateAttrs({
	align: new Attribute({default: "left"})
})

Now we need to represent this attribute in the DOM, this is done with to to_dom transform, which defers to the serializeDOM method of a node type

Paragraph.prototype.serializeDOM = (function(oldSerialize) {
    return function (node, s) {
	let ele = oldSerialize.call(this, node, s)
	this.setDomDisplayProperties(node, ele) //will be explained next
	return ele
    }
}) (Paragraph.prototype.serializeDOM)

This maintains the old serialize implementation, but adds some additional functionality. You’ll need to do this to every NodeType that needs alignment. Since each NoteType defines its own, I haven’t found a good way to make it fully reusable. I did crate the setDomDisplayProperties method to handle actually adding the change to the DOM element, which is reuseable

Textblock.prototype.setDomDisplayProperties = function setDomDisplayProperties(node, ele) {
	ele.style.textAlign = node.attrs.align
}

So now it should render! Like I mentioned serializeDOM has to be changed for all NodeTypes, here’s heading’s for good measure:

Heading.prototype.serializeDOM = (function(oldSerialize) {
    return function serializeDOM(node, s) {
	let ele = oldSerialize.call(this,node, s)
	this.setDomDisplayProperties(node, ele)
	return ele
    }
}) (Heading.prototype.serializeDOM)

One final thing we need to do is make sure it works the other way, from_dom, which comes into play when copy and pasting or with drag and drop.

First we register a new parser. By default they have a rank of 50, so we give ours a lower rank to make sure it matches first

export function fromHTMLBlock(dom, state) {
	state.wrapIn(dom, this, this.deserializeDOMAttrs(dom))
}
Paragraph.register('parseDOM', 'p', {
	parse: fromHTMLBlock,
	rank: 40
})

fromHTMLDOM is pretty much the same as the ‘block’ parser but calls this.deserializeDOMAttrs(dom) to add the attributes as well.

Textblock.prototype.deserializeDOMAttrs = function deserializeDOMAttrs(dom) {
	let attrs = {}
	attrs.align = dom.style.textAlign || "left"
	return attrs
}

This will be inherited by all textblocks, but they can define their own or add to the returned value in a different parse funtion, if they have additional properties. Heading, for example:

for (let i = 1; i <= 6; i++) Heading.registerComputed("parseDOM", "h" + i, type => {
  if (i <= type.maxLevel) return {
    parse: function(dom, state) {
      let attrs = this.deserializeDOMAttrs(dom)
      attrs.level = i
      state.wrapIn(dom, this, attrs)
    },
    rank: 40
  }
})

I think that about covers it, hope this helped!

@mattberkowitz Helpful description…

I wanted to be able to align about anything so I built an Align block which wrapped content in a div with the proper alignment. Here is a demo and the source

so we give ours a lower rank to make sure it matches first

(Since 0.3.0 your newly registered parser will, because it targets the same tag in the same node, overwrite the old one, so no need to worry about the rank.)

Thanks a million, it’s very helpful!

Hi again @mattberkowitz, I am implementing this in a similar way to the one you explained here, but I’m struggling with the commands and wiring it up with the menu. How did you do this, or do you have a custom way of interacting with the document?

Because registering the commands on a Paragraph works fine, but to do it “higher up”, like the Textblock, leads to problems and getting it to align e.g. Headings in any other way also seems difficult.

The way @peteb has implemented it fixes this problem, but has other properties I don’t like so I would be quite interested to get some insight in your way of solving this.

What are the problems you are encountering? I am sure my users will run into the same problems. Perhaps we all can find a better solution.

I’m just starting to tinker with ProseMirror and I like it a lot so far, however it seems to me that there’s no way to handle attributes and primarily styling not in a MarkType format where you operate within the selection inside of the block node, but allow to operate on the parent node itself.

For example, I’d love to have the ability to create custom Mark called “AlignCenter” which would have access to the block node/nodes under selection. Rough example: let’s say I set caret withing a

element. If I call a command associated with AlignCenter Mark, it should attach “text-align: center” style to the parent

instead of either wrapping selected content in a span with given style and/or wrapping parent node with some other node with a given style.

Well, at least that’s the direction I’ve been digging currently. Sure, @mattberkowitz 's way is one possible approach, but to me it feels like a hack instead of a proper editor feature (which, as evident, is desired).

1 Like