API Docs for: 1.0.0
Show:

File: src/gallery-layout/js/PageLayout.js

"use strict";

/**
 * Provides fluid layout for the content on a page.
 *
 * @module gallery-layout
 */

/**
 * Manages header (layout-hd), body (layout-bd), footer (layout-ft) stacked
 * vertically to either fit inside the viewport (fit-to-viewport) or adjust
 * to the size of the body content (fit-to-content).
 * 
 * The body content is sub-divided into modules, arranged either in rows or
 * columns.  The layout is automatically detected based on the marker
 * classes attached to the two layers of divs inside layout-bd:  either
 * layout-module-row > layout-module or layout-module-col > layout-module
 * 
 * Each module has an optional header (layout-m-hd), a body (layout-m-bd),
 * and an optional footer (layout-m-ft).  You can have multiple
 * layout-m-bd's, but only one can be visible at a time.  If you change the
 * DOM in any way that affects the height of any module header, body, or
 * footer, or if you switch bodies, you must call `elementResized()` to
 * reflow the layout.  (Technically, you do not have to call
 * `elementResized()` if you modify a module body in fit-to-viewport mode,
 * but if you later decide to switch to fit-to-content, your optimization
 * will cause trouble.)
 * 
 * If you want a row, column, or module to have a fixed size, add the class
 * layout-not-managed to the layout-module-row, layout-module-column, or
 * layout-module.  Then use CSS to set the width of layout-module (for a
 * row) or layout-module-col (for a col), or the height of layout-m-bd.
 * 
 * If the body content is a single module, it expands as the content
 * expands (fit-to-content) until it would push the footer below the fold.
 * Then it switches to fit-to-viewport so the scrollbar appears on the
 * module instead of the entire viewport.  (If you do not want this
 * behavior in a particular case, add the class FORCE_FIT to layout-bd.)
 * 
 * Note that a non-zero margin-top on the top element or a non-zero
 * margin-bottom on the bottom element inside any container will break the
 * layout because browsers lie about the total height of the container in
 * this case.  Use padding instead of margin on elements inside headers and
 * footers.
 * 
 * If you wish to display a loading message that automatically disappears
 * after the first time the layout is calculated, add the class
 * `layout-loading` to the div containing the message.  (To be visible,
 * this div must not be inside the div with class `layout-bd`, since that
 * has `visibility:hidden`.)
 *
 * @class PageLayout
 * @extends Base
 * @constructor
 * @param config {Object}
 */

function PageLayout()
{
	PageLayout.superclass.constructor.apply(this, arguments);
}

PageLayout.NAME = "pagelayout";

/**
 * @property FIT_TO_VIEWPORT
 * @static
 */
PageLayout.FIT_TO_VIEWPORT = 0;

/**
 * @property FIT_TO_CONTENT
 * @static
 */
PageLayout.FIT_TO_CONTENT = 1;

