ftlabs / ftcolumnflow

A polyfill that fixes the inadequacies of CSS column layouts
MIT License
633 stars 51 forks source link

FTColumnflow Build Status

FTColumnflow is a polyfill that fixes the inadequacies of CSS column layouts. It is developed by FT Labs, part of the Financial Times. It is extensively used in the FT Web App, where it allows us to publish articles with complex newspaper/magazine style layouts, including features such as:

It is designed with the same column dimension specification API as the CSS3 multi-column specification (specify columnWidth, columnCount and/or columnGap), but gives far greater flexibility over element positioning within those columns.

Usage

Include FTColumnflow.js in your JavaScript bundle or add it to your HTML page like this:

<script type='text/javascript' src='/src/FTColumnflow.js'></script>

The script must be loaded prior to instantiating FTColumnflow on any element of the page. FTColumnflow adds pages and columns to the DOM inside a specified target element, which must be a child of the viewport element. The resulting pages are the same dimensions as the viewport, which allows for a scrolling window of multiple targets and pages to sit inside it.

FTColumnflow accepts two types of content—fixed and flowed—which can be specified either as text strings or as DOM nodes from which to copy elements. Fixed elements can be positioned using CSS classes to specify page number, vertical/horizontal anchoring, column span and span direction. Flowed elements will be flowed over columns and pages (created automatically) and can optionally include CSS classes to control wrapping behaviour.

To activate FTColumnflow on an article, create a target element inside a viewport:

<section id="viewport">
    ...
    <article id="article-1"></article>
    ...
</section>

Create a new instance of FTColumnflow, passing either ID names or DOM element references for target and viewport, along with an object of configuration parameters (all of which are optional):

var cf = new FTColumnflow('article-1', 'viewport', {
    columnCount: 3,
    standardiseLineHeight: true,
    pagePadding: 30,
});

or:

var articleEl  = document.getElementById('article-1');
var viewportEl = document.getElementById('viewport');

var cf = new FTColumnflow(articleEl, viewportEl, {
    columnCount: 3,
    standardiseLineHeight: true,
    pagePadding: 30,
});

To render flowed content, pass either text strings or DOM nodes into the FTColumnflow.flow() method. For example, if you have the following content, separated into flowed and fixed groups:

<div id="flowedContent">
    <p>One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections.</p>
    <p>The bedding was hardly able to cover it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked. "What's happened to me? " he thought. It wasn't a dream.</p>
    ...
</div>

<div id="fixedContent">
    <div class="col-span-2">
        <h1>The Metamorphosis</h1>
        <h2>Franz Kafka, 1915</h2>
    </div>
    <figure class="anchor-bottom-col-2">
        <img src="http://upload.wikimedia.org/wikipedia/commons/thumb/7/71/Metamorphosis.jpg/147px-Metamorphosis.jpg" style="width: 147px; height: 239px;" />
    </figure>
</div>

You could apply FTColumnflow to this content with code such as:

var flowedContent = document.getElementById('flowedContent'),
    fixedContent  = document.getElementById('fixedContent');

cf.flow(flowedContent, fixedContent);

Alternatively, you can pass your content into the flow method directly:

cf.flow(
    '<p>One morning, when Gregor Samsa woke from troubled dreams...',
    '<div class="col-span-2"><h1>The Metamorphosis</h1><h2>Franz Kafka, 1915</h2></div>...'
);

Examples

Here are some examples of FTColumnflow in use - feel free to copy the code and use as the basis for your own projects.

How does it work?

With FTColumnflow, FTLabs have addressed some of the limitations of the CSS3 multi-column specification. We needed an approach which would give accurate and flexible newspaper-style column layouts, to which we could add fixed-position elements spanning over any number of columns.

Flowing text over columns using JavaScript is not so easy: although it's trivial for a human to spot the last word before a column boundary, not so for a computer. Our first iteration looped through each word in the flowed text to determine whether or not it was within the bounds of the current column. When the first out-of-bounds word was found, the paragraph was split, and the second part moved over to a new column. However, this was found to be very slow and DOM-heavy, especially with long paragraphs.

We then realised that we didn't need to split the paragraphs to prevent out-of-bounds words being seen - we could do the same using overflow: hidden. So the new approach is to determine where in a paragraph the column's bottom boundary will fall, and to copy that paragraph to a new column, with a negative top margin equal to that of the overflow. This example shows a FTColumnflow layout with its internals exposed - it can be seen that paragraphs overflow the purple column boundaries, but are repeated in the following column, shifted up so that the next line of text is visible.

One important consideration for this approach is that, using overflow: hidden, it's possible for a column's boundary to chop off part of a line of text - see this broken example. Here, the line-height of the page elements is not correctly configured in relation to the height of the columns - there is no consistent baseline grid. For a succesful layout with FTColumnflow, it is important that the column height is a whole multiple of the grid height, and that all elements are placed on the grid.

Setting the standardiseLineHeight configuration option to true (it defaults to false) will automatically determine the baseline grid from your page's CSS. It will ensure that column heights are multiples of the grid height, and will pad all fixed and flowed elements to ensure they conform to the grid. See the same example with standardiseLineHeight: true.

Configuration

Configuration options can be specified at create-time by passing a JSON object as the third argument to the FTColumnflow constructor. All parameters are optional; any which are specified will override default values.

Column dimension configuration is designed to be as close as possible to the CSS3 multi-column specification, using the same logic to determine columnWidth, columnCount and columnGap.

Public interface

Constructor

Methods

Properties

All public properties are read-only.

{
    "pageInnerWidth":  740,
    "pageInnerHeight": 600,
    "colDefaultTop":   0,
    "colDefaultLeft":  30,
    "columnCount":     3,
    "columnWidth":     236,
    "columnGap":       16
}

Compatibility

FTColumnflow supports the following browsers:

Testing

FTColumnflow is a fully Test-Driven codebase. Modifcations to the code should have an accompanying test case added to verify the new feature, or confirm the regression or bug is fixed.

NOTE: as of 28th June 2012, FTColumnflow no longer uses JsTestDriver as a test framework - we found it was too limited in scope, and that Buster.js fulfilled all the features and more.

FTColumnflow uses Buster.js as a TDD framework. This requires Node and NPM - installation instructions are at http://busterjs.org/docs/getting-started/. Buster.js creates an HTTP server to which any number of real browsers can be attached; the tests will be performed on each browser.

Usage

Credits and collaboration

The lead developer of FTColumnflow is George Crawford at FT Labs. All open source code released by FT Labs is licenced under the MIT licence. We welcome comments, feedback and suggestions. Please feel free to raise an issue or pull request. Enjoy.