Implementing pagination with prosemirror

Hi, my team and I are working on an editor that will be open source with a set of schema constraints, fonts, styles and specific features. We are exploring some way to introduce pagination layout in the most right way possible, trying to keep the presentation part out of the schema.

The user will be able to customize pages with different sizes and orientation (a4, letter etc.), integrating common nodes like paragraph, tables, custom widgets and also allowing in a future the collaborative editing. He should be able to immediately see how the page is rendered because he can then save it in PDF

Currently, we are trying to do the integration using a page+ node in order to have a set of pages directly in the state, but we realize that this deviates from our approach of trying to keep the presentation part out.

We were wondering whether maybe we could actually use decorators to manage all this logic, as the documentation says Decorations make it possible to influence the way the document is drawn, without actually changing the document, but what could happen with more advanced nodes like tables? Could this be a viable approach?

The alternative, but probably the least manageable, was that we could extend the current EditorView to handle pagination during rendering, but I suspect that this is not a scalable approach, and we would have problems when we have to divide the nodes in multiple pages, for example regarding interaction (selection, typing) etc.

There are several topics about it but this is actually still a little studied concept, we would like to have an opinion or ideas from someone experienced

The only way I see decorations being used for pagination is likely not the right way about it. It would probably break down for “book size” projects (tens or hundreds of pages.)

Decorations could be used to selectively hide whatever pages (prosemirror nodes) the user is not viewing. Ex: page 2, hide blocks 1-50 and 100-End)

This becomes pretty expensive the more prosemirror nodes / html nodes we are managing. Yet it does achieve the desire - to not worry about presentation given a large prosemirror document; and this solution is amenable to the user deciding whether to view in seamless layout or paged mode, and edits work effortlessly (no need to worry about stitching pages together or storing an array of documents etc.)

This would bypass the need to slice prosemirror documents up, and the page boundaries become dynamic which may or may not be a good thing. It’s a type of occlusion culling but in the direction of virtual pagination. The problem being how many blocks to consider a page? This has been discussed elsewhere but there isn’t really a straightforward way to do this given a dynamic page width and dynamic block size. There would ideally be some primitive algorithm to predict rough page sizes by computing virtual heights of elements, possibly outside the context of prosemirror in a document fragment. But this already is becoming … more complex than one may desire.

Large documents continue to be a kind of sore point for most javascript text editors. I find that the bottleneck though is not prosemirror code, but rather just large numbers of DOM nodes that need to be created and thus exist in a very large DOM tree (even if visible: hidden via decorations - this helps a lot but not enough)

The app I am working on does not do this for pagination. It does it for dynamic filtering of document matches. So given a large document, hide everything with decorations but those that pass some filter predicate. It’s pretty neat for this use case. And although it would theoretically work for pagination (and occlusion culling overall) I have not yet incorporated any implementation.

1 Like

I think you have to count the DOM element heights if you want automatic paging. Or have some fixed unit x for lines per page. Then you’d iterate the doc’s block nodes, rendering the pages as you fit the elements per page.

However, that is terribly inefficient as mentioned and every block change would cause cascading calculation. I think you have compute it lazily and generate the paging only as the user scrolls the document (or opens print preview). And you’d only need to calculate it scrolling downwards.

So a windowing approach as @bZichett has done might be the optimal solution. You should be able to calculate all the pages when you open the doc and fit them nicely. Only then, as you edit, you’d recalculate the overflowing block nodes or whether content from previous page would fit in. But yeah, that could be complicated.

The rerendering part is what worries me if you don’t split your ProseMirror doc (which causes other problems). Hiding elements with decorations might be the best bet. If performance becomes an issue, then you have to figure out a better way.

Decorations can have keys and be cached. There will still inevitably be a problem for large enough documents. Just a matter of when, so what is one’s expectation for themselves and their users is the most important question to answer (like usual.)

But this is primarily not due to any library decision here, just the consequences of vast DOM hierarchies, render life cycles - it’s not the most optimal implementation, and on some level requires culling non-visible elements which feels a lot like pagination anyway, so I am still interested in this as a higher level feature. Pagination is a child implementation of occlusion culling is what I am getting at.

My personal preference with how I use my own software is to avoid, at all cost, “book” like formats or approaches where a single document grows in size and there are easy ways to divide them. There is usually some hierarchical format, in which ‘chapters’ / ‘sections’ can be made into a separate documents. A “master” document can link a collection of them and act as a wrapper (table of contents of sorts). There is an extra abstraction, but that organization usually pays off some how. I rarely if ever have to print numbered pages. Only for any legal work I do (contracts.) I bring them into Microsoft Office or Google Docs for that.

That being said, I have been working on my own knowledge management for software for over a decade, and am continually inspired on how to make it better and better. There is a lot of room for improvement, still.

@bZichett Thanks for answering :grin:

Decorations could be used to selectively hide whatever pages (prosemirror nodes) the user is not viewing. Ex: page 2, hide blocks 1-50 and 100-End). This becomes pretty expensive the more prosemirror nodes / html nodes we are managing. Yet it does achieve the desire - to not worry about presentation given a large prosemirror document; and this solution is amenable to the user deciding whether to view in seamless layout or paged mode, and edits work effortlessly (no need to worry about stitching pages together or storing an array of documents etc.)

I’m not yet very expert in prosemirror so I have a couple of doubts about this. With “selectively hide whatever pages the user is not viewing” do you mean applying a “node” decoration for each node so as to have e.g. a class that hides it in the dom? This mean also that the page could not be defined in the schema

Just an example

// User is viewing the 2nd page and the first two paragraphs shuold be present in the first page
<p class="hidden">...</p>
<p class="hidden">...</p>
<p>...</p>
<p>...</p>

About how much the document is large, our use case may be very specific and the pages will probably be limited for now anyway. I don’t expect “large documents” with lots of nodes at the moment. However, I am imagining that with this approach you cannot manage pages of different sizes or the splitting of custom nodes or nodes for tables etc, or you may not be able to show 2 pages at the same time :thinking:.

@TeemuKoivisto Yes, in case of automatic page I think I should go to counting the DOM elements height, and put them into the page until there is available space. I’m looking also the GitHub - pagedjs/pagedjs: Display paginated content in the browser and generate print books using web technology impl of their chunker.

So do you believe that page should be present in the doc schema to achieve that? For the rerendering part, the other thing that came to mind was to have multiple editorViews that shares the same state. Each EditorView could be a page which use a decoration plugin which only shows the interested content, hiding everything else and filtering the affected transactions. The user should be able to see in that way the pages correctly, but this means that he could edit one page at a time since there are some focus/selection issue. But maybe I’m dreaming :sweat_smile: :joy:

This is what I implemented hoping it helps you and I hope you have better ideas

@riccardoperra Well, I am afraid you are dreaming :slightly_smiling_face:. I don’t think it’s technically possible to divide state between multiple editor views. But what could be possible, if you had the time, is to separate the EditorState and apply the transaction as normal — one huge doc. Extract only the content of what is currently within the view port and render that in EditorView.

It should be fairly cheap operation, even when scrolling, although you probably want to debounce it. Of course you have to translate the selection position constantly, ensure transactions are correct. Seems like a huge hassle so I say, don’t sweat on it until you have the first prototype complete.

Implemenation-wise I think decorations should be able to wrap the block nodes into a page element. Unless you want to persist the pagination and/or add some metadata to it in which case page node is probably better. Moving content between pages just becomes more difficult.

Also this reminded me of the reason why Google Docs moved away from DOM to canvas. Once you start generating pixel-perfect layouts it stands to reason why bother with complicated DOM at all.