Forum How do I...?

Absolutely positioned sidenotes

lukasbestle
I'm working on a layout that is quite similar to the one in the official "Textbook" sample document.

Basically the layout will have two columns: One "main" column and a "sidenote" column that is supposed to always be on the outside of the page.

My current approach is the one from the "Textbook" sample: Floating the sidenote to the outside with a negative margin to the outside.
The issue is: That will only work if the element that contains the sidenote (the paragraph or heading in my case) does not have any padding whatsoever. Once there is any padding involved (like in an <ul>), the negative margin I applied will be incorrect and the sidenote will not be floated to the correct position.

I have tried the following solutions:

  • Correct the negative margin by the amount of the padding with a "li > .sidenote" selector: Won't work, because the sidenotes are always floated to the outside, so either to the left or to the right of the main column. Result: A corrected negative margin only works on right pages, on left pages the margin is even more wrong. Also the whole "correct the negative margin" thing is quite a hack.
  • Symmetric padding for <ul>s so that the correction can be the same no matter if on the left or on the right: Even more a hack and won't scale with larger paddings because it actually creates padding on the wrong side that was not intended.

So basically I can say that I can't solve this layout problem with floats. The negative margins are just a very bad hack. It works for simple stuff like in the "Textbook" sample, but not for my use-case because of the paddings.

What would work is absolute positioning. Basically I would always position my sidenotes to be at a specific distance from the outside.
For this to work, I'd need one of these two features in Prince:

  • New CSS properties called "outside" and "inside" that work just like "left" and "right" but respect the current page (either being a left or right page).
  • The ability to style elements differently depending on the page they are on. Basically the ability to have CSS rules for elements inside the @page rules that currently only accept styles for the page directly.

Unfortunately both of these features are not available, so I don't see a way to use absolute positioning either.

Is there maybe another way that I haven't thought of? Or is one of the features I proposed above planned for the future?
Thanks in advance!

Lukas
lukasbestle
Ping :)
mikeday
Proper sidenotes are definitely on our roadmap, but challenging to design; currently the best we have is floats with negative margins which have the issues you have encountered, and absolutely positioning the @footnotes area, which does not maintain their vertical positioning.

If vertical alignment is not a consideration then you could try the @footnotes approach, otherwise for now I think it may be necessary to do a two pass process with JavaScript to detect which page the floats end up on and apply custom CSS to each one; this is complicated unfortunately.

We hope to provide better solutions for sidenotes in the future.
lukasbestle
Thank you for your reply!

Vertical alignment is important for my use-case as the sidenotes contain additional information for the text next to them (to be specific: they contain references to other sections of the document, so it doesn't make sense to put them into "real" footnotes).

But wouldn't the first feature I proposed (outside and inside CSS properties for absolute positioning) be rather simple to add to Prince? Basically my CSS would look like this:

.sidenote {
  position: absolute;
  outside: 20mm;
}


The "outside" property would just be an alias to either "left" or "right" depending on the page the element is displayed on. It would work just like the "float: outside" property but wouldn't have the float issues because it uses absolute positioning. Do you know what I mean?
lukasbestle
I have built myself a "two-pass solution" for the meantime. In case anyone has a similar problem, here is my JavaScript code:

/**
 * Marks all matching elements that are on a left page
 *
 * @package   Prince Sidenotes
 * @author    Lukas Bestle <project-prince-sidenotes@lukasbestle.com>
 * @copyright 2018 Lukas Bestle
 */
(function(Prince) {
	'use strict';
	
	// Only run the following code in a Prince environment
	if(!Prince) return;
	
	// Track boxes so that we can later get the page number for each element
	Prince.trackBoxes = true;
	
	/**
	 * Returns the page number of a given element
	 *
	 * @return {integer}
	 */
	Element.prototype.getPageNum = function() {
		var boxes = this.getPrinceBoxes();
		return (boxes.length > 0)? boxes[0].pageNum : null;
	};
	
	/**
	 * Returns whether the element is printed on
	 * a left (even) page; uses the page number
	 * to determine that
	 *
	 * @return {Boolean}
	 */
	Element.prototype.isOnLeftPage = function() {
		return this.getPageNum() % 2 === 0;
	}
	
	/**
	 * Process all sidenotes after layout is done
	 */
	Prince.addEventListener('complete', function() {
		var html = document.documentElement;
		
		// Get all sidenote elements; you can modify the selector here
		var sidenotes = document.querySelectorAll('.sidenote');
		
		// Add a class to each sidenote on a left page
		[].forEach.call(sidenotes, function(sidenote) {
			if(sidenote.isOnLeftPage()) {
				sidenote.className = (sidenote.className)? sidenote.className + ' on-left-page' : 'on-left-page';
			}
		});
		
		// Get the attributes of the <html> element for printing them later
		var attrString = '';
		[].forEach.call(html.attributes, function(attr) {
			attrString += ' ' + attr.name + '="' + attr.value + '"';
		});
		
		// Print the full source for the "second pass"
		console.log('<!DOCTYPE html>');
		console.log('<html' + attrString + '>' + html.innerHTML + '</html>');
	});
})(Prince);


You can put it directly into a <script> tag in your document or load it as an external file. After that, you can run it like this:

# "First pass"
prince --javascript your-document.html > your-document.processed.html

# "Second pass"
prince your-document.processed.html

# Cleanup
mv your-document.processed.pdf your-document.pdf
rm your-document.processed.html

Edited by lukasbestle

mikeday
Nice, very elegant code!