PageLayout.ATTRS =
{
	/**
	 * FIT_TO_VIEWPORT sizes the rows to fit everything inside the
	 * browser's viewport.  FIT_TO_CONTENT sizes the rows to eliminate all
	 * scrollbars on module bodies.  Note that you can configure this
	 * property by putting the CSS class "FIT_TO_VIEWPORT" or
	 * "FIT_TO_CONTENT" on layout-bd.
	 *
	 * @attribute mode
	 * @type PageLayout.FIT_TO_VIEWPORT or PageLayout.FIT_TO_CONTENT
	 * @default PageLayout.FIT_TO_VIEWPORT
	 */
	mode:
	{
		value:     PageLayout.FIT_TO_VIEWPORT,
		validator: function(value)
		{
			return (value === PageLayout.FIT_TO_VIEWPORT || value === PageLayout.FIT_TO_CONTENT);
		}
	},

	/**
	 * Minimum page width, measured in em's.  The page content will not
	 * collapse narrower than this width.  If the viewport is smaller, the
	 * brower's horizontal scrollbar will appear.
	 * 
	 * @attribute minWidth
	 * @type {Number} em's
	 * @default 73 (em) 950px @ 13px font
	 */
	minWidth:
	{
		value:     73,
		validator: function(value)
		{
			return (Y.Lang.isNumber(value) && value > 0);
		}
	},

	/**
	 * Minimum page height in FIT_TO_VIEWPORT mode, measured in em's.  The
	 * page content will not collapse lower than this height.  If the
	 * viewport is smaller, the brower's vertical scrollbar will appear.
	 * 
	 * @attribute minHeight
	 * @type {Number} em's
	 * @default 44 (em) 570px @ 13px font
	 */
	minHeight:
	{
		value:     44,
		validator: function(value)
		{
			return (Y.Lang.isNumber(value) && value > 0);
		}
	},

	/**
	 * In FIT_TO_CONTENT mode, set this to true to make the footer stick to
	 * the bottom of the viewport.  The default is for the footer to scroll
	 * along with the rest of the page content.
	 *
	 * @attribute stickyFooter
	 * @type {Boolean}
	 * @default false
	 */
	stickyFooter:
	{
		value:     false,
		validator: Y.Lang.isBoolean
	},

	/**
	 * When organizing modules into columns in FIT_TO_CONTENT mode, set
	 * this to false to allow each column to be a different height.
	 *
	 * @attribute matchColumnHeights
	 * @type {Boolean}
	 * @default true
	 */
	matchColumnHeights:
	{
		value:     true,
		validator: Y.Lang.isBoolean
	},

	/**
	 * Selector identifying the element which contains layout-(hd|bd|ft).
	 * This cannot be used to attach PageLayout to only part of the page.
	 * It should only be used when the page content is unavoidably embedded
	 * inside an element which fills the page.
	 * 
	 * @attribute body
	 * @type {String|Node}
	 * @default "body"
	 */
	body:
	{
		value:     'body',
		validator: function(value)
		{
			return (Y.Lang.isString(value) || value._node);
		}
	}
};

/**
 * @event beforeReflow
 * @description Fires before the layout is reflowed.
 */
/**
 * @event afterReflow
 * @description Fires after the layout is completely reflowed, including viewport scrollbar changes.
 */

/**
 * @event beforeExpandModule
 * @description Fires before a module is expanded.
 * @param bd {Node} the module body (layout-m-bd)
 */
/**
 * @event afterExpandModule
 * @description Fires after a module is expanded.
 * @param bd {Node} the module body (layout-m-bd)
 */

/**
 * @event beforeCollapseModule
 * @description Fires before a module is collapsed.
 * @param bd {Node} the module body (layout-m-bd)
 */
/**
 * @event afterCollapseModule
 * @description Fires after a module is collapsed.
 * @param bd {Node} the module body (layout-m-bd)
 */

/**
 * @event beforeResizeModule
 * @description Fires before a module is resized.
 * @param bd {Node} the module body (layout-m-bd)
 * @param height {Number} new height in pixels or "auto"
 * @param width {Number} new width in pixels or "auto"
 */
/**
 * @event afterResizeModule
 * @description Fires after a module is resized.
 * @param bd {Node} the module body (layout-m-bd)
 * @param height {Number} new height in pixels
 * @param width {Number} new width in pixels
 */

/**
 * @property fit_to_viewport_class
 * @type {String}
 * @default "FIT_TO_VIEWPORT"
 * @static
 */
PageLayout.fit_to_viewport_class = 'FIT_TO_VIEWPORT';

/**
 * @property fit_to_content_class
 * @type {String}
 * @default "FIT_TO_CONTENT"
 * @static
 */
PageLayout.fit_to_content_class = 'FIT_TO_CONTENT';

/**
 * @property force_fit_class
 * @type {String}
 * @default "FORCE_FIT"
 * @static
 */
PageLayout.force_fit_class = 'FORCE_FIT';

/**
 * @property page_header_class
 * @type {String}
 * @default "layout-hd"
 * @static
 */
PageLayout.page_header_class = 'layout-hd';

/**
 * @property page_body_class
 * @type {String}
 * @default "layout-bd"
 * @static
 */
PageLayout.page_body_class = 'layout-bd';

/**
 * @property page_footer_class
 * @type {String}
 * @default "layout-ft"
 * @static
 */
PageLayout.page_footer_class = 'layout-ft';

/**
 * @property module_rows_class
 * @type {String}
 * @value "layout-module-row"
 * @static
 */
PageLayout.module_rows_class = 'layout-module-row';

/**
 * @property module_cols_class
 * @type {String}
 * @value "layout-module-col"
 * @static
 */
