In this weekly series Martin describes a question that was raised by a Fonto developer, how it was resolved and why Fonto behaved like that in the first place. This week, a developer reached out with a question we get more often: how do I build the indent / outdent feature you have in the trial editor?
A support issue came in that went like this:
Hello Fonto support,
Our XML is very focused on nesting content. We have seen your trial, and we really like how it approaches indenting content. I’m talking about this component:
We would like to reproduce that in our editor. Could you point us to the documentation on how we can do this ourselves? We’d like to make the following:
- The current text paragraph becomes the new section title
- Their sibling paragraphs becomes the new section content
- Source structure:
<div> <p>Section Title</p> <p>existing paragraph</p> <p>existing paragraph</p> </div>
- Target structure:
<section> <header>Section Title (was paragraph 1)</header> <text> <p>existing paragraph</p> <p>existing paragraph</p> </text> </section>
Can you help us setting this up?
Kind regards,
A Fonto partner
This is a common question. We built this feature a long time ago and it used private API. In practice, most schemas differ in how titles or sections are named. There might be more elements needed, or some other elements may need to be renamed. All in all, not something that is easily made generic. This is the level of customization where we expect a partner to implement their own custom mutation.
But let’s see how we would implement such a custom operation. Let’s start with indent. XQuery Update Facility is usually an easy way to implement these kind of features.
Indenting a structure is (in terms of XML) a matter of wrapping an item plus its siblings into a section element.
In XQuery Update Facility that you can use with the execute-update-script operation, we can split this up. The first step is to insert a new section that contains everything that should be in there, followed by removing what we just wrapped.
let $focusParagraph := fonto:selection-common-ancestor()/ancestor::p return insert nodes <section>{ $focusParagraph, $focusParagraph/following-sibling::p }</section> before $focusParagraph
The second step is to remove everything we just moved into the section. XQuery Update Facility will not automatically clean this up, so we have to do it by hand.
let $focusParagraph := fonto:selection-common-ancestor()/ancestor::p delete nodes ($focusParagraph, $focusParagraph/following-sibling::p)
This can all be tried out in our on-line playground as well.
These two statements can be moved into a single function, in an XQuery module. This XQuery module can then be called from an operation, like this:
module namespace app = "http://app"; declare function app:indent-to-section ($focusParagraph as element()?) { (: If there is no paragraph, just do not do anything :) if (empty($focusParagraph)) then () else insert nodes <section>{ $focusParagraph, $focusParagraph/following-sibling::p }</section> before $focusParagraph , delete nodes ($focusParagraph, $focusParagraph/following-sibling::p) };
{ "indent-my-section": { "label": "Promote this to a section", "steps": [ { "type": "operation/execute-update-script", "data": { "expression": "import module namespace app='http://app'; app:indent-to-section($data?contextNode)", "contextNodeId": "x__fonto:selection-common-ancestor()/ancestor-or-self::p[1]" } } ] } }
This can then be linked into the UI, with this component:
<FxOperationButton label="" operationName="indent-my-section" /
Outdenting is very similar. It is a matter of moving anything after the focus back into the parent section. This ready-made function can just be added to the module we created earlier. The
declare %public %updating function app:outdent-section ($secNode as element()?) { if (empty($secNode)) then () else insert node <section>{$secNode/@*, $secNode/node(), $secNode/following-sibling::node()}</section> after $secNode/.., delete node $secNode, delete nodes ($secNode/following-sibling::node()) };
This also available on the on-line playground. It can be integrated into the operation and in the UI in the same way as the indent option. For this function though, the closest section ancestor should be passed, instead of the closest p ancestor. Of course, you can change this for your use-case, your schema, whether you want to convert paragraphs into titles or whether any attributes need to be updated.
Why
But why is this not just default Fonto behaviour? Why do we need to program this ourselves?
Our philosophy with Fonto APIs is to provide building blocks as much as we can. It is very difficult to provide an end-to-end solution flexible enough to address any need, so we sometimes provide more basic APIs that can be chained together to make it more generic. Indenting sections is like that. While this does mean we have to provide more recipes, partners keep the full flexibility of the same APIs we use to build these building blocks.
I hope this explained how Fonto works and why it works like that. During the years we built quite the product, and we are aware some parts work in unexpected ways for those who have not been with it from the start. If you have any points of Fonto you would like some focus on, we are always ready and willing to share! Reach out on Twitter to Martin Middel or file a support issue!
Developer advocate / Evangelist. Has been with Fonto since it all began in 2013. He’s currently designing the next steps in Fonto Developer APIs with the input of our valuable partners.
In his spare time, Martin is an avid home brewer.
Receive updates on new Fonto Why & How blog posts by email