-
-
Notifications
You must be signed in to change notification settings - Fork 147
/
Copy pathjquery.columnizer.js
621 lines (574 loc) · 21.4 KB
/
jquery.columnizer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
// version 1.6.0
// http://welcome.totheinter.net/columnizer-jquery-plugin/
// created by: Adam Wulf @adamwulf, [email protected]
(function($){
$.fn.columnize = function(options) {
var defaults = {
// default width of columns
width: 400,
// optional # of columns instead of width
columns : false,
// true to build columns once regardless of window resize
// false to rebuild when content box changes bounds
buildOnce : false,
// an object with options if the text should overflow
// it's container if it can't fit within a specified height
overflow : false,
// this function is called after content is columnized
doneFunc : function(){},
// if the content should be columnized into a
// container node other than it's own node
target : false,
// re-columnizing when images reload might make things
// run slow. so flip this to true if it's causing delays
ignoreImageLoading : true,
// should columns float left or right
columnFloat : "left",
// resize container to width of filled columns only
// works with width and height options set
resizeContainer : false,
// ensure the last column is never the tallest column
lastNeverTallest : false,
// (int) the minimum number of characters to jump when splitting
// text nodes. smaller numbers will result in higher accuracy
// column widths, but will take slightly longer
accuracy : false,
// don't automatically layout columns, only use manual columnbreak
manualBreaks : false,
// previx for all the CSS classes used by this plugin
// default to empty string for backwards compatibility
cssClassPrefix : ""
};
var options = $.extend(defaults, options);
if(typeof(options.width) == "string"){
options.width = parseInt(options.width);
if(isNaN(options.width)){
options.width = defaults.width;
}
}
return this.each(function() {
var $inBox = options.target ? $(options.target) : $(this);
var maxHeight = $(this).height();
var $cache = $('<div></div>'); // this is where we'll put the real content
var lastWidth = 0;
var columnizing = false;
var manualBreaks = options.manualBreaks;
var cssClassPrefix = defaults.cssClassPrefix;
if(typeof(options.cssClassPrefix) == "string"){
cssClassPrefix = options.cssClassPrefix;
}
var adjustment = 0;
$cache.append($(this).contents().clone(true));
// images loading after dom load
// can screw up the column heights,
// so recolumnize after images load
if(!options.ignoreImageLoading && !options.target){
if(!$inBox.data("imageLoaded")){
$inBox.data("imageLoaded", true);
if($(this).find("img").length > 0){
// only bother if there are
// actually images...
var func = function($inBox,$cache){ return function(){
if(!$inBox.data("firstImageLoaded")){
$inBox.data("firstImageLoaded", "true");
$inBox.empty().append($cache.children().clone(true));
$inBox.columnize(options);
}
}}($(this), $cache);
$(this).find("img").one("load", func);
$(this).find("img").one("abort", func);
return;
}
}
}
$inBox.empty();
columnizeIt();
if(!options.buildOnce){
$(window).resize(function() {
if(!options.buildOnce && $.browser.msie){
if($inBox.data("timeout")){
clearTimeout($inBox.data("timeout"));
}
$inBox.data("timeout", setTimeout(columnizeIt, 200));
}else if(!options.buildOnce){
columnizeIt();
}else{
// don't rebuild
}
});
}
function prefixTheClassName(className, withDot){
var dot = withDot ? "." : "";
if(cssClassPrefix.length){
return dot + cssClassPrefix + "-" + className;
}
return dot + className;
}
/**
* this fuction builds as much of a column as it can without
* splitting nodes in half. If the last node in the new column
* is a text node, then it will try to split that text node. otherwise
* it will leave the node in $pullOutHere and return with a height
* smaller than targetHeight.
*
* Returns a boolean on whether we did some splitting successfully at a text point
* (so we know we don't need to split a real element). return false if the caller should
* split a node if possible to end this column.
*
* @param putInHere, the jquery node to put elements into for the current column
* @param $pullOutHere, the jquery node to pull elements out of (uncolumnized html)
* @param $parentColumn, the jquery node for the currently column that's being added to
* @param targetHeight, the ideal height for the column, get as close as we can to this height
*/
function columnize($putInHere, $pullOutHere, $parentColumn, targetHeight){
//
// add as many nodes to the column as we can,
// but stop once our height is too tall
while((manualBreaks || $parentColumn.height() < targetHeight) &&
$pullOutHere[0].childNodes.length){
var node = $pullOutHere[0].childNodes[0]
//
// Because we're not cloning, jquery will actually move the element"
// http://welcome.totheinter.net/2009/03/19/the-undocumented-life-of-jquerys-append/
if($(node).find(prefixTheClassName("columnbreak", true)).length){
//
// our column is on a column break, so just end here
return;
}
if($(node).hasClass(prefixTheClassName("columnbreak"))){
//
// our column is on a column break, so just end here
return;
}
$putInHere.append(node);
}
if($putInHere[0].childNodes.length == 0) return;
// now we're too tall, so undo the last one
var kids = $putInHere[0].childNodes;
var lastKid = kids[kids.length-1];
$putInHere[0].removeChild(lastKid);
var $item = $(lastKid);
//
// now lets try to split that last node
// to fit as much of it as we can into this column
if($item[0].nodeType == 3){
// it's a text node, split it up
var oText = $item[0].nodeValue;
var counter2 = options.width / 18;
if(options.accuracy)
counter2 = options.accuracy;
var columnText;
var latestTextNode = null;
while($parentColumn.height() < targetHeight && oText.length){
var indexOfSpace = oText.indexOf(' ', counter2);
if (indexOfSpace != -1) {
columnText = oText.substring(0, oText.indexOf(' ', counter2));
} else {
columnText = oText;
}
latestTextNode = document.createTextNode(columnText);
$putInHere.append(latestTextNode);
if(oText.length > counter2 && indexOfSpace != -1){
oText = oText.substring(indexOfSpace);
}else{
oText = "";
}
}
if($parentColumn.height() >= targetHeight && latestTextNode != null){
// too tall :(
$putInHere[0].removeChild(latestTextNode);
oText = latestTextNode.nodeValue + oText;
}
if(oText.length){
$item[0].nodeValue = oText;
}else{
return false; // we ate the whole text node, move on to the next node
}
}
if($pullOutHere.contents().length){
$pullOutHere.prepend($item);
}else{
$pullOutHere.append($item);
}
return $item[0].nodeType == 3;
}
/**
* Split up an element, which is more complex than splitting text. We need to create
* two copies of the element with it's contents divided between each
*/
function split($putInHere, $pullOutHere, $parentColumn, targetHeight){
if($putInHere.contents(":last").find(prefixTheClassName("columnbreak", true)).length){
//
// our column is on a column break, so just end here
return;
}
if($putInHere.contents(":last").hasClass(prefixTheClassName("columnbreak"))){
//
// our column is on a column break, so just end here
return;
}
if($pullOutHere.contents().length){
var $cloneMe = $pullOutHere.contents(":first");
//
// make sure we're splitting an element
if($cloneMe.get(0).nodeType != 1) return;
//
// clone the node with all data and events
var $clone = $cloneMe.clone(true);
//
// need to support both .prop and .attr if .prop doesn't exist.
// this is for backwards compatibility with older versions of jquery.
if($cloneMe.hasClass(prefixTheClassName("columnbreak"))){
//
// ok, we have a columnbreak, so add it into
// the column and exit
$putInHere.append($clone);
$cloneMe.remove();
}else if (manualBreaks){
// keep adding until we hit a manual break
$putInHere.append($clone);
$cloneMe.remove();
}else if($clone.get(0).nodeType == 1 && !$clone.hasClass(prefixTheClassName("dontend"))){
$putInHere.append($clone);
if($clone.is("img") && $parentColumn.height() < targetHeight + 20){
//
// we can't split an img in half, so just add it
// to the column and remove it from the pullOutHere section
$cloneMe.remove();
}else if(!$cloneMe.hasClass(prefixTheClassName("dontsplit")) && $parentColumn.height() < targetHeight + 20){
//
// pretty close fit, and we're not allowed to split it, so just
// add it to the column, remove from pullOutHere, and be done
$cloneMe.remove();
}else if($clone.is("img") || $cloneMe.hasClass(prefixTheClassName("dontsplit"))){
//
// it's either an image that's too tall, or an unsplittable node
// that's too tall. leave it in the pullOutHere and we'll add it to the
// next column
$clone.remove();
}else{
//
// ok, we're allowed to split the node in half, so empty out
// the node in the column we're building, and start splitting
// it in half, leaving some of it in pullOutHere
$clone.empty();
if(!columnize($clone, $cloneMe, $parentColumn, targetHeight)){
// this node still has non-text nodes to split
// add the split class and then recur
$cloneMe.addClass(prefixTheClassName("split"));
if($cloneMe.children().length){
split($clone, $cloneMe, $parentColumn, targetHeight);
}
}else{
// this node only has text node children left, add the
// split class and move on.
$cloneMe.addClass(prefixTheClassName("split"));
}
if($clone.get(0).childNodes.length == 0){
// it was split, but nothing is in it :(
$clone.remove();
}
}
}
}
}
function singleColumnizeIt() {
if ($inBox.data("columnized") && $inBox.children().length == 1) {
return;
}
$inBox.data("columnized", true);
$inBox.data("columnizing", true);
$inBox.empty();
$inBox.append($("<div class='"
+ prefixTheClassName("first") + " "
+ prefixTheClassName("last") + " "
+ prefixTheClassName("column") + " "
+ "' style='width:100%; float: " + options.columnFloat + ";'></div>")); //"
$col = $inBox.children().eq($inBox.children().length-1);
$destroyable = $cache.clone(true);
if(options.overflow){
targetHeight = options.overflow.height;
columnize($col, $destroyable, $col, targetHeight);
// make sure that the last item in the column isn't a "dontend"
if(!$destroyable.contents().find(":first-child").hasClass(prefixTheClassName("dontend"))){
split($col, $destroyable, $col, targetHeight);
}
while($col.contents(":last").length && checkDontEndColumn($col.contents(":last").get(0))){
var $lastKid = $col.contents(":last");
$lastKid.remove();
$destroyable.prepend($lastKid);
}
var html = "";
var div = document.createElement('DIV');
while($destroyable[0].childNodes.length > 0){
var kid = $destroyable[0].childNodes[0];
if(kid.attributes){
for(var i=0;i<kid.attributes.length;i++){
if(kid.attributes[i].nodeName.indexOf("jQuery") == 0){
kid.removeAttribute(kid.attributes[i].nodeName);
}
}
}
div.innerHTML = "";
div.appendChild($destroyable[0].childNodes[0]);
html += div.innerHTML;
}
var overflow = $(options.overflow.id)[0];
overflow.innerHTML = html;
}else{
$col.append($destroyable);
}
$inBox.data("columnizing", false);
if(options.overflow && options.overflow.doneFunc){
options.overflow.doneFunc();
}
}
/**
* returns true if the input dom node
* should not end a column.
* returns false otherwise
*/
function checkDontEndColumn(dom){
if(dom.nodeType == 3){
// text node. ensure that the text
// is not 100% whitespace
if(/^\s+$/.test(dom.nodeValue)){
//
// ok, it's 100% whitespace,
// so we should return checkDontEndColumn
// of the inputs previousSibling
if(!dom.previousSibling) return false;
return checkDontEndColumn(dom.previousSibling);
}
return false;
}
if(dom.nodeType != 1) return false;
if($(dom).hasClass(prefixTheClassName("dontend"))) return true;
if(dom.childNodes.length == 0) return false;
return checkDontEndColumn(dom.childNodes[dom.childNodes.length-1]);
}
function columnizeIt() {
//reset adjustment var
adjustment = 0;
if(lastWidth == $inBox.width()) return;
lastWidth = $inBox.width();
var numCols = Math.round($inBox.width() / options.width);
var optionWidth = options.width;
var optionHeight = options.height;
if(options.columns) numCols = options.columns;
if(manualBreaks){
numCols = $cache.find(prefixTheClassName("columnbreak", true)).length + 1;
optionWidth = false;
}
// if ($inBox.data("columnized") && numCols == $inBox.children().length) {
// return;
// }
if(numCols <= 1){
return singleColumnizeIt();
}
if($inBox.data("columnizing")) return;
$inBox.data("columnized", true);
$inBox.data("columnizing", true);
$inBox.empty();
$inBox.append($("<div style='width:" + (Math.floor(100 / numCols))+ "%; float: " + options.columnFloat + ";'></div>")); //"
$col = $inBox.children(":last");
$col.append($cache.clone());
maxHeight = $col.height();
$inBox.empty();
var targetHeight = maxHeight / numCols;
var firstTime = true;
var maxLoops = 3;
var scrollHorizontally = false;
if(options.overflow){
maxLoops = 1;
targetHeight = options.overflow.height;
}else if(optionHeight && optionWidth){
maxLoops = 1;
targetHeight = optionHeight;
scrollHorizontally = true;
}
//
// We loop as we try and workout a good height to use. We know it initially as an average
// but if the last column is higher than the first ones (which can happen, depending on split
// points) we need to raise 'adjustment'. We try this over a few iterations until we're 'solid'.
//
// also, lets hard code the max loops to 20. that's /a lot/ of loops for columnizer,
// and should keep run aways in check. if somehow someone has content combined with
// options that would cause an infinite loop, then this'll definitely stop it.
for(var loopCount=0;loopCount<maxLoops && maxLoops < 20;loopCount++){
$inBox.empty();
var $destroyable;
try{
$destroyable = $cache.clone(true);
}catch(e){
// jquery in ie6 can't clone with true
$destroyable = $cache.clone();
}
$destroyable.css("visibility", "hidden");
// create the columns
for (var i = 0; i < numCols; i++) {
/* create column */
var className = (i == 0) ? prefixTheClassName("first") : "";
className += " " + prefixTheClassName("column");
var className = (i == numCols - 1) ? (prefixTheClassName("last") + " " + className) : className;
$inBox.append($("<div class='" + className + "' style='width:" + (Math.floor(100 / numCols))+ "%; float: " + options.columnFloat + ";'></div>")); //"
}
// fill all but the last column (unless overflowing)
var i = 0;
while(i < numCols - (options.overflow ? 0 : 1) || scrollHorizontally && $destroyable.contents().length){
if($inBox.children().length <= i){
// we ran out of columns, make another
$inBox.append($("<div class='" + className + "' style='width:" + (Math.floor(100 / numCols))+ "%; float: " + options.columnFloat + ";'></div>")); //"
}
var $col = $inBox.children().eq(i);
columnize($col, $destroyable, $col, targetHeight);
// make sure that the last item in the column isn't a "dontend"
split($col, $destroyable, $col, targetHeight);
while($col.contents(":last").length && checkDontEndColumn($col.contents(":last").get(0))){
var $lastKid = $col.contents(":last");
$lastKid.remove();
$destroyable.prepend($lastKid);
}
i++;
//
// https://github.com/adamwulf/Columnizer-jQuery-Plugin/issues/47
//
// check for infinite loop.
//
// this could happen when a dontsplit or dontend item is taller than the column
// we're trying to build, and its never actually added to a column.
//
// this results in empty columns being added with the dontsplit item
// perpetually waiting to get put into a column. lets force the issue here
if($col.contents().length == 0 && $destroyable.contents().length){
//
// ok, we're building zero content columns. this'll happen forever
// since nothing can ever get taken out of destroyable.
//
// to fix, lets put 1 item from destroyable into the empty column
// before we iterate
$col.append($destroyable.contents(":first"));
}else if(i == numCols - (options.overflow ? 0 : 1) && !options.overflow){
//
// ok, we're about to exit the while loop because we're done with all
// columns except the last column.
//
// if $destroyable still has columnbreak nodes in it, then we need to keep
// looping and creating more columns.
if($destroyable.find(prefixTheClassName("columnbreak", true)).length){
numCols ++;
}
}
}
if(options.overflow && !scrollHorizontally){
var IE6 = false /*@cc_on || @_jscript_version < 5.7 @*/;
var IE7 = (document.all) && (navigator.appVersion.indexOf("MSIE 7.") != -1);
if(IE6 || IE7){
var html = "";
var div = document.createElement('DIV');
while($destroyable[0].childNodes.length > 0){
var kid = $destroyable[0].childNodes[0];
for(var i=0;i<kid.attributes.length;i++){
if(kid.attributes[i].nodeName.indexOf("jQuery") == 0){
kid.removeAttribute(kid.attributes[i].nodeName);
}
}
div.innerHTML = "";
div.appendChild($destroyable[0].childNodes[0]);
html += div.innerHTML;
}
var overflow = $(options.overflow.id)[0];
overflow.innerHTML = html;
}else{
$(options.overflow.id).empty().append($destroyable.contents().clone(true));
}
}else if(!scrollHorizontally){
// the last column in the series
$col = $inBox.children().eq($inBox.children().length-1);
while($destroyable.contents().length) $col.append($destroyable.contents(":first"));
var afterH = $col.height();
var diff = afterH - targetHeight;
var totalH = 0;
var min = 10000000;
var max = 0;
var lastIsMax = false;
var numberOfColumnsThatDontEndInAColumnBreak = 0;
$inBox.children().each(function($inBox){ return function($item){
var $col = $inBox.children().eq($item);
var endsInBreak = $col.children(":last").find(prefixTheClassName("columnbreak", true)).length;
if(!endsInBreak){
var h = $col.height();
lastIsMax = false;
totalH += h;
if(h > max) {
max = h;
lastIsMax = true;
}
if(h < min) min = h;
numberOfColumnsThatDontEndInAColumnBreak++;
}
}}($inBox));
var avgH = totalH / numberOfColumnsThatDontEndInAColumnBreak;
if(totalH == 0){
//
// all columns end in a column break,
// so we're done here
loopCount = maxLoops;
}else if(options.lastNeverTallest && lastIsMax){
// the last column is the tallest
// so allow columns to be taller
// and retry
//
// hopefully this'll mean more content fits into
// earlier columns, so that the last column
// can be shorter than the rest
adjustment += 30;
targetHeight = targetHeight + 30;
if(loopCount == maxLoops-1) maxLoops++;
}else if(max - min > 30){
// too much variation, try again
targetHeight = avgH + 30;
}else if(Math.abs(avgH-targetHeight) > 20){
// too much variation, try again
targetHeight = avgH;
}else {
// solid, we're done
loopCount = maxLoops;
}
}else{
// it's scrolling horizontally, fix the width/classes of the columns
var nonEmptyColumns = 0;
$inBox.children().each(function(i){
$col = $inBox.children().eq(i);
$col.width(optionWidth + "px");
if(i==0){
$col.addClass(prefixTheClassName("first"));
}else if(i==$inBox.children().length-1){
$col.addClass(prefixTheClassName("last"));
}else{
$col.removeClass(prefixTheClassName("first"));
$col.removeClass(prefixTheClassName("last"));
}
if($col.html() != ''){
nonEmptyColumns++;
}
});
if(nonEmptyColumns && options.resizeContainer){
$inBox.width(nonEmptyColumns * optionWidth + "px");
}else{
$inBox.width($inBox.children().length * optionWidth + "px");
}
}
$inBox.append($("<br style='clear:both;'>"));
}
$inBox.find(prefixTheClassName("column", true)).find(":first" + prefixTheClassName("removeiffirst", true)).remove();
$inBox.find(prefixTheClassName("column", true)).find(':last' + prefixTheClassName("removeiflast", true)).remove();
$inBox.data("columnizing", false);
if(options.overflow && options.overflow.doneFunc){
options.overflow.doneFunc();
}
options.doneFunc();
}
});
};
})(jQuery);