PageLayout.module_cols_class = 'layout-module-col';

/**
 * @property module_class
 * @type {String}
 * @default "layout-module"
 * @static
 */
PageLayout.module_class = 'layout-module';

/**
 * @property module_header_class
 * @type {String}
 * @default "layout-m-hd"
 * @static
 */
PageLayout.module_header_class = 'layout-m-hd';

/**
 * @property module_body_class
 * @type {String}
 * @default "layout-m-bd"
 * @static
 */
PageLayout.module_body_class = 'layout-m-bd';

/**
 * @property module_footer_class
 * @type {String}
 * @default "layout-m-ft"
 * @static
 */
PageLayout.module_footer_class = 'layout-m-ft';

/**
 * @property not_managed_class
 * @type {String}
 * @default "layout-not-managed"
 * @static
 */
PageLayout.not_managed_class = 'layout-not-managed';

/**
 * @property collapse_vert_nub_class
 * @type {String}
 * @default "layout-vert-collapse-nub"
 * @static
 */
PageLayout.collapse_vert_nub_class = 'layout-vert-collapse-nub';

/**
 * @property collapse_left_nub_class
 * @type {String}
 * @default "layout-left-collapse-nub"
 * @static
 */
PageLayout.collapse_left_nub_class = 'layout-left-collapse-nub';

/**
 * @property collapse_right_nub_class
 * @type {String}
 * @default "layout-right-collapse-nub"
 * @static
 */
PageLayout.collapse_right_nub_class = 'layout-right-collapse-nub';

/**
 * @property expand_vert_nub_class
 * @type {String}
 * @default "layout-vert-expand-nub"
 * @static
 */
PageLayout.expand_vert_nub_class = 'layout-vert-expand-nub';

/**
 * @property expand_left_nub_class
 * @type {String}
 * @default "layout-left-expand-nub"
 * @static
 */
PageLayout.expand_left_nub_class = 'layout-left-expand-nub';

/**
 * @property expand_right_nub_class
 * @type {String}
 * @default "layout-right-expand-nub"
 * @static
 */
PageLayout.expand_right_nub_class = 'layout-right-expand-nub';

/**
 * @property collapsed_vert_class
 * @type {String}
 * @default "layout-collapsed-vert"
 * @static
 */
PageLayout.collapsed_vert_class = 'layout-collapsed-vert';

/**
 * @property collapsed_horiz_class
 * @type {String}
 * @default "layout-collapsed-horiz"
 * @static
 */
PageLayout.collapsed_horiz_class = 'layout-collapsed-horiz';

/**
 * @property min_module_height
 * @type {Number}
 * @default 10 (px)
 * @static
 */
PageLayout.min_module_height = 10; // px

PageLayout.unmanaged_size = -1; // smaller than any module size (collapsed size = - normal size)

var mode_regex          = /\bFIT_TO_[A-Z_]+/,
	row_height_class_re = /(?:^|\s)height:([0-9]+)%/,
	col_width_class_re  = /(?:^|\s)width:([0-9]+)%/,

	reflow_delay = 100, // ms

	plugin_info =
	{
		row:
		{
			module:     'gallery-layout-rows',
			plugin:     'PageLayoutRows',
			outer_size: row_height_class_re,
			inner_size: col_width_class_re
		},
		col:
		{
			module:     'gallery-layout-cols',
			plugin:     'PageLayoutCols',
			outer_size: col_width_class_re,
			inner_size: row_height_class_re
		}
	};
/*
	dd_group_name:            'satg-layout-dd-group',
	drag_target_class:        'satg-layout-dd-target',
	drag_nub_class:           'satg-layout-dragnub',
	module_header_drag_class: 'satg-layout-draggable',
	module_no_drag_class:     'satg-layout-drag-disabled',
	bomb_sight_class:         'satg-layout-bomb-sight satg-layout-bomb-sight-rows',

	the_dd_targets = {};
	the_dd_nubs    = {};
*/

