├── .gitignore ├── test ├── css │ ├── ReflowTest.css │ ├── ConfigTest.css │ ├── AllTests.css │ ├── ColumnWrapTest.css │ ├── FixedElementsTest.css │ └── BaselineGridTest.css ├── helpers │ ├── helpers.js │ └── stylesheets.js ├── buster.js ├── reflow-test.js ├── config-test.js ├── baselinegrid-test.js ├── fixedelements-test.js └── columnwrap-test.js ├── .github └── CODEOWNERS ├── .travis.yml ├── component.json ├── bower.json ├── GruntFile.js ├── .gitattributes ├── package.json ├── LICENCE.txt ├── examples ├── 4.html ├── 6.html ├── 3.html ├── 1.html ├── 5.html ├── 2.html └── 7.html ├── README.md └── src └── FTColumnflow.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /test/css/ReflowTest.css: -------------------------------------------------------------------------------- 1 | p, div, .cf-column { 2 | line-height: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Guessed from commit history 2 | * @Financial-Times/apps 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: 2 | - "npm test" 3 | 4 | language: node_js 5 | 6 | node_js: 7 | - "6" 8 | -------------------------------------------------------------------------------- /test/css/ConfigTest.css: -------------------------------------------------------------------------------- 1 | .columnGaptest { font-size: 10px; } 2 | .columnGaptest #viewportid { font-size: 1.2em; } 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ftcolumnflow", 3 | "description": "A polyfill that fixes the inadequacies of CSS column layouts.", 4 | "version": "0.2.2", 5 | "main": "src/FTColumnflow.js", 6 | "scripts": [ 7 | "src/FTColumnflow.js" 8 | ], 9 | "license": "MIT" 10 | } 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "ftcolumnflow", 4 | "description": "A polyfill that fixes the inadequacies of CSS column layouts.", 5 | "version": "1.0.0", 6 | "main": "src/FTColumnflow.js", 7 | "scripts": [ 8 | "src/FTColumnflow.js" 9 | ], 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /GruntFile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | buster: { 5 | test: { 6 | config: 'test/buster.js' 7 | }, 8 | server: { 9 | port: 1111 10 | } 11 | } 12 | }); 13 | grunt.loadNpmTasks('grunt-buster'); 14 | grunt.registerTask('test', ['buster:test']); 15 | }; -------------------------------------------------------------------------------- /test/helpers/helpers.js: -------------------------------------------------------------------------------- 1 | var cf, target, viewport; 2 | 3 | function createCf(config) { 4 | cf = new FTColumnflow('targetid', 'viewportid', config || { 5 | columnGap : 25, 6 | columnCount : 3 7 | }); 8 | target = document.getElementById('targetid'); 9 | viewport = document.getElementById('viewportid'); 10 | 11 | return cf; 12 | } 13 | 14 | function cssProp(element, property) { 15 | return window.getComputedStyle(element, null).getPropertyValue(property); 16 | } 17 | 18 | var assert = buster.assert; 19 | var refute = buster.refute; 20 | var expect = buster.expect; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /test/css/AllTests.css: -------------------------------------------------------------------------------- 1 | #viewportid { width: 800px; height: 600px; margin-top: 50px} 2 | 3 | 4 | .height50 { height: 50px; } 5 | .height100 { height: 100px; } 6 | .height200 { height: 200px; } 7 | .height300 { height: 300px; } 8 | .height400 { height: 400px; } 9 | .height500 { height: 500px; } 10 | .height600 { height: 600px; } 11 | .height700 { height: 700px; } 12 | .height1000 { height: 1000px; } 13 | .height3000 { height: 3000px; } 14 | 15 | 16 | /* Colours */ 17 | 18 | #targetid { background-color: blue; } 19 | .cf-column { background-color: gold; } 20 | .cf-page { background-color: green; } 21 | .cf-fixed { background-color: red; } 22 | -------------------------------------------------------------------------------- /test/css/ColumnWrapTest.css: -------------------------------------------------------------------------------- 1 | 2 | div { 3 | line-height: 20px; 4 | } 5 | 6 | .cf-column p { 7 | margin: 20px 0; 8 | line-height: 20px; 9 | } 10 | 11 | .simulated-parags { 12 | height: 290px; 13 | margin-bottom: 20px; 14 | } 15 | 16 | 17 | 18 | /* Colours */ 19 | 20 | .cf-page { 21 | outline: 1px solid white; 22 | } 23 | 24 | #viewportid { 25 | background-color: gray; 26 | } 27 | 28 | .cf-column p { 29 | background-color: rgba(0, 0, 0, 0.1); 30 | outline: 1px solid white; 31 | color: white; 32 | background-image: -webkit-repeating-linear-gradient(red 0px, red 1px, transparent 1px, transparent 20px); 33 | } 34 | 35 | .cf-column div { 36 | outline: 1px solid white; 37 | color: white; 38 | background-image: -webkit-repeating-linear-gradient(red 0px, red 1px, transparent 1px, transparent 100px); 39 | } 40 | 41 | .simulated-parags { 42 | background-color: #700; 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ftcolumnflow", 3 | "version": "1.0.0", 4 | "author": "FT Labs (http://labs.ft.com/)", 5 | "description": "FTColumnflow is a polyfill that fixes the inadequacies of CSS column layouts.", 6 | "contributors": [ 7 | "George Crawford" 8 | ], 9 | "main": "src/FTColumnflow.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:ftlabs/ftcolumnflow.git" 13 | }, 14 | "scripts": { 15 | "test": "./node_modules/.bin/grunt test" 16 | }, 17 | "keywords": [ 18 | "css column", 19 | "css regions", 20 | "css", 21 | "polyfill" 22 | ], 23 | "devDependencies": { 24 | "buster": "~0.7", 25 | "grunt": "~0.4", 26 | "grunt-cli": "~0.1", 27 | "grunt-buster": "~0.3", 28 | "phantomjs": "~2" 29 | }, 30 | "license": "MIT", 31 | "homepage": "https://github.com/ftlabs/ftcolumnflow" 32 | } 33 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 The Financial Times Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/css/FixedElementsTest.css: -------------------------------------------------------------------------------- 1 | .fixed { 2 | height: 200px; 3 | width: 250px; 4 | } 5 | 6 | .fixed100 { 7 | height: 100px; 8 | } 9 | 10 | .fixed205 { 11 | height: 205px; 12 | } 13 | 14 | .fixed500 { 15 | height: 500px; 16 | } 17 | 18 | .fixed600 { 19 | height: 600px; 20 | } 21 | 22 | p, div, .cf-column { 23 | line-height: 20px; 24 | } 25 | 26 | p { 27 | margin: 20px 0; 28 | } 29 | 30 | .fixed.col-span-1, .fixed.col-span-1-left { 31 | width: 250px; 32 | } 33 | 34 | .fixed.col-span-2, .fixed.col-span-2-left { 35 | width: 525px; 36 | } 37 | 38 | .fixed.col-span-3, .fixed.col-span-3-left { 39 | width: 800px; 40 | } 41 | 42 | .shift-up { 43 | margin-top: -20px; 44 | } 45 | 46 | .auto-width { 47 | width: auto; 48 | } 49 | 50 | #width-50 { 51 | width: 50px !important; 52 | } 53 | 54 | 55 | /* Colours */ 56 | 57 | .cf-column p, .cf-column div { 58 | background-color: rgba(0, 0, 0, 0.1); 59 | background-image: -webkit-repeating-linear-gradient(red 0px, red 1px, transparent 1px, transparent 20px); 60 | } 61 | 62 | .cf-page { 63 | outline: 1px solid white; 64 | } 65 | -------------------------------------------------------------------------------- /test/buster.js: -------------------------------------------------------------------------------- 1 | var config = module.exports; 2 | 3 | 4 | config["My tests"] = { 5 | rootPath: "../", 6 | environment: "browser", // or "node" 7 | sources: [ 8 | "src/FTColumnflow.js", 9 | ], 10 | tests: [ 11 | "test/*-test.js" 12 | ], 13 | testHelpers: [ 14 | "test/helpers/*.js" 15 | ], 16 | resources: [ 17 | { 18 | path: "/", 19 | content: '\n' 20 | + '\n' 21 | + ' \n' 22 | + ' Custom Buster.JS test bed\n' 23 | + ' \n' 24 | + ' \n' 25 | + ' \n' 26 | + '' 27 | }, 28 | { 29 | path: '/all.css', 30 | file: 'test/css/AllTests.css', 31 | }, 32 | { 33 | path: '/config.css', 34 | file: 'test/css/ConfigTest.css', 35 | }, 36 | { 37 | path: '/columnwrap.css', 38 | file: 'test/css/ColumnWrapTest.css', 39 | }, 40 | { 41 | path: '/baselinegrid.css', 42 | file: 'test/css/BaselineGridTest.css', 43 | }, 44 | { 45 | path: '/fixedelements.css', 46 | file: 'test/css/FixedElementsTest.css', 47 | }, 48 | { 49 | path: '/reflow.css', 50 | file: 'test/css/ReflowTest.css', 51 | } 52 | ] 53 | } 54 | 55 | // Add more configuration groups as needed 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /test/css/BaselineGridTest.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | p { 4 | margin: 20px 0; 5 | line-height: 20px; 6 | background-color: rgba(0, 0, 0, 0.1); 7 | background-image: -webkit-repeating-linear-gradient(red 0px, red 1px, transparent 1px, transparent 20px); 8 | } 9 | 10 | img { width: 100%; height: auto; } 11 | 12 | 13 | /* Odd heights */ 14 | 15 | .height1140 { height: 1140px; } 16 | .height40 { height: 40px; } 17 | .height620 { height: 620px; } 18 | .height580 { height: 580px; } 19 | 20 | 21 | 22 | /* 17px grid */ 23 | 24 | .lineheight17 p { 25 | margin: 17px 0; 26 | line-height: 17px; 27 | } 28 | 29 | .lineheight-in-ems p { 30 | margin: 17px 0; 31 | font-size: 10px; 32 | line-height: 1.7em; 33 | } 34 | 35 | .lineheight-in-percent p { 36 | margin: 17px 0; 37 | font-size: 10px; 38 | line-height: 170%; 39 | } 40 | 41 | .lineheight-multiplier p { 42 | margin: 17px 0; 43 | font-size: 10px; 44 | line-height: 1.7; 45 | } 46 | 47 | .lineheight-inherit .cf-column { 48 | line-height: 17px; 49 | } 50 | 51 | .lineheight-inherit p { 52 | margin: 17px 0; 53 | font-size: 10px; 54 | line-height: inherit; 55 | } 56 | 57 | .lineheight-normal p { 58 | margin: 17px 0; 59 | font-size: 10px; 60 | line-height: normal; 61 | } 62 | 63 | 64 | /* Variable lineheight, median 17px grid */ 65 | 66 | .lineheight-variable p { 67 | margin: 17px 0; 68 | line-height: 17px; 69 | } 70 | 71 | .lineheight-variable p:first-of-type { 72 | line-height: 23px; 73 | } 74 | 75 | 76 | /* Paragraphs do not conform to 20px grid */ 77 | 78 | .unpadded-parags p { 79 | height: 53px; 80 | } 81 | 82 | 83 | /* Paragraphs have 40px top margin and 20px bottom */ 84 | 85 | .unequal-margin p { 86 | margin: 40px 0 20px; 87 | } 88 | 89 | /* Paragraphs have notional 1px margin which should be rounded up to a grid line */ 90 | .margin1px p { 91 | margin: 1px 0 0; 92 | height: 60px; 93 | } 94 | 95 | .margin1px p:first-of-type { 96 | margin-top: 0; 97 | } 98 | 99 | /* Paragraphs have 21px margin which should be rounded up to two grid lines */ 100 | .margin21px p { 101 | margin: 21px 0 0; 102 | height: 60px; 103 | } 104 | 105 | .margin21px p:first-of-type { 106 | margin-top: 0; 107 | } 108 | 109 | /* Paragraphs are 7px short of the grid, and have notional 1px margin which should therefore 110 | be rounded up to a grid line*/ 111 | .unevenmargin1px p { 112 | margin: 1px 0 0; 113 | height: 53px; 114 | } 115 | 116 | .unevenmargin1px p:first-of-type { 117 | margin-top: 0; 118 | } 119 | 120 | -------------------------------------------------------------------------------- /examples/4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 25 | 26 | 27 | 28 | 29 | 30 |

noWrap class and noWrapOnTags config setting

31 |

Back

32 | 33 |
34 |
35 |
36 |
37 | 38 |
39 |

One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin.

40 | 41 |

Image allowed to wrap:

42 | 43 | 44 |

His room, a proper human room although a little too small, lay peacefully between its four familiar walls. A collection of textile samples lay spread out on the table - Samsa was a travelling salesman.

45 | 46 |

Wrapping prevented using nowrap class, and heading has keepwithnext:

47 |

A picture:

48 | 49 | 50 |

It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer.

51 |
52 | 53 | 54 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /examples/6.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | 19 | 20 |

Native CSS3 columns

21 |

Back

22 | 23 |
24 |

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.

25 | 26 |

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.

27 | 28 |

His room, a proper human room although a little too small, lay peacefully between its four familiar walls. A collection of textile samples lay spread out on the table - Samsa was a travelling salesman - and above it there hung a picture that he had recently cut out of an illustrated magazine and housed in a nice, gilded frame.

29 | 30 |

It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look out the window at the dull weather. Drops of rain could be heard hitting the pane, which made him feel quite sad.

31 | 32 |

"How about if I sleep a little bit longer and forget all this nonsense", he thought, but that was something he was unable to do because he was used to sleeping on his right, and in his present state couldn't get into that position. However hard he threw himself onto his right, he always rolled back to where he was.

33 | 34 |

He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the floundering legs, and only stopped when he began to feel a mild, dull pain there that he had never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen! Travelling day in and day out. Doing business like this takes much more effort than doing your own business at home, and on top of that there's the curse of travelling, worries about making train connections, bad and irregular food, contact with different people all the time so that you can never get to know anyone or become friendly with them. It can all go to Hell!"

35 | 36 |

He felt a slight itch up on his belly; pushed himself slowly up on his back towards the headboard so that he could lift his head better; found where the itch was, and saw that it was covered with lots of little white spots which he didn't know what to make of; and when he tried to feel the place with one of his legs he drew it quickly back because as soon as he touched it he was overcome by a cold shudder. He slid back into his former position. "Getting up early all the time", he thought, "it makes you stupid. You've got to get enough sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to the guest house during the morning to copy out the contract, these gentlemen are always still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get kicked out on the spot. But who knows, maybe that would be the best thing for me."

37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /test/reflow-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTColumnflow Reflow test suite 3 | * 4 | * @copyright The Financial Times Limited [All Rights Reserved] 5 | */ 6 | 7 | 8 | buster.testCase('Reflow', { 9 | 10 | setUp : function(done) { 11 | this.timeout = 1000; 12 | document.body.innerHTML = '
'; 13 | addStylesheets(['all.css', 'reflow.css'], done); 14 | }, 15 | 16 | tearDown : function() { 17 | removeStyleSheets(); 18 | document.body.className = ''; 19 | }, 20 | 21 | 'ShouldRewriteCssBlockAndNotAddItAgain' : function() { 22 | 23 | var lengthBefore = document.getElementsByTagName('style').length; 24 | 25 | createCf().flow('
height600
'); 26 | 27 | var lengthAfter = document.getElementsByTagName('style').length; 28 | 29 | cf.flow(); 30 | 31 | assert.match(document.getElementsByTagName('style').length, lengthAfter); 32 | }, 33 | 34 | '//ShouldReflow' : function() { 35 | 36 | // page is 800 x 600, columns are 350 x 600 37 | createCf({ 38 | columnGap : 100, 39 | columnCount : 2, 40 | }).flow('
height3000
'); 41 | 42 | // Change viewport dimensions 43 | viewport.style.width = "600px"; 44 | viewport.style.height = "500px"; 45 | 46 | // page is 600 x 500, columns are 250 x 500 47 | cf.reflow(); 48 | 49 | assert.match(target.querySelectorAll('.cf-page-1').length, 1); 50 | 51 | var page = target.querySelector('.cf-page-1'); 52 | 53 | assertTrue(page instanceof HTMLElement); 54 | assert.match(page.clientWidth, 600); 55 | assert.match(page.clientHeight, 500); 56 | 57 | var column1 = page.querySelector('.cf-column-1'); 58 | var column2 = page.querySelector('.cf-column-2'); 59 | 60 | assert.match(column1.clientWidth, 250); 61 | assert.match(column1.clientHeight, 500); 62 | 63 | assert.match(column1.childNodes.length, 1); 64 | assert.match(column2.childNodes.length, 1); 65 | 66 | assert.match(column2.childNodes[0].style.marginTop, '-500px'); 67 | }, 68 | 69 | 'ShouldReflowUsingNewConfig' : function() { 70 | 71 | createCf({ 72 | columnGap : 100, 73 | columnCount : 2 74 | }).flow('
height3000
'); 75 | 76 | var column1 = target.querySelector('.cf-page-1 .cf-column-1'); 77 | 78 | assert.match(column1.clientWidth, 350); 79 | assert.match(column1.clientHeight, 600); 80 | assert.match(target.querySelectorAll('.cf-page-1 .cf-column').length, 2); 81 | assert.match(column1.childNodes.length, 1); 82 | assert.className(column1.childNodes[0], 'height3000'); 83 | 84 | cf.reflow({ 85 | columnGap : 25, 86 | columnCount : 3, 87 | }); 88 | 89 | var column1 = target.querySelector('.cf-page-1 .cf-column-1'); 90 | 91 | assert.match(column1.clientWidth, 250); 92 | assert.match(column1.clientHeight, 600); 93 | assert.match(target.querySelectorAll('.cf-page-1 .cf-column').length, 3); 94 | assert.match(column1.childNodes.length, 1); 95 | }, 96 | 97 | 'ShouldRemoveStylesAndDomNodesOnDestroy' : function() { 98 | 99 | var stylesBefore = document.querySelectorAll('style').length; 100 | 101 | createCf().flow('
height3000
'); 102 | 103 | assert.defined(target); 104 | refute.equals(target.innerHTML, ''); 105 | assert.match(document.querySelectorAll('style').length, (stylesBefore + 1)); 106 | 107 | cf.destroy(); 108 | 109 | assert.isNull(document.getElementById('targetid')); 110 | assert.match(document.querySelectorAll('style').length, stylesBefore); 111 | }, 112 | 113 | 'ShouldPreventAReflowWhenConfigFlagIsFalse' : function() { 114 | 115 | createCf({ 116 | allowReflow: false 117 | }).flow('
height3000
'); 118 | 119 | assert.exception(function test() { 120 | cf.reflow(); 121 | }, 'FTColumnflowReflowException'); 122 | }, 123 | 124 | 125 | /* 126 | 127 | 128 | // NB - height sanitizing routine should not round up every time the line-height is changed! Need to work with the original, untouched elements and padding each time, and round up without modifying them somehow. 129 | 130 | //*/ 131 | 132 | }); -------------------------------------------------------------------------------- /examples/3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | 21 | 22 | 23 | 24 |

Vertically-orientated pages

25 |

Back

26 | 27 |
28 |
29 |
30 |
31 | 32 |
33 |

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.

34 | 35 |

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.

36 | 37 |

His room, a proper human room although a little too small, lay peacefully between its four familiar walls. A collection of textile samples lay spread out on the table - Samsa was a travelling salesman - and above it there hung a picture that he had recently cut out of an illustrated magazine and housed in a nice, gilded frame.

38 | 39 |

It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look out the window at the dull weather. Drops of rain could be heard hitting the pane, which made him feel quite sad.

40 | 41 |

"How about if I sleep a little bit longer and forget all this nonsense", he thought, but that was something he was unable to do because he was used to sleeping on his right, and in his present state couldn't get into that position. However hard he threw himself onto his right, he always rolled back to where he was.

42 | 43 |

He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the floundering legs, and only stopped when he began to feel a mild, dull pain there that he had never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen! Travelling day in and day out. Doing business like this takes much more effort than doing your own business at home, and on top of that there's the curse of travelling, worries about making train connections, bad and irregular food, contact with different people all the time so that you can never get to know anyone or become friendly with them. It can all go to Hell!"

44 | 45 |

He felt a slight itch up on his belly; pushed himself slowly up on his back towards the headboard so that he could lift his head better; found where the itch was, and saw that it was covered with lots of little white spots which he didn't know what to make of; and when he tried to feel the place with one of his legs he drew it quickly back because as soon as he touched it he was overcome by a cold shudder. He slid back into his former position. "Getting up early all the time", he thought, "it makes you stupid. You've got to get enough sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to the guest house during the morning to copy out the contract, these gentlemen are always still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get kicked out on the spot. But who knows, maybe that would be the best thing for me."

46 |
47 | 48 |
49 |
50 |

The Metamorphosis

51 |

Franz Kafka, 1915

52 |
53 |
54 | 55 | 56 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 |

Basic usage example

29 |

Back

30 | 31 |
32 |
33 |
34 |
35 | 36 |
37 |

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.

38 | 39 |

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.

40 | 41 |

His room, a proper human room although a little too small, lay peacefully between its four familiar walls. A collection of textile samples lay spread out on the table - Samsa was a travelling salesman - and above it there hung a picture that he had recently cut out of an illustrated magazine and housed in a nice, gilded frame.

42 | 43 |

It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look out the window at the dull weather. Drops of rain could be heard hitting the pane, which made him feel quite sad.

44 | 45 |

"How about if I sleep a little bit longer and forget all this nonsense", he thought, but that was something he was unable to do because he was used to sleeping on his right, and in his present state couldn't get into that position. However hard he threw himself onto his right, he always rolled back to where he was.

46 | 47 |

He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the floundering legs, and only stopped when he began to feel a mild, dull pain there that he had never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen! Travelling day in and day out. Doing business like this takes much more effort than doing your own business at home, and on top of that there's the curse of travelling, worries about making train connections, bad and irregular food, contact with different people all the time so that you can never get to know anyone or become friendly with them. It can all go to Hell!"

48 | 49 |

He felt a slight itch up on his belly; pushed himself slowly up on his back towards the headboard so that he could lift his head better; found where the itch was, and saw that it was covered with lots of little white spots which he didn't know what to make of; and when he tried to feel the place with one of his legs he drew it quickly back because as soon as he touched it he was overcome by a cold shudder. He slid back into his former position. "Getting up early all the time", he thought, "it makes you stupid. You've got to get enough sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to the guest house during the morning to copy out the contract, these gentlemen are always still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get kicked out on the spot. But who knows, maybe that would be the best thing for me."

50 |
51 | 52 |
53 |
54 |

The Metamorphosis

55 |

Franz Kafka, 1915

56 |
57 |
58 | 59 |
60 |
61 | 62 | 63 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /examples/5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 |

Complex layout

29 |

Back

30 | 31 |
32 |
33 |
34 |
35 | 36 |
37 |

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.

38 | 39 |

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.

40 | 41 |

His room, a proper human room although a little too small, lay peacefully between its four familiar walls. A collection of textile samples lay spread out on the table - Samsa was a travelling salesman - and above it there hung a picture that he had recently cut out of an illustrated magazine and housed in a nice, gilded frame.

42 | 43 |

It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look out the window at the dull weather. Drops of rain could be heard hitting the pane, which made him feel quite sad.

44 | 45 |

"How about if I sleep a little bit longer and forget all this nonsense", he thought, but that was something he was unable to do because he was used to sleeping on his right, and in his present state couldn't get into that position. However hard he threw himself onto his right, he always rolled back to where he was.

46 | 47 |

He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the floundering legs, and only stopped when he began to feel a mild, dull pain there that he had never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen! Travelling day in and day out. Doing business like this takes much more effort than doing your own business at home, and on top of that there's the curse of travelling, worries about making train connections, bad and irregular food, contact with different people all the time so that you can never get to know anyone or become friendly with them. It can all go to Hell!"

48 | 49 |

He felt a slight itch up on his belly; pushed himself slowly up on his back towards the headboard so that he could lift his head better; found where the itch was, and saw that it was covered with lots of little white spots which he didn't know what to make of; and when he tried to feel the place with one of his legs he drew it quickly back because as soon as he touched it he was overcome by a cold shudder. He slid back into his former position. "Getting up early all the time", he thought, "it makes you stupid. You've got to get enough sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to the guest house during the morning to copy out the contract, these gentlemen are always still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get kicked out on the spot. But who knows, maybe that would be the best thing for me."

50 |
51 | 52 |
53 |
54 |

The Metamorphosis

55 |
56 |
57 |

Franz Kafka, 1915

58 |
59 |
60 | 61 |
62 |
63 | 64 | 65 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 25 | 26 | 27 | 28 | 29 |

FTColumnflow elements highlighted

30 |

Back

31 | 32 |
33 |
34 |
35 |
36 | 37 |
38 |

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.

39 | 40 |

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.

41 | 42 |

His room, a proper human room although a little too small, lay peacefully between its four familiar walls. A collection of textile samples lay spread out on the table - Samsa was a travelling salesman - and above it there hung a picture that he had recently cut out of an illustrated magazine and housed in a nice, gilded frame.

43 | 44 |

It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look out the window at the dull weather. Drops of rain could be heard hitting the pane, which made him feel quite sad.

45 | 46 |

"How about if I sleep a little bit longer and forget all this nonsense", he thought, but that was something he was unable to do because he was used to sleeping on his right, and in his present state couldn't get into that position. However hard he threw himself onto his right, he always rolled back to where he was.

47 | 48 |

He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the floundering legs, and only stopped when he began to feel a mild, dull pain there that he had never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen! Travelling day in and day out. Doing business like this takes much more effort than doing your own business at home, and on top of that there's the curse of travelling, worries about making train connections, bad and irregular food, contact with different people all the time so that you can never get to know anyone or become friendly with them. It can all go to Hell!"

49 | 50 |

He felt a slight itch up on his belly; pushed himself slowly up on his back towards the headboard so that he could lift his head better; found where the itch was, and saw that it was covered with lots of little white spots which he didn't know what to make of; and when he tried to feel the place with one of his legs he drew it quickly back because as soon as he touched it he was overcome by a cold shudder. He slid back into his former position. "Getting up early all the time", he thought, "it makes you stupid. You've got to get enough sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to the guest house during the morning to copy out the contract, these gentlemen are always still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get kicked out on the spot. But who knows, maybe that would be the best thing for me."

51 |
52 | 53 |
54 |
55 |

The Metamorphosis

56 |

Franz Kafka, 1915

57 |
58 |
59 | 60 |
61 |
62 | 63 | 64 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/7.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | 28 |

Broken FTColumnflow example

29 |

Back

30 |

Here, the line-height of the paragraphs, their bottom margin, and the height of the columns do not conform to a 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, or clipping will occur.

31 |

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.

32 | 33 |
34 |
35 |
36 |
37 | 38 |
39 |

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.

40 | 41 |

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.

42 | 43 |

His room, a proper human room although a little too small, lay peacefully between its four familiar walls. A collection of textile samples lay spread out on the table - Samsa was a travelling salesman - and above it there hung a picture that he had recently cut out of an illustrated magazine and housed in a nice, gilded frame.

44 | 45 |

It showed a lady fitted out with a fur hat and fur boa who sat upright, raising a heavy fur muff that covered the whole of her lower arm towards the viewer. Gregor then turned to look out the window at the dull weather. Drops of rain could be heard hitting the pane, which made him feel quite sad.

46 | 47 |

"How about if I sleep a little bit longer and forget all this nonsense", he thought, but that was something he was unable to do because he was used to sleeping on his right, and in his present state couldn't get into that position. However hard he threw himself onto his right, he always rolled back to where he was.

48 | 49 |

He must have tried it a hundred times, shut his eyes so that he wouldn't have to look at the floundering legs, and only stopped when he began to feel a mild, dull pain there that he had never felt before. "Oh, God", he thought, "what a strenuous career it is that I've chosen! Travelling day in and day out. Doing business like this takes much more effort than doing your own business at home, and on top of that there's the curse of travelling, worries about making train connections, bad and irregular food, contact with different people all the time so that you can never get to know anyone or become friendly with them. It can all go to Hell!"

50 | 51 |

He felt a slight itch up on his belly; pushed himself slowly up on his back towards the headboard so that he could lift his head better; found where the itch was, and saw that it was covered with lots of little white spots which he didn't know what to make of; and when he tried to feel the place with one of his legs he drew it quickly back because as soon as he touched it he was overcome by a cold shudder. He slid back into his former position. "Getting up early all the time", he thought, "it makes you stupid. You've got to get enough sleep. Other travelling salesmen live a life of luxury. For instance, whenever I go back to the guest house during the morning to copy out the contract, these gentlemen are always still sitting there eating their breakfasts. I ought to just try that with my boss; I'd get kicked out on the spot. But who knows, maybe that would be the best thing for me."

52 |
53 | 54 |
55 |
56 |

The Metamorphosis

57 |

Franz Kafka, 1915

58 |
59 |
60 | 61 |
62 |
63 | 64 | 65 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /test/helpers/stylesheets.js: -------------------------------------------------------------------------------- 1 | function addStylesheets(urls, callback) { 2 | 3 | var url = urls.shift(), 4 | 5 | _loaded = function(success, element) { 6 | if (success) { 7 | if ((url = urls.shift())) { 8 | loadStyleSheet(url, _loaded); 9 | } else { 10 | callback(); 11 | } 12 | } else { 13 | console.error('Failed to load stylesheet', url, arguments); 14 | } 15 | } 16 | 17 | loadStyleSheet(url, _loaded); 18 | } 19 | 20 | 21 | function removeStyleSheets() { 22 | var styles = document.getElementsByTagName('style'); 23 | for (var i = 0, len = styles.length; i < len; i++) { 24 | if (styles[i] && styles[i].nodeType == 1) styles[i].parentNode.removeChild(styles[i]); 25 | } 26 | } 27 | 28 | 29 | 30 | 31 | 32 | 33 | /* http://thudjs.tumblr.com/post/637855087/stylesheet-onload-or-lack-thereof */ 34 | 35 | ( function( GLOBAL, WIN ) { 36 | var ERROR_TIMEOUT = 15000, // How long to wait (in milliseconds) before realising the style sheet has failed to load 37 | HEAD = WIN.document.getElementsByTagName( 'head' )[0], // a reference to the document.head for inserting link nodes into 38 | ID_PREFIX = 'stylesheet-', 39 | READYSTATE_INTERVAL = 10, // How often to check (in milliseconds) whether the style sheet has loaded successfully 40 | callbacks = {}, count = 0, cssRules, loaded = {}, queue = {}, sheet; 41 | 42 | function loadStyleSheet( path, fn, scope ) { 43 | addCallback( path, fn, scope ); // add the callback for this stylesheet 44 | if ( queue[path] ) return GLOBAL; // if the style sheet is already queued the we just need to wait 45 | if ( loaded[path] && inDoc( loaded[path].id ) ) { // if the style sheet is already in the document then just fire the last callback that was added 46 | fireStyleSheetLoaded( path, true, loaded[path] ); 47 | return GLOBAL; 48 | } 49 | 50 | var el = createStyleSheet( path ), id, 51 | interval_id = setTimeout( partial( onError, path ), ERROR_TIMEOUT ), // start counting down to FAIL! 52 | timeout_id = setInterval( partial( checkStyleSheetLoaded, path ), READYSTATE_INTERVAL ); // start checking if the style sheet is loaded 53 | 54 | queue[path] = { el : el, interval : interval_id, path : path, timeout : timeout_id }; // add the style sheet to the queue of loading style sheets 55 | 56 | if ( 'onload' in el ) el.onload = partial( _handleOnLoad, path ); 57 | if ( 'onreadystatechange' in el ) el.onreadystatechange = partial( _handleReadyState, path ); 58 | 59 | id = setTimeout( function() { // pop out of current stack to prevent browser lock 60 | clearTimeout( id ); id = null; 61 | HEAD.appendChild( el ); // insert the link node into the DOM, this will actually start the browser trying to load the style sheet 62 | }, 1 ); 63 | 64 | return GLOBAL; 65 | } 66 | 67 | function _handleOnLoad( path ) { 68 | var o = queue[path]; 69 | return this[sheet][cssRules].length ? onLoad( o ) : onError( path ); 70 | } 71 | 72 | function _handleReadyState( path ) { 73 | if ( this.readyState == 'complete' || this.readyState == 'loaded' ) _handleOnLoad.call( this, path ); 74 | } 75 | 76 | function addCallback( path, fn, scope ) { 77 | if ( !isFunc( fn ) ) return; 78 | callbacks[path] || ( callbacks[path] = [] ); // create a callback array for this path if one doesn't exist already 79 | callbacks[path].push( { fn : fn, scope : scope } ); // add the callback function and (optional) scope to the array 80 | } 81 | 82 | function checkStyleSheetLoaded( path ) { // checking to see if the stylesheet is loaded 83 | var o = queue[path], el; 84 | if ( !o ) return false; // fixes an issue with MSIE that calls this again after the style sheet has been removed from the queue 85 | el = o.el; 86 | try { el[sheet] && el[sheet][cssRules].length && onLoad( o ); } // this is where we check that the stylesheet has loaded successfully and if so fire the onLoad function 87 | catch( e ) { return false; } 88 | } 89 | 90 | function clear( o ) { // when the style sheet has loaded or has failed to load we want to: 91 | delete queue[o.path]; // delete it from the queue of loading style sheets 92 | clearInterval( o.interval ); // stop checking it has loaded 93 | clearTimeout( o.timeout ); // clear the fail timeout (for efficiency) 94 | var el = o.el; 95 | if ( 'onload' in el ) el.onload = null; 96 | if ( 'onreadystatechange' in el ) el.onreadystatechange = null; 97 | } 98 | 99 | function createStyleSheet( path ) { // pretty self explanatory 100 | var el = document.createElement( 'link' ); 101 | el.id = ID_PREFIX + ( ++count ); 102 | el.setAttribute( 'href', path ); 103 | el.setAttribute( 'rel', 'stylesheet' ); 104 | el.setAttribute( 'type', 'text/css' ); 105 | 106 | if ( !sheet ) { // only assign these once 107 | cssRules = 'cssRules'; sheet = 'sheet'; 108 | if ( !( sheet in el ) ) { // MSIE uses non-standard property names 109 | cssRules = 'rules'; 110 | sheet = 'styleSheet'; 111 | } 112 | } 113 | 114 | return el; 115 | } 116 | 117 | function fireStyleSheetLoaded( path, success, el ) { 118 | var cbs = callbacks[path], o; 119 | if ( !cbs ) return; 120 | // we shift all the callbacks off to clear the callbacks queue for this specific style sheet to prevent them been called again 121 | while ( o = cbs.shift() ) fireCallback( o.fn, o.scope, success, el ); 122 | } 123 | 124 | function fireCallback( fn, scope, success, el ) { fn.call( scope || WIN, success, el ); } 125 | 126 | function inDoc( id ) { return !!WIN.document.getElementById( id ); } // checks whether a style sheet is still in the document, if not we can load it again 127 | 128 | function isFunc( fn ) { return typeof fn == 'function'; } 129 | 130 | function onError( path ) { 131 | var o = queue[path], el = o.el; 132 | clear( o ); 133 | HEAD.removeChild( el ); // since the style sheet failed to load, let's remove it from the DOM 134 | fireStyleSheetLoaded( path, false, el ); 135 | } 136 | 137 | function onLoad( o ) { 138 | var el = o.el, path = o.path; 139 | clear( o ); 140 | loaded[path] = el; 141 | fireStyleSheetLoaded( path, true , el ); 142 | } 143 | 144 | function partial() { 145 | var slice = Array.prototype.slice, 146 | args = slice.call( arguments ), 147 | fn = args.shift(); 148 | return !args.length ? fn : function() { 149 | return fn.apply( this, args.concat( slice.call( arguments ) ) ); 150 | } 151 | } 152 | 153 | GLOBAL.loadStyleSheet = loadStyleSheet; 154 | 155 | } )( this /* <- a reference to your global namespace object, will assign the loadStyleSheet method there. 156 | e.g. pass your "$" object to be able to call $.loadStyleSheet(); 157 | alternatively, leave as "this" to use as window.loadStyleSheet(); */, 158 | this /* <- a reference to the current window object */ ); 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /test/config-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTColumnflow Config test suite 3 | * 4 | * @copyright The Financial Times Limited [All Rights Reserved] 5 | */ 6 | 7 | "use strict"; 8 | 9 | buster.testCase('Config', { 10 | 11 | setUp : function(done) { 12 | document.body.innerHTML = '
'; 13 | addStylesheets(['all.css', 'config.css'], done); 14 | }, 15 | 16 | tearDown : function() { 17 | removeStyleSheets(); 18 | document.body.className = ''; 19 | }, 20 | 21 | "InstantiateWithNoParamsThrowsException" : function() { 22 | 23 | assert.exception(function test() { 24 | new FTColumnflow(); 25 | }, 'FTColumnflowParameterException'); 26 | }, 27 | 28 | "ThrowExceptionOnInvalidFirstParam" : function() { 29 | 30 | assert.exception(function test() { 31 | new FTColumnflow(1, 'test'); 32 | }, 'FTColumnflowParameterException'); 33 | 34 | assert.exception(function test() { 35 | new FTColumnflow(new Array, 'test'); 36 | }, 'FTColumnflowParameterException'); 37 | 38 | }, 39 | 40 | "ThrowExceptionOnInvalidSecondParam" : function() { 41 | 42 | assert.exception(function test() { 43 | new FTColumnflow('targetid', 1); 44 | }, 'FTColumnflowParameterException'); 45 | 46 | assert.exception(function test() { 47 | new FTColumnflow('targetid', new Array); 48 | }, 'FTColumnflowParameterException'); 49 | 50 | }, 51 | 52 | "StringTargetMustExist" : function() { 53 | 54 | refute.exception(function test() { 55 | new FTColumnflow('targetid', 'viewportid'); 56 | }); 57 | 58 | assert.exception(function test() { 59 | new FTColumnflow('missingid', 'viewportid'); 60 | }, 'FTColumnflowSelectorException'); 61 | 62 | }, 63 | 64 | "StringViewportMustExist" : function() { 65 | 66 | refute.exception(function test() { 67 | new FTColumnflow('targetid', 'viewportid'); 68 | }); 69 | 70 | assert.exception(function test() { 71 | new FTColumnflow('targetid', 'missingid'); 72 | }, 'FTColumnflowSelectorException'); 73 | 74 | }, 75 | 76 | "TargetElementAcceptedAsFirstParam" : function() { 77 | 78 | refute.exception(function test() { 79 | new FTColumnflow(document.getElementById('targetid'), 'viewportid'); 80 | }); 81 | }, 82 | 83 | "ViewportElementAcceptedAsSecondParam" : function() { 84 | 85 | refute.exception(function test() { 86 | new FTColumnflow('targetid', document.getElementById('viewportid')); 87 | }); 88 | }, 89 | 90 | "TargetMustBeAChildOfViewport" : function() { 91 | 92 | refute.exception(function test() { 93 | new FTColumnflow('targetid', 'viewportid'); 94 | }); 95 | 96 | assert.exception(function test() { 97 | new FTColumnflow('viewportid', 'targetid'); 98 | }, 'FTColumnflowInheritanceException'); 99 | }, 100 | 101 | "ShouldUseDefaultValueForColumnCount" : function() { 102 | 103 | var cf = new FTColumnflow('targetid', 'viewportid'); 104 | 105 | assert.equals(1, cf.layoutDimensions.columnCount); 106 | }, 107 | 108 | "ShouldUseViewportWidthForColumnWidthByDefault" : function() { 109 | 110 | var cf = new FTColumnflow('targetid', 'viewportid'); 111 | 112 | assert.equals(800, cf.layoutDimensions.columnWidth); 113 | }, 114 | 115 | "ShouldUse1emByDefaultForColumnGap" : function() { 116 | 117 | document.body.className = 'columnGaptest'; 118 | var cf = new FTColumnflow('targetid', 'viewportid'); 119 | 120 | assert.equals(12, cf.layoutDimensions.columnGap); 121 | }, 122 | 123 | "ShouldThrowExceptionOnInvalidConfigParameter" : function() { 124 | 125 | assert.exception(function test() { 126 | new FTColumnflow('targetid', 'viewportid', { 127 | 'invalid' : 'abc' 128 | }); 129 | }, 'FTColumnflowParameterException'); 130 | 131 | }, 132 | 133 | "ShouldThrowExceptionOnInvalidColumnDimensionType" : function() { 134 | 135 | assert.exception(function test() { 136 | new FTColumnflow('targetid', 'viewportid', { 137 | 'columnGap' : 'abc' 138 | }); 139 | }, 'FTColumnflowColumnDimensionException'); 140 | 141 | assert.exception(function test() { 142 | new FTColumnflow('targetid', 'viewportid', { 143 | 'columnCount' : 'abc' 144 | }); 145 | }, 'FTColumnflowColumnDimensionException'); 146 | 147 | assert.exception(function test() { 148 | new FTColumnflow('targetid', 'viewportid', { 149 | 'columnWidth' : 'abc' 150 | }); 151 | }, 'FTColumnflowColumnDimensionException'); 152 | }, 153 | 154 | "ShouldThrowExceptionOnNegativeColumnDimensionValue" : function() { 155 | 156 | assert.exception(function test() { 157 | new FTColumnflow('targetid', 'viewportid', { 158 | 'columnGap' : -20 159 | }); 160 | }, 'FTColumnflowColumnDimensionException'); 161 | 162 | assert.exception(function test() { 163 | new FTColumnflow('targetid', 'viewportid', { 164 | 'columnCount' : -20 165 | }); 166 | }, 'FTColumnflowColumnDimensionException'); 167 | 168 | assert.exception(function test() { 169 | new FTColumnflow('targetid', 'viewportid', { 170 | 'columnWidth' : -20 171 | }); 172 | }, 'FTColumnflowColumnDimensionException'); 173 | }, 174 | 175 | "ShouldRespectSpecifiedColumnGap" : function() { 176 | 177 | document.body.className = 'columnGaptest'; 178 | var cf = new FTColumnflow('targetid', 'viewportid', { 179 | 'columnGap' : 20 180 | }); 181 | 182 | assert.equals(20, cf.layoutDimensions.columnGap); 183 | }, 184 | 185 | 186 | // Pseudo-code from http://www.w3.org/TR/css3-multicol/#pseudo-algorithm 187 | // 188 | // if (column-width = auto) and (column-count != auto) then 189 | // N := column-count 190 | // W := max(0, (available-width - ((N - 1) * column-gap)) / N) 191 | // exit 192 | "ShouldCalculateColumnWidthWhenCountIsSet" : function() { 193 | 194 | var cf = new FTColumnflow('targetid', 'viewportid', { 195 | 'columnGap' : 25, 196 | 'columnCount' : 5, 197 | }); 198 | 199 | assert.equals(140, cf.layoutDimensions.columnWidth); 200 | }, 201 | 202 | "ShouldUseWholeWidthWhenOnlyOneColumn" : function() { 203 | 204 | var cf = new FTColumnflow('targetid', 'viewportid', { 205 | 'columnCount' : 1, 206 | }); 207 | 208 | assert.equals(800, cf.layoutDimensions.columnWidth); 209 | }, 210 | 211 | 212 | // if (column-width != auto) and (column-count = auto) then 213 | // N := max(1, floor((available-width + column-gap) / (column-width + column-gap))) 214 | // W := ((available-width + column-gap) / N) - column-gap 215 | // exit 216 | "ShouldUseViewportWidthWhenColumnWidthIsTooLarge" : function() { 217 | 218 | var cf = new FTColumnflow('targetid', 'viewportid', { 219 | 'columnWidth' : 900, 220 | }); 221 | 222 | assert.equals(800, cf.layoutDimensions.columnWidth); 223 | assert.equals(1, cf.layoutDimensions.columnCount); 224 | }, 225 | 226 | "ShouldUseSpecifiedWidthWhenItFitsExactly" : function() { 227 | 228 | var cf = new FTColumnflow('targetid', 'viewportid', { 229 | 'columnGap' : 25, 230 | 'columnWidth' : 140, 231 | }); 232 | 233 | assert.equals(5, cf.layoutDimensions.columnCount); 234 | }, 235 | 236 | "ShouldWidenSmallColumnsToFit" : function() { 237 | 238 | var cf = new FTColumnflow('targetid', 'viewportid', { 239 | 'columnGap' : 25, 240 | 'columnWidth' : 130, 241 | }); 242 | 243 | assert.equals(140, cf.layoutDimensions.columnWidth); 244 | assert.equals(5, cf.layoutDimensions.columnCount); 245 | }, 246 | 247 | 248 | // if (column-width != auto) and (column-count != auto) then 249 | // N := min(column-count, floor((available-width + column-gap) / (column-width + column-gap))) 250 | // W := ((available-width + column-gap) / N) - column-gap 251 | // exit 252 | "ShouldReduceCountIfThereIsNoSpace" : function() { 253 | 254 | var cf = new FTColumnflow('targetid', 'viewportid', { 255 | 'columnGap' : 25, 256 | 'columnWidth' : 140, 257 | 'columnCount' : 6, 258 | }); 259 | 260 | assert.equals(140, cf.layoutDimensions.columnWidth); 261 | assert.equals(5, cf.layoutDimensions.columnCount); 262 | }, 263 | 264 | "ShouldTreatCountAsMaximumWhenWidthIsAlsoDefined" : function() { 265 | 266 | var cf = new FTColumnflow('targetid', 'viewportid', { 267 | 'columnGap' : 25, 268 | 'columnWidth' : 140, 269 | 'columnCount' : 3, 270 | }); 271 | 272 | assert.equals(250, cf.layoutDimensions.columnWidth); 273 | assert.equals(3, cf.layoutDimensions.columnCount); 274 | }, 275 | 276 | 277 | "ShouldThrowExceptionOnInvalidClassNames" : function() { 278 | 279 | assert.exception(function test() { 280 | new FTColumnflow('targetid', 'viewportid', { 281 | 'pageClass' : 20 282 | }); 283 | }, 'FTColumnflowClassnameException'); 284 | 285 | assert.exception(function test() { 286 | new FTColumnflow('targetid', 'viewportid', { 287 | 'columnClass' : new Array() 288 | }); 289 | }, 'FTColumnflowClassnameException'); 290 | 291 | }, 292 | 293 | "ShouldNormaliseClassNamesAndProvideGetter" : function() { 294 | 295 | var cf = new FTColumnflow('targetid', 'viewportid', { 296 | 'pageClass' : 'class with spaces', 297 | 'columnClass' : 'class&with*illegal@chars' 298 | }); 299 | 300 | assert.equals('class-with-spaces', cf.pageClass); 301 | assert.equals('class-with-illegal-chars', cf.columnClass); 302 | }, 303 | 304 | "ShouldThrowAnExceptionOnInvalidPageArrangementValue" : function() { 305 | 306 | assert.exception(function test() { 307 | new FTColumnflow('targetid', 'viewportid', { 308 | 'pageArrangement' : 20 309 | }); 310 | }, 'FTColumnflowArrangementException'); 311 | 312 | assert.exception(function test() { 313 | new FTColumnflow('targetid', 'viewportid', { 314 | 'pageArrangement' : 'invalid' 315 | }); 316 | }, 'FTColumnflowArrangementException'); 317 | 318 | refute.exception(function test() { 319 | new FTColumnflow('targetid', 'viewportid', { 320 | 'pageArrangement' : 'vertical' 321 | }); 322 | }); 323 | 324 | refute.exception(function test() { 325 | new FTColumnflow('targetid', 'viewportid', { 326 | 'pageArrangement' : 'horizontal' 327 | }); 328 | }); 329 | }, 330 | 331 | "ShouldThrowExceptionOnInvalidMarginType" : function() { 332 | 333 | assert.exception(function test() { 334 | new FTColumnflow('targetid', 'viewportid', { 335 | 'pagePadding' : 'invalid' 336 | }); 337 | }, 'FTColumnflowPaddingException'); 338 | 339 | refute.exception(function test() { 340 | new FTColumnflow('targetid', 'viewportid', { 341 | 'pagePadding' : 50 342 | }); 343 | }); 344 | }, 345 | 346 | "ShouldThrowExceptionIfViewportHasNoWidthOrHeight" : function() { 347 | 348 | document.body.innerHTML = '
'; 349 | 350 | assert.exception(function test() { 351 | new FTColumnflow('unstyled-targetid', 'unstyled-viewportid'); 352 | }, 'FTColumnflowViewportException'); 353 | }, 354 | 355 | "ShouldThrowExceptionOnInvalidStandardiseLineHeightType" : function() { 356 | 357 | assert.exception(function test() { 358 | new FTColumnflow('targetid', 'viewportid', { 359 | 'standardiseLineHeight' : 'invalid' 360 | }); 361 | }, 'FTColumnflowStandardiseLineheightException'); 362 | 363 | refute.exception(function test() { 364 | new FTColumnflow('targetid', 'viewportid', { 365 | 'standardiseLineHeight' : true 366 | }); 367 | }); 368 | }, 369 | 370 | "ShouldThrowExceptionOnInvalidMinColumnHeight" : function() { 371 | 372 | assert.exception(function test() { 373 | new FTColumnflow('targetid', 'viewportid', { 374 | 'columnFragmentMinHeight' : 'invalid' 375 | }); 376 | }, 'FTColumnflowColumnDimensionException'); 377 | 378 | refute.exception(function test() { 379 | new FTColumnflow('targetid', 'viewportid', { 380 | 'columnFragmentMinHeight' : 20 381 | }); 382 | }); 383 | }, 384 | //*/ 385 | 386 | }); -------------------------------------------------------------------------------- /test/baselinegrid-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTColumnflow BaselineGrid test suite 3 | * 4 | * @copyright The Financial Times Limited [All Rights Reserved] 5 | */ 6 | 7 | 8 | buster.testCase('BaselineGrid', { 9 | setUp : function(done) { 10 | document.body.innerHTML = '
'; 11 | addStylesheets(['all.css', 'baselinegrid.css'], done); 12 | }, 13 | 14 | tearDown : function() { 15 | removeStyleSheets(); 16 | document.body.className = ''; 17 | }, 18 | 19 | 'ShouldRemoveTopMarginOnFirstElement' : function() { 20 | 21 | createCf().flow('

flowedContent

'); 22 | 23 | var column = target.querySelector('.cf-column-1'); 24 | var p = column.querySelector('p'); 25 | 26 | assert.match(p.offsetTop, 0); 27 | assert.match(cssProp(p, 'margin-top'), '0px'); 28 | }, 29 | 30 | 'ShouldReduceColumnHeightToNearestMultipleOfLineheight' : function() { 31 | 32 | document.body.className = 'lineheight17'; 33 | 34 | createCf().flow('

flowedContent

'); 35 | 36 | var column = target.querySelector('.cf-column-1'); 37 | 38 | assert.match(column.offsetHeight, 595); 39 | }, 40 | 41 | 'ShouldCalculateCorrectGridHeightWhenLineheightIsInEms' : function() { 42 | 43 | document.body.className = 'lineheight-in-ems'; 44 | 45 | createCf({ 46 | columnGap : 20, 47 | columnCount : 2, 48 | pagePadding : 50, 49 | pageArrangement : 'vertical', 50 | }).flow('

flowedContent

'); 51 | 52 | var column = target.querySelector('.cf-column-1'); 53 | 54 | assert.match(column.offsetHeight, 493); 55 | }, 56 | 57 | 'ShouldCalculateCorrectGridHeightWhenLineheightIsInPercent' : function() { 58 | 59 | document.body.className = 'lineheight-in-percent'; 60 | 61 | createCf({ 62 | columnGap : 20, 63 | columnCount : 2, 64 | pagePadding : 50, 65 | pageArrangement : 'vertical', 66 | }).flow('

flowedContent

'); 67 | 68 | var column = target.querySelector('.cf-column-1'); 69 | 70 | assert.match(column.offsetHeight, 493); 71 | }, 72 | 73 | 'ShouldCalculateCorrectGridHeightWhenLineheightIsInherit' : function() { 74 | 75 | document.body.className = 'lineheight-inherit'; 76 | 77 | createCf({ 78 | columnGap : 20, 79 | columnCount : 2, 80 | pagePadding : 50, 81 | pageArrangement : 'vertical', 82 | }).flow('

flowedContent

'); 83 | 84 | var column = target.querySelector('.cf-column-1'); 85 | 86 | assert.match(column.offsetHeight, 493); 87 | }, 88 | 89 | 'ShouldCalculateCorrectGridHeightWhenLineheightIsMultiplier' : function() { 90 | 91 | document.body.className = 'lineheight-multiplier'; 92 | 93 | createCf({ 94 | columnGap : 20, 95 | columnCount : 2, 96 | pagePadding : 50, 97 | pageArrangement : 'vertical', 98 | }).flow('

flowedContent

'); 99 | 100 | var column = target.querySelector('.cf-column-1'); 101 | 102 | assert.match(column.offsetHeight, 493); 103 | }, 104 | 105 | 'ShouldCalculateCorrectGridHeightWhenLineheightIsNormal' : function() { 106 | 107 | document.body.className = 'lineheight-normal'; 108 | 109 | createCf().flow('

Test paragraph

 
 
 
 
 
 
 
 
 
 

'); 110 | 111 | var column = target.querySelector('.cf-column-1'); 112 | var span = column.querySelector('.ten-elements'); 113 | 114 | var lineHight = span.offsetHeight / 10; 115 | 116 | assert.match((column.offsetHeight % lineHight) , 0); 117 | }, 118 | 119 | 'ShouldUseTheMedianLineheightFromASampleOfElements' : function() { 120 | 121 | document.body.className = 'lineheight-variable'; 122 | 123 | createCf().flow('

Test paragraph

Test paragraph

Test paragraph

Test paragraph

Test paragraph

'); 124 | 125 | var column = target.querySelector('.cf-column-1'); 126 | 127 | assert.match(column.offsetHeight, 595); 128 | }, 129 | 130 | 'ShouldAddMoreElementsIfThereAreNotEnoughToGetAMedianSample' : function() { 131 | 132 | document.body.className = 'lineheight-variable'; 133 | 134 | createCf().flow('

Test paragraph

Test paragraph

'); 135 | 136 | var column = target.querySelector('.cf-column-1'); 137 | 138 | assert.match(column.offsetHeight, 595); 139 | }, 140 | 141 | 'ShouldUseLineHeightIfSpecifiedAndNotCalculateIt' : function() { 142 | 143 | document.body.className = 'lineheight-variable'; 144 | 145 | createCf({ 146 | columnGap : 25, 147 | columnCount : 3, 148 | lineHeight : 19 149 | }).flow('

Test paragraph

Test paragraph

Test paragraph

Test paragraph

Test paragraph

'); 150 | 151 | var column = target.querySelector('.cf-column-1'); 152 | 153 | assert.match(column.offsetHeight, 589); 154 | }, 155 | 156 | 'ShouldNotPadElementsByDefault' : function() { 157 | 158 | document.body.className = 'unpadded-parags'; 159 | 160 | createCf().flow('

Test paragraph

Test paragraph

Test paragraph

'); 161 | 162 | var column = target.querySelector('.cf-column-1'); 163 | var parags = column.getElementsByTagName('p'); 164 | 165 | assert.match(parags[0].offsetHeight, 53); 166 | assert.match(parags[1].offsetHeight, 53); 167 | assert.match(parags[2].offsetHeight, 53); 168 | 169 | assert.match(parags[0].offsetTop, 0); 170 | assert.match(parags[1].offsetTop, 73); 171 | assert.match(parags[2].offsetTop, 146); 172 | }, 173 | 174 | 'ShouldRoundUpAndCollapseElementMarginsWhenConfigured' : function() { 175 | 176 | document.body.className = 'unpadded-parags'; 177 | 178 | createCf({ 179 | standardiseLineHeight : true, 180 | }).flow('

Test paragraph

Test paragraph

Test paragraph

'); 181 | 182 | var column = target.querySelector('.cf-column-1'); 183 | var parags = column.getElementsByTagName('p'); 184 | 185 | // 53-px parags with a collapsed 20px top/bottom margin, so rounded-up to 80px 186 | assert.match(parags[0].offsetTop, 0); 187 | assert.match(parags[1].offsetTop, 80); 188 | assert.match(parags[2].offsetTop, 160); 189 | }, 190 | 191 | 'ShouldRoundUpTopMargin' : function() { 192 | 193 | document.body.className = 'unpadded-parags top-margin'; 194 | 195 | createCf({ 196 | standardiseLineHeight : true, 197 | }).flow('

Test paragraph

Test paragraph

Test paragraph

'); 198 | 199 | var column = target.querySelector('.cf-column-1'); 200 | var parags = column.getElementsByTagName('p'); 201 | 202 | // 53-px parags with a 20px margin, so rounded-up to 80px 203 | assert.match(parags[0].offsetTop, 0); 204 | assert.match(parags[1].offsetTop, 80); 205 | assert.match(parags[2].offsetTop, 160); 206 | }, 207 | 208 | 'ShouldRoundUpBottomMargin' : function() { 209 | 210 | document.body.className = 'unpadded-parags bottom-margin'; 211 | 212 | createCf({ 213 | standardiseLineHeight : true, 214 | }).flow('

Test paragraph

Test paragraph

Test paragraph

'); 215 | 216 | var column = target.querySelector('.cf-column-1'); 217 | var parags = column.getElementsByTagName('p'); 218 | 219 | // 53-px parags with a 20px margin, so rounded-up to 80px 220 | assert.match(parags[0].offsetTop, 0); 221 | assert.match(parags[1].offsetTop, 80); 222 | assert.match(parags[2].offsetTop, 160); 223 | }, 224 | 225 | '1pxMarginShouldRoundUpTo1GridLine' : function() { 226 | 227 | document.body.className = 'margin1px'; 228 | 229 | createCf({ 230 | standardiseLineHeight : true, 231 | }).flow('

Test paragraph

Test paragraph

Test paragraph

'); 232 | 233 | var column = target.querySelector('.cf-column-1'); 234 | var parags = column.getElementsByTagName('p'); 235 | 236 | // 60-px parags with a 1px top margin (except for first), so rounded-up to 80px 237 | assert.match(parags[0].offsetTop, 0); 238 | assert.match(parags[1].offsetTop, 80); 239 | assert.match(parags[2].offsetTop, 160); 240 | }, 241 | 242 | '21pxMarginShouldRoundUpTo2GridLines' : function() { 243 | 244 | document.body.className = 'margin21px'; 245 | 246 | createCf({ 247 | standardiseLineHeight : true, 248 | }).flow('

Test paragraph

Test paragraph

Test paragraph

'); 249 | 250 | var column = target.querySelector('.cf-column-1'); 251 | var parags = column.getElementsByTagName('p'); 252 | 253 | // 60-px parags with a 1px top margin (except for first), so rounded-up to 80px 254 | assert.match(parags[0].offsetTop, 0); 255 | assert.match(parags[1].offsetTop, 100); 256 | assert.match(parags[2].offsetTop, 200); 257 | }, 258 | 259 | '1pxMarginOnUnevenHeightShouldRoundUpTo1GridLine' : function() { 260 | 261 | document.body.className = 'unevenmargin1px'; 262 | 263 | createCf({ 264 | standardiseLineHeight : true, 265 | }).flow('

Test paragraph

Test paragraph

Test paragraph

'); 266 | 267 | var column = target.querySelector('.cf-column-1'); 268 | var parags = column.getElementsByTagName('p'); 269 | 270 | // 53-px parags with a 1px top margin (except for first), so rounded-up to 80px 271 | assert.match(parags[0].offsetTop, 0); 272 | assert.match(parags[1].offsetTop, 80); 273 | assert.match(parags[2].offsetTop, 160); 274 | }, 275 | 276 | 'StandardisedLineHeightShouldNotAffectElementHeight' : function() { 277 | 278 | document.body.className = 'unpadded-parags'; 279 | 280 | createCf({ 281 | standardiseLineHeight : true, 282 | }).flow('

Test paragraph

Test paragraph

Test paragraph

'); 283 | 284 | var column = target.querySelector('.cf-column-1'); 285 | var parags = column.getElementsByTagName('p'); 286 | 287 | assert.match(parags[0].offsetHeight, 53); 288 | assert.match(parags[1].offsetHeight, 53); 289 | assert.match(parags[2].offsetHeight, 53); 290 | }, 291 | 292 | 'ShouldRoundUpElementsIgnoringPlainTextNodes' : function() { 293 | 294 | document.body.className = 'unpadded-parags'; 295 | 296 | createCf({ 297 | standardiseLineHeight : true, 298 | }).flow('

Test paragraph

\n

Test paragraph

\n

Test paragraph

'); 299 | 300 | var column = target.querySelector('.cf-column-1'); 301 | var parags = column.getElementsByTagName('p'); 302 | 303 | assert.match(parags[0].offsetTop, 0); 304 | assert.match(parags[1].offsetTop, 80); 305 | assert.match(parags[2].offsetTop, 160); 306 | }, 307 | 308 | 'RegressionItShouldNotMissOffEndOfLastElement' : function() { 309 | 310 | document.body.className = 'unequal-margin'; 311 | 312 | createCf().flow('

height1140

height40

'); 313 | 314 | var column1 = target.querySelector('.cf-column-1'); 315 | var column2 = target.querySelector('.cf-column-2'); 316 | var column3 = target.querySelector('.cf-column-3'); 317 | 318 | assert.match(column1.childNodes.length, 1); 319 | assert.match(column2.childNodes.length, 2); 320 | assert.match(column3.childNodes.length, 1); 321 | 322 | assert.match(column3.childNodes[0].style.marginTop, '-20px'); 323 | }, 324 | 325 | 'RegressionItShouldNotRepeatALine' : function() { 326 | 327 | document.body.className = 'unequal-margin'; 328 | 329 | createCf().flow('

height600

height620

'); 330 | 331 | var column1 = target.querySelector('.cf-column-1'); 332 | var column2 = target.querySelector('.cf-column-2'); 333 | var column3 = target.querySelector('.cf-column-3'); 334 | 335 | assert.match(column1.childNodes.length, 1); 336 | assert.match(column2.childNodes.length, 1); 337 | assert.match(column3.childNodes.length, 1); 338 | 339 | assert.match(column3.childNodes[0].style.marginTop, '-600px'); 340 | }, 341 | 342 | 'RegressionItShouldRemoveTopMarginOfFirstParagInAColumn' : function() { 343 | 344 | document.body.className = 'unequal-margin'; 345 | 346 | createCf().flow('

test parag

'); 347 | 348 | var column1 = target.querySelector('.cf-column-1'); 349 | var column2 = target.querySelector('.cf-column-2'); 350 | 351 | assert.match(column1.childNodes.length, 1); 352 | assert.match(column2.childNodes.length, 1); 353 | 354 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 355 | }, 356 | 357 | //*/ 358 | 359 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FTColumnflow [![Build Status](https://api.travis-ci.com/ftlabs/ftcolumnflow.svg)](https://travis-ci.com/ftlabs/ftcolumnflow) 2 | 3 | FTColumnflow is a polyfill that fixes the inadequacies of CSS column layouts. It is developed by [FT Labs](http://labs.ft.com), part of the Financial Times. It is extensively used in the [FT Web App](http://app.ft.com), where it allows us to publish articles with complex newspaper/magazine style layouts, including features such as: 4 | 5 | * Configurable column widths, gutters and margins 6 | * Fixed-position elements 7 | * Elements spanning columns 8 | * Keep-with-next to avoid headings at the bottom of a column 9 | * No-wrap class to avoid splitting elements across columns 10 | * Grouping of columns into pages 11 | * Horizontal or vertical alignment of pages 12 | * Standardised line height to align text baseline to a grid 13 | * Rapid reflow as required by events such as device orientation or font-size change 14 | 15 | It is designed with the same column dimension specification API as the [CSS3 multi-column specification](http://www.w3.org/TR/css3-multicol/) (specify `columnWidth`, `columnCount` and/or `columnGap`), but gives far greater flexibility over element positioning within those columns. 16 | 17 | 18 | ## Usage 19 | 20 | Include FTColumnflow.js in your JavaScript bundle or add it to your HTML page like this: 21 | 22 | ```html 23 | 24 | ``` 25 | 26 | 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 `target`s and pages to sit inside it. 27 | 28 | 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. 29 | 30 | To activate FTColumnflow on an article, create a target element inside a viewport: 31 | 32 | ```html 33 |
34 | ... 35 |
36 | ... 37 |
38 | ``` 39 | 40 | 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): 41 | 42 | ```javascript 43 | var cf = new FTColumnflow('article-1', 'viewport', { 44 | columnCount: 3, 45 | standardiseLineHeight: true, 46 | pagePadding: 30, 47 | }); 48 | ``` 49 | 50 | or: 51 | 52 | ```javascript 53 | var articleEl = document.getElementById('article-1'); 54 | var viewportEl = document.getElementById('viewport'); 55 | 56 | var cf = new FTColumnflow(articleEl, viewportEl, { 57 | columnCount: 3, 58 | standardiseLineHeight: true, 59 | pagePadding: 30, 60 | }); 61 | ``` 62 | 63 | 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: 64 | 65 | ```html 66 |
67 |

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.

68 |

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.

69 | ... 70 |
71 | 72 |
73 |
74 |

The Metamorphosis

75 |

Franz Kafka, 1915

76 |
77 |
78 | 79 |
80 |
81 | ``` 82 | 83 | You could apply FTColumnflow to this content with code such as: 84 | 85 | ```javascript 86 | var flowedContent = document.getElementById('flowedContent'), 87 | fixedContent = document.getElementById('fixedContent'); 88 | 89 | cf.flow(flowedContent, fixedContent); 90 | ``` 91 | 92 | Alternatively, you can pass your content into the flow method directly: 93 | 94 | ```javascript 95 | cf.flow( 96 | '

One morning, when Gregor Samsa woke from troubled dreams...', 97 | '

The Metamorphosis

Franz Kafka, 1915

...' 98 | ); 99 | ``` 100 | 101 | 102 | ## Examples 103 | 104 | Here are some examples of FTColumnflow in use - feel free to copy the code and use as the basis for your own projects. 105 | 106 | * [Basic usage example](http://ftlabs.github.com/ftcolumnflow/1.html) 107 | * [FTColumnflow elements highlighted](http://ftlabs.github.com/ftcolumnflow/2.html), which shows the derived baseline grid, the pages and columns, and also exposes the mechanism by which FTColumnflow overflows columns with content, then hides the overflow. 108 | * [Vertically-orientated pages](http://ftlabs.github.com/ftcolumnflow/3.html) 109 | * [noWrap class and noWrapOnTags config setting](http://ftlabs.github.com/ftcolumnflow/4.html) 110 | * [Another layout](http://ftlabs.github.com/ftcolumnflow/5.html) 111 | * [Native CSS3 columns](http://ftlabs.github.com/ftcolumnflow/6.html), demonstrating the capability of CSS columns without using FTColumnflow. 112 | 113 | ## How does it work? 114 | 115 | 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. 116 | 117 | 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. 118 | 119 | 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](http://ftlabs.github.com/ftcolumnflow/2.html) 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. 120 | 121 | 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](http://ftlabs.github.com/ftcolumnflow/7.html). 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. 122 | 123 | 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`](http://ftlabs.github.com/ftcolumnflow/1.html). 124 | 125 | ## Configuration 126 | 127 | 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. 128 | 129 | Column dimension configuration is designed to be as close as possible to the [CSS3 multi-column specification](http://www.w3.org/TR/css3-multicol/), using the same logic to determine `columnWidth`, `columnCount` and `columnGap`. 130 | 131 | * `pageClass: 'mypageclass',` 132 | 133 | Class name to add to each page created by FTColumnflow. Class names are normalised (invalid characters are replaced with a `-`). *(String, default 'cf-page')* 134 | 135 | * `columnClass: 'mycolumnclass',` 136 | 137 | Class name to add to each column created by FTColumnflow. Class names are normalised (invalid characters are replaced with a `-`). *(String, default 'cf-column')* 138 | 139 | * `viewportWidth: 800,` 140 | 141 | Viewport width in pixels if known, otherwise it will be measured. Note this is not the browser viewport, but refers to a DOM element passed to the FTColumnflow constructor. See Public interface for details. *(Integer, default null)* 142 | 143 | * `viewportHeight: 600,` 144 | 145 | Viewport height in pixels if known, otherwise it will be measured. Note this is not the browser viewport, but refers to a DOM element passed to the FTColumnflow constructor. See Public interface for details. *(Integer, default null)* 146 | 147 | * `layoutDimensionsCache: {...},` 148 | 149 | Pass in cached values from a previous invocation of FTColumnflow with exactly the same configuration parameters and viewport dimensions. These can be obtained from a previous flow using `cf.layoutDimensions;` *(Object, default null)* 150 | 151 | * `pageArrangement: 'horizontal',` 152 | 153 | Pages are absolutely positioned with respect to the target parent container. This parameter determines their arrangement. *('horizontal'|'vertical', default 'horizontal')* 154 | 155 | * `pagePadding: 10,` 156 | 157 | Padding in pixels to add to each page (resulting `page + padding` will equal the viewport dimensions). For horizontal arrangement, padding is added to left/right only, and for vertical to the top/bottom only. *(Integer, default 0)* 158 | 159 | * `columnFragmentMinHeight: 20,` 160 | 161 | Minimum height of each column 'fragment', in pixels. If fixed-position elements result in shortened, fragmented columns, no blocks of text will be shorter than this value. *(Integer, default 0)* 162 | 163 | * `columnWidth: 200,` 164 | 165 | Optimal column width in pixels, or 'auto'. Integer must be greater than 0. The actual columns may be wider (to fill the available space) or narrower if the specified `columnWidth` is greater than the available width. A value of auto will result in a column width determined by using the other properties (columnCount and columnGap). *(Integer|'auto', default 'auto')* 166 | 167 | * `columnCount: 3,` 168 | 169 | Optimal number of columns per page, or 'auto'. Integer must be greater than 0. If both `columnWidth` and `columnCount` are defined, columnCount is the *maximum* number of columns per page. A value of auto will result in a column count determined by using the other properties (columnWidth and columnGap). *(Integer|'auto', default 'auto')* 170 | 171 | * `columnGap: 20,` 172 | 173 | Column gap in pixels, or 'normal'. Integer must be greater than 0. The default value, 'normal', is set to 1em. *(Integer|'auto', default 'normal')* 174 | 175 | * `standardiseLineHeight: true,` 176 | 177 | If false, FTColumnflow assumes all column content is corrected/padded to conform to a baseline grid (for example, paragraph margins should be a multiple of their line-height value), and determines the grid height from the lineheight of a paragraph. If true, FTColumnflow uses the mode of the first few line-heights found, and adds margin to all other element to conform to the grid. *(Boolean, default false)* 178 | 179 | * `lineHeight: 20` 180 | 181 | Specify the line-height of the flowed content. If not set, it will be determined by analysing the content. *(Integer, default null)* 182 | 183 | * `minFixedPadding: 0.5,` 184 | 185 | Minimum space between fixed elements and columns, expressed as a multiple of the grid height. *(Float, default 1)* 186 | 187 | * `noWrapOnTags: ['figure', 'h3', ...],` 188 | 189 | Assume a 'nowrap' class for every element which matches the list of tags. *(Array, default [])* 190 | 191 | * `allowReflow: true,` 192 | 193 | Allow a `reflow()` call to occur. The advantage of disabling this is that ColumnFlow will clean up all preload DOM nodes after the initial `flow`, which are otherwise reused on `reflow()`. *(Boolean, default true)* 194 | 195 | * `showGrid: true,` 196 | 197 | Show the baseline grid - very useful for debugging line-height issues. *(Boolean, default false)* 198 | 199 | * `debug: true,` 200 | 201 | Print internal calls to `_log()` to the console (Useful for development, not used in this release). *(Boolean, default false)* 202 | 203 | 204 | ## Public interface 205 | 206 | ### Constructor 207 | 208 | * `var cf = new FTColumnflow(target, viewport, {...});` 209 | 210 | Instantiate an FTColumnflow instance, which will operate on `target` within `viewport`. 211 | 212 | * `target` 213 | 214 | DOM element, or element ID attribute, into which FTColumnflow should write pages and columns. This should be an empty container; all contents will be overwritten. 215 | 216 | * `viewport` 217 | 218 | DOM element, or element ID attribute, for the viewport from which to determine page and column dimensions. 219 | 220 | * `config` 221 | 222 | Object of key/value configuration pairs (see **Configuration** above). 223 | 224 | When FTColumnflow is instantiated on an element, the return value from the constructor is an object that offers a number of public properties, methods and events. 225 | 226 | ### Methods 227 | 228 | * `flow(flowed, fixed);` 229 | 230 | Flow `flowed` content over columns and pages, and position `fixed` content according to style rules on the elements. 231 | 232 | * `flowed` 233 | 234 | DOM element containing content to flow, or an HTML string of elements. Flowed elements may optionally specify CSS classes to control wrapping behaviour: 235 | 236 | * `nowrap` 237 | 238 | This element must not span a column break (eg. an image, header, etc.). 239 | 240 | * `keepwithnext` 241 | 242 | Avoid a column or page break after this element; prefer a break before if necessary 243 | 244 | * `fixed` 245 | 246 | DOM element containing content to position absolutely, or an HTML string of elements. Fixed elements are anchored in the order they appear in the DOM, so the second item to be anchored top-left will appear underneath the first (with a margin equal to one grid height). Elements should be given CSS classes to specify their position on the page: 247 | 248 | * `anchor--` OR `anchor--col-` 249 | 250 | Page position to which fixed element should be anchored. (Default `anchor-top-left`). 251 | 252 | * *vertical-pos* : [top, middle, bottom] 253 | * *horizontal-pos* : [left, right] 254 | * *n* : integer 255 | 256 | * `col-span-[-|-]` 257 | 258 | Fixed element should span `n` columns (or all columns), optionally specifying a direction in which to span. (Default: col-span-1, default span direction: right). 259 | 260 | * `attach-page-` 261 | 262 | If numeric `n` is specified, fixed element should appear on page `n`. `attach-page-last` will create a new, empty page at the end, after all the fixed and flowed content is rendered. Multiple `attach-page-last` elements will appear on individual pages, in the order of declaration. (Default: attach-page-1). 263 | 264 | * `reflow({});` 265 | 266 | Re-flow the same content using different configuration parameters. This is useful when a hand-held device is rotated (orientationchange event), or font-sized is changed on the page, for example. 267 | 268 | * `config` 269 | 270 | Object of key/value configuration pairs (see **Configuration** above). 271 | 272 | * `destroy();` 273 | 274 | Destroy the FTColumnflow DOM nodes, and free up memory. 275 | 276 | ### Properties 277 | 278 | All public properties are read-only. 279 | 280 | * `pageClass` 281 | 282 | The normalised class name added to pages (see pageClass in **Configuration** above). 283 | 284 | * `columnClass` 285 | 286 | The normalised class name added to columns (see columnClass in **Configuration** above). 287 | 288 | * `pageCount` 289 | 290 | The number of pages created by the last run of `cf.flow()` 291 | 292 | * `layoutDimensions` 293 | 294 | The dimensions configuration object produced by the last run of `cf.flow()`. This can be cached externally, and fed back into FTColumnflow using the `layoutDimensionsCache` config parameter (see **Configuration** above) to reduce calculations and DOM reads on subsequent flows. 295 | 296 | Example output: 297 | 298 | ```javascript 299 | { 300 | "pageInnerWidth": 740, 301 | "pageInnerHeight": 600, 302 | "colDefaultTop": 0, 303 | "colDefaultLeft": 30, 304 | "columnCount": 3, 305 | "columnWidth": 236, 306 | "columnGap": 16 307 | } 308 | ``` 309 | 310 | ## Compatibility 311 | 312 | FTColumnflow supports the following browsers: 313 | 314 | * Google Chrome (18+) 315 | * Apple Safari (5+) 316 | * Firefox (10+) 317 | * Mobile Safari (iOS 5+) 318 | * Android browser (ICS+) 319 | * Blackberry browser (PlaybookOS 2.0.1+) 320 | * Microsoft Internet Explorer (9+ - IE9 needs a [classList polyfill](https://github.com/eligrey/classList.js)) 321 | 322 | ## Testing 323 | 324 | 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. 325 | 326 | **NOTE: as of [28th June 2012](https://github.com/ftlabs/ftcolumnflow/commit/73d83b0ad601bf2f2cac7ce62a524dc9bffcc8a9), 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.** 327 | 328 | FTColumnflow uses [Buster.js](http://busterjs.org/) as a TDD framework. This requires Node and NPM - installation instructions are at [http://busterjs.org/docs/getting-started/](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. 329 | 330 | ### Usage 331 | 332 | * Change to FTColumnflow directory: 333 | 334 | $ cd [FTColumnflow root directory] 335 | * Start test suite server: 336 | 337 | $ buster server 338 | buster-server running on http://localhost:1111 339 | Buster.js has now started a server - in this case, on port 1111 (use `$ buster server -p 1234` to specify the port). Visit [http://localhost:1111](http://localhost:1111) in any number of browsers, and capture them to run tests in those browser. 340 | 341 | * Run tests in a new shell: 342 | 343 | $ buster test 344 | 345 | There are a number of options for test output - see the [reporters documentation](http://busterjs.org/docs/test/reporters/) and the **Reporters** examples at the bottom of [the overview page](http://busterjs.org/docs/overview/). 346 | 347 | * Alternatively, the tests can be run in PhantomJs, without the need for a Buster server or any captured browsers, using the simple command: 348 | 349 | $ grunt test 350 | 351 | ## Credits and collaboration 352 | 353 | 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. 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /test/fixedelements-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTColumnflow FixedElements test suite 3 | * 4 | * @copyright The Financial Times Limited [All Rights Reserved] 5 | */ 6 | 7 | buster.testCase('FixedElements', { 8 | 9 | setUp : function(done) { 10 | document.body.innerHTML = '
'; 11 | addStylesheets(['all.css', 'fixedelements.css'], done); 12 | }, 13 | 14 | tearDown : function() { 15 | removeStyleSheets(); 16 | document.body.className = ''; 17 | }, 18 | 19 | 'ShouldCreateAHiddenPreloadArea' : function() { 20 | 21 | createCf().flow(); 22 | 23 | var preload = target.querySelector('.cf-preload-fixed'); 24 | 25 | assert(preload instanceof HTMLElement); 26 | assert.match(cssProp(preload, 'visibility'), 'hidden'); 27 | assert.match(cssProp(preload, 'position'), 'absolute'); 28 | }, 29 | 30 | 'ShouldAddFlowedContentToPreloadArea' : function() { 31 | 32 | createCf().flow('', '
fixedContent
'); 33 | 34 | var preload = target.querySelector('.cf-preload-fixed'); 35 | 36 | assert.match(preload.innerHTML, /^fixedContent<\/div>$/); 37 | }, 38 | 39 | 'AFixedElementShouldBePlacedOnPage1' : function() { 40 | 41 | createCf().flow('

flowedContent

', '
fixedContent
'); 42 | 43 | var page = target.querySelector('.cf-page-1'); 44 | var fixed = page.querySelector('.fixed'); 45 | 46 | assert.match(page.childNodes.length, 2); 47 | assert(fixed instanceof HTMLElement); 48 | assert.match(fixed.innerHTML, 'fixedContent'); 49 | }, 50 | 51 | 'TextNodesShouldBeIgnored' : function() { 52 | 53 | createCf().flow('

flowedContent

', '\n
fixedContent
\n'); 54 | 55 | var page = target.querySelector('.cf-page-1'); 56 | var fixed = page.querySelector('.fixed'); 57 | 58 | assert.match(page.childNodes.length, 2); 59 | assert(fixed instanceof HTMLElement); 60 | assert.match(fixed.innerHTML, 'fixedContent'); 61 | }, 62 | 63 | 'AFixedElementShouldBePlacedAbsoluteTopLeftByDefault' : function() { 64 | 65 | createCf().flow('

flowedContent

', '
fixedContent
'); 66 | 67 | var page = target.querySelector('.cf-page-1'); 68 | var fixed = page.querySelector('.fixed'); 69 | 70 | assert.match(cssProp(fixed, 'position'), 'absolute'); 71 | assert.match(fixed.offsetTop, 0); 72 | assert.match(fixed.offsetLeft, 0); 73 | }, 74 | 75 | 'AFixedElementShouldRespectPagePadding' : function() { 76 | 77 | createCf({ 78 | columnGap : 25, 79 | columnCount : 3, 80 | pagePadding : 50, 81 | }).flow('

flowedContent

', '
fixedContent
'); 82 | 83 | var page = target.querySelector('.cf-page-1'); 84 | var fixed = page.querySelector('.fixed'); 85 | 86 | assert.match(cssProp(fixed, 'position'), 'absolute'); 87 | assert.match(fixed.offsetTop, 0); 88 | assert.match(fixed.offsetLeft, 50); 89 | }, 90 | 91 | 'AFixedElementShouldRespectPagePaddingInVerticalArrangement' : function() { 92 | 93 | createCf({ 94 | columnGap : 25, 95 | columnCount : 3, 96 | pagePadding : 50, 97 | pageArrangement : 'vertical', 98 | }).flow('

flowedContent

', '
fixedContent
'); 99 | 100 | var page = target.querySelector('.cf-page-1'); 101 | var fixed = page.querySelector('.fixed'); 102 | 103 | assert.match(cssProp(fixed, 'position'), 'absolute'); 104 | assert.match(fixed.offsetTop, 50); 105 | assert.match(fixed.offsetLeft, 0); 106 | }, 107 | 108 | 'ShouldShortenTheAffectedColumnAndPlaceItLower' : function() { 109 | 110 | createCf().flow('

flowedContent

', '
fixedContent
'); 111 | 112 | var page = target.querySelector('.cf-page-1'); 113 | var column = page.querySelector('.cf-column'); 114 | 115 | assert.match(column.offsetTop, 220); 116 | assert.match(column.offsetHeight, 380); 117 | }, 118 | 119 | 'ShouldReduceTheAvailableSpaceInAColumn' : function() { 120 | 121 | createCf().flow('
height600
', '
fixedContent
'); 122 | 123 | var page = target.querySelector('.cf-page-1'); 124 | var column1 = page.querySelector('.cf-column-1'); 125 | var column2 = page.querySelector('.cf-column-2'); 126 | 127 | assert(column2 instanceof HTMLElement); 128 | 129 | var element = column2.childNodes[0]; 130 | assert.match(cssProp(element, 'margin-top'), '-380px'); 131 | }, 132 | 133 | 'ShouldShortenMultipleColumnsWhenTheElementSpans' : function() { 134 | 135 | createCf().flow('
height600
height600
', '
fixedContent
'); 136 | 137 | var page = target.querySelector('.cf-page-1'); 138 | var column1 = page.querySelector('.cf-column-1'); 139 | var column2 = page.querySelector('.cf-column-2'); 140 | var column3 = page.querySelector('.cf-column-3'); 141 | 142 | assert.match(column1.offsetTop, 220); 143 | assert.match(column1.offsetHeight, 380); 144 | 145 | assert.match(column2.offsetTop, 220); 146 | assert.match(column2.offsetHeight, 380); 147 | 148 | assert.match(column3.offsetTop, 0); 149 | assert.match(column3.offsetHeight, 600); 150 | 151 | var element = column3.childNodes[0]; 152 | assert.match(cssProp(element, 'margin-top'), '-160px'); 153 | }, 154 | 155 | 'ShouldReduceASpanValueIfTooLarge' : function() { 156 | 157 | createCf().flow('
height600
height600
', '
fixedContent
'); 158 | 159 | var page = target.querySelector('.cf-page-1'); 160 | 161 | assert.match(page.querySelectorAll('.cf-column').length, 3); 162 | }, 163 | 164 | 'ShouldSpanAllColumns' : function() { 165 | 166 | createCf().flow('
height1000
', '
fixedContent
'); 167 | 168 | var page = target.querySelector('.cf-page-1'); 169 | var fixed = target.querySelector('.fixed'); 170 | var column3 = page.querySelector('.cf-column-3'); 171 | var element = column3.childNodes[0]; 172 | 173 | assert.match(cssProp(fixed, 'width'), '800px'); 174 | assert.match(cssProp(element, 'margin-top'), '-760px'); 175 | }, 176 | 177 | 'ShouldRoundUpTheHeightOfEachFixedElement' : function() { 178 | 179 | createCf().flow('

flowedContent

', '
fixedContent
'); 180 | 181 | var page = target.querySelector('.cf-page-1'); 182 | var column = page.querySelector('.cf-column'); 183 | 184 | assert.match(column.offsetHeight, 360); 185 | assert.match(column.offsetTop, 240); 186 | }, 187 | 188 | 'ShouldPlaceSecondFixedElementUnderneathFirstWithAGap' : function() { 189 | 190 | createCf().flow('
height600
', '
fixedContent
fixedContent
'); 191 | 192 | var page = target.querySelector('.cf-page-1'); 193 | var fixed1 = page.querySelector('.col-span-2'); 194 | var fixed2 = page.querySelector('.col-span-1'); 195 | var column1 = page.querySelector('.cf-column-1'); 196 | var column2 = page.querySelector('.cf-column-2'); 197 | var column3 = page.querySelector('.cf-column-3'); 198 | 199 | assert.match(fixed1.offsetTop, 0); 200 | assert.match(fixed2.offsetTop, 220); 201 | 202 | assert.match(column1.offsetTop, 440); 203 | assert.match(column1.offsetHeight, 160); 204 | 205 | assert.match(column2.offsetTop, 220); 206 | assert.match(column2.offsetHeight, 380); 207 | 208 | assert.match(cssProp(column2.childNodes[0], 'margin-top'), '-160px'); 209 | assert.match(cssProp(column3.childNodes[0], 'margin-top'), '-540px'); 210 | }, 211 | 212 | 'ShouldRespectSpecifiedPageAttachment' : function() { 213 | 214 | createCf().flow('
height600
height600
height600
height600
', '
fixedContent
'); 215 | 216 | 217 | var page1 = target.querySelector('.cf-page-1'); 218 | var page2 = target.querySelector('.cf-page-2'); 219 | var fixed = page2.querySelector('.fixed'); 220 | 221 | var p2col1 = page2.querySelector('.cf-column-1'); 222 | 223 | assert(fixed instanceof HTMLElement); 224 | assert.match(fixed.offsetTop, 0); 225 | 226 | assert.match(p2col1.offsetTop, 220); 227 | assert.match(p2col1.offsetHeight, 380); 228 | }, 229 | 230 | 'ShouldAnchorToTopRight' : function() { 231 | 232 | createCf().flow('
height600
height600
height600
', '
fixedContent
'); 233 | 234 | var page = target.querySelector('.cf-page-1'); 235 | var fixed = page.querySelector('.fixed'); 236 | var column3 = page.querySelector('.cf-column-3'); 237 | 238 | assert.match(fixed.offsetTop, 0); 239 | assert.match(fixed.offsetLeft, 550); 240 | 241 | assert.match(column3.offsetTop, 220); 242 | assert.match(cssProp(column3.childNodes[0], 'margin-top'), '0px'); 243 | }, 244 | 245 | 'ShouldAnchorToTopRightAndSpanLeft' : function() { 246 | 247 | createCf().flow('
height600
height600
height600
', '
fixedContent
'); 248 | 249 | var page = target.querySelector('.cf-page-1'); 250 | var fixed = page.querySelector('.fixed'); 251 | 252 | var column2 = page.querySelector('.cf-column-2'); 253 | var column3 = page.querySelector('.cf-column-3'); 254 | 255 | assert.match(fixed.offsetTop, 0); 256 | assert.match(fixed.offsetLeft, 275); 257 | 258 | assert.match(column2.offsetTop, 220); 259 | assert.match(column3.offsetTop, 220); 260 | }, 261 | 262 | 'ShouldShiftAcrossToLeftToFit' : function() { 263 | 264 | createCf().flow('
height600
height600
height600
', '
fixedContent
'); 265 | 266 | var page = target.querySelector('.cf-page-1'); 267 | var fixed = page.querySelector('.fixed'); 268 | 269 | var column1 = page.querySelector('.cf-column-1'); 270 | var column2 = page.querySelector('.cf-column-2'); 271 | var column3 = page.querySelector('.cf-column-3'); 272 | 273 | assert.match(fixed.offsetTop, 0); 274 | assert.match(fixed.offsetLeft, 275); 275 | 276 | assert.match(column1.offsetTop, 0); 277 | assert.match(column2.offsetTop, 220); 278 | assert.match(column3.offsetTop, 220); 279 | }, 280 | 281 | 'ShouldShiftAcrossToRightToFit' : function() { 282 | 283 | createCf().flow('
height600
height600
height600
', '
fixedContent
'); 284 | 285 | var page = target.querySelector('.cf-page-1'); 286 | var fixed = page.querySelector('.fixed'); 287 | 288 | var column1 = page.querySelector('.cf-column-1'); 289 | var column2 = page.querySelector('.cf-column-2'); 290 | var column3 = page.querySelector('.cf-column-3'); 291 | 292 | assert.match(fixed.offsetTop, 0); 293 | assert.match(fixed.offsetLeft, 0); 294 | 295 | assert.match(column1.offsetTop, 220); 296 | assert.match(column2.offsetTop, 220); 297 | assert.match(column3.offsetTop, 0); 298 | }, 299 | 300 | 'ShouldReduceImpossibleSpanToLeft' : function() { 301 | 302 | createCf().flow('
height600
height600
height600
', '
fixedContent
'); 303 | 304 | var page = target.querySelector('.cf-page-1'); 305 | var fixed = page.querySelector('.fixed'); 306 | 307 | var column1 = page.querySelector('.cf-column-1'); 308 | var column2 = page.querySelector('.cf-column-2'); 309 | var column3 = page.querySelector('.cf-column-3'); 310 | 311 | assert.match(fixed.offsetTop, 0); 312 | assert.match(fixed.offsetLeft, 0); 313 | 314 | assert.match(column1.offsetTop, 220); 315 | assert.match(column2.offsetTop, 220); 316 | assert.match(column3.offsetTop, 220); 317 | }, 318 | 319 | 'ShouldAnchorToBottomRightAndSpanLeft' : function() { 320 | 321 | createCf().flow('
height600
height600
height600
', '
fixedContent
'); 322 | 323 | var page = target.querySelector('.cf-page-1'); 324 | var fixed = page.querySelector('.fixed205'); 325 | 326 | var column2 = page.querySelector('.cf-column-2'); 327 | var column3 = page.querySelector('.cf-column-3'); 328 | 329 | assert.match(fixed.offsetTop, 395); 330 | assert.match(fixed.offsetLeft, 275); 331 | 332 | assert.match(column2.offsetTop, 0); 333 | assert.match(column2.offsetHeight, 360); 334 | 335 | assert.match(column3.offsetTop, 0); 336 | assert.match(column3.offsetHeight, 360); 337 | }, 338 | 339 | 'ShouldAnchorToBottomRightAndStack' : function() { 340 | 341 | createCf().flow('
height600
height600
height600
', '
fixedContent
fixedContent
'); 342 | 343 | var page = target.querySelector('.cf-page-1'); 344 | var fixed1 = page.querySelector('.fixed-1'); 345 | var fixed2 = page.querySelector('.fixed-2'); 346 | 347 | var column2 = page.querySelector('.cf-column-2'); 348 | var column3 = page.querySelector('.cf-column-3'); 349 | 350 | assert.match(fixed1.offsetTop, 400); 351 | assert.match(fixed1.offsetLeft, 275); 352 | 353 | assert.match(fixed2.offsetTop, 180); 354 | assert.match(fixed2.offsetLeft, 550); 355 | 356 | assert.match(column2.offsetTop, 0); 357 | assert.match(column2.offsetHeight, 380); 358 | 359 | assert.match(column3.offsetTop, 0); 360 | assert.match(column3.offsetHeight, 160); 361 | }, 362 | 363 | 'ShouldAnchorToColumn2' : function() { 364 | 365 | createCf({ 366 | columnGap : 25, 367 | columnCount : 5 368 | }).flow('
height600
height600
height600
', '
fixedContent
'); 369 | 370 | var page = target.querySelector('.cf-page-1'); 371 | var fixed = page.querySelector('.cf-render-area .fixed'); 372 | var column1 = page.querySelector('.cf-column-1'); 373 | var column2 = page.querySelector('.cf-column-2'); 374 | var column3 = page.querySelector('.cf-column-3'); 375 | 376 | assert.match(fixed.offsetTop, 0); 377 | assert.match(fixed.offsetLeft, 165); 378 | assert.match(fixed.offsetWidth, 305); 379 | 380 | assert.match(column1.offsetTop, 0); 381 | assert.match(column1.offsetHeight, 600); 382 | 383 | assert.match(column2.offsetTop, 220); 384 | assert.match(column2.offsetHeight, 380); 385 | 386 | assert.match(column3.offsetTop, 220); 387 | assert.match(column3.offsetHeight, 380); 388 | }, 389 | /* 390 | 391 | 'ShouldAnchorToSpecifiedColumn' : function() { 392 | 393 | createCf().flow('
height600
height600
height600
', '
fixedContent
'); 394 | 395 | var page = target.querySelector('.cf-page-1'); 396 | var fixed = page.querySelector('.fixed'); 397 | 398 | var column2 = page.querySelector('.cf-column-2'); 399 | var column3 = page.querySelector('.cf-column-3'); 400 | 401 | assert.match(fixed.offsetTop, 0); 402 | assert.match(fixed.offsetLeft, 275); 403 | 404 | assert.match(column2.offsetTop, 220); 405 | assert.match(column3.offsetTop, 220); 406 | }, 407 | */ 408 | 409 | 'AFixedElementAtTheBottomShouldRespectPagePadding' : function() { 410 | 411 | createCf({ 412 | columnGap : 25, 413 | columnCount : 3, 414 | pagePadding : 50, 415 | pageArrangement : 'vertical' 416 | }).flow('

flowedContent

', '
fixedContent
'); 417 | 418 | var page = target.querySelector('.cf-page-1'); 419 | var fixed = page.querySelector('.fixed'); 420 | 421 | assert.match(fixed.offsetTop, 350); 422 | }, 423 | 424 | 'ShouldOverlapElementsRatherThanOmitThem' : function() { 425 | 426 | createCf().flow('
height100
', '
fixedContent
fixedContent
fixedContent
fixedContent
'); 427 | 428 | var page = target.querySelector('.cf-page-1'); 429 | 430 | var fixed1 = page.querySelector('.fixed-1'); 431 | var fixed2 = page.querySelector('.fixed-2'); 432 | var fixed3 = page.querySelector('.fixed-3'); 433 | var fixed4 = page.querySelector('.fixed-4'); 434 | 435 | assert.match(fixed1.offsetTop, 0); 436 | assert.match(fixed2.offsetTop, 220); 437 | assert.match(fixed3.offsetTop, 400); 438 | assert.match(fixed4.offsetTop, 400); 439 | }, 440 | 441 | 'ShouldOverlapBottomElementsToo' : function() { 442 | 443 | createCf().flow('
height100
', '
fixedContent
fixedContent
fixedContent
fixedContent
'); 444 | 445 | var page = target.querySelector('.cf-page-1'); 446 | 447 | var fixed1 = page.querySelector('.fixed-1'); 448 | var fixed2 = page.querySelector('.fixed-2'); 449 | var fixed3 = page.querySelector('.fixed-3'); 450 | var fixed4 = page.querySelector('.fixed-4'); 451 | 452 | assert.match(fixed1.offsetTop, 400); 453 | assert.match(fixed2.offsetTop, 180); 454 | assert.match(fixed3.offsetTop, 0); 455 | assert.match(fixed4.offsetTop, 0); 456 | }, 457 | 458 | 'ShouldNotPrintAColumnWhenFixedElementsHaveFilledTheSpace' : function() { 459 | 460 | createCf().flow('
height100
', '
fixedContent
'); 461 | 462 | var page = target.querySelector('.cf-page-1'); 463 | var column2 = page.querySelector('.cf-column-2'); 464 | 465 | assert.isNull(page.querySelector('.cf-column-1')); 466 | 467 | assert.match(column2.offsetTop, 0); 468 | assert.match(column2.offsetLeft, 275); 469 | assert.match(column2.childNodes.length, 1); 470 | }, 471 | 472 | 'ShouldNotPrintAnyColumnsOnPage1WhenFixedElementsHaveFilledTheSpace' : function() { 473 | 474 | createCf().flow('
height100
', '
fixedContent
'); 475 | 476 | var page1 = target.querySelector('.cf-page-1'); 477 | var page2 = target.querySelector('.cf-page-2'); 478 | 479 | var fixed = page1.querySelector('.fixed600'); 480 | var column = page2.querySelector('.cf-column-1'); 481 | 482 | assert(fixed instanceof HTMLElement); 483 | assert.match(page1.querySelectorAll('.cf-column').length, 0); 484 | 485 | assert.match(column.offsetTop, 0); 486 | assert.match(column.offsetLeft, 0); 487 | assert.match(column.childNodes.length, 1); 488 | }, 489 | 490 | 'ShouldVerticallyCenterAFixedElement' : function() { 491 | 492 | createCf().flow('
height200
', '
fixedContent
'); 493 | 494 | var page = target.querySelector('.cf-page-1'); 495 | var fixed = page.querySelector('.fixed'); 496 | 497 | assert.match(fixed.offsetTop, 200); 498 | assert.match(fixed.offsetLeft, 0); 499 | 500 | var columns = page.querySelectorAll('.cf-column-1'); 501 | assert.match(columns.length, 2); 502 | 503 | var topCol1 = columns[0]; 504 | var bottomCol1 = columns[1]; 505 | 506 | assert.match(topCol1.offsetLeft, 0); 507 | assert.match(topCol1.offsetTop, 0); 508 | assert.match(topCol1.offsetHeight, 180); 509 | 510 | assert.match(bottomCol1.offsetLeft, 0); 511 | assert.match(bottomCol1.offsetTop, 420); 512 | assert.match(bottomCol1.offsetHeight, 180); 513 | 514 | assert.match(cssProp(bottomCol1.childNodes[0], 'margin-top'), '-180px'); 515 | }, 516 | 517 | 'ShouldCorrectlyHandleATallVerticallyCenteredElement' : function() { 518 | 519 | createCf().flow('
height200
', '
fixedContent
'); 520 | 521 | var page = target.querySelector('.cf-page-1'); 522 | var fixed = page.querySelector('.fixed600'); 523 | 524 | assert.match(fixed.offsetTop, 0); 525 | assert.match(fixed.offsetLeft, 0); 526 | 527 | assert.isNull(page.querySelector('.cf-column-1')); 528 | 529 | var column2 = page.querySelector('.cf-column-2'); 530 | 531 | assert.match(column2.offsetTop, 0); 532 | assert.match(cssProp(column2.childNodes[0], 'margin-top'), '0px'); 533 | }, 534 | 535 | 'ShouldCorrectlyHandleATallVerticallyCenteredElementInCol2' : function() { 536 | 537 | createCf().flow('
height1000
', '
fixedContent
'); 538 | 539 | var page = target.querySelector('.cf-page-1'); 540 | var fixed = page.querySelector('.fixed600'); 541 | 542 | assert.isNull(target.querySelector('.cf-page-2')); 543 | 544 | assert.match(fixed.offsetTop, 0); 545 | assert.match(fixed.offsetLeft, 275); 546 | 547 | assert.isNull(page.querySelector('.cf-column-2')); 548 | 549 | var column1 = page.querySelector('.cf-column-1'); 550 | var column3 = page.querySelector('.cf-column-3'); 551 | 552 | assert.match(column1.offsetTop, 0); 553 | assert.match(cssProp(column1.childNodes[0], 'margin-top'), '0px'); 554 | 555 | assert.match(column3.offsetTop, 0); 556 | assert.match(cssProp(column3.childNodes[0], 'margin-top'), '-600px'); 557 | }, 558 | 559 | 'ShouldHandleANormalAndACenteredElementInTheSameColumn' : function() { 560 | 561 | createCf().flow('
height600
', '
fixedContent
fixedContent
'); 562 | 563 | var target = document.getElementById('targetid'); 564 | var page = target.querySelector('.cf-page-1'); 565 | var fixedTop = page.querySelector('.anchor-top-left'); 566 | var fixedMid = page.querySelector('.anchor-middle-left'); 567 | 568 | assert.isNull(target.querySelector('.cf-page-2')); 569 | 570 | assert.match(fixedTop.offsetTop, 0); 571 | assert.match(fixedMid.offsetTop, 250); 572 | 573 | var column1s = page.querySelectorAll('.cf-column-1'); 574 | assert.match(column1s.length, 2); 575 | 576 | assert.match(column1s[0].offsetTop, 120); 577 | assert.match(column1s[0].offsetHeight, 100); 578 | 579 | assert.match(column1s[1].offsetTop, 380); 580 | assert.match(column1s[1].offsetHeight, 220); 581 | assert.match(cssProp(column1s[1].childNodes[0], 'margin-top'), '-100px'); 582 | 583 | var column2 = page.querySelector('.cf-column-2'); 584 | assert.match(cssProp(column2.childNodes[0], 'margin-top'), '-320px'); 585 | }, 586 | 587 | 'ShouldHandleANormalAndACenteredElementInTheSameColumnButSwapped' : function() { 588 | 589 | createCf().flow('
height600
', '
fixedContent
fixedContent
'); 590 | 591 | var target = document.getElementById('targetid'); 592 | var page = target.querySelector('.cf-page-1'); 593 | var fixedTop = page.querySelector('.anchor-top-left'); 594 | var fixedMid = page.querySelector('.anchor-middle-left'); 595 | 596 | assert.isNull(target.querySelector('.cf-page-2')); 597 | 598 | assert.match(fixedTop.offsetTop, 0); 599 | assert.match(fixedMid.offsetTop, 250); 600 | 601 | var column1s = page.querySelectorAll('.cf-column-1'); 602 | assert.match(column1s.length, 2); 603 | 604 | assert.match(column1s[0].offsetTop, 120); 605 | assert.match(column1s[0].offsetHeight, 100); 606 | 607 | assert.match(column1s[1].offsetTop, 380); 608 | assert.match(column1s[1].offsetHeight, 220); 609 | assert.match(cssProp(column1s[1].childNodes[0], 'margin-top'), '-100px'); 610 | 611 | var column2 = page.querySelector('.cf-column-2'); 612 | assert.match(cssProp(column2.childNodes[0], 'margin-top'), '-320px'); 613 | }, 614 | 615 | 'ShouldAllowABottomAlignedElementUnderneathACenteredElement' : function() { 616 | 617 | createCf().flow('
height600
', '
fixedContent
fixedContent
'); 618 | 619 | var target = document.getElementById('targetid'); 620 | var page = target.querySelector('.cf-page-1'); 621 | var fixedBot = page.querySelector('.anchor-bottom-left'); 622 | var fixedMid = page.querySelector('.anchor-middle-left'); 623 | 624 | assert.isNull(target.querySelector('.cf-page-2')); 625 | 626 | assert.match(fixedBot.offsetTop, 500); 627 | assert.match(fixedMid.offsetTop, 250); 628 | 629 | var column1 = page.querySelectorAll('.cf-column-1'); 630 | assert.match(column1.length, 2); 631 | 632 | assert.match(column1[0].offsetTop, 0); 633 | assert.match(column1[0].offsetHeight, 220); 634 | 635 | assert.match(column1[1].offsetTop, 380); 636 | assert.match(column1[1].offsetHeight, 100); 637 | }, 638 | 639 | 'ShouldHandleACollisionBetweenCenteredElements' : function() { 640 | 641 | createCf().flow('
height600
', '
fixedContent
fixedContent
'); 642 | 643 | var page = target.querySelector('.cf-page-1'); 644 | var fixed1 = page.querySelector('.fixed100'); 645 | var fixed2 = page.querySelector('.fixed'); 646 | 647 | assert.match(fixed1.offsetTop, 250); 648 | assert.match(fixed2.offsetTop, 200); 649 | 650 | var column1 = page.querySelectorAll('.cf-column-1'); 651 | assert.match(column1.length, 2); 652 | 653 | assert.match(column1[0].offsetTop, 0); 654 | assert.match(column1[0].offsetHeight, 180); 655 | 656 | assert.match(column1[1].offsetTop, 420); 657 | assert.match(column1[1].offsetHeight, 180); 658 | 659 | var column2 = page.querySelector('.cf-column-2'); 660 | assert.match(cssProp(column2.childNodes[0], 'margin-top'), '-360px'); 661 | }, 662 | 663 | 'ShouldHonourTheMinimumColumnHeight' : function() { 664 | 665 | createCf({ 666 | columnGap : 25, 667 | columnCount : 3, 668 | columnFragmentMinHeight : 100, 669 | }).flow('
height600
', '
fixedContent
'); 670 | 671 | var page = target.querySelector('.cf-page-1'); 672 | 673 | assert.isNull(page.querySelector('.cf-column-1')); 674 | 675 | var column2 = page.querySelector('.cf-column-2'); 676 | assert.match(column2.offsetTop, 0); 677 | assert.match(column2.offsetHeight, 600); 678 | assert.match(cssProp(column2.childNodes[0], 'margin-top'), '0px'); 679 | }, 680 | 681 | 'RegressionPageClassShouldNotResultInEmptyPages' : function() { 682 | 683 | createCf({ 684 | columnGap : 25, 685 | columnCount : 3, 686 | columnFragmentMinHeight : 100 687 | }).flow('
height600
', '
fixed1
fixed2
'); 688 | 689 | var page1 = target.querySelector('.cf-page-1'); 690 | var page2 = target.querySelector('.cf-page-2'); 691 | var column = page1.querySelector('.cf-column-1'); 692 | 693 | assert(column instanceof HTMLElement); 694 | assert.isNull(page2.querySelector('.cf-column-1')); 695 | }, 696 | 697 | 'ShouldCorrectlyFlowContentWhenColspan1IsFollowedByColspan2' : function() { 698 | 699 | createCf({ 700 | columnGap : 25, 701 | columnCount : 3, 702 | columnFragmentMinHeight : 100 703 | }).flow('
height700
', '
1
2
'); 704 | 705 | var page = target.querySelector('.cf-page-1'); 706 | var column1 = page.querySelector('.cf-column-1'); 707 | 708 | assert.match(column1.offsetTop, 440); 709 | assert.match(column1.offsetHeight, 160); 710 | 711 | var column2s = page.querySelectorAll('.cf-column-2'); 712 | assert.match(column2s.length, 2); 713 | 714 | assert.match(column2s[0].offsetTop, 0); 715 | assert.match(column2s[0].offsetHeight, 200); 716 | 717 | assert.match(column2s[1].offsetTop, 440); 718 | assert.match(column2s[1].offsetHeight, 160); 719 | 720 | }, 721 | 722 | 'ShouldAllowFixedElementsToBeShiftedVertically' : function() { 723 | 724 | createCf().flow('
height300
', '
1
'); 725 | 726 | var page = target.querySelector('.cf-page-1'); 727 | var column1 = page.querySelector('.cf-column-1'); 728 | 729 | assert.match(column1.offsetTop, 200); 730 | assert.match(column1.offsetHeight, 400); 731 | 732 | }, 733 | 734 | 'ShouldSetExplicitWidthOnFixedElementsWithAutoWidth' : function() { 735 | 736 | createCf().flow('
height300
', '
1
'); 737 | 738 | var page = target.querySelector('.cf-page-1'); 739 | var column1 = page.querySelector('.cf-column-1'); 740 | var fixed = page.querySelector('.fixed'); 741 | 742 | assert.match(fixed.style.width, "250px"); 743 | assert.match(parseInt(cssProp(fixed, 'width'), 10), 250); 744 | }, 745 | 746 | 'ShouldSetExplicitWidthOnFixedElementsOverTwoColumns' : function() { 747 | 748 | createCf().flow('
height300
', '
1
'); 749 | 750 | var page = target.querySelector('.cf-page-1'); 751 | var column1 = page.querySelector('.cf-column-1'); 752 | var fixed = page.querySelector('.fixed'); 753 | 754 | assert.match(fixed.style.width, "525px"); 755 | assert.match(parseInt(cssProp(fixed, 'width'), 10), 525); 756 | }, 757 | 758 | 'ShouldRespectAFloatValueForMinFixedPadding' : function() { 759 | 760 | // 205px, minimum gap 20px, next element starts at 240px 761 | createCf().flow('

flowedContent

', '
fixedContent
'); 762 | 763 | var page = target.querySelector('.cf-page-1'); 764 | var column = page.querySelector('.cf-column'); 765 | 766 | assert.match(column.offsetTop, 240); 767 | assert.match(column.offsetHeight, 360); 768 | 769 | 770 | // 205px, minimum gap 10px, next element starts at 220px 771 | createCf({ 772 | columnGap : 25, 773 | columnCount : 3, 774 | minFixedPadding : 0.5 775 | }).flow('

flowedContent

', '
fixedContent
'); 776 | 777 | page = target.querySelector('.cf-page-1'); 778 | column = page.querySelector('.cf-column'); 779 | 780 | assert.match(column.offsetTop, 220); 781 | assert.match(column.offsetHeight, 380); 782 | 783 | 784 | // 200px, minimum gap 30px, next element starts at 240px 785 | createCf({ 786 | columnGap : 25, 787 | columnCount : 3, 788 | minFixedPadding : 1.5 789 | }).flow('

flowedContent

', '
fixedContent
'); 790 | 791 | page = target.querySelector('.cf-page-1'); 792 | column = page.querySelector('.cf-column'); 793 | 794 | assert.match(column.offsetTop, 240); 795 | assert.match(column.offsetHeight, 360); 796 | }, 797 | 798 | '//ShouldSetExplicitHeightOnImagesWithSpecifiedAspectRatio' : function() { 799 | 800 | createCf().flow('
height300
', '
'); 801 | 802 | var page = target.querySelector('.cf-page-1'); 803 | var column1 = page.querySelector('.cf-column-1'); 804 | var fixed = page.querySelector('.fixed'); 805 | var img = fixed.querySelector('img'); 806 | 807 | assert.match(img.style.width, "200px"); 808 | assert.match(img.style.height, "100px"); 809 | assert.match(parseInt(cssProp(img, 'width'), 10), 200); 810 | assert.match(parseInt(cssProp(img, 'height'), 10), 100); 811 | }, 812 | 813 | 'ShouldAddLastPageElementToFirstPageWhenThereIsNoOtherContent' : function() { 814 | 815 | createCf().flow('', '
last page content
'); 816 | 817 | var page1 = target.querySelector('.cf-page-1'); 818 | var fixed = page1.querySelector('.fixed'); 819 | 820 | assert(fixed instanceof HTMLElement); 821 | assert.match(fixed.offsetTop, 0); 822 | }, 823 | 824 | 'ShouldAddLastPageElementToLastPageWhenThereIsOtherContent' : function() { 825 | 826 | createCf().flow('
height300
', '
last page content
fixed2
'); 827 | 828 | var page2 = target.querySelector('.cf-page-2'); 829 | var page3 = target.querySelector('.cf-page-3'); 830 | 831 | var fixedP2 = page2.querySelector('.fixed.attach-page-2'); 832 | var fixedLast = page3.querySelector('.fixed.attach-page-last'); 833 | 834 | assert(fixedLast instanceof HTMLElement); 835 | assert.match(fixedLast.offsetTop, 0); 836 | }, 837 | 838 | 'ShouldAddMultipleLastPageElementsInOrder' : function() { 839 | 840 | createCf().flow('
height300
', '
last page content
last page content
'); 841 | 842 | var page2 = target.querySelector('.cf-page-2'); 843 | var page3 = target.querySelector('.cf-page-3'); 844 | 845 | var fixedLast1 = page2.querySelector('.attach-page-last-1'); 846 | var fixedLast2 = page3.querySelector('.attach-page-last-2'); 847 | 848 | assert(fixedLast1 instanceof HTMLElement); 849 | assert(fixedLast2 instanceof HTMLElement); 850 | 851 | assert.match(fixedLast1.offsetTop, 0); 852 | assert.match(fixedLast2.offsetTop, 0); 853 | }, 854 | 855 | 856 | 857 | //*/ 858 | 859 | }); -------------------------------------------------------------------------------- /test/columnwrap-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FTColumnflow ColumnWrap test suite 3 | * 4 | * @copyright The Financial Times Limited [All Rights Reserved] 5 | */ 6 | 7 | "use strict"; 8 | 9 | var _exactHeightWrap = '
height500
height100
height100
'; 10 | var _wrapToPage2 = '
height600
height600
height600
height100
'; 11 | var _overflowedElement = '
height500
height50
height100
'; 12 | 13 | 14 | 15 | 16 | 17 | buster.testCase('ColumnWrap', { 18 | 19 | setUp : function(done) { 20 | document.body.innerHTML = '
'; 21 | addStylesheets(['all.css', 'columnwrap.css'], done); 22 | }, 23 | 24 | tearDown : function() { 25 | removeStyleSheets(); 26 | document.body.className = ''; 27 | }, 28 | 29 | 'ShouldAcceptNodeOrStringAsContentParameter' : function() { 30 | 31 | createCf(); 32 | 33 | refute.exception(function test() { 34 | cf.flow(document.createElement('p')); 35 | }); 36 | 37 | refute.exception(function test() { 38 | cf.flow('

foo

'); 39 | }); 40 | 41 | assert.exception(function test() { 42 | cf.flow(new Array); 43 | }, 'FTColumnflowFlowedContentException'); 44 | }, 45 | 46 | 'ShouldAcceptNodeOrStringAsFixedParameter' : function() { 47 | 48 | createCf(); 49 | 50 | refute.exception(function test() { 51 | cf.flow('', document.createElement('p')); 52 | }); 53 | 54 | refute.exception(function test() { 55 | cf.flow('', '

foo

'); 56 | }); 57 | 58 | assert.exception(function test() { 59 | cf.flow('', new Array); 60 | }, 'FTColumnflowFixedContentException'); 61 | }, 62 | 63 | 'ShouldNotChangeExistingTargetId' : function() { 64 | 65 | createCf().flow(); 66 | assert(target instanceof HTMLElement); 67 | }, 68 | 69 | 'ShouldAddAnIdToTargetIfNotSet' : function() { 70 | 71 | document.body.innerHTML = '
'; 72 | var target = document.querySelector('.targetClass'); 73 | 74 | new FTColumnflow(target, 'viewportid').flow(); 75 | 76 | assert.match(target.id, /^cf-target-\d{10}$/); 77 | }, 78 | 79 | 'ShouldCreateAHiddenPreloadArea' : function() { 80 | 81 | createCf().flow(); 82 | 83 | var preload = target.querySelector('.cf-preload'); 84 | 85 | assert(preload instanceof HTMLElement); 86 | assert.className(preload, 'cf-page'); 87 | 88 | assert.match(cssProp(preload, 'visibility'), 'hidden'); 89 | assert.match(cssProp(preload, 'position'), 'absolute'); 90 | }, 91 | 92 | 'ShouldCreateAPreloadColumnOfTheCorrectWidth' : function() { 93 | 94 | createCf().flow(); 95 | 96 | var preload = target.querySelector('.cf-preload'); 97 | var column = preload.querySelector('.cf-column'); 98 | 99 | assert.match(preload.childNodes.length, 1); 100 | assert(column instanceof HTMLElement); 101 | assert.match(column.clientWidth, 250); 102 | }, 103 | 104 | 'ShouldRespectConfigColumnAndClassNamesInPreloadArea' : function() { 105 | 106 | createCf({ 107 | pageClass : 'mypage', 108 | columnClass : 'mycol', 109 | }).flow(); 110 | 111 | var preload = target.querySelector('.cf-preload'); 112 | assert.className(preload, 'mypage'); 113 | 114 | var column = preload.childNodes[0]; 115 | assert.className(column, 'mycol'); 116 | }, 117 | 118 | 'ShouldAddFlowedContentToPreloadArea' : function() { 119 | 120 | createCf().flow('

Hello there!

'); 121 | 122 | var preload = target.querySelector('.cf-preload'); 123 | var column = preload.childNodes[0]; 124 | 125 | assert.match(column.innerHTML, /^Hello there!<\/p>$/); 126 | }, 127 | 128 | 'ShouldAddContentFromAnExistingElement' : function() { 129 | 130 | var contentContainer = document.createElement('div'); 131 | var flowedContent = document.createElement('p'); 132 | var text = document.createTextNode('Hello there!'); 133 | flowedContent.appendChild(text); 134 | contentContainer.appendChild(flowedContent); 135 | 136 | createCf().flow(contentContainer); 137 | 138 | var preload = target.querySelector('.cf-preload'); 139 | var column = preload.childNodes[0]; 140 | 141 | assert.match(column.innerHTML, /^Hello there!<\/p>$/); 142 | }, 143 | 144 | 'ShouldLeaveOriginalContentUntouched' : function() { 145 | 146 | var contentContainer = document.createElement('div'); 147 | contentContainer.id = 'contentContainer'; 148 | var flowedContent = document.createElement('p'); 149 | var text = document.createTextNode('Hello there!'); 150 | flowedContent.appendChild(text); 151 | contentContainer.appendChild(flowedContent); 152 | 153 | document.getElementById('viewportid').appendChild(contentContainer); 154 | 155 | createCf().flow(contentContainer); 156 | 157 | assert.match(document.getElementById('contentContainer').innerHTML, '

Hello there!

'); 158 | }, 159 | 160 | 'ShouldAddAShortParagraphToPage1Column1' : function() { 161 | 162 | createCf().flow('

Hello there!

'); 163 | 164 | var page = target.querySelector('.cf-page-1'); 165 | assert(page instanceof HTMLElement); 166 | assert.className(page, 'cf-page'); 167 | 168 | var column = page.querySelector('.cf-column-1'); 169 | assert(column instanceof HTMLElement); 170 | assert.className(column, 'cf-column'); 171 | 172 | assert.match(column.innerHTML, /^Hello there!<\/p>$/); 173 | }, 174 | 175 | 'ShouldOverwriteTargetContents' : function() { 176 | 177 | target.innerHTML = 'OVERWRITE'; 178 | 179 | createCf().flow('

Hello there!

'); 180 | 181 | refute.match(target.innerHTML, /OVERWRITE/); 182 | }, 183 | 184 | 'ShouldSetCorrectPageDimensions' : function() { 185 | 186 | createCf().flow('

Hello there!

'); 187 | 188 | var page = target.querySelector('.cf-page-1'); 189 | 190 | assert.match(page.clientWidth, 800); 191 | assert.match(page.clientHeight, 600); 192 | }, 193 | 194 | 'ShouldRespectConfigColumnAndClassNames' : function() { 195 | 196 | createCf({ 197 | pageClass : 'mypage', 198 | columnClass : 'mycol', 199 | }).flow('

Hello there!

'); 200 | 201 | var page = target.querySelector('.mypage-1'); 202 | assert.className(page, 'mypage'); 203 | 204 | var column = page.querySelector('.mycol-1'); 205 | assert.className(column, 'mycol'); 206 | }, 207 | 208 | 'ShouldHideOverflowOnGeneratedColumns' : function() { 209 | 210 | createCf().flow('

Hello there!

'); 211 | 212 | var column = target.querySelector('.cf-column-1'); 213 | 214 | assert.match(cssProp(column, 'overflow'), 'hidden'); 215 | }, 216 | 217 | 'ShouldSetTheCorrectWidthAndHeightOnTheGeneratedColumn' : function() { 218 | 219 | createCf().flow('

Hello there!

'); 220 | 221 | var column = target.querySelector('.cf-column-1'); 222 | 223 | assert.match(column.clientWidth, 250); 224 | assert.match(column.clientHeight, 600); 225 | }, 226 | 227 | 'ShouldSetCorrectAbsolutePositioningCss' : function() { 228 | 229 | createCf().flow('

Hello there!

'); 230 | 231 | var page = target.querySelector('.cf-page-1'); 232 | var column = target.querySelector('.cf-column-1'); 233 | 234 | assert.match(cssProp(target, 'position'), 'relative'); 235 | assert.match(cssProp(page, 'position'), 'absolute'); 236 | assert.match(cssProp(column, 'position'), 'absolute'); 237 | }, 238 | 239 | 'ShouldSetTheCorrectPositionOnColumn1' : function() { 240 | 241 | createCf().flow('

Hello there!

'); 242 | 243 | var column = target.querySelector('.cf-column-1'); 244 | 245 | assert.match(column.offsetTop, 0); 246 | assert.match(column.offsetLeft, 0); 247 | }, 248 | 249 | 'ShouldCreateASecondColumnWhenFirstIsFull' : function() { 250 | 251 | createCf().flow(_exactHeightWrap); 252 | 253 | var column1 = target.querySelector('.cf-column-1'); 254 | 255 | var column2 = target.querySelector('.cf-column-2'); 256 | assert(column2 instanceof HTMLElement); 257 | 258 | }, 259 | 260 | 'ShouldSetCorrectDimensionsAndPositionOnSecondColumn' : function() { 261 | 262 | createCf().flow(_exactHeightWrap); 263 | 264 | var column1 = target.querySelector('.cf-column-1'); 265 | var column2 = target.querySelector('.cf-column-2'); 266 | 267 | assert.match(column1.clientWidth, 250); 268 | assert.match(column1.clientHeight, 600); 269 | 270 | assert.match(column2.clientWidth, 250); 271 | assert.match(column2.offsetTop, 0); 272 | assert.match(column2.offsetLeft, 275); 273 | }, 274 | 275 | 'ShouldWriteCorrectElementsToColumns' : function() { 276 | 277 | createCf().flow(_exactHeightWrap); 278 | 279 | var column1 = target.querySelector('.cf-column-1'); 280 | var column2 = target.querySelector('.cf-column-2'); 281 | 282 | assert.match(column1.childNodes.length, 2); 283 | 284 | assert.className(column1.childNodes[0], 'height500'); 285 | assert.className(column1.childNodes[1], 'height100'); 286 | 287 | assert.match(column2.childNodes.length, 1); 288 | 289 | assert.className(column2.childNodes[0], 'height100'); 290 | }, 291 | 292 | 'ShouldFillColumnsAndCreateSecondPage' : function() { 293 | 294 | createCf().flow(_wrapToPage2); 295 | 296 | var page1 = target.querySelector('.cf-page-1'); 297 | var page2 = target.querySelector('.cf-page-2'); 298 | 299 | assert.match(page1.childNodes.length, 3); 300 | 301 | assert(page2 instanceof HTMLElement); 302 | assert.match(page2.childNodes.length, 1); 303 | 304 | var page2col1 = page2.querySelector('.cf-column-1'); 305 | 306 | assert.match(page2col1.childNodes.length, 1); 307 | assert.className(page2col1.childNodes[0], 'height100'); 308 | }, 309 | 310 | 'ShouldDisplayPagesHorizontallyByDefault' : function() { 311 | 312 | createCf().flow(_wrapToPage2); 313 | 314 | var page1 = target.querySelector('.cf-page-1'); 315 | var page2 = target.querySelector('.cf-page-2'); 316 | 317 | assert.match(cssProp(page1, 'left'), '0px'); 318 | assert.match(cssProp(page1, 'top'), '0px'); 319 | 320 | assert.match(cssProp(page2, 'left'), '800px'); 321 | assert.match(cssProp(page2, 'top'), '0px'); 322 | }, 323 | 324 | 'ShouldDisplayPagesVerticallyWhenSpecified' : function() { 325 | 326 | createCf({ 327 | columnGap : 25, 328 | columnCount : 3, 329 | pageArrangement : 'vertical', 330 | }).flow(_wrapToPage2); 331 | 332 | var page1 = target.querySelector('.cf-page-1'); 333 | var page2 = target.querySelector('.cf-page-2'); 334 | 335 | assert.match(cssProp(page1, 'left'), '0px'); 336 | assert.match(cssProp(page1, 'top'), '0px'); 337 | 338 | assert.match(cssProp(page2, 'left'), '0px'); 339 | assert.match(cssProp(page2, 'top'), '600px'); 340 | }, 341 | 342 | 'ShouldAddPagePadding' : function() { 343 | 344 | createCf({ 345 | pagePadding : 50, 346 | columnGap : 25, 347 | columnCount : 5, 348 | }).flow(_wrapToPage2 + _wrapToPage2); 349 | 350 | var page1 = target.querySelector('.cf-page-1'); 351 | var page2 = target.querySelector('.cf-page-2'); 352 | 353 | assert.match(page1.clientWidth, 800); 354 | assert.match(page1.clientHeight, 600); 355 | 356 | assert.match(page2.clientWidth, 800); 357 | assert.match(page2.clientHeight, 600); 358 | 359 | assert.match(cssProp(page1, 'left'), '0px'); 360 | assert.match(cssProp(page1, 'top'), '0px'); 361 | 362 | assert.match(cssProp(page2, 'left'), '800px'); 363 | assert.match(cssProp(page2, 'top'), '0px'); 364 | 365 | assert.match(page1.querySelector('.cf-column-1').offsetLeft, 50); 366 | assert.match(page1.querySelector('.cf-column-1').offsetWidth, 120); 367 | 368 | assert.match(page1.querySelector('.cf-column-2').offsetLeft, 195); 369 | assert.match(page1.querySelector('.cf-column-5').offsetLeft, 630); 370 | 371 | assert.match(page2.querySelector('.cf-column-1').offsetLeft, 50); 372 | }, 373 | 374 | 'ShouldAddVerticalPagePadding' : function() { 375 | 376 | createCf({ 377 | pagePadding : 50, 378 | columnGap : 25, 379 | columnCount : 5, 380 | pageArrangement : 'vertical', 381 | }).flow(_wrapToPage2 + _wrapToPage2); 382 | 383 | var page1 = target.querySelector('.cf-page-1'); 384 | var page2 = target.querySelector('.cf-page-2'); 385 | 386 | assert.match(page1.clientWidth, 800); 387 | assert.match(page1.clientHeight, 600); 388 | 389 | assert.match(page2.clientWidth, 800); 390 | assert.match(page2.clientHeight, 600); 391 | 392 | assert.match(cssProp(page1, 'left'), '0px'); 393 | assert.match(cssProp(page1, 'top'), '0px'); 394 | 395 | assert.match(cssProp(page2, 'left'), '0px'); 396 | assert.match(cssProp(page2, 'top'), '600px'); 397 | 398 | assert.match(page1.querySelector('.cf-column-1').offsetLeft, 0); 399 | assert.match(page1.querySelector('.cf-column-1').offsetTop, 50); 400 | assert.match(page1.querySelector('.cf-column-1').offsetWidth, 140); 401 | assert.match(page1.querySelector('.cf-column-1').offsetHeight, 500); 402 | 403 | assert.match(page2.querySelector('.cf-column-1').offsetTop, 50); 404 | }, 405 | 406 | 'ShouldRepeatAnOverflowedElementOnTheNextColumn' : function() { 407 | 408 | createCf().flow(_overflowedElement); 409 | 410 | var column1 = target.querySelector('.cf-column-1'); 411 | 412 | assert.className(column1.childNodes[column1.childNodes.length - 1], 'height100'); 413 | 414 | var column2 = target.querySelector('.cf-column-2'); 415 | assert(column2 instanceof HTMLElement); 416 | 417 | assert.match(column2.childNodes.length, 1); 418 | assert.className(column2.childNodes[0], 'height100'); 419 | 420 | assert.match(column2.childNodes[0].innerHTML, column1.childNodes[2].innerHTML); 421 | }, 422 | 423 | 'ShouldSetNegativeTopMarginOnRemainderOfOverflowedElement' : function() { 424 | 425 | createCf().flow(_overflowedElement); 426 | 427 | var column2 = target.querySelector('.cf-column-2'); 428 | var element = column2.childNodes[0]; 429 | 430 | assert.match(cssProp(element, 'margin-top'), '-50px'); 431 | }, 432 | 433 | 'ShouldCorrectlyWrapALargeElementOverManyColumns' : function() { 434 | 435 | createCf().flow('
height300
height1000
'); 436 | 437 | var column2 = target.querySelector('.cf-column-2'); 438 | var column3 = target.querySelector('.cf-column-3'); 439 | 440 | assert.match(cssProp(column2.childNodes[0], 'margin-top'), '-300px'); 441 | assert.match(cssProp(column3.childNodes[0], 'margin-top'), '-900px'); 442 | }, 443 | 444 | 'ShouldCorrectlyWrapAHugeElementOverManyColumns' : function() { 445 | 446 | createCf().flow('
height3000
'); 447 | 448 | var page2 = target.querySelector('.cf-page-2'); 449 | var p2col2 = page2.querySelector('.cf-column-2'); 450 | 451 | assert.match(cssProp(p2col2.childNodes[0], 'margin-top'), '-2400px'); 452 | assert.isNull(page2.querySelector('.cf-column-3')); 453 | }, 454 | 455 | 'ShouldWrapPlainTextInParagraphTags' : function() { 456 | 457 | createCf().flow('plain text'); 458 | 459 | var column = target.querySelector('.cf-column-1'); 460 | 461 | assert.match(column.innerHTML, /^plain text<\/p>$/); 462 | }, 463 | 464 | 'ShouldIgnoreEmptyTextNodes' : function() { 465 | 466 | createCf().flow('\n

parag 1

\n

parag 2

\n'); 467 | 468 | var column = target.querySelector('.cf-column-1'); 469 | 470 | assert.match(column.innerHTML, /^parag 1<\/p>parag 2<\/p>$/); 471 | }, 472 | 473 | 'ShouldNotCarryParagraphBottomMarginsOverToNextColumn' : function() { 474 | 475 | createCf().flow('
simulated-parags
simulated-parags
simulated-parags
'); 476 | 477 | var column1 = target.querySelector('.cf-column-1'); 478 | var column2 = target.querySelector('.cf-column-2'); 479 | 480 | assert.match(column1.childNodes.length, 2); 481 | assert.match(column2.childNodes.length, 1); 482 | 483 | var element = column2.childNodes[0]; 484 | assert.match(element.style.marginTop, '0px'); 485 | }, 486 | 487 | 'ShouldNotWrapAnElementWithNowrapClass' : function() { 488 | 489 | createCf().flow('
height500
height200 nowrap
'); 490 | 491 | var column1 = target.querySelector('.cf-column-1'); 492 | var column2 = target.querySelector('.cf-column-2'); 493 | 494 | assert.match(column1.childNodes.length, 1); 495 | assert.match(column2.childNodes.length, 1); 496 | 497 | var element = column2.childNodes[0]; 498 | 499 | assert.className(element, 'nowrap'); 500 | assert.match(element.style.marginTop, '0px'); 501 | }, 502 | 503 | 'ShouldNotMoveAnElementWithNowrapClassWhichFitsInAColumn' : function() { 504 | 505 | createCf().flow('
height500
height100 nowrap
'); 506 | 507 | var column1 = target.querySelector('.cf-column-1'); 508 | var column2 = target.querySelector('.cf-column-2'); 509 | 510 | assert.match(column1.childNodes.length, 2); 511 | assert.isNull(column2); 512 | }, 513 | 514 | 'ShouldCorrectlyPositionSuccessiveNowrapElements' : function() { 515 | 516 | createCf().flow('
height500
height500 nowrap
height500 nowrap
'); 517 | 518 | var column1 = target.querySelector('.cf-column-1'); 519 | var column2 = target.querySelector('.cf-column-2'); 520 | var column3 = target.querySelector('.cf-column-3'); 521 | 522 | assert.match(column1.childNodes.length, 1); 523 | assert.match(column2.childNodes.length, 1); 524 | assert.match(column3.childNodes.length, 1); 525 | 526 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 527 | assert.match(column3.childNodes[0].style.marginTop, '0px'); 528 | }, 529 | 530 | 'ShouldCropATallNowrapElement' : function() { 531 | 532 | createCf().flow('
height1000 nowrap
'); 533 | 534 | var column1 = target.querySelector('.cf-column-1'); 535 | var column2 = target.querySelector('.cf-column-2'); 536 | 537 | assert.match(column1.childNodes.length, 1); 538 | assert.isNull(column2); 539 | }, 540 | 541 | 'ShouldMoveThenCropATallNowrapElement' : function() { 542 | 543 | createCf().flow('
height500
height1000 nowrap
'); 544 | 545 | var column1 = target.querySelector('.cf-column-1'); 546 | var column2 = target.querySelector('.cf-column-2'); 547 | var column3 = target.querySelector('.cf-column-3'); 548 | 549 | assert.match(column1.childNodes.length, 1); 550 | assert.match(column2.childNodes.length, 1); 551 | assert.isNull(column3); 552 | 553 | var element = column2.childNodes[0]; 554 | 555 | assert.className(element, 'nowrap'); 556 | assert.match(element.style.marginTop, '0px'); 557 | }, 558 | 559 | 'ShouldNotWrapAnElementWhichMatchesNoWrapOnTags' : function() { 560 | 561 | createCf({ 562 | columnGap : 25, 563 | columnCount : 3, 564 | noWrapOnTags : ['section'], 565 | }).flow('
height500
height200 nowrap
'); 566 | 567 | var column1 = target.querySelector('.cf-column-1'); 568 | var column2 = target.querySelector('.cf-column-2'); 569 | 570 | assert.match(column1.childNodes.length, 1); 571 | assert.match(column2.childNodes.length, 1); 572 | 573 | var element = column2.childNodes[0]; 574 | 575 | assert.tagName(element, 'section'); 576 | assert.match(element.style.marginTop, '0px'); 577 | }, 578 | 579 | 'ShouldIgnoreCaseOfNoWrapOnTags' : function() { 580 | 581 | createCf({ 582 | columnGap : 25, 583 | columnCount : 3, 584 | noWrapOnTags : ['SECTION'], 585 | }).flow('
height500
height200 nowrap
'); 586 | 587 | var column1 = target.querySelector('.cf-column-1'); 588 | var column2 = target.querySelector('.cf-column-2'); 589 | 590 | assert.match(column1.childNodes.length, 1); 591 | assert.match(column2.childNodes.length, 1); 592 | 593 | var element = column2.childNodes[0]; 594 | 595 | assert.tagName(element, 'section'); 596 | assert.match(element.style.marginTop, '0px'); 597 | }, 598 | 599 | 'ShouldNotWrapAnElementWithKeepwithnextClass' : function() { 600 | 601 | createCf().flow('
height500
height200 keepwithnext
height100
'); 602 | 603 | var column1 = target.querySelector('.cf-column-1'); 604 | var column2 = target.querySelector('.cf-column-2'); 605 | 606 | assert.match(column1.childNodes.length, 1); 607 | assert.match(column2.childNodes.length, 2); 608 | 609 | var element = column2.childNodes[0]; 610 | 611 | assert.className(element, 'keepwithnext'); 612 | assert.match(element.style.marginTop, '0px'); 613 | }, 614 | 615 | 'ShouldWrapAKeepwithnextElementWhenItsTheFinalElement' : function() { 616 | 617 | createCf().flow('
height500
height200 keepwithnext
'); 618 | 619 | var column1 = target.querySelector('.cf-column-1'); 620 | var column2 = target.querySelector('.cf-column-2'); 621 | 622 | assert.match(column1.childNodes.length, 2); 623 | assert.match(column2.childNodes.length, 1); 624 | 625 | var element = column2.childNodes[0]; 626 | 627 | assert.className(element, 'keepwithnext'); 628 | assert.match(element.style.marginTop, '-100px'); 629 | }, 630 | 631 | 'ShouldMoveThenCropATallKeepwithnextElement' : function() { 632 | 633 | createCf().flow('
height500
height1000 keepwithnext
height100
'); 634 | 635 | var column1 = target.querySelector('.cf-column-1'); 636 | var column2 = target.querySelector('.cf-column-2'); 637 | var column3 = target.querySelector('.cf-column-3'); 638 | 639 | assert.match(column1.childNodes.length, 1); 640 | assert.match(column2.childNodes.length, 1); 641 | assert.match(column3.childNodes.length, 1); 642 | 643 | assert.className(column2.childNodes[0], 'keepwithnext'); 644 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 645 | 646 | assert.className(column3.childNodes[0], 'height100'); 647 | assert.match(column3.childNodes[0].style.marginTop, '0px'); 648 | }, 649 | 650 | 'ShouldCropATallKeepwithnextElement' : function() { 651 | 652 | createCf().flow('
height1000 keepwithnext
height100
'); 653 | 654 | var column1 = target.querySelector('.cf-column-1'); 655 | var column2 = target.querySelector('.cf-column-2'); 656 | 657 | assert.match(column1.childNodes.length, 1); 658 | assert.match(column2.childNodes.length, 1); 659 | 660 | assert.className(column2.childNodes[0], 'height100'); 661 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 662 | }, 663 | 664 | 'ShouldMoveAKeepwithnextElementToJoinFollowingPlainTextElement' : function() { 665 | 666 | createCf().flow('
height500
height100 keepwithnext
height100
'); 667 | 668 | var column1 = target.querySelector('.cf-column-1'); 669 | var column2 = target.querySelector('.cf-column-2'); 670 | 671 | assert.match(column1.childNodes.length, 1); 672 | assert.match(column2.childNodes.length, 2); 673 | 674 | assert.className(column2.childNodes[0], 'keepwithnext'); 675 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 676 | }, 677 | 678 | 'ShouldIgnoreKeepwithnextClassOnFinalElement' : function() { 679 | 680 | createCf().flow('
height400
height100 keepwithnext
'); 681 | 682 | var column1 = target.querySelector('.cf-column-1'); 683 | 684 | assert.match(column1.childNodes.length, 2); 685 | assert.isNull(target.querySelector('.cf-column-2')); 686 | }, 687 | 688 | 'ShouldCorrectlyHandleAnElementWithBothKeepwithnextAndNowrapClasses' : function() { 689 | 690 | createCf().flow('
height500
height200 keepwithnext
height100
'); 691 | 692 | var column1 = target.querySelector('.cf-column-1'); 693 | var column2 = target.querySelector('.cf-column-2'); 694 | 695 | assert.match(column1.childNodes.length, 1); 696 | assert.match(column2.childNodes.length, 2); 697 | 698 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 699 | }, 700 | 701 | 'NowrapShouldNotAffectFollowingColumns' : function() { 702 | 703 | createCf().flow('
height500
height200 nowrap
height500
'); 704 | 705 | var column1 = target.querySelector('.cf-column-1'); 706 | var column2 = target.querySelector('.cf-column-2'); 707 | var column3 = target.querySelector('.cf-column-3'); 708 | 709 | assert.match(column1.childNodes.length, 1); 710 | assert.match(column2.childNodes.length, 2); 711 | assert.match(column3.childNodes.length, 1); 712 | 713 | assert.match(column3.childNodes[0].style.marginTop, '-400px'); 714 | }, 715 | 716 | 'KeepwithnextShouldNotAffectFollowingColumns' : function() { 717 | 718 | createCf().flow('
height500
height100 keepwithnext
height500
height500
'); 719 | 720 | var column1 = target.querySelector('.cf-column-1'); 721 | var column2 = target.querySelector('.cf-column-2'); 722 | var column3 = target.querySelector('.cf-column-3'); 723 | 724 | assert.match(column1.childNodes.length, 1); 725 | assert.match(column2.childNodes.length, 2); 726 | assert.match(column3.childNodes.length, 1); 727 | 728 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 729 | assert.match(column3.childNodes[0].style.marginTop, '0px'); 730 | }, 731 | 732 | 'ShouldCorrectlyPositionANowrapFollowingAKeepwithnext' : function() { 733 | 734 | createCf().flow('
height400
height100 keepwithnext
height200 nowrap
'); 735 | 736 | var column1 = target.querySelector('.cf-column-1'); 737 | var column2 = target.querySelector('.cf-column-2'); 738 | 739 | assert.match(column1.childNodes.length, 1); 740 | assert.match(column2.childNodes.length, 2); 741 | assert.isNull(target.querySelector('.cf-column-3')); 742 | 743 | assert.className(column2.childNodes[0], 'keepwithnext'); 744 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 745 | }, 746 | 747 | 'ShouldObeyKeepwithnextWhenNextElementHasNowrapClassAndOverflows' : function() { 748 | 749 | createCf().flow('
height400
height100 keepwithnext
height200 nowrap
'); 750 | 751 | var column1 = target.querySelector('.cf-column-1'); 752 | var column2 = target.querySelector('.cf-column-2'); 753 | 754 | assert.match(column1.childNodes.length, 1); 755 | assert.match(column2.childNodes.length, 2); 756 | 757 | var element = column2.childNodes[0]; 758 | 759 | assert.className(element, 'keepwithnext'); 760 | assert.match(element.style.marginTop, '0px'); 761 | }, 762 | 763 | 'Regression1ItShouldUpdateColumnHeightAfterAnElementIsCropped' : function() { 764 | 765 | createCf().flow('
700 keepwithnext
300
300
'); 766 | 767 | var column1 = target.querySelector('.cf-column-1'); 768 | var column2 = target.querySelector('.cf-column-2'); 769 | 770 | assert.match(column1.childNodes.length, 1); 771 | assert.match(column2.childNodes.length, 2); 772 | assert.isNull(target.querySelector('.cf-column-3')); 773 | }, 774 | 775 | 'Regression2MissingLastElement' : function() { 776 | 777 | createCf().flow('
700
200 keepwithnext
100
'); 778 | 779 | var column1 = target.querySelector('.cf-column-1'); 780 | var column2 = target.querySelector('.cf-column-2'); 781 | 782 | assert.match(column1.childNodes.length, 1); 783 | assert.match(column2.childNodes.length, 3); 784 | assert.isNull(target.querySelector('.cf-column-3')); 785 | 786 | assert.className(column2.childNodes[0], 'height700'); 787 | assert.match(column2.childNodes[0].style.marginTop, '-600px'); 788 | }, 789 | 790 | 'Regression5LargeSpaceAdded' : function() { 791 | 792 | createCf().flow('
700
200 nowrap keepwithnext
500
'); 793 | 794 | var column1 = target.querySelector('.cf-column-1'); 795 | var column2 = target.querySelector('.cf-column-2'); 796 | var column3 = target.querySelector('.cf-column-3'); 797 | 798 | assert.match(column1.childNodes.length, 1); 799 | assert.match(column2.childNodes.length, 3); 800 | assert.match(column3.childNodes.length, 1); 801 | 802 | assert.className(column3.childNodes[0], 'height500'); 803 | assert.match(column3.childNodes[0].style.marginTop, '-300px'); 804 | }, 805 | 806 | 'Regression7ItShouldNotGetIntoAnEndlessLoop' : function() { 807 | 808 | // With the loop-protection removed, this test should cause an endless loop if the bug is not fixed. 809 | 810 | createCf().flow('
1: 600 keepwithnext
2: 700
'); 811 | assert(true); 812 | 813 | }, 814 | 815 | 'Regression10TheLastElementShouldNotBeCropped' : function() { 816 | 817 | createCf().flow('
5: 500 keepwithnext
7: 200 keepwithnext
'); 818 | 819 | 820 | var column1 = target.querySelector('.cf-column-1'); 821 | var column2 = target.querySelector('.cf-column-2'); 822 | 823 | assert.match(column1.childNodes.length, 2); 824 | assert.match(column2.childNodes.length, 1); 825 | 826 | assert.match(column2.childNodes[0].style.marginTop, '-100px'); 827 | }, 828 | 829 | 'Regression10SecondElementShouldBeInColumn2' : function() { 830 | 831 | createCf().flow('
2: 300 keepwithnext
3: 400 keepwithnext
4: 200
'); 832 | 833 | 834 | var column1 = target.querySelector('.cf-column-1'); 835 | var column2 = target.querySelector('.cf-column-2'); 836 | 837 | assert.match(column1.childNodes.length, 1); 838 | assert.match(column2.childNodes.length, 2); 839 | 840 | assert.className(column2.childNodes[0], 'height400'); 841 | assert.className(column2.childNodes[0], 'keepwithnext'); 842 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 843 | }, 844 | 845 | 'RegressionItShouldCorrectlyPlaceSuccessiveFullheightElements' : function() { 846 | 847 | createCf().flow('

height600

height600

'); 848 | 849 | var column1 = target.querySelector('.cf-column-1'); 850 | var column2 = target.querySelector('.cf-column-2'); 851 | 852 | assert.match(column1.childNodes.length, 1); 853 | assert.match(column2.childNodes.length, 1); 854 | 855 | assert.isNull(target.querySelector('.cf-column-3')); 856 | assert.match(column2.childNodes[0].style.marginTop, '0px'); 857 | }, 858 | 859 | 'ShouldCorrectlyReportSinglePageCount' : function() { 860 | 861 | createCf({ 862 | columnCount : 1 863 | }).flow('
height300
'); 864 | 865 | assert.match(cf.pageCount, 1); 866 | }, 867 | 868 | 'ShouldCorrectlyReportLargerPageCount' : function() { 869 | 870 | createCf({ 871 | columnCount : 1 872 | }).flow('
height3000
'); 873 | 874 | assert.match(cf.pageCount, 5); 875 | }, 876 | 877 | 'ShouldAddExplicitWidthAndHeightToTarget' : function() { 878 | 879 | createCf().flow('
height300
'); 880 | 881 | assert.match(target.style.width, '800px'); 882 | assert.match(cssProp(target, 'width'), '800px'); 883 | assert.match(cssProp(target, 'height'), '600px'); 884 | }, 885 | 886 | 'ShouldCorrectlyReportLayoutDimensions' : function() { 887 | 888 | createCf({ 889 | columnGap : 20, 890 | columnCount : 4, 891 | pagePadding : 50 892 | }).flow('
height300
'); 893 | 894 | var dimesions = cf.layoutDimensions; 895 | 896 | assert.match(dimesions.pageInnerWidth, 700); 897 | assert.match(dimesions.pageInnerHeight, 600); 898 | assert.match(dimesions.colDefaultTop, 0); 899 | assert.match(dimesions.colDefaultLeft, 50); 900 | assert.match(dimesions.columnCount, 4); 901 | assert.match(dimesions.columnWidth, 160); 902 | assert.match(dimesions.columnGap, 20); 903 | }, 904 | 905 | 'ShouldRemovePreloadAreasWhenAllowReflowIsFalse' : function() { 906 | 907 | createCf({ 908 | allowReflow: false 909 | }).flow('
height300
', '
fixedContent
'); 910 | 911 | refute(target.querySelector('.cf-preload')); 912 | refute(target.querySelector('.cf-preload-fixed')); 913 | }, 914 | 915 | '//ShouldSetExplicitHeightOnImagesWithSpecifiedAspectRatio' : function() { 916 | 917 | createCf().flow('
height300
height300
'); 918 | 919 | var page = target.querySelector('.cf-page-1'); 920 | var column1 = page.querySelector('.cf-column-1'); 921 | var column2 = page.querySelector('.cf-column-2'); 922 | var img = column1.querySelector('img'); 923 | var div2 = column2.querySelector('div'); 924 | 925 | assert.match(img.style.width, "200px"); 926 | assert.match(img.style.height, "100px"); 927 | assert.match(parseInt(cssProp(img, 'width')), 200); 928 | assert.match(parseInt(cssProp(img, 'height')), 100); 929 | 930 | assert.match(div2.style.marginTop, '-200px'); 931 | }, 932 | 933 | '//ShouldObeyAllHistoricalKeepwithnextElementsWhichFitInNextColumn' : function() { 934 | 935 | createCf().flow('
1 height100 keepwithnext
2 height100 plaintext
3 height100 keepwithnext
4 height100 keepwithnext
5 height100 keepwithnext
6 height100 keepwithnext
7 height100 plaintext
'); 936 | }, 937 | 938 | '//ShouldMoveLastInAStringOfKeepwithnextElementIntoNextColumn' : function() {}, 939 | 940 | '//ShouldSplitALongStreamOfKeepwithnextElementsWhenAColumnIsFull' : function() {}, 941 | 942 | '//ShouldObeyAllHistoricalKeepwithnextElementsUntilStartOfColumn' : function() {}, 943 | 944 | 945 | //*/ 946 | 947 | 948 | 949 | // Regression 4 - The two 100 keepwithnext should be in col 1. 950 | //
100
100
200 nowrap
100 nowrap keepwithnext
100 nowrap keepwithnext
500 keepwithnext
200
300
951 | 952 | // Consider some 'typographical measure' logic - reduce the number of columns per page if the measure is too small (because the font size is too large) 953 | // We should remove any padding on the viewport, and any padding or margin on the articles. 954 | // Run Arrhythmia("body").validateRhythm(); (https://github.com/mattbaker/Arrhythmia) to check vertical rhythm after I've added padding functionality. 955 | // Add 'near-parag-14' or similar class to images, and attempt to get them onto the same page. If an inline image overflows the bottom of a column, there are two options: 1) leave whitespace and place image at top of next col. 2) start the paragraph of text, and place image at top of next col, continuing parag afterwards. 956 | // Allow column-spans for inline images. This is a future feature request. 957 | // Consider a breakafter class, or similar, to always break after this element. 958 | // Shouldn't most tags (img, headings, etc.) be nowrap by default? Perhaps have a default list of tags which are NOT nowrap, and allow it to be overridden. 959 | 960 | }); -------------------------------------------------------------------------------- /src/FTColumnflow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve FTColumnflow is a polyfill that fixes the inadequacies of CSS column layouts. 3 | * 4 | * It is developed by FT Labs (http://labs.ft.com), part of the Financial Times. 5 | * It is extensively used in the FT Web App (http://app.ft.com), where it allows us to 6 | * publish articles with complex newspaper/magazine style layouts, including features such as: 7 | * - Configurable column widths, gutters and margins 8 | * - Fixed-position elements 9 | * - Elements spanning columns 10 | * - Keep-with-next to avoid headings at the bottom of a column 11 | * - No-wrap class to avoid splitting elements across columns 12 | * - Grouping of columns into pages 13 | * - Horizontal or vertical alignment of pages 14 | * - Standardised line height to align text baseline to a grid 15 | * - Rapid reflow as required by events such as device orientation or font-size change 16 | * It is designed with the same column dimension specification API as the CSS3 multi-column specification 17 | * (http://www.w3.org/TR/css3-multicol/), but gives far greater flexibility over element positioning within those columns. 18 | * 19 | * @copyright The Financial Times Limited [All Rights Reserved] 20 | * @license MIT License (see LICENCE.txt) 21 | * @codingstandard ftlabs-jslint 22 | */ 23 | 24 | /*jslint browser:true, es5: true*/ 25 | /*global Node*/ 26 | 27 | 28 | // UMD from https://gist.github.com/wilsonpage/8598603 29 | ;(function(define){define(function(require,exports,module){ 30 | 31 | "use strict"; 32 | 33 | 34 | // Set up FTColumnflowException class 35 | function FTColumnflowException(name, message) { 36 | this.name = 'FTColumnflow' + name || 'FTColumnflowException'; 37 | this.message = message || ''; 38 | } 39 | FTColumnflowException.prototype = new Error(); 40 | FTColumnflowException.constructor = FTColumnflowException; 41 | 42 | 43 | 44 | /* Scope vars */ 45 | 46 | var 47 | 48 | layoutDimensionList = [ 49 | 'pageInnerWidth', 50 | 'pageInnerHeight', 51 | 'colDefaultTop', 52 | 'colDefaultLeft', 53 | 'columnCount', 54 | 'columnWidth', 55 | 'columnGap' 56 | ], 57 | 58 | defaultConfig = { 59 | layoutDimensions: null, 60 | columnFragmentMinHeight: 0, 61 | viewportWidth: null, 62 | viewportHeight: null, 63 | columnWidth: 'auto', 64 | columnCount: 'auto', 65 | columnGap: 'normal', 66 | pageClass: 'cf-page', 67 | columnClass: 'cf-column', 68 | pageArrangement: 'horizontal', 69 | pagePadding: 0, 70 | debug: false, 71 | showGrid: false, 72 | standardiseLineHeight: false, 73 | minFixedPadding: 1, 74 | lineHeight: null, 75 | noWrapOnTags: [], 76 | allowReflow: true 77 | }, 78 | 79 | // CSS Style declarations 80 | cssStyles = '#[targetId] { position: relative; height: 100%; }\n' + 81 | '#[targetId] .[preloadAreaClassName].[pageClass] { visibility: hidden; position: absolute; overflow: hidden; }\n' + 82 | '#[targetId] .[preloadFixedAreaClassName] { visibility: hidden; position: absolute; }\n' + 83 | '#[targetId] .[pageClass] { position: absolute; width: [viewportWidth]px; height: [viewportHeight]px; [pageArrangement] }\n' + 84 | '#[targetId] .[columnClass] { position: absolute; width: [columnWidth]px; overflow: hidden; }\n' + 85 | '#[targetId] .[pageClass] .[fixedElementClassName] { position: absolute; }\n' + 86 | '#[targetId] .[pageClass] .[columnClass] > :first-child { margin-top: 0px; }\n', 87 | 88 | cssColumnStyles = '#[targetId] .[columnClass].[columnClass]-[columnNum] { left: [leftPos]px; }\n', 89 | 90 | showGridStyles = '#[targetId] .[pageClass] { background-image: -webkit-linear-gradient(skyblue 1px, transparent 1px); background-size: 100% [lh]px; background-origin: content-box; }', 91 | 92 | 93 | // Implement outerHTML in browsers which don't support it 94 | // Adapted from Modernizr's outerHTML polyfill: bit.ly/polyfills 95 | _outerHTML = (function() { 96 | var outerHTMLContainer; 97 | if (typeof document !== "undefined" && !document.documentElement.hasOwnProperty('outerHTML')) { 98 | outerHTMLContainer = document.createElementNS("http://www.w3.org/1999/xhtml", "_"); 99 | return function _outerHTMLNode(node) { 100 | var html; 101 | outerHTMLContainer.appendChild(node.cloneNode(false)); 102 | html = outerHTMLContainer.innerHTML.replace("><", ">" + node.innerHTML + "<"); 103 | outerHTMLContainer.innerHTML = ""; 104 | return html; 105 | }; 106 | } else { 107 | return function _outerHTMLNative(node) { 108 | return node.outerHTML; 109 | }; 110 | } 111 | }()); 112 | 113 | 114 | 115 | function FTColumnflow(target, viewport, userConfig) { 116 | 117 | var 118 | 119 | // Instance configuration 120 | config = {}, 121 | 122 | // Debugging 123 | showGrid, 124 | 125 | // String or DOM node 126 | flowedContent, 127 | fixedContent, 128 | 129 | // Dimensions 130 | colDefaultBottom, 131 | colMiddle, 132 | minFixedPadding, 133 | fixedPadding, 134 | 135 | // DOM elements 136 | renderArea, 137 | preloadColumn, 138 | fixedPreloadArea, 139 | headElement, 140 | headerStyles, 141 | 142 | // Class names 143 | targetIdPrefix = 'cf-target-', 144 | renderAreaClassName = 'cf-render-area', 145 | preloadAreaClassName = 'cf-preload', 146 | preloadFixedAreaClassName = 'cf-preload-fixed', 147 | fixedElementClassName = 'cf-fixed', 148 | nowrapClassName = 'nowrap', 149 | keepwithnextClassName = 'keepwithnext', 150 | 151 | // HTML fragments 152 | lineHeightTestContents, 153 | 154 | // Collections 155 | pagedContent = [], 156 | pagedEndContent = [], 157 | 158 | // Counters 159 | borderElementIndex, 160 | indexedPageNum, 161 | indexedColumnNum, 162 | indexedColumnFrag, 163 | topElementOverflow, 164 | totalColumnHeight, 165 | 166 | // Object references 167 | workingPage, 168 | workingColumn, 169 | workingColumnFrag, 170 | 171 | // Copy of parent scope 172 | that = this; 173 | 174 | 175 | /* Constructor */ 176 | 177 | this.target = target; 178 | this.viewport = viewport; 179 | 180 | this._checkInstanceArgs(); 181 | 182 | _setConfig(userConfig); 183 | 184 | _setLayoutDimensions(); 185 | 186 | 187 | 188 | function _setConfig(userConfig) { 189 | var i; 190 | 191 | // Reference local config as public property 192 | that.config = config = {}; 193 | 194 | // Copy defaultConfig settings into config 195 | for (i in defaultConfig) { 196 | if (defaultConfig.hasOwnProperty(i)) { 197 | config[i] = defaultConfig[i]; 198 | } 199 | } 200 | 201 | // Merge userConfig settings into config 202 | for (i in userConfig) { 203 | if (userConfig.hasOwnProperty(i)) { 204 | if (config[i] === undefined) { 205 | throw new FTColumnflowException('ParameterException', 'Unknown config parameter [' + i + '].'); 206 | } 207 | config[i] = userConfig[i]; 208 | } 209 | } 210 | 211 | // Enable showGrid? 212 | if (config.showGrid !== undefined) showGrid = !!config.showGrid; 213 | 214 | // Check page params 215 | if ('horizontal' !== config.pageArrangement && 'vertical' !== config.pageArrangement) { 216 | throw new FTColumnflowException('ArrangementException', config.pageArrangement + ' is not a valid Page Arrangement value.'); 217 | } 218 | 219 | config.pagePadding = parseInt(config.pagePadding, 10); 220 | if (isNaN(config.pagePadding)) { 221 | throw new FTColumnflowException('PaddingException', config.pagePadding + ' is not a valid Page Padding value.'); 222 | } 223 | 224 | // Check column dimension specifications are valid 225 | ['columnWidth', 'columnCount', 'columnGap', 'columnFragmentMinHeight'].forEach(function _checkColumnSpecification(type) { 226 | 227 | if (defaultConfig[type] === config[type]) return true; 228 | config[type] = parseInt(config[type], 10); 229 | 230 | if (isNaN(config[type]) || config[type] < 0) { 231 | throw new FTColumnflowException('ColumnDimensionException', type + ' must be an positive integer or "' + defaultConfig[type] + '".'); 232 | } 233 | }); 234 | 235 | // Check standardiseLineHeight 236 | if (typeof config.standardiseLineHeight !== 'boolean') { 237 | throw new FTColumnflowException('StandardiseLineheightException', 'standardiseLineHeight must be a boolean value.'); 238 | } 239 | 240 | // Check minFixedPadding 241 | config.minFixedPadding = parseFloat(config.minFixedPadding); 242 | if (isNaN(config.minFixedPadding)) { 243 | throw new FTColumnflowException('MinFixedPaddingException', 'minFixedPadding must be a float or integer.'); 244 | } 245 | 246 | // Check viewportWidth, if specified 247 | if ((config.viewportWidth !== null) && isNaN(config.viewportWidth = parseInt(config.viewportWidth, 10))) { 248 | throw new FTColumnflowException('ViewportWidthException', 'viewportWidth must be an integer.'); 249 | } 250 | 251 | // Check viewportHeight, if specified 252 | if ((config.viewportHeight !== null) && isNaN(config.viewportHeight = parseInt(config.viewportHeight, 10))) { 253 | throw new FTColumnflowException('ViewportHeightException', 'viewportHeight must be an integer.'); 254 | } 255 | 256 | // Check lineHeight, if specified 257 | if ((config.lineHeight !== null) && isNaN(config.lineHeight = parseInt(config.lineHeight, 10))) { 258 | throw new FTColumnflowException('LineheightException', 'lineHeight must be an integer.'); 259 | } 260 | 261 | // Check class names are valid 262 | config.pageClass = _normaliseClassName('pageClass', config.pageClass); 263 | config.columnClass = _normaliseClassName('columnClass', config.columnClass); 264 | 265 | // Check noWrapOnTags 266 | if (!Array.isArray(config.noWrapOnTags)) { 267 | throw new FTColumnflowException('NoWrapException', 'noWrapOnTags must be an Array.'); 268 | } 269 | 270 | // Ensure tags are lowercase 271 | config.noWrapOnTags = config.noWrapOnTags.map(function _lowercase(item) { 272 | return item.toLowerCase(); 273 | }); 274 | } 275 | 276 | 277 | function _setLayoutDimensions() { 278 | 279 | var i, l, derivedColumnCount, computedStyle; 280 | 281 | // If the layoutDimensions parameter was passed in 282 | if (config.layoutDimensions !== null) { 283 | 284 | // Check each one 285 | for (i = 0, l = layoutDimensionList.length; i < l; i++) { 286 | if (isNaN(Number(config.layoutDimensions[layoutDimensionList[i]]))) { 287 | throw new FTColumnflowException('DimensionCacheException', 'Must specify an integer value for ' + layoutDimensionList[i]); 288 | } 289 | } 290 | 291 | // Done 292 | return; 293 | } 294 | 295 | config.layoutDimensions = {}; 296 | 297 | // Determine viewport dimensions if they have not been specified 298 | if (!config.viewportWidth) { 299 | computedStyle = window.getComputedStyle(that.viewport); 300 | config.viewportWidth = parseInt(computedStyle.getPropertyValue('width'), 10); 301 | } 302 | 303 | if (!config.viewportHeight) { 304 | if (!computedStyle) computedStyle = window.getComputedStyle(that.viewport); 305 | config.viewportHeight = parseInt(computedStyle.getPropertyValue('height'), 10); 306 | } 307 | 308 | if (!config.viewportWidth || !config.viewportHeight) { 309 | throw new FTColumnflowException('ViewportException', 'Viewport element must have width and height.'); 310 | } 311 | 312 | // Determine column gap - 'normal' defaults to 1em 313 | if ('normal' === config.columnGap) { 314 | if (!computedStyle) computedStyle = window.getComputedStyle(that.viewport); 315 | config.layoutDimensions.columnGap = parseInt(computedStyle.fontSize, 10); 316 | } else { 317 | config.layoutDimensions.columnGap = config.columnGap; 318 | } 319 | 320 | // Determine page dimensions 321 | if ('horizontal' === config.pageArrangement) { 322 | config.layoutDimensions.pageInnerWidth = config.viewportWidth - (2 * config.pagePadding); 323 | config.layoutDimensions.pageInnerHeight = config.viewportHeight; 324 | config.layoutDimensions.colDefaultTop = 0; 325 | config.layoutDimensions.colDefaultLeft = config.pagePadding; 326 | } else { 327 | config.layoutDimensions.pageInnerWidth = config.viewportWidth; 328 | config.layoutDimensions.pageInnerHeight = config.viewportHeight - (2 * config.pagePadding); 329 | config.layoutDimensions.colDefaultTop = config.pagePadding; 330 | config.layoutDimensions.colDefaultLeft = 0; 331 | } 332 | 333 | // Determine columns per page and column dimensions. 334 | // For logic, see pseudo-code at http://www.w3.org/TR/css3-multicol/#pseudo-algorithm 335 | if ('auto' === config.columnWidth && 'auto' === config.columnCount) { 336 | 337 | // Auto - default to 1 column 338 | config.layoutDimensions.columnCount = 1; 339 | config.layoutDimensions.columnWidth = config.layoutDimensions.pageInnerWidth; 340 | } else if ('auto' === config.columnWidth && 'auto' !== config.columnCount) { 341 | 342 | // Determine column width from specified column count and page width 343 | config.layoutDimensions.columnCount = config.columnCount; 344 | config.layoutDimensions.columnWidth = (config.layoutDimensions.pageInnerWidth - ((config.columnCount - 1) * config.layoutDimensions.columnGap)) / config.columnCount; 345 | 346 | // Column width is specified. 347 | } else { 348 | 349 | // Derive the optimal column count 350 | // COMPLEX:GC:20120312: Add 1px to the page width to avoid precision errors in the case that the config values 351 | // result in a column count very near to a whole number 352 | derivedColumnCount = Math.max(1, Math.floor((config.layoutDimensions.pageInnerWidth + 1 + config.layoutDimensions.columnGap) / (config.columnWidth + config.layoutDimensions.columnGap))); 353 | 354 | if ('auto' === config.columnCount) { 355 | 356 | // Use the derived count 357 | config.layoutDimensions.columnCount = derivedColumnCount; 358 | config.layoutDimensions.columnWidth = ((config.layoutDimensions.pageInnerWidth + config.layoutDimensions.columnGap) / config.layoutDimensions.columnCount) - config.layoutDimensions.columnGap; 359 | } else { 360 | 361 | // Count is specified, but we may be able to fit more 362 | config.layoutDimensions.columnCount = Math.min(config.columnCount, derivedColumnCount); 363 | config.layoutDimensions.columnWidth = ((config.layoutDimensions.pageInnerWidth + config.layoutDimensions.columnGap) / config.layoutDimensions.columnCount) - config.layoutDimensions.columnGap; 364 | } 365 | } 366 | 367 | config.layoutDimensions.columnHeight = config.lineHeight ? _roundDownToGrid(config.layoutDimensions.pageInnerHeight) : config.layoutDimensions.pageInnerHeight; 368 | } 369 | 370 | 371 | /* Add the necessary CSS to */ 372 | 373 | function _writeTargetStyles() { 374 | 375 | var styleContents, columnNum; 376 | 377 | if (!headElement) headElement = document.querySelector('head'); 378 | 379 | // Set a random ID on the target 380 | if (!that.target.id) that.target.id = targetIdPrefix + Math.floor(Math.random() * (9000000000) + 1000000000); 381 | 382 | // Main styles 383 | styleContents = _replaceStringTokens(cssStyles, { 384 | targetId: that.target.id, 385 | preloadAreaClassName: preloadAreaClassName, 386 | preloadFixedAreaClassName: preloadFixedAreaClassName, 387 | fixedElementClassName: fixedElementClassName, 388 | pageClass: config.pageClass, 389 | columnClass: config.columnClass, 390 | columnWidth: config.layoutDimensions.columnWidth, 391 | viewportWidth: config.viewportWidth, 392 | viewportHeight: config.viewportHeight, 393 | pageArrangement: ('horizontal' === config.pageArrangement) ? 'top: 0;' : 'left: 0;' 394 | }); 395 | 396 | // Column-specific styles 397 | for (columnNum = 1; columnNum <= config.layoutDimensions.columnCount; columnNum++) { 398 | 399 | styleContents += _replaceStringTokens(cssColumnStyles, { 400 | targetId: that.target.id, 401 | columnClass: config.columnClass, 402 | columnNum: columnNum, 403 | leftPos: config.layoutDimensions.colDefaultLeft + (config.layoutDimensions.columnWidth * (columnNum - 1)) + (config.layoutDimensions.columnGap * (columnNum - 1)) 404 | }); 405 | } 406 | 407 | if (headerStyles) { 408 | 409 | // Remove existing CSS 410 | while (headerStyles.hasChildNodes()) { 411 | headerStyles.removeChild(headerStyles.firstChild); 412 | } 413 | 414 | } else { 415 | headerStyles = document.createElement('style'); 416 | headerStyles.setAttribute('type', 'text/css'); 417 | } 418 | 419 | headerStyles.appendChild(document.createTextNode(styleContents)); 420 | headElement.appendChild(headerStyles); 421 | } 422 | 423 | function _createTargetElements() { 424 | 425 | var preloadElement, targetChildren; 426 | 427 | // Create the preload and render areas 428 | targetChildren = document.createDocumentFragment(); 429 | preloadElement = targetChildren.appendChild(document.createElement('div')); 430 | renderArea = targetChildren.appendChild(document.createElement('div')); 431 | 432 | // Add the flowed content to the preload area 433 | if ('string' === typeof flowedContent || !flowedContent) { 434 | preloadColumn = preloadElement.appendChild(document.createElement('div')); 435 | if (flowedContent) preloadColumn.innerHTML = flowedContent; 436 | } else if (flowedContent instanceof HTMLElement) { 437 | preloadColumn = flowedContent.cloneNode(true); 438 | preloadElement.appendChild(preloadColumn); 439 | } else { 440 | throw new FTColumnflowException('FlowedContentException', 'flowedContent must be a HTML string or a DOM element.'); 441 | } 442 | 443 | // Add the fixed content to the preload area 444 | if ('string' === typeof fixedContent || !fixedContent) { 445 | fixedPreloadArea = targetChildren.appendChild(document.createElement('div')); 446 | if (fixedContent) fixedPreloadArea.innerHTML = fixedContent; 447 | } else if (fixedContent instanceof HTMLElement) { 448 | fixedPreloadArea = fixedContent.cloneNode(true); 449 | targetChildren.appendChild(fixedPreloadArea); 450 | } else { 451 | throw new FTColumnflowException('FixedContentException', 'fixedContent must be a HTML string or a DOM element.'); 452 | } 453 | 454 | preloadElement.className = preloadAreaClassName + ' ' + config.pageClass; 455 | renderArea.className = renderAreaClassName; 456 | preloadColumn.className = config.columnClass; 457 | fixedPreloadArea.className = preloadFixedAreaClassName; 458 | 459 | that.target.appendChild(targetChildren); 460 | } 461 | 462 | 463 | /* Determine the baseline grid for the flowed content from the line-height */ 464 | 465 | function _findLineHeight() { 466 | 467 | var lineHeights, i, l, node, testNode, testLineHeight; 468 | 469 | // If the grid height is not pre-set, we need to determine it 470 | if (!config.lineHeight) { 471 | 472 | if (!lineHeightTestContents) { 473 | 474 | // 10 lines of text 475 | lineHeightTestContents = new Array(10).join('x
') + 'x'; 476 | } 477 | 478 | // Here we take the mode (most common) line-height value from the first few 479 | // elements (maximum 10), and assume that that is our desired baseline grid value 480 | lineHeights = []; 481 | for (i = 0, l = Math.min(10, (preloadColumn.childNodes.length)); i < l; i++) { 482 | 483 | node = preloadColumn.childNodes[i]; 484 | if (Node.ELEMENT_NODE !== node.nodeType) continue; 485 | 486 | testLineHeight = parseInt(window.getComputedStyle(node).getPropertyValue('line-height'), 10); 487 | 488 | // We haven't found a pixel lineheight, so it must be 'normal' or 'inherit'. Unless there's 489 | // a better way of doing this, for now we just create a (similar) element at the end of the 490 | // column with 10 lines of text, and measure its height 491 | if (!testLineHeight) { 492 | 493 | testNode = node.cloneNode(false); 494 | 495 | if (node.className) testNode.className = node.className; 496 | testNode.style.padding = "0px"; 497 | testNode.style.border = "none"; 498 | testNode.style.height = "auto"; 499 | testNode.innerHTML = lineHeightTestContents; 500 | 501 | preloadColumn.appendChild(testNode); 502 | testLineHeight = testNode.offsetHeight / 10; 503 | preloadColumn.removeChild(testNode); 504 | } 505 | 506 | lineHeights.push(testLineHeight); 507 | } 508 | 509 | if (lineHeights.length < 5) { 510 | 511 | // If we haven't yet build up a large enough sample, add some simple paragraphs 512 | testNode = document.createElement('p'); 513 | 514 | testNode.style.padding = "0px"; 515 | testNode.style.border = "none"; 516 | testNode.style.height = "auto"; 517 | testNode.innerHTML = lineHeightTestContents; 518 | 519 | preloadColumn.appendChild(testNode); 520 | testLineHeight = testNode.offsetHeight / 10; 521 | preloadColumn.removeChild(testNode); 522 | 523 | for (i = lineHeights.length; i < 5; i++) lineHeights.push(testLineHeight); 524 | } 525 | 526 | config.lineHeight = _mode(lineHeights); 527 | } 528 | 529 | // Now the line-height is known, the column height can be determined 530 | config.layoutDimensions.columnHeight = config.lineHeight ? _roundDownToGrid(config.layoutDimensions.pageInnerHeight) : config.layoutDimensions.pageInnerHeight; 531 | 532 | // For debugging, show the grid lines with CSS 533 | if (showGrid) { 534 | 535 | headerStyles.innerHTML += _replaceStringTokens(showGridStyles, { 536 | targetId: that.target.id, 537 | pageClass: config.pageClass, 538 | 'lh': config.lineHeight 539 | }); 540 | } 541 | } 542 | 543 | 544 | function _setFixedElementHeight(element) { 545 | 546 | var computedStyle, indexedColStart, indexedColEnd, 547 | matches, anchorY, anchorX, colSpan, spanDir; 548 | 549 | // Don't do any manipulation on text nodes, or nodes which are hidden 550 | if (Node.TEXT_NODE === element.nodeType) return false; 551 | 552 | computedStyle = window.getComputedStyle(element); 553 | if ('none' === computedStyle.getPropertyValue('display')) return false; 554 | 555 | // Determine the anchor point 556 | matches = element.className.match(/(\s|^)anchor-(top|middle|bottom)-(left|right|(?:col-(\d+)))(\s|$)/); 557 | if (matches) { 558 | 559 | anchorY = matches[2]; 560 | 561 | if (matches[4]) { 562 | 563 | // A numeric column anchor 564 | anchorX = Math.max(0, (Math.min(matches[4], config.layoutDimensions.columnCount) - 1)); 565 | } else { 566 | 567 | // Left or right 568 | anchorX = matches[3]; 569 | } 570 | 571 | } else { 572 | anchorY = 'top'; 573 | anchorX = 'left'; 574 | } 575 | 576 | // Determine the affected columns 577 | matches = element.className.match(/(\s|^)col-span-(\d+|all)(-(left|right))?(\s|$)/); 578 | if (matches) { 579 | 580 | spanDir = matches[4] || 'right'; 581 | 582 | if (matches[2] === 'all') { 583 | colSpan = config.layoutDimensions.columnCount; 584 | } else { 585 | colSpan = parseInt(matches[2], 10); 586 | } 587 | 588 | if ('left' === anchorX) { 589 | indexedColStart = 0; 590 | indexedColEnd = Math.min(colSpan, config.layoutDimensions.columnCount) - 1; 591 | } else if ('right' === anchorX) { 592 | indexedColEnd = config.layoutDimensions.columnCount - 1; 593 | indexedColStart = config.layoutDimensions.columnCount - Math.min(colSpan, config.layoutDimensions.columnCount); 594 | } else { 595 | if ('right' === spanDir) { 596 | indexedColStart = anchorX; 597 | indexedColEnd = Math.min((indexedColStart + colSpan), config.layoutDimensions.columnCount) - 1; 598 | } else { 599 | indexedColEnd = anchorX; 600 | indexedColStart = Math.max((indexedColEnd - colSpan - 1), 0); 601 | } 602 | } 603 | } else { 604 | 605 | if ('left' === anchorX) { 606 | indexedColStart = 0; 607 | } else if ('right' === anchorX) { 608 | indexedColStart = config.layoutDimensions.columnCount - 1; 609 | } else { 610 | indexedColStart = anchorX; 611 | } 612 | 613 | indexedColEnd = indexedColStart; 614 | } 615 | 616 | // Set an explicit width to that of the columnspan attribute 617 | element.style.width = _round(((indexedColEnd - indexedColStart) * (config.layoutDimensions.columnWidth + config.layoutDimensions.columnGap)) + config.layoutDimensions.columnWidth) + 'px'; 618 | 619 | return { 620 | element: element, 621 | preComputedStyle: computedStyle, 622 | indexedColStart: indexedColStart, 623 | indexedColEnd: indexedColEnd, 624 | anchorY: anchorY, 625 | anchorX: anchorX 626 | }; 627 | } 628 | 629 | 630 | function _addFixedElement(elementDefinition) { 631 | 632 | var element = elementDefinition.element, 633 | matches, pageNum, workingPage, normalisedElementHeight, 634 | elementTopPos, elementBottomPos, lowestTopPos, highestBottomPos, 635 | topSplitPoint, bottomSplitPoint, 636 | firstColFragment, lastColFragment, newColumnFragment, newFragmentHeight, 637 | fragment, column, fragNum, fragLen, columnNum; 638 | 639 | 640 | // Determine the page 641 | if (element.classList.contains('attach-page-last')) { 642 | 643 | // Add to a separate store of elements to be added after all the other content is rendered 644 | pagedEndContent.push({ 645 | 'fixed': [{ 646 | content: _outerHTML(element), 647 | top: config.layoutDimensions.colDefaultTop, 648 | left: config.layoutDimensions.colDefaultLeft 649 | }] 650 | }); 651 | return; 652 | 653 | } else { 654 | 655 | // Look for a numeric page 656 | matches = element.className.match(/(\s|^)attach-page-(\d+)(\s|$)/); 657 | if (matches) { 658 | pageNum = matches[2] - 1; 659 | } else { 660 | pageNum = 0; 661 | } 662 | } 663 | 664 | // Create any necessary page objects 665 | _createPageObjects(pageNum); 666 | workingPage = pagedContent[pageNum]; 667 | 668 | // Determine the height of the element, taking into account any vertical shift applied to it using margin-top 669 | normalisedElementHeight = element.offsetHeight + parseInt(elementDefinition.preComputedStyle.getPropertyValue('margin-top'), 10); 670 | 671 | // Find the most appropriate available space for the element on the page 672 | switch (elementDefinition.anchorY) { 673 | 674 | case 'top': 675 | elementTopPos = config.layoutDimensions.colDefaultTop; 676 | lowestTopPos = colDefaultBottom - normalisedElementHeight; 677 | 678 | for (columnNum = elementDefinition.indexedColStart; columnNum <= elementDefinition.indexedColEnd; columnNum++) { 679 | 680 | // Find the topmost column fragment 681 | firstColFragment = workingPage.columns[columnNum].fragments[0]; 682 | 683 | if (!firstColFragment) { 684 | 685 | // Column is full, so place element at the bottom 686 | elementTopPos = lowestTopPos; 687 | } else { 688 | 689 | // If the fragment starts below the element top position, move the element down 690 | if (firstColFragment.top > elementTopPos) { 691 | elementTopPos = (firstColFragment.top > lowestTopPos) ? lowestTopPos : firstColFragment.top; 692 | } 693 | } 694 | } 695 | elementBottomPos = elementTopPos + normalisedElementHeight; 696 | topSplitPoint = elementTopPos - config.lineHeight; 697 | bottomSplitPoint = _roundUpToGrid(elementBottomPos, true); 698 | break; 699 | 700 | case 'middle': 701 | elementTopPos = colMiddle - (normalisedElementHeight / 2); 702 | topSplitPoint = _roundDownToGrid(elementTopPos, true); 703 | bottomSplitPoint = _roundUpToGrid(elementTopPos + normalisedElementHeight, true); 704 | 705 | if (topSplitPoint < 0) topSplitPoint = 0; 706 | if (bottomSplitPoint > config.layoutDimensions.columnHeight) bottomSplitPoint = config.layoutDimensions.columnHeight; 707 | break; 708 | 709 | case 'bottom': 710 | elementBottomPos = colDefaultBottom; 711 | highestBottomPos = normalisedElementHeight; 712 | 713 | for (columnNum = elementDefinition.indexedColStart; columnNum <= elementDefinition.indexedColEnd; columnNum++) { 714 | 715 | // Find the bottommost column fragment 716 | lastColFragment = workingPage.columns[columnNum].fragments[workingPage.columns[columnNum].fragments.length - 1]; 717 | 718 | if (!lastColFragment) { 719 | 720 | // Column is full, so place element at the top 721 | elementBottomPos = highestBottomPos; 722 | } else { 723 | 724 | // If the fragment ends above the element bottom position, move the element up 725 | if (lastColFragment.bottom < elementBottomPos) { 726 | elementBottomPos = (lastColFragment.bottom < highestBottomPos) ? highestBottomPos : lastColFragment.bottom; 727 | } 728 | } 729 | } 730 | 731 | elementTopPos = elementBottomPos - normalisedElementHeight; 732 | topSplitPoint = _roundDownToGrid(elementTopPos, true); 733 | bottomSplitPoint = elementBottomPos + config.lineHeight; 734 | break; 735 | } 736 | 737 | 738 | /* Alter dimensions and placing of any affected column fragments. */ 739 | 740 | // Loop the columns spanned by the element 741 | for (columnNum = elementDefinition.indexedColStart; columnNum <= elementDefinition.indexedColEnd; columnNum++) { 742 | 743 | column = workingPage.columns[columnNum]; 744 | 745 | // Loop the fragments 746 | for (fragNum = 0, fragLen = column.fragments.length; fragNum < fragLen; fragNum++) { 747 | 748 | fragment = column.fragments[fragNum]; 749 | 750 | // The fragment is entirely overlapped by the fixed element, so delete it and continue the loop 751 | if (topSplitPoint < fragment.top && bottomSplitPoint > fragment.bottom) { 752 | column.fragments.splice(fragNum, 1); 753 | fragLen--; 754 | continue; 755 | } else if (topSplitPoint > fragment.bottom || bottomSplitPoint < fragment.top) { 756 | 757 | // The fragment is not disturbed by the element at all 758 | continue; 759 | } 760 | 761 | // Determine the height of the new fragment 762 | newFragmentHeight = fragment.top + fragment.height - bottomSplitPoint; 763 | 764 | // Modify the original column fragment 765 | fragment.height = topSplitPoint - fragment.top; 766 | fragment.bottom = fragment.top + fragment.height; 767 | 768 | 769 | if (!fragment.height || fragment.height < config.columnFragmentMinHeight) { 770 | 771 | // The fragment is now too small, so delete it and decrement the iteration counter 772 | column.fragments.splice(fragNum--, 1); 773 | fragLen--; 774 | } 775 | 776 | // Only create the new fragment if it has enough height 777 | if (newFragmentHeight && newFragmentHeight >= config.columnFragmentMinHeight) { 778 | 779 | // Create a new column fragment 780 | newColumnFragment = _createColumnFragment(); 781 | 782 | newColumnFragment.top = bottomSplitPoint; 783 | newColumnFragment.height = newFragmentHeight; 784 | newColumnFragment.bottom = newColumnFragment.top + newColumnFragment.height; 785 | 786 | // Insert it into the collection, and increment the iteration counter 787 | column.fragments.splice(++fragNum, 0, newColumnFragment); 788 | fragNum++; 789 | fragLen++; 790 | } 791 | } 792 | } 793 | 794 | // Save the fixed content string, plus positioning details 795 | workingPage.fixed.push({ 796 | content: _outerHTML(element), 797 | top: elementTopPos, 798 | left: (config.layoutDimensions.colDefaultLeft + (('left' === elementDefinition.anchorX) ? 0 : ((config.layoutDimensions.columnWidth + config.layoutDimensions.columnGap) * elementDefinition.indexedColStart))) 799 | }); 800 | 801 | } 802 | 803 | function _normaliseFlowedElement(element) { 804 | 805 | var p; 806 | 807 | if (Node.TEXT_NODE !== element.nodeType) return; 808 | 809 | if (element.nodeValue.match(/^\s*$/)) { 810 | 811 | // A plain text node, containing only white space 812 | element.parentNode.removeChild(element); 813 | 814 | } else { 815 | 816 | // A plain text node, containing more than just white space. 817 | // Wrap it in a

tag 818 | p = document.createElement('p'); 819 | 820 | p.appendChild(document.createTextNode(element.nodeValue)); 821 | element.parentNode.replaceChild(p, element); 822 | } 823 | } 824 | 825 | 826 | function _flowContent() { 827 | 828 | var fixedElementDefinitions = [], 829 | fixedElementDefinition, i, l; 830 | 831 | // Initialise some variables 832 | pagedContent = []; 833 | pagedEndContent = []; 834 | 835 | indexedPageNum = 836 | indexedColumnNum = 837 | indexedColumnFrag = 838 | borderElementIndex = 839 | indexedColumnNum = 840 | indexedColumnFrag = 841 | topElementOverflow = 842 | totalColumnHeight = 0; 843 | 844 | // Set the maximum column height to a multiple of the lineHeight 845 | colDefaultBottom = config.layoutDimensions.columnHeight + config.layoutDimensions.colDefaultTop; 846 | colMiddle = config.layoutDimensions.colDefaultTop + (config.layoutDimensions.columnHeight / 2); 847 | minFixedPadding = config.minFixedPadding * config.lineHeight; 848 | fixedPadding = _roundUpToGrid(minFixedPadding); 849 | 850 | // Add each fixed element to a page in the correct position, 851 | // and determine the remaining free space for columns 852 | // Two loops are run: the first sets an explicit width on the element, therefore invalidating the layout, 853 | // and the second reads the element's width, forcing a recalculation of styles. This batching avoids layout thrashing. 854 | for (i = 0, l = fixedPreloadArea.childNodes.length; i < l; i++) { 855 | fixedElementDefinition = _setFixedElementHeight(fixedPreloadArea.childNodes[i]); 856 | if (fixedElementDefinition) fixedElementDefinitions.push(fixedElementDefinition); 857 | } 858 | 859 | for (i = 0, l = fixedElementDefinitions.length; i < l; i++) { 860 | _addFixedElement(fixedElementDefinitions[i]); 861 | } 862 | 863 | /* Loop through the preload elements, and determine which column to put them in */ 864 | 865 | // Preliminary loop to remove whitespace, and wrap plain text nodes 866 | for (i = preloadColumn.childNodes.length - 1; i >= 0; i--) { 867 | _normaliseFlowedElement(preloadColumn.childNodes[i]); 868 | } 869 | 870 | if (!preloadColumn.childNodes.length) return; 871 | 872 | // Select the first available column for content 873 | _createPageObjects(indexedPageNum); 874 | 875 | workingPage = pagedContent[indexedPageNum]; 876 | workingColumn = workingPage.columns[indexedColumnNum]; 877 | workingColumnFrag = workingColumn.fragments[indexedColumnFrag]; 878 | 879 | if (!workingColumnFrag) { 880 | _advanceWorkingColumnFragment(); 881 | } 882 | 883 | // Start with the free space in the first available column 884 | totalColumnHeight = workingColumnFrag.height; 885 | 886 | // TODO:GC: Save these measurements, so there's no need to re-measure 887 | // when we return to an orientation we've already rendered. 888 | for (i = 0, l = preloadColumn.childNodes.length; i < l; i++) { 889 | _addFlowedElement(preloadColumn.childNodes[i], i); 890 | } 891 | 892 | // Wrap one more time, to add everything from borderElementIndex to the the final element 893 | _wrapColumn(l - 1, false); 894 | 895 | if (!config.allowReflow) { 896 | if (fixedPreloadArea.parentNode) fixedPreloadArea.parentNode.removeChild(fixedPreloadArea); 897 | fixedPreloadArea = null; 898 | 899 | if (preloadColumn.parentNode && preloadColumn.parentNode.parentNode) preloadColumn.parentNode.parentNode.removeChild(preloadColumn.parentNode); 900 | preloadColumn = null; 901 | } 902 | } 903 | 904 | function _addFlowedElement(element, index) { 905 | 906 | var originalMargin, existingMargin, nextElementOffset, elementHeight, 907 | newMargin, largestMargin, overflow, loopCount, 908 | 909 | nextElement = element.nextSibling; 910 | 911 | // Check if it's necessary to sanitize elements to conform to the baseline grid 912 | if (config.standardiseLineHeight) { 913 | 914 | existingMargin = parseFloat(window.getComputedStyle(element).getPropertyValue('margin-bottom'), 10); 915 | 916 | // If reflowing is enabled, try to read the original margin for the 917 | // element, in case it was already modified 918 | if (config.allowReflow) { 919 | originalMargin = parseFloat(element.getAttribute('data-cf-original-margin'), 10) || null; 920 | if (null === originalMargin) { 921 | originalMargin = existingMargin; 922 | element.setAttribute('data-cf-original-margin', originalMargin); 923 | } else if (originalMargin !== existingMargin) { 924 | 925 | // Return the element to its original margin 926 | element.style.marginBottom = originalMargin + 'px'; 927 | } 928 | } else { 929 | originalMargin = existingMargin; 930 | } 931 | 932 | nextElementOffset = _getNextElementOffset(element, nextElement); 933 | elementHeight = element.offsetHeight; 934 | 935 | // The next element's top is not aligned to the grid 936 | if (nextElementOffset % config.lineHeight) { 937 | 938 | // Allow for collapsing margins 939 | largestMargin = Math.max(existingMargin, nextElement ? parseFloat(window.getComputedStyle(nextElement).getPropertyValue('margin-top'), 10) : 0); 940 | newMargin = _roundUpToGrid(elementHeight) - elementHeight + _roundUpToGrid(largestMargin); 941 | 942 | if (newMargin !== existingMargin) { 943 | element.style.marginBottom = newMargin + 'px'; 944 | } 945 | } 946 | } 947 | 948 | element.offsetBottom = element.offsetTop + element.offsetHeight; 949 | 950 | // TODO:GC: Remove this loop-protection check 951 | loopCount = 0; 952 | while ((element.offsetBottom >= totalColumnHeight || (nextElement && nextElement.offsetTop >= totalColumnHeight)) && (loopCount++ < 30)) { 953 | 954 | overflow = (element.offsetBottom > totalColumnHeight); 955 | _wrapColumn(index, overflow); 956 | } 957 | 958 | // TODO:GC: Remove this loop-protection check 959 | if (loopCount >= 30) console.error('FTColumnflow: Caught and destroyed a loop when wrapping columns for element', element.outerHTML.substr(0, 200) + '...'); 960 | } 961 | 962 | function _getNextElementOffset(element, nextElement) { 963 | if (!element.getBoundingClientRect) { 964 | return nextElement ? (nextElement.offsetTop - element.offsetTop) : element.offsetHeight; 965 | } 966 | return nextElement ? nextElement.getBoundingClientRect().top - element.getBoundingClientRect().top : element.getBoundingClientRect().height; 967 | } 968 | 969 | function _wrapColumn(currentElementIndex, overflow) { 970 | 971 | var nowrap, keepwithnext, prevElementKeepwithnext, firstInColumn, finalColumnElementIndex, 972 | cropCurrentElement, pushElement, pushFromElementIndex, i, lastElement, element, 973 | 974 | currentElement = preloadColumn.childNodes[currentElementIndex], 975 | nextElement = currentElement.nextSibling, 976 | prevElement = currentElement.previousSibling; 977 | 978 | // Determine any special classes 979 | nowrap = currentElement.classList.contains(nowrapClassName); 980 | keepwithnext = currentElement.classList.contains(keepwithnextClassName); 981 | prevElementKeepwithnext = (prevElement && prevElement.classList.contains(keepwithnextClassName)); 982 | 983 | // Assume nowrap if element's tag is in the noWrapOnTags list 984 | if (-1 !== config.noWrapOnTags.indexOf(currentElement.tagName.toLowerCase())) { 985 | nowrap = true; 986 | } 987 | 988 | // Is this the last element of all? 989 | if (!nextElement) { 990 | lastElement = true; 991 | } 992 | 993 | // Is this the first element of a column? 994 | if (currentElementIndex === borderElementIndex) { 995 | firstInColumn = true; 996 | } 997 | 998 | // Does the element fit if we collapse the bottom margin? 999 | if (currentElement.offsetBottom === totalColumnHeight) { 1000 | overflow = false; 1001 | if (nextElement) totalColumnHeight = nextElement.offsetTop; 1002 | } 1003 | 1004 | // Do we need to crop the current element? 1005 | if ((nowrap || (keepwithnext && nextElement)) && overflow && (firstInColumn)) { 1006 | cropCurrentElement = true; 1007 | } 1008 | 1009 | // Do we need to push the current element to the next column? 1010 | if (!cropCurrentElement && !firstInColumn && ((nowrap && overflow) || (keepwithnext && nextElement))) { 1011 | pushElement = true; 1012 | pushFromElementIndex = currentElementIndex; 1013 | } 1014 | 1015 | // Do we need to push the previous element to the next column? 1016 | if ((currentElementIndex - 1) > borderElementIndex && prevElementKeepwithnext && overflow && nowrap) { 1017 | pushElement = true; 1018 | pushFromElementIndex = currentElementIndex - 1; 1019 | } 1020 | 1021 | // Determine the final element in this column 1022 | finalColumnElementIndex = pushElement ? pushFromElementIndex - 1 : currentElementIndex; 1023 | 1024 | if (finalColumnElementIndex < borderElementIndex) { 1025 | 1026 | // We've already added all the elements. We're finished. 1027 | return; 1028 | } 1029 | 1030 | // Set the overflow for the current column to the value determined in the last iteration 1031 | workingColumnFrag.overflow = topElementOverflow; 1032 | 1033 | // Loop through all elements from the last column border element up to the current element 1034 | for (i = borderElementIndex; i <= finalColumnElementIndex; i++) { 1035 | 1036 | element = preloadColumn.childNodes[i]; 1037 | 1038 | // Add the content of the element to the column 1039 | workingColumnFrag.elements.push({ 1040 | content: _outerHTML(element) 1041 | }); 1042 | } 1043 | 1044 | // Determine the new border element 1045 | if (pushElement) { 1046 | borderElementIndex = pushFromElementIndex; 1047 | } else if (overflow && !cropCurrentElement) { 1048 | borderElementIndex = currentElementIndex; 1049 | } else { 1050 | borderElementIndex = currentElementIndex + 1; 1051 | } 1052 | 1053 | if (pushElement) { 1054 | 1055 | // By pushing an element to the next column prematurely, white space has effectively been added to the stream of 1056 | // column elements. Measurements will therefore be wrong unless the total column height is changed to reflect this. Set 1057 | // the column height to be the top of the pushed element. 1058 | totalColumnHeight = preloadColumn.childNodes[pushFromElementIndex].offsetTop; 1059 | 1060 | } else if (cropCurrentElement && nextElement) { 1061 | 1062 | // By cropping an element, white space has been removed, so adjust the 1063 | // column height to be equal to the top of the next element. 1064 | totalColumnHeight = nextElement.offsetTop; 1065 | } 1066 | 1067 | // Set the required negative top margin for the first element in the next column 1068 | if (!overflow || (nowrap || (keepwithnext && nextElement))) { 1069 | topElementOverflow = 0; 1070 | } else { 1071 | topElementOverflow = totalColumnHeight - currentElement.offsetTop; 1072 | } 1073 | 1074 | // Add the height of the next column 1075 | _advanceWorkingColumnFragment(); 1076 | totalColumnHeight += workingColumnFrag.height; 1077 | } 1078 | 1079 | 1080 | /* Add the flowed and fixed content to the target, arranged in pages and columns */ 1081 | 1082 | function _renderFlowedContent() { 1083 | 1084 | var outputHTML = '', indexedPageNum, page_len, pageHTML, page, i, l, element, indexedColumnNum, 1085 | column_len, column, indexedColumnFrag, fragLen, el, fragment; 1086 | 1087 | for (indexedPageNum = 0, page_len = pagedContent.length; indexedPageNum < page_len; indexedPageNum++) { 1088 | 1089 | pageHTML = ''; 1090 | page = pagedContent[indexedPageNum]; 1091 | 1092 | // Add any fixed elements for this page 1093 | for (i = 0, l = page.fixed.length; i < l; i++) { 1094 | 1095 | element = page.fixed[i]; 1096 | element.content = _addClass(element.content, fixedElementClassName); 1097 | pageHTML += _addStyleRule(element.content, 'top:' + _round(element.top) + 'px;left:' + _round(element.left) + 'px;'); 1098 | } 1099 | 1100 | // Add flowed content for this page 1101 | // First loop the columns 1102 | for (indexedColumnNum = 0, column_len = page.columns.length; indexedColumnNum < column_len; indexedColumnNum++) { 1103 | 1104 | column = page.columns[indexedColumnNum]; 1105 | 1106 | // Loop the column fragments 1107 | for (indexedColumnFrag = 0, fragLen = column.fragments.length; indexedColumnFrag < fragLen; indexedColumnFrag++) { 1108 | 1109 | fragment = column.fragments[indexedColumnFrag]; 1110 | 1111 | // Don't write empty columns to the page 1112 | if (0 === fragment.elements.length) { 1113 | continue; 1114 | } 1115 | 1116 | // Open a column div 1117 | pageHTML += _openColumn(fragment, indexedColumnNum); 1118 | 1119 | for (el = 0, l = fragment.elements.length; el < l; el++) { 1120 | 1121 | element = fragment.elements[el]; 1122 | 1123 | // Set the top margin on the first element of the column 1124 | if (el === 0) { 1125 | 1126 | // Set a *negative* top margin to shift the element up and hide the content already displayed 1127 | element.content = _addStyleRule(element.content, 'margin-top:' + (-fragment.overflow) + 'px;'); 1128 | } 1129 | 1130 | pageHTML += element.content; 1131 | } 1132 | 1133 | // Close the column 1134 | pageHTML += ''; 1135 | } 1136 | } 1137 | 1138 | // Don't write empty pages 1139 | if ('' === pageHTML) { 1140 | pagedContent.splice(indexedPageNum, 1); 1141 | indexedPageNum--; 1142 | page_len--; 1143 | continue; 1144 | } 1145 | 1146 | // Add the page contents to the HTML string 1147 | outputHTML += _openPage(indexedPageNum) + pageHTML + ''; 1148 | } 1149 | 1150 | // Add any end pages 1151 | for (indexedPageNum = 0, page_len = pagedEndContent.length; indexedPageNum < page_len; indexedPageNum++) { 1152 | pageHTML = ''; 1153 | page = pagedEndContent[indexedPageNum]; 1154 | 1155 | for (i = 0, l = page.fixed.length; i < l; i++) { 1156 | 1157 | element = page.fixed[i]; 1158 | element.content = _addClass(element.content, fixedElementClassName); 1159 | pageHTML += _addStyleRule(element.content, 'top:' + _round(element.top) + 'px;left:' + _round(element.left) + 'px;'); 1160 | } 1161 | 1162 | // Add the page contents to the HTML string 1163 | outputHTML += _openPage(pagedContent.length + indexedPageNum) + pageHTML + ''; 1164 | } 1165 | 1166 | renderArea.innerHTML = outputHTML; 1167 | page_len = pagedContent.length + pagedEndContent.length; 1168 | 1169 | // Set an explicit width on the target - not necessary but will allow adjacent content to flow around the flowed columns normally 1170 | that.target.style.width = (config.viewportWidth * page_len) + 'px'; 1171 | 1172 | // Update the instance page counter 1173 | that.pagedContentCount = page_len; 1174 | } 1175 | 1176 | 1177 | /* Private methods */ 1178 | 1179 | function _addClass(element, className) { 1180 | 1181 | // Modify the opening tag of the element 1182 | return element.replace(/<(\w+)([^>]*)>/, function _addRuleToTag(string, tag, attributes) { 1183 | 1184 | // If there's not yet a style attribute, add one 1185 | if (!string.match(/class\s*=/)) { 1186 | string = '<' + tag + ' class="" ' + attributes + '>'; 1187 | } 1188 | 1189 | // Add the class name 1190 | string = string.replace(/class=(["'])/, 'class=$1 ' + className + ' '); 1191 | return string; 1192 | }); 1193 | } 1194 | 1195 | 1196 | function _addStyleRule(element, rule) { 1197 | 1198 | // Modify the opening tag of the element 1199 | return element.replace(/<(\w+)([^>]*)>/, function _addRuleToTag(string, tag, attributes) { 1200 | 1201 | // If there's not yet a style attribute, add one 1202 | if (!string.match(/style\s*=/)) { 1203 | string = '<' + tag + ' style="" ' + attributes + '>'; 1204 | } 1205 | 1206 | // Add the style rule 1207 | string = string.replace(/style=(["'])/, 'style=$1 ' + rule); 1208 | return string; 1209 | }); 1210 | } 1211 | 1212 | 1213 | function _roundDownToGrid(val, addPadding) { 1214 | var resized = val - (val % config.lineHeight); 1215 | 1216 | // If the difference after rounding down is less than the minimum padding, also subtract one grid line 1217 | if (addPadding && ((val - resized) < minFixedPadding)) { 1218 | resized -= fixedPadding; 1219 | } 1220 | 1221 | return resized; 1222 | } 1223 | 1224 | 1225 | function _roundUpToGrid(val, addPadding) { 1226 | 1227 | var delta = val % config.lineHeight, 1228 | resized = (delta ? (val - delta + config.lineHeight) : val); 1229 | 1230 | // If the difference after rounding up is less than the minimum padding, also add one grid line 1231 | if (addPadding && ((resized - val) < minFixedPadding)) { 1232 | resized += fixedPadding; 1233 | } 1234 | 1235 | return resized; 1236 | } 1237 | 1238 | function _round(val) { 1239 | return Math.round(val * 100) / 100; 1240 | } 1241 | 1242 | function _replaceStringTokens(string, tokens) { 1243 | return string.replace(/\[(\w+)\]/g, 1244 | function _replace(a, b) { 1245 | var r = tokens[b]; 1246 | return typeof r === 'string' || typeof r === 'number' ? r : a; 1247 | }); 1248 | } 1249 | 1250 | 1251 | function _normaliseClassName(type, value) { 1252 | 1253 | if (typeof value !== 'string') { 1254 | throw new FTColumnflowException('ClassnameException', type + ' must be a string.'); 1255 | } 1256 | 1257 | return value.replace(/[^\w]/g, '-'); 1258 | } 1259 | 1260 | 1261 | function _createPageObjects(indexedPageNum) { 1262 | var pageNum, indexedColNum; 1263 | 1264 | for (pageNum = pagedContent.length; pageNum <= indexedPageNum; pageNum++) { 1265 | 1266 | pagedContent.push({ 1267 | 'fixed': [], 1268 | 'columns': [] 1269 | }); 1270 | 1271 | for (indexedColNum = 0; indexedColNum < config.layoutDimensions.columnCount; indexedColNum++) { 1272 | pagedContent[pageNum].columns.push({ 1273 | fragments: [_createColumnFragment()] 1274 | }); 1275 | } 1276 | } 1277 | } 1278 | 1279 | 1280 | function _createColumnFragment() { 1281 | 1282 | return { 1283 | elements: [], 1284 | overflow: 0, 1285 | height: config.layoutDimensions.columnHeight, 1286 | top: config.layoutDimensions.colDefaultTop, 1287 | bottom: colDefaultBottom 1288 | }; 1289 | } 1290 | 1291 | 1292 | function _advanceWorkingColumnFragment() { 1293 | 1294 | // Advance the fragment counter and check for another fragment 1295 | if (!workingColumn.fragments[++indexedColumnFrag]) { 1296 | indexedColumnFrag = 0; 1297 | 1298 | // Advance the column counter and check for another column 1299 | if (!workingPage.columns[++indexedColumnNum]) { 1300 | indexedColumnNum = 0; 1301 | 1302 | // Advance the page counter and create another page if necessary 1303 | indexedPageNum++; 1304 | _createPageObjects(indexedPageNum); 1305 | } 1306 | } 1307 | 1308 | workingPage = pagedContent[indexedPageNum]; 1309 | workingColumn = workingPage.columns[indexedColumnNum]; 1310 | workingColumnFrag = workingColumn.fragments[indexedColumnFrag]; 1311 | 1312 | if (!workingColumnFrag) { 1313 | _advanceWorkingColumnFragment(); 1314 | } 1315 | } 1316 | 1317 | 1318 | function _openPage(indexedPageNum) { 1319 | var pagePos; 1320 | 1321 | if ('horizontal' === config.pageArrangement) { 1322 | pagePos = 'left: ' + (indexedPageNum * config.viewportWidth) + 'px;'; 1323 | } else { 1324 | pagePos = 'top: ' + (indexedPageNum * config.viewportHeight) + 'px;'; 1325 | } 1326 | 1327 | return '

'; 1328 | } 1329 | 1330 | 1331 | function _openColumn(column, indexedColumnNum) { 1332 | return '
'; 1333 | } 1334 | 1335 | 1336 | function _mode(array) { 1337 | 1338 | var modeMap = {}, 1339 | maxEl, maxCount, i, el; 1340 | 1341 | if (array.length === 0) { 1342 | return null; 1343 | } 1344 | 1345 | maxEl = array[0]; 1346 | maxCount = 1; 1347 | 1348 | for (i = 0; i < array.length; i++) { 1349 | el = array[i]; 1350 | if (modeMap[el] === undefined) { 1351 | modeMap[el] = 1; 1352 | } else { 1353 | modeMap[el]++; 1354 | } 1355 | if (modeMap[el] > maxCount) { 1356 | maxEl = el; 1357 | maxCount = modeMap[el]; 1358 | } 1359 | } 1360 | return maxEl; 1361 | } 1362 | 1363 | 1364 | /* Public methods */ 1365 | 1366 | this.flow = function(flowed, fixed) { 1367 | 1368 | flowedContent = flowed; 1369 | fixedContent = fixed; 1370 | 1371 | _writeTargetStyles(); 1372 | _createTargetElements(); 1373 | 1374 | _findLineHeight(); 1375 | _flowContent(); 1376 | _renderFlowedContent(); 1377 | }; 1378 | 1379 | this.reflow = function(newConfig) { 1380 | 1381 | if (!config.allowReflow) { 1382 | throw new FTColumnflowException('ReflowException', 'reflow() was called but "allowReflow" config option was false.'); 1383 | } 1384 | 1385 | if (newConfig) { 1386 | _setConfig(newConfig); 1387 | } 1388 | 1389 | _setLayoutDimensions(); 1390 | _writeTargetStyles(); 1391 | 1392 | _findLineHeight(); 1393 | _flowContent(); 1394 | _renderFlowedContent(); 1395 | }; 1396 | 1397 | this.destroy = function() { 1398 | 1399 | if (headerStyles) { 1400 | headerStyles.parentNode.removeChild(headerStyles); 1401 | headerStyles = null; 1402 | } 1403 | 1404 | if (that.target) { 1405 | that.target.parentNode.removeChild(that.target); 1406 | that.target = null; 1407 | } 1408 | }; 1409 | } 1410 | 1411 | FTColumnflow.prototype = { 1412 | get layoutDimensions() { 1413 | return this.config.layoutDimensions; 1414 | }, 1415 | set layoutDimensions(value) { 1416 | throw new FTColumnflowException('SetterException', 'Setter not defined for layoutDimensions.'); 1417 | }, 1418 | get pageClass() { 1419 | return this.config.pageClass; 1420 | }, 1421 | set pageClass(value) { 1422 | throw new FTColumnflowException('SetterException', 'Setter not defined for pageClass.'); 1423 | }, 1424 | get columnClass() { 1425 | return this.config.columnClass; 1426 | }, 1427 | set columnClass(value) { 1428 | throw new FTColumnflowException('SetterException', 'Setter not defined for columnClass.'); 1429 | }, 1430 | get pageCount() { 1431 | return this.pagedContentCount; 1432 | }, 1433 | set pageCount(value) { 1434 | throw new FTColumnflowException('SetterException', 'Setter not defined for pageCount.'); 1435 | }, 1436 | 1437 | _checkInstanceArgs: function () { 1438 | 1439 | var that = this; 1440 | 1441 | // Check type of required target and viewport parameters 1442 | ['target', 'viewport'].forEach(function _checkArg(name) { 1443 | 1444 | var arg = that[name]; 1445 | 1446 | switch (typeof arg) { 1447 | 1448 | case 'string': 1449 | 1450 | arg = document.getElementById(arg); 1451 | if (!arg) throw new FTColumnflowException('SelectorException', name + ' must be a valid DOM element.'); 1452 | break; 1453 | 1454 | case 'object': 1455 | if (!(arg instanceof HTMLElement)) { 1456 | throw new FTColumnflowException('ParameterException', name + ' must be a string ID or DOM element.'); 1457 | } 1458 | break; 1459 | 1460 | default: 1461 | throw new FTColumnflowException('ParameterException', name + ' must be a string ID or DOM element.'); 1462 | } 1463 | 1464 | that[name] = arg; 1465 | }); 1466 | 1467 | // Check target is a child of viewport 1468 | if (this.viewport.compareDocumentPosition(this.target) < this.viewport.DOCUMENT_POSITION_CONTAINED_BY) { 1469 | throw new FTColumnflowException('InheritanceException', 'Target element must be a child of the viewport.'); 1470 | } 1471 | 1472 | // Ensure we have an empty target 1473 | while (this.target.lastChild) { 1474 | this.target.removeChild(this.target.lastChild); 1475 | } 1476 | } 1477 | }; 1478 | 1479 | module.exports = FTColumnflow; 1480 | 1481 | // Close UMD 1482 | });})(typeof define=='function'&&define.amd?define 1483 | :(function(n,w){'use strict';return typeof module=='object'?function(c){ 1484 | c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){ 1485 | return w[n];},m.exports,m);w[n]=m.exports;};})('FTColumnflow',this)); 1486 | --------------------------------------------------------------------------------