function init()
{
	this.viewport =
	{
		w:   0,
		h:   0,
		bcw: 0
	};

	// find header, body, footer

	var page_blocks = Y.one(this.get('body')).get('children');

	var list = page_blocks.filter('.'+PageLayout.page_header_class);
	if (list.size() > 1)
	{
		throw Error('There must be at most one div with class ' + PageLayout.page_header_class);
	}
	this.header_container = (list.isEmpty() ? null : list.item(0));

	list = page_blocks.filter('.'+PageLayout.page_body_class);
	if (list.size() != 1)
	{
		throw Error('There must be exactly one div with class ' + PageLayout.page_body_class);
	}
	this.body_container = list.item(0);

	this.body_horiz_mbp = this.body_container.horizMarginBorderPadding();
	this.body_vert_mbp  = this.body_container.vertMarginBorderPadding();

	var m = this.body_container.get('className').match(mode_regex);
	if (m && m.length)
	{
		this.set('mode', PageLayout[ m[0] ]);
	}

	list = page_blocks.filter('.'+PageLayout.page_footer_class);
	if (list.size() > 1)
	{
		throw Error('There must be at most one div with class ' + PageLayout.page_footer_class);
	}
	this.footer_container = (list.isEmpty() ? null : list.item(0));

	Y.one(Y.config.win).on('resize', resize, this);

	updateFitClass.call(this);
	reparentFooter.call(this);
	this.rescanBody();

	// stay in sync

	this.after('modeChange', function()
	{
		updateFitClass.call(this);

		if (this.body_container)
		{
			this.body_container.scrollTop = 0;
		}

		reparentFooter.call(this);
		resize.call(this);
	});

	this.after('minWidthChange', resize);
	this.after('minHeightChange', resize);

	this.after('stickyFooterChange', function()
	{
		reparentFooter.call(this);
		resize.call(this);
	});

	this.after('matchColumnHeightsChange', resize);
}

/*
 * Normalize the list of sizes so they add up to 100%.
 */
function normalizeSizes(
	/* array */	list,
	/* regex */	pattern)
{
	// collect sizes

	var sizes = Y.map(list, function(module)
	{
		if (module.hasClass(PageLayout.not_managed_class))
		{
			return PageLayout.unmanaged_size;
		}

		var m = module.get('className').match(pattern);
		return (m && m.length ? parseInt(m[1], 10) : 0);
	});

	// analyze

	var info = Y.reduce(sizes, [0,0], function(value, size)
	{
		if (size > 0)
		{
			value[0] += size;
		}
		else if (size === 0)
		{
			value[1]++;
		}
		return value;
	});

	var sum = info[0], blank_count = info[1];

	// fill in blanks

	if (blank_count > 0)
	{
		var blank_size = Math.max((100 - sum) / blank_count, 10);

		sizes = Y.map(sizes, function(size)
		{
			return (size === 0 ? blank_size : size);
		});

		sum = Y.reduce(sizes, 0, function(sum, size, i)
		{
			return (size < 0 ? sum : sum + size);
		});
	}

	// normalize

	return Y.map(sizes, function(size)
	{
		return (size > 0 ? size * (100.0 / sum) : size);
	});
}

function updateFitClass()
{
	this.body_container.replaceClass('FIT_TO_(VIEWPORT|CONTENT)',
		this.get('mode') === PageLayout.FIT_TO_VIEWPORT ? 'FIT_TO_VIEWPORT' : 'FIT_TO_CONTENT');
}

function reparentFooter()
{
	if (!this.footer_container)
	{
		return;
	}

	if (this.get('mode') === PageLayout.FIT_TO_VIEWPORT || this.get('stickyFooter'))
	{
		this.body_container.get('parentNode').insertBefore(this.footer_container, this.body_container.next(function(node)
		{
			return node.get('tagName') != 'SCRIPT';
		}));
	}
	else
	{
		this.body_container.appendChild(this.footer_container);
	}
}

function resize()
{
	if (!this.layout_plugin || !this.body_container)
	{
		return;
	}

	// check if viewport changed

	var mode          = this.single_module ? Y.PageLayout.FIT_TO_VIEWPORT : this.get('mode');
	var sticky_footer = this.get('stickyFooter');

	this.body_container.setStyle('overflowX',
		mode === Y.PageLayout.FIT_TO_CONTENT ? 'auto' : 'hidden');
	this.body_container.setStyle('overflowY',
		mode === Y.PageLayout.FIT_TO_CONTENT ? 'scroll' : 'hidden');

	var viewport =
	{
		w: Y.DOM.winWidth(),
		h: Y.DOM.winHeight()
	};

	var resize_event = arguments[0] && arguments[0].type == 'resize';
	if (resize_event &&
		(viewport.w === this.viewport.w &&
		 viewport.h === this.viewport.h))
	{
		return;
	}

	this.viewport = viewport;

	this.fire('beforeReflow');	// after confirming that viewport really has changed

	saveScrollPositions.call(this);

	// set width of hd,bd,ft and height of bd

	var min_width  = Y.Node.emToPx(this.get('minWidth'));
	var body_width = Math.max(this.viewport.w, min_width);
	if (this.header_container)
	{
		this.header_container.setStyle('width', body_width+'px');
	}
	this.body_container.setStyle('width', (body_width - this.body_horiz_mbp)+'px');
	if (this.footer_container)
	{
		this.footer_container.setStyle('width', sticky_footer ? body_width+'px' : 'auto');
	}
	body_width = this.body_container.get('clientWidth') - this.body_horiz_mbp;

	this.viewport.bcw = this.body_container.get('clientWidth');

	var h     = this.viewport.h;
	var h_min = Y.Node.emToPx(this.get('minHeight'));
	if (mode === Y.PageLayout.FIT_TO_VIEWPORT && h < h_min)
	{
		h = h_min;
		Y.one(document.documentElement).setStyle('overflowY', 'auto');
	}
	else if (!window.console || !window.console.layout_force_viewport_scrollbars)	// remove inactive vertical scrollbar in IE
	{
		Y.one(document.documentElement).setStyle('overflowY', 'hidden');
	}

	if (this.header_container)
	{
		h -= this.header_container.get('offsetHeight');
	}
	if (this.footer_container &&
		(mode === Y.PageLayout.FIT_TO_VIEWPORT || sticky_footer))
	{
		h -= this.footer_container.get('offsetHeight');
	}

	if (mode === Y.PageLayout.FIT_TO_VIEWPORT)
	{
		var body_height = h - this.body_vert_mbp;
	}
	else if (h < 0)						// FIT_TO_CONTENT doesn't enforce min height
	{
		h = 10 + this.body_vert_mbp;	// arbitrary, positive number
	}

	this.body_container.setStyle('height', (h - this.body_vert_mbp)+'px');

	// resize modules

	this.layout_plugin.resize.call(this, mode, body_width, body_height);

	restoreScrollPositions.call(this);

	// show body and footer

	this.body_container.setStyle('visibility', 'visible');
	if (this.footer_container)
	{
		this.footer_container.setStyle('visibility', 'visible');
	}

	Y.later(100, this, checkViewportSize);
}

/*
 * Check if the viewport size has changed, usually due to the browser
 * removing no-longer-needed scrollbars.  If the viewport size is
 * stable, fires the afterReflow event.
 */
function checkViewportSize()
{
	if (Y.DOM.winWidth()                       != this.viewport.w ||
		Y.DOM.winHeight()                      != this.viewport.h ||
		this.body_container.get('clientWidth') != this.viewport.bcw)
	{
		resize.call(this);
	}
	else
	{
		this.fire('afterReflow');
	}
}

function saveScrollPositions()
{
	var outer_count = this.body_info.outers.size();
	for (var i=0; i<outer_count; i++)
	{
		var modules     = this.body_info.modules[i];
		var inner_count = modules.size();
		for (var j=0; j<inner_count; j++)
		{
			var module   = modules.item(j),
				children = this._analyzeModule(module);

			module._page_layout = !children.bd ? null :
			{
				children:     children,
				bdScrollTop:  children.bd.get('scrollTop'),
				bdScrollLeft: children.bd.get('scrollLeft')
			};
		}
	}
}

function restoreScrollPositions()
{
	var outer_count = this.body_info.outers.size();
	for (var i=0; i<outer_count; i++)
	{
		var modules     = this.body_info.modules[i];
		var inner_count = modules.size();
		for (var j=0; j<inner_count; j++)
		{
			var module = modules.item(j);
			if (module._page_layout)
			{
				var bd = module._page_layout.children.bd;
				bd.set('scrollTop', module._page_layout.bdScrollTop);
				bd.get('scrollLeft', module._page_layout.bdScrollLeft);
			};
		}
	}
}

/*
 * Expand the module containing the event target.
 */
function expandModule(
	/* event */	e)
{
	var node = e.currentTarget;

	function expand(
		/* string */	parent_class_name,
		/* string */	collapsed_class)
	{
		var p = node.getAncestorByClassName(this.layout_plugin.collapse_classes[parent_class_name]);
		if (p && p.hasClass(collapsed_class))
		{
			var children = this._analyzeModule(p);
			this.fire('beforeExpandModule', { bd: children.bd });

			p.removeClass(collapsed_class);
			resize.call(this);

			this.fire('afterExpandModule', { bd: children.bd });
		}
	}

	if (node.hasClass(PageLayout.expand_vert_nub_class))
	{
		expand.call(this, 'vert_parent_class', PageLayout.collapsed_vert_class);
	}
	else
	{
		expand.call(this, 'horiz_parent_class', PageLayout.collapsed_horiz_class);
	}
}

/*
 * Collapse the module containing the event target.
 */
function collapseModule(
	/* event */	e)
{
	var node = e.currentTarget;

	function collapse(
		/* string */	parent_class_name,
		/* string */	collapsed_class)
	{
		var p = node.getAncestorByClassName(this.layout_plugin.collapse_classes[parent_class_name]);
		if (p && !p.hasClass(collapsed_class))
		{
			var children = this._analyzeModule(p);
			this.fire('beforeCollapseModule', { bd: children.bd });

			p.addClass(collapsed_class);
			resize.call(this);

			this.fire('afterCollapseModule', { bd: children.bd });
		}
	}

	if (node.hasClass(PageLayout.collapse_vert_nub_class))
	{
		collapse.call(this, 'vert_parent_class', PageLayout.collapsed_vert_class);
	}
	else
	{
		collapse.call(this, 'horiz_parent_class', PageLayout.collapsed_horiz_class);
	}
}

Y.extend(PageLayout, Y.Base,
{
	initializer: function()
	{
		Y.on('domready', init, this);
	},

	/**
	 * Call this after manually adding or removing modules on the page.
	 * 
	 * @method rescanBody
	 */
	rescanBody: function()
	{
		Y.detach('PageLayoutCollapse|click');

		this.body_info =
		{
			outers:      [],
			modules:     [],	// list of modules inside each row
			outer_sizes: [],	// list of percentages
			inner_sizes: []		// list of lists of percentages
		};

		var outer_list  = this.body_container.all('div.' + PageLayout.module_rows_class);
		var plugin_data = plugin_info.row;
		if (outer_list.isEmpty())
		{
			outer_list  = this.body_container.all('div.' + PageLayout.module_cols_class);
			plugin_data = plugin_info.col;
		}
		if (outer_list.isEmpty())
		{
			throw Error('There must be at least one ' + PageLayout.module_rows_class + ' or ' + PageLayout.module_cols_class + ' inside ' + PageLayout.page_body_class + '.');
		}
		this.body_info.outers = outer_list;

		var collapse_nub_pattern =
			'(' +
			PageLayout.collapse_vert_nub_class + '|' +
			PageLayout.collapse_left_nub_class + '|' +
			PageLayout.collapse_right_nub_class +
			')';

		var expand_nub_pattern =
			'(' +
			PageLayout.expand_vert_nub_class + '|' +
			PageLayout.expand_left_nub_class + '|' +
			PageLayout.expand_right_nub_class +
			')';

		var row_count = this.body_info.outers.size();
		Y.each(this.body_info.outers, function(row)
		{
			var row_id = row.generateID();
			this.body_info.outer_sizes.push(100.0/row_count);

			var list = row.all('div.' + PageLayout.module_class);
			if (list.isEmpty())
			{
				this.body_info.outers  = [];
				this.body_info.modules = [];
				throw Error('There must be at least one ' + PageLayout.module_class + ' inside ' + PageLayout.module_rows_class + '.');
			}

			this.body_info.modules.push(list);

			Y.each(list, function(module)
			{
				var nub = module.getFirstElementByClassName(collapse_nub_pattern);
				if (nub)
				{
					nub.on('PageLayoutCollapse|click', collapseModule, this);
				}

				nub = module.getFirstElementByClassName(expand_nub_pattern);
				if (nub)
				{
					nub.on('PageLayoutCollapse|click', expandModule, this);
				}
			},
			this);
/*
			if (PageLayoutDDProxy)
			{
				var has_nubs = false;
				Y.each(list, function(module)
				{
					var id = module.generateID();
					module.removeClass(PageLayout.module_no_drag_class);

					if (the_dd_nubs[id])
					{
						has_nubs = (the_dd_nubs[id] != 'none');
					}
					else
					{
						var nub = module.getFirstElementByClassName(PageLayout.drag_nub_class);
						if (nub)
						{
							var children = this._analyzeModule(module);
							if (children.hd)
							{
								children.hd.addClass(PageLayout.module_header_drag_class);
								the_dd_nubs[id] =
									new PageLayoutDDProxy(this, id, children.hd, PageLayout.dd_group_name);
								has_nubs = true;
							}
						}

						if (!the_dd_nubs[id])
						{
							the_dd_nubs[id] = 'none';
						}
					}
				},
				this);

				if (!the_dd_targets[ row_id ] &&
					(has_nubs || row.hasClass(PageLayout.drag_target_class)))
				{
					the_dd_targets[ row_id ] = new DDTarget(row_id, PageLayout.dd_group_name);
				}

				if (list.size() == 1)
				{
					list.item(0).addClass(PageLayout.module_no_drag_class);
				}
			}
*/
			this.body_info.inner_sizes.push(
				normalizeSizes(list, plugin_data.inner_size));
		},
		this);

		this.body_info.outer_sizes =
			normalizeSizes(this.body_info.outers, plugin_data.outer_size);

		this.single_module = false;
		if (this.body_info.outers.size() == 1 && this.body_info.modules[0].size() == 1 &&
			!this.body_container.hasClass(PageLayout.force_fit_class))
		{
			plugin_data        = plugin_info.row;
			this.single_module = true;
		}

		var self = this;
		Y.use(plugin_data.module, function(Y)
		{
			Y.all('div.layout-loading').each(function(n)
			{
				n.setStyle('display', 'none');
			});

			self.layout_plugin = Y[ plugin_data.plugin ];
			updateFitClass.call(self);	// plugin may modify it
			resize.call(self);
		});
	},

	/**
	 * @method getHeaderHeight
	 * @return {Number} the height of the sticky header in pixels
	 */
	getHeaderHeight: function()
	{
		return (this.header_container ? this.header_container.get('offsetHeight') : 0);
	},

	/**
	 * @method getHeaderContainer
	 * @return {Node} the header container (layout-hd) or null if there is no header
	 */
	getHeaderContainer: function()
	{
		return this.header_container;
	},

	/**
	 * @method getBodyHeight
	 * @return {Number} the height of the scrolling body in pixels
	 */
	getBodyHeight: function()
	{
		return this.body_container.get('offsetHeight');
	},

	/**
	 * @method getBodyContainer
	 * @return {Node} the body container (layout-bd)
	 */
	getBodyContainer: function()
	{
		return this.body_container;
	},

	/**
	 * @method getFooterHeight
	 * @return {Number} the height of the sticky footer in pixels or zero if the footer is not sticky
	 */
	getFooterHeight: function()
	{
		return (this.get('stickyFooter') && this.footer_container ?
				this.footer_container.get('offsetHeight') : 0);
	},

	/**
	 * @method getFooterContainer
	 * @return {Node} the footer container (layout-ft), or null if there is no footer
	 */
	getFooterContainer: function()
	{
		return this.footer_container;
	},

	/**
	 * @method moduleIsCollapsed
	 * @param node {String|Node} .layout-module
	 * @return {Boolean} true if module is collapsed
	 */
	moduleIsCollapsed: function(
		/* string/Node */	node)
	{
		var collapsed_pattern =
			'(' +
			PageLayout.collapsed_horiz_class + '|' +
			PageLayout.collapses_vert_class +
			')';

		node = Y.one(node);
		if (node.getFirstElementByClassName(this.layout_plugin.collapse_classes.collapse_parent_pattern))
		{
			node = node.get('parentNode');
		}

		return node.hasClass(collapsed_pattern);
	},

	/**
	 * Expand the specified module.
	 * 
	 * @method expandModule
	 * @param node {String|Node} .layout-module
	 */
	expandModule: function(
		/* string/Node */	node)
	{
		node    = Y.one(node);
		var nub = node.getFirstElementByClassName(PageLayout.expand_vert_nub_class);
		if (!nub)
		{
			var expand_horiz_nub_pattern =
				'(' +
				PageLayout.expand_left_nub_class + '|' +
				PageLayout.expand_right_nub_class +
				')';

			nub = node.getFirstElementByClassName(expand_horiz_nub_pattern);
		}

		if (nub)
		{
			expandModule.call(this, { currentTarget: nub });
		}
	},

	/**
	 * Collapse the specified module.
	 * 
	 * @method collapseModule
	 * @param node {String|Node} .layout-module
	 */
	collapseModule: function(
		/* string/Node */	node)
	{
		node    = Y.one(node);
		var nub = node.getFirstElementByClassName(PageLayout.collapse_vert_nub_class);
		if (!nub)
		{
			var collapse_horiz_nub_pattern =
				'(' +
				PageLayout.collapse_left_nub_class + '|' +
				PageLayout.collapse_right_nub_class +
				')';

			nub = node.getFirstElementByClassName(collapse_horiz_nub_pattern);
		}

		if (nub)
		{
			collapseModule.call(this, { currentTarget: nub });
		}
	},

	/**
	 * Toggle the collapsed state of the specified layout-module.
	 * 
	 * @method toggleModule
	 * @param module {String|Node} .layout-module
	 */
	toggleModule: function(
		/* string/Node */	module)
	{
		module = Y.one(module);	// optimization
		if (this.moduleIsCollapsed(module))
		{
			this.expandModule(module);
		}
		else
		{
			this.collapseModule(module);
		}
	},

	/**
	 * Call this when something changes size, to request a reflow of the
	 * layout.
	 * 
	 * @method elementResized
	 * @param el {String|Node} element that changed size
	 * @return {Boolean} true if the element is inside the managed containers
	 */
	elementResized: function(
		/* string/Node */	el)
	{
		el = Y.one(el);

		if ((this.header_container && this.header_container.contains(el)) ||
			(this.body_container && this.body_container.contains(el)) ||
			(this.footer_container && this.footer_container.contains(el)))
		{
			if (this.refresh_timer)
			{
				this.refresh_timer.cancel();
			}

			var t1 = (new Date()).getTime();
			this.refresh_timer = Y.later(reflow_delay, this, function()
			{
				this.refresh_timer = null;

				// if JS is really busy, wait a bit longer

				var t2 = (new Date()).getTime();
				if (t2 > t1 + 2*reflow_delay)
				{
					Y.log('deferred reflow: ' + (t2-t1), 'info', 'layout');
					this.elementResized(el);
				}
				else
				{
					resize.call(this);
				}
			});

			return true;
		}
		else
		{
			return false;
		}
	},

	/**
	 * Returns the components of the module.
	 * 
	 * @method _analyzeModule
	 * @private
	 * @param root {Node} .layout-module
	 * @return {Object} root,hd,bd,ft
	 */
	_analyzeModule: function(
		/* node */	root)
	{
		var result =
		{
			root: root,
			hd:   null,
			bd:   null,
			ft:   null
		};

		// two step process avoid scanning into the module body

		var bd = root.one('.'+PageLayout.module_body_class);
		if (!bd)
		{
			return result;
		}

		var list = bd.siblings().filter('.'+PageLayout.module_body_class);
		list.unshift(bd);
		result.bd = list.find(function(n)
		{
			return (n.get('offsetWidth') > 0);
		});
		if (!result.bd)
		{	
			result.bd = bd;
		}

		if (result.bd)
		{
			result.hd = result.bd.siblings().filter('.'+PageLayout.module_header_class).item(0);
			result.ft = result.bd.siblings().filter('.'+PageLayout.module_footer_class).item(0);
		}

		return result;
	},

	/**
	 * Set the width of a module.
	 * 
	 * @method _setWidth
	 * @private
	 * @param children {Object} root,hd,bd,ft
	 * @param w {Number} width in pixels
	 */
	_setWidth: function(
		/* object */	children,
		/* int */		w)
	{
		children.root.setStyle('width', w+'px');
	}
});

Y.PageLayout = PageLayout;