c9-core/plugins/c9.ide.ui/docklayout.js

1850 wiersze
70 KiB
JavaScript

define(function(require, exports, module) {
main.consumes = ["Plugin", "util", "layout"];
main.provides = ["DockableLayout", "DockableWidget", "DockableAbsoluteRenderer"];
return main;
function main(options, imports, register) {
var Plugin = imports.Plugin;
var util = imports.util;
var layout = imports.layout;
var event = require("ace/lib/event");
/***** Initialization *****/
var counter = 0;
/*
@todo mix flex/percentage/fixed
- resizeTo - use split
- hsplit/vsplit - always leave existing flex col
(where widget is) and create new fixed col.
- [DONE] clean - use merge
@todo [Harutyun] Support fixed width widgets - preserving their width/height when dragging/dropping
@todo Add constraints such as min-width/max-width min-height/max-height, container size
@todo Move to cell should give an as big an area as possible (multiple cells)
BUGS:
@todo What does this mean? http://screencast.com/t/vi8WMm6nc20z
@todo Moving filters to split values in the bottom doesnt resize values
@todo Off by padding when resizing
@todo Steps:
- Resize grid
- Move values to below grid
- Move Columns to right side of values
*/
function DockableLayout(developer, deps, options) {
var plugin = new Plugin(developer, deps);
var emit = plugin.getEmitter();
var parent = options.parent;
var renderer = options.renderer;
var model = [];
var renderId = 0;
var widgets = [];
var edge = [0, 0, 0, 0];
var paused = false;
var container, columns, rows, changes, pending, padding;
var parentLayout;
var CHANGE_INIT = 1;
var CHANGE_COLUMNS = 2;
var CHANGE_ROWS = 4;
var CHANGE_REDRAW = 7;
var CHANGE_RESIZE = 6;
var loaded = false;
function load() {
if (loaded) return false;
loaded = true;
if (parent)
attachToParent(parent);
}
function attachToParent(parent) {
container = document.createElement("div");
parent.appendChild(container);
container.style.position = "absolute";
container.style.left = "0";
container.style.top = "0";
container.style.right = "0";
container.style.bottom = "0";
// Start Render Loop
schedule(CHANGE_REDRAW);
// Hook resize
layout.on("resize", resize, plugin);
}
function applyChanges(){
if (changes && !paused) {
var changeset = calculate(changes);
render(changeset);
changes = 0;
}
}
var inited = false;
function schedule(change) {
changes = changes | change;
if (paused || pending) return;
pending = true;
util.nextFrame(function(){
if (inited)
emit("change");
inited = true;
pending = false;
applyChanges();
});
}
/***** Methods *****/
function initModel(){
model = [];
if (rows && rows.length && columns && columns.length) {
for (var i = 0; i < columns.length; i++) {
model[i] = [];
for (var j = 0; j < rows.length; j++) {
model[i][j] = null;
}
}
}
}
function getWidgetAtPoint(x, y) {
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i];
var computed = widget.computed;
if (computed.top < y && computed.top + computed.height > y
&& computed.left < x && computed.left + computed.width > x) {
return widget;
}
}
var rect = container.getBoundingClientRect();
// Make sure x/y are in the layout
if (x < rect.width && y < rect.height) {
var total = edge[3];
var pos = {
widget: { empty: true, rowspan: 1, colspan: 1 },
layout: plugin,
insertionPoint: "full"
};
// Find row/col
columns.every(function(c, i) {
if (total + c.computed > x) {
pos.left = total;
pos.width = c.computed;
pos.widget.col = i;
return false;
}
else {
total += c.computed + padding;
return true;
}
});
total = edge[0];
rows.every(function(c, i) {
if (total + c.computed > y) {
pos.top = total;
pos.height = c.computed;
pos.widget.row = i;
return false;
}
else {
total += c.computed + padding;
return true;
}
});
if (model[pos.widget.col] && model[pos.widget.col][pos.widget.row])
return model[pos.widget.col][pos.widget.row];
return fill(pos);
}
}
function fill(pos) {
// Expand to empty space
var start, passed, end;
var col = pos.widget.col;
var row = pos.widget.row;
for (var i = 0; i < model.length; i++) {
if (!model[i][row]) {
if (start === undefined)
start = i;
if (i == col)
passed = true;
}
else {
if (!passed)
start = undefined;
else {
end = i - 1;
break;
}
}
}
if (!end) end = i - 1;
function searchRows(i) {
var rstart, rend;
// Before Row
for (var j = row; j >= 0; j--) {
if (!model[i][j])
rstart = j;
else
break;
}
// After Row
for (var j = row; j < rows.length; j++) {
if (!model[i][j])
rend = j;
else
break;
}
return { start: rstart, end: rend };
}
var info, rstart = 0, rend = 900;
for (var i = start; i <= end; i++) {
info = searchRows(i);
rstart = Math.max(info.start, rstart);
rend = Math.min(info.end, rend);
}
pos.widget.col = start;
pos.widget.colspan = end - start + 1;
pos.widget.row = rstart;
pos.widget.rowspan = rend - rstart + 1;
var left = edge[3], width = 0;
for (var i = 0; i <= end; i++) {
if (i < start)
left += columns[i].computed + padding;
else
width += columns[i].computed + padding;
}
width -= padding;
var top = edge[0], height = 0;
for (var i = 0; i <= rend; i++) {
if (i < rstart)
top += rows[i].computed + padding;
else
height += rows[i].computed + padding;
}
height -= padding;
pos.left = left;
pos.top = top;
pos.width = width;
pos.height = height;
return pos;
}
/**
* This is now a simple algorithm that simply allows a widget to be
* inserted at a side of the widget that it is hovering above.
* In the future this should be expanded to a more complex algo
* where the full spectrum of insertion points is addressed.
* @todo make this pluggable
*/
function getInsertionPoint(widget, x, y) {
// Expand into parent layout
if (x < 0 || y < 0) { // @todo beyond max size
if (parentLayout) {
var r1 = plugin.container.getBoundingClientRect();
var r2 = parentLayout.container.getBoundingClientRect();
return parentLayout.getInsertionPoint(widget,
x + (r1.left - r2.left), y + (r1.top - r2.top));
}
return false;
}
// Find widget in this layout
var targetWidget = getWidgetAtPoint(x, y);
if (!targetWidget)
return false;
if (targetWidget.widget)
return targetWidget; // Which is actually pos
var target = targetWidget.computed;
var pos, widgetSize;
// Dive into child layout
if (targetWidget.innerLayout && targetWidget != widget) {
pos = targetWidget.innerLayout.getInsertionPoint(widget,
x - target.left, y - target.top);
if (pos)
return pos;
}
pos = { widget: targetWidget, layout: plugin };
var xdiff = x - target.left;
var ydiff = y - target.top;
var xfar = target.width / 2 < xdiff ? target.width - xdiff : false;
var yfar = target.height / 2 < ydiff ? target.height - ydiff : false;
var xshort = (xfar !== false ? xfar : xdiff) / target.width;
var yshort = (yfar !== false ? yfar : ydiff) / target.height;
if (xshort < yshort) {
pos.width = (target.width - padding) / 2; //widget.computed.width; //
pos.height = target.height;
pos.width = clip(pos.width, widget.minWidth, widget.maxWidth);
pos.height = clip(pos.height, widget.minHeight, widget.maxHeight);
pos.isVertical = true;
widgetSize = widget.getPreferedSize(pos);
if (widgetSize) {
pos.width = widgetSize.width || pos.width;
pos.height = widgetSize.height || pos.height;
}
pos.left = xfar === false ? target.left :
target.left + (target.width - pos.width);
pos.top = target.top;
pos.insertionPoint = xfar === false ? "x0" : "x1";
}
else {
pos.width = target.width;
pos.height = (target.height - padding) / 2; //widget.computed.height; //
pos.width = clip(pos.width, widget.minWidth, widget.maxWidth);
pos.height = clip(pos.height, widget.minHeight, widget.maxHeight);
pos.isVertical = false;
widgetSize = widget.getPreferedSize(pos);
if (widgetSize) {
pos.width = widgetSize.width || pos.width;
pos.height = widgetSize.height || pos.height;
}
pos.left = target.left;
pos.top = yfar === false ? target.top :
target.top + (target.height - pos.height);
pos.insertionPoint = yfar === false ? "y0" : "y1";
}
widget.computed.lastDragPosition = pos;
return pos;
}
function insertWidget(widget, pos) {
// Remove widget from layout
if (widget.layout) {
var other = widget.layout != plugin && widget.layout;
widget.layout.remove(widget);
if (other)
other.clean();
}
var args, info;
var point = pos.insertionPoint;
if (point == "x0") {
// Add widget to this column
args = [widget, pos.widget.col,
pos.widget.row, 1, pos.widget.rowspan];
info = hsplit(pos.widget, pos.width);
args[1] = info.col;
args[3] = info.colspan;
}
else if (point == "x1") {
// Add widget to this column
args = [widget, pos.widget.col + pos.widget.colspan,
pos.widget.row, 1, pos.widget.rowspan];
info = hsplit(pos.widget, pos.width, true);
args[1] = info.col;
args[3] = info.colspan;
}
else if (point == "y0") {
// Add widget to this row
args = [widget, pos.widget.col,
pos.widget.row, pos.widget.colspan, 1];
info = vsplit(pos.widget, pos.height);
args[2] = info.row;
args[4] = info.rowspan;
}
else if (point == "y1") {
// Add widget to this row
args = [widget, pos.widget.col, pos.widget.row
+ pos.widget.rowspan, pos.widget.colspan, 1];
info = vsplit(pos.widget, pos.height, true);
args[2] = info.row;
args[4] = info.rowspan;
}
else if (point == "full") {
// Add widget to this row
args = [widget, pos.widget.col, pos.widget.row,
pos.widget.colspan, pos.widget.rowspan];
}
// Move dragged widget to it's new location
moveTo.apply(this, args);
// Remove Unused Columns & Rows
clean();
schedule(CHANGE_REDRAW);
}
function vsplit(widget, height, far) {
var result, start, i;
function innerloop(){
if (rows[i].pixels < height) {
height -= rows[i].pixels;
}
else if (rows[i].pixels == height) {
if (widget.rowspan == 1) throw new Error();
// Move split widget
moveTo(widget, widget.col, widget.row + (far ? 0 : 1),
widget.colspan, i - widget.row + (far ? -1 : 1));
return { row: i, rowspan: start - i + 1 };
}
else {
// Create a row
insertRow(height, i + (far ? 1 : 0), far);
// Move split widget
moveTo(widget, widget.col, widget.row + (far ? 0 : 1),
widget.colspan, i - widget.row + 1);
return { row: i + (far ? 1 : 0), rowspan: start - i + 1 };
}
}
if (far) {
// Find Row number and height
start = widget.row + widget.rowspan - 1;
for (i = start; i >= widget.row; i--) {
result = innerloop();
if (result) return result;
}
}
else {
// Find Row number and height
start = widget.row;
for (i = start; i < widget.row + widget.rowspan; i++) {
result = innerloop();
if (result) return result;
}
}
}
function hsplit(widget, width, far) {
var result, start, i;
function innerloop(){
if (columns[i].pixels < width) {
width -= columns[i].pixels;
}
else if (columns[i].pixels == width) {
if (widget.colspan == 1) throw new Error();
// Move split widget
moveTo(widget, widget.col + (far ? 0 : 1), widget.row,
i - widget.col + (far ? -1 : 1), widget.rowspan);
return { col: i, colspan: start - i + 1 };
}
else {
// Create a col
insertColumn(width, i + (far ? 1 : 0), far);
// Move split widget
moveTo(widget, widget.col + (far ? 0 : 1), widget.row,
i - widget.col + 1, widget.rowspan);
return { col: i + (far ? 1 : 0), colspan: start - i + 1 };
}
}
if (far) {
// Find Column number and height
start = widget.col + widget.colspan - 1;
for (i = start; i >= widget.col; i--) {
result = innerloop();
if (result) return result;
}
}
else {
// Find Column number and height
start = widget.col;
for (i = start; i < widget.col + widget.colspan; i++) {
result = innerloop();
if (result) return result;
}
}
}
function insertColumn(width, index, copyFromLeft) {
// Add to columns
columns.splice(index, 0, getRowColEntry(width));
// Move & Resize items
if (model[index]) {
var newcol = [], done = {};
model[index - (copyFromLeft ? 1 : 0)].forEach(function(widget) {
newcol.push(widget);
if (widget && !done[widget.name]) {
done[widget.name] = true;
widget.colspan++;
}
});
model.splice(index, 0, newcol);
// Move all subsequent widgets to column + 1
for (var i = index + (copyFromLeft ? 1 : 2); i < model.length; i++) {
model[i].forEach(function(widget) {
if (widget && !done["s" + widget.name] && widget.col == i - 1) {
widget.col++;
done["s" + widget.name] = true;
}
});
}
}
else {
model[index] = [];
rows.forEach(function(n, i) {
model[index][i] = null;
});
// @todo expand fill?
}
schedule(CHANGE_COLUMNS);
}
function removeColumn(index) {
var col = model[index];
// Move & Resize items
if (col.length) {
var done = {};
col.forEach(function(widget) {
if (!widget || done[widget.name]) return;
if (widget.colspan == 1)
remove(widget);
else
widget.colspan--;
done[widget.name] = true;
});
// Move all subsequent widgets to column - 1
for (var i = index + 1; i < model.length; i++) {
model[i].forEach(function(widget) {
if (widget && !done["s" + widget.name] && widget.col == i) {
widget.col--;
done["s" + widget.name] = true;
}
});
}
}
// Remove from model
model.splice(index, 1);
// Remove from columns
columns.splice(index, 1);
schedule(CHANGE_COLUMNS);
}
function insertRow(height, index, copyFromTop) {
// Add to rows
rows.splice(index, 0, getRowColEntry(height));
// Move & Resize items
var done = {};
model.forEach(function(col) {
var widget = col[index - (copyFromTop ? 1 : 0)];
col.splice(index, 0, widget || null);
if (widget) {
if (!done[widget.name]) {
done[widget.name] = true;
widget.rowspan++;
}
}
// Move all subsequent widgets to row + 1
for (var i = index + (copyFromTop ? 1 : 2); i < col.length; i++) {
widget = col[i];
if (widget && !done["s" + widget.name] && widget.row == i - 1) {
widget.row++;
done["s" + widget.name] = true;
}
}
});
schedule(CHANGE_ROWS);
}
function removeRow(index) {
// Move & Resize items
var done = {};
model.forEach(function(col) {
var widget = col[index];
if (widget) {
if (!done[widget.name]){
if (widget.rowspan == 1)
remove(widget);
else
widget.rowspan--;
done[widget.name] = true;
}
}
// Move all subsequent widgets to row + 1
for (var i = index + 1; i < col.length; i++) {
widget = col[i];
if (widget && !done["s" + widget.name] && widget.row == i) {
widget.row--;
done["s" + widget.name] = true;
}
}
// Remove from model
col.splice(index, 1);
});
// Remove from rows
rows.splice(index, 1);
schedule(CHANGE_ROWS);
}
function merge(name, indexFrom, indexTo) {
var defSet = name == "columns" ? columns : rows;
var from = defSet[indexFrom];
var to = defSet[indexTo];
// Keep pixels
if (to.pixels && from.pixels) {
to.pixels += from.pixels + padding;
}
// Keep flex
else if (to.flex || from.flex) {
// Simple case, both have flex
if (to.flex && from.flex)
to.flex += from.flex;
else {
var flex = defSet.filter(function(n){ return n.flex });
if (!to.flex) {
delete to.pixels;
delete to.percentage;
}
// There is only 1 flex item (so missing px/%
// will be moved to this one automatically)
if (flex.length === 1) {
if (!to.flex)
to.flex = from.flex;
}
// Lets convert the px/% into flex units
else {
var flextotal = 0;
flex.forEach(function(n) {
if (n != to && n != from)
flextotal += n.flex;
});
// 200,200,200,200: 1,1,1,1
// 300,166,166,166: 1.5,0.83,0.83,0.83
var f, p, a;
if (to.flex)
f = to.flex, p = to.computed, a = from.computed;
else
f = from.flex, p = from.computed, a = to.computed;
var newflex = (f / p) * (p + a);
var delta = (newflex - f) / flextotal;
flex.forEach(function(n) {
if (n != to && n != from)
n.flex -= delta;
});
to.flex = f + delta;
}
}
}
// Keep percentage
else if (to.percentage || from.percentage) {
if (to.percentage && from.percentage) {
to.percentage += from.percentage;
}
else {
// @todo unused right now
}
}
if (name == "columns")
removeColumn(indexFrom);
else
removeRow(indexFrom);
}
function clean(){
var empty = rows.map(function(){ return true; });
for (var i = model.length - 1; i >= 0; i--) {
var col = model[i];
var prev = model[i - 1];
// Unused columns
var unused = prev && col.every(function(n, j) {
// return !n && !prev[j] || n && n.colspan > 1 && n.col != i;
return n == prev[j];
});
if (unused)
merge("columns", i, i - 1);
// Unused rows
col.forEach(function(n, j) {
if (!empty[j]) return;
// if (n && (n.rowspan == 1 || n.row == j))
if (empty[j] && col[j - 1] != n)
empty[j] = false;
// if (!n && !col[j - 1])
// empty[j] = false;
});
}
// Unused rows
for (var i = empty.length; i > 0; i--) {
if (empty[i])
merge("rows", i, i - 1);
}
}
function serializeRowCol(m) {
if (m.pixels)
return m.pixels + "px";
if (m.flex)
return m.flex;
if (m.percentage)
return m.percentage + "%";
}
function getRowColEntry(m) {
if (typeof m == "number") {
// if (m < 0) throw new Error("Invalid column size");
return { pixels: m };
}
else if (parseFloat(m) == m) // Flex
return { flex: parseFloat(m) };
else if (m.indexOf("%") > -1) // Percentage
return { percentage: parseFloat(m) / 100 };
else if (m.indexOf("px") > -1) // Pixels
return { pixels: parseInt(m, 10) };
else throw new Error("Invalid Row/Column Configuration");
}
function calculate(changes) {
renderId++;
if (renderId > 30000)
renderId = 0;
// Get width/height of container
var width = container.offsetWidth
- edge[1] - edge[3] - ((columns.length - 1) * padding);
var height = container.offsetHeight
- edge[2] - edge[0] - ((rows.length - 1) * padding);
if (!width || !height) {
console.warn("Invalid size of dockable layout");
return [];
}
var flexCols = [], totalCols = 0, totalColFlex = 0;
var flexRows = [], totalRows = 0, totalRowFlex = 0;
// Calc width of each col
var col;
for (var i = 0; i < columns.length; i++) {
col = columns[i];
if (col.flex) {
flexCols.push(col);
totalColFlex += col.flex;
}
else {
col.computed = col.pixels || col.percentage * width;
totalCols += col.computed;
}
}
flexCols.forEach(function(col) {
col.computed = (width - totalCols) / totalColFlex * col.flex;
});
// Calc height of each row
var row;
for (var i = 0; i < rows.length; i++) {
row = rows[i];
if (row.flex) {
flexRows.push(row);
totalRowFlex += row.flex;
}
else {
row.computed = row.pixels || row.percentage * height;
totalRows += row.computed;
}
}
flexRows.forEach(function(row) {
row.computed = (height - totalRows) / totalRowFlex * row.flex;
});
var changed = [];
// Loop over elements and calc new width/height, left/top
var curwidth = edge[3];
var widget, computed, curheight, wchanged, hasChanges;
for (var i = 0, li = model.length; i < li; i++) {
col = model[i];
curheight = edge[0];
if (i > 0)
curwidth += padding;
for (var k, j = 0, lj = col.length; j < lj; j++) {
widget = col[j];
computed = widget && widget.computed;
if (!computed || computed.renderId == renderId) {
curheight += (computed
? computed.height
: rows[j].computed) + padding;
continue;
}
computed.renderId = renderId;
if (changes & CHANGE_INIT) {
computed.left =
computed.top =
computed.width =
computed.height = null;
}
wchanged = { widget: widget };
hasChanges = false;
if (computed.left != curwidth) {
wchanged.left = computed.left = curwidth;
hasChanges = true;
}
if (computed.top != curheight) {
wchanged.top = computed.top = curheight;
hasChanges = true;
}
if (changes & CHANGE_COLUMNS) {
width = 0;
for (k = 0; k < widget.colspan; k++) {
width += columns[i + k].computed
+ (k > 0 ? padding : 0);
}
if (computed.width != width) {
wchanged.width = computed.width = width;
hasChanges = true;
}
}
height = 0;
for (k = 0; k < widget.rowspan; k++) {
height += rows[j + k].computed
+ (k > 0 ? padding : 0);
}
j += k - 1; //Lets skip the next row items
if (changes & CHANGE_ROWS) {
if (computed.height != height) {
wchanged.height = computed.height = height;
hasChanges = true;
}
}
if (hasChanges)
changed.push(wchanged);
curheight += height + padding;
}
curwidth += columns[i].computed;
}
return changed;
}
function render(changeset) {
renderer.render(changeset, plugin);
}
function add(widget, col, row, colspan, rowspan) {
widget.row = row;
widget.col = col;
widget.rowspan = rowspan || (rowspan = 1);
widget.colspan = colspan || (colspan = 1);
widget.layout = plugin;
if (widgets.indexOf(widget) == -1)
widgets.push(widget);
var rowindex, colindex;
for (var i = 0; i < colspan; i++) {
colindex = col + i;
var colset = model[colindex];
if (!colset)
colset = model[colindex] = [];
for (var j = 0; j < rowspan; j++) {
rowindex = row + j;
if (colset[rowindex]) {
var w = colset[rowindex];
throw new Error("Conflict occurred adding widget to col: "
+ colindex + " and row: " + rowindex
+ ". The following widget is already there, col: " + w.col
+ " row: " + w.row);
}
colset[rowindex] = widget;
}
}
schedule(CHANGE_REDRAW);
}
function moveTo(widget, col, row, colspan, rowspan) {
if (widget.layout) {
if (widget.layout != plugin)
throw new Error("Moving a widget that is not first added to this layout.");
remove(widget);
}
add(widget, col, row, colspan, rowspan);
}
function isEmptyCol(col, widget) {
var c = model[col];
for (var i = widget.row; i < widget.row + widget.rowspan; i++) {
if (c[i] && c[i] != widget) return false;
}
return true;
}
function isEmptyRow(row, widget) {
for (var i = widget.col; i < widget.col + widget.colspan; i++) {
if (model[i][row] && model[i][row] != widget) return false;
}
return true;
}
function resizeTo(widget, width, fromLeft, height, fromTop) {
var delta, shrink, shrinkOther, insertIndex,copyFromTop;
var copyFromLeft, snap;
width = getRowColEntry(width);
height = getRowColEntry(height);
width.pixels = clip(width.pixels, widget.minWidth, widget.maxWidth);
height.pixels = clip(height.pixels, widget.minHeight, widget.maxHeight);
// Find the right column / size
var i, l, size, total = 0, collission = false;
if (fromLeft) {
for (i = widget.col + widget.colspan - 1; i >= 0; i--) {
size = columns[i].computed;
if (total + size > width.pixels
|| (collission = !isEmptyCol(i, widget)))
break;
total += size + padding;
}
}
else {
for (i = widget.col, l = columns.length; i < l; i++) {
size = columns[i].computed;
if (total + size > width.pixels
|| (collission = !isEmptyCol(i, widget)))
break;
total += size + padding;
}
}
// Move to column
delta = width.pixels - total + padding;
snap = !delta;
shrinkOther = fromLeft && widget.computed.width > width.pixels;
var col, colspan;
if (collission) {
// Resize current row
curcol = columns[i - (fromTop ? -1 : 1)];
if (curcol && curcol.pixels)
curcol.pixels += delta;
else {
// @todo
}
}
else if (!snap) {
var curcol = columns[i];
if (curcol && curcol.pixels)
curcol.pixels -= delta;
insertIndex = i + (fromLeft || i > 0 && shrink ? 1 : 0);
copyFromLeft = fromLeft && width.pixels < widget.computed.width
|| !fromLeft && width.pixels > widget.computed.width;
insertColumn(width.pixels - total, insertIndex, copyFromLeft);
col = fromLeft ? i + 1 : widget.col;
colspan = fromLeft
? widget.col + widget.colspan - i - 1
: i - widget.col + 1;
moveTo(widget, col, widget.row, colspan, widget.rowspan);
}
// Snap
else {
col = fromLeft ? i + 1 : widget.col;
colspan = fromLeft
? widget.col + widget.colspan - i - 1
: i - col;
moveTo(widget, col, widget.row, colspan, widget.rowspan);
}
// Find the right row / size
total = 0;
collission = false;
if (fromTop) {
for (i = widget.row + widget.rowspan - 1; i >= 0; i--) {
size = rows[i].computed;
if (total + size > height.pixels
|| (collission = !isEmptyRow(i, widget)))
break;
total += size + padding;
}
}
else {
for (i = widget.row, l = rows.length; i < l; i++) {
size = rows[i].computed;
if (total + size > height.pixels
|| (collission = !isEmptyRow(i, widget)))
break;
total += size + padding;
}
}
// Move to row
delta = height.pixels - total + padding;
shrinkOther = fromTop && widget.computed.height > height.pixels;
var row, rowspan;
if (collission) {
// Resize current row
currow = rows[i - (fromTop ? -1 : 1)];
if (currow && currow.pixels)
currow.pixels += delta;
else {
// @todo
}
}
else if (delta) {
var currow = rows[i];
if (currow && currow.pixels)
currow.pixels -= delta;
insertIndex = i + (fromTop || i > 0 && shrink ? 1 : 0);
copyFromTop = fromTop && height.pixels < widget.computed.height
|| !fromTop && height.pixels > widget.computed.height;
insertRow(height.pixels - total, insertIndex, copyFromTop);
row = fromTop ? i + 1 : widget.row;
rowspan = fromTop
? widget.row + widget.rowspan - i - 1
: i - widget.row + 1;
moveTo(widget, widget.col, row, widget.colspan, rowspan);
}
// Snap
else {
row = fromTop ? i + 1 : widget.row;
rowspan = fromTop
? widget.row + widget.rowspan - i - 1
: i - row;
moveTo(widget, widget.col, row, widget.colspan, rowspan);
}
clean();
}
function remove(widget, deep) {
var index = widgets.indexOf(widget);
if (index === -1)
return;
widgets.splice(index, 1);
var col = widget.col;
var row = widget.row;
var colspan = widget.colspan;
var rowspan = widget.rowspan;
for (var i = 0; i < colspan; i++) {
var colset = model[col + i];
for (var j = rowspan - 1; j >= 0; j--) {
colset[row + j] = null;
}
// if (deep && colset.length === 0)
// removeColumn(col + i);
}
widget.computed = {
width: widget.computed.width,
height: widget.computed.height
};
widget.layout = null;
schedule(CHANGE_REDRAW);
}
function getState(){
var state = {};
widgets.forEach(function(w) {
state[w.name] = w.getState();
});
state.columns = columns.map(serializeRowCol).join(",");
state.rows = rows.map(serializeRowCol).join(",");
return state;
}
function setState(state) {
plugin.columns = state.columns;
plugin.rows = state.rows;
widgets.forEach(function(w) {
if (state[w.name]) {
w.setState(state[w.name]);
add(w, w.col, w.row, w.colspan, w.rowspan);
}
});
schedule(CHANGE_REDRAW);
}
function getCoords(col, row) {
if (col > columns.length)
col = columns.length - 1;
if (row > rows.length)
row = rows.length - 1;
var y = edge[3];
for (var i = 0; i < row; i++) y += rows[i].computed;
y += row * padding;
y += 1;
var x = edge[0];
for (var i = 0; i < col; i++) x += columns[i].computed;
x += row * padding;
x += 1;
return { x: x, y: y };
}
function resize(){
changes = CHANGE_RESIZE;
applyChanges();
}
function clip(w, min, max) {
if (w < min) return min;
if (w > max) return max;
return w;
}
function pause(){
paused = true;
}
function resume(){
paused = false;
resize();
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("enable", function() {
});
plugin.on("disable", function() {
});
plugin.on("unload", function() {
loaded = false;
});
/***** Register and define API *****/
// This is a baseclass
plugin.freezePublicAPI.baseclass();
/**
*
**/
plugin.freezePublicAPI({
get container(){ return container;},
get paused(){ return paused;},
get children(){ return widgets;},
get columns(){ return columns/* && columns.join(", ")*/; },
set columns(v) {
columns = String(v).split(/\s*,\s*/).map(function(m) {
return getRowColEntry(m);
});
initModel();
},
get rows(){ return rows /*&& rows.join(", ")*/; },
set rows(v) {
rows = String(v).split(/\s*,\s*/).map(function(m) {
return getRowColEntry(m);
});
initModel();
},
get padding(){ return padding; },
set padding(v){ padding = parseInt(v, 10); },
get edge(){ return edge; },
set edge(v){ edge = util.getBox(v); },
get parentLayout(){ return parentLayout; },
set parentLayout(el){ parentLayout = el; },
/**
*
*/
getWidgetAtPoint: getWidgetAtPoint,
/**
*
*/
resize: resize,
/**
*
*/
attachToParent: attachToParent,
/**
*
*/
clean: clean,
/**
*
*/
fill: fill,
/**
*
*/
getCoords: getCoords,
/**
*
*/
getState: getState,
/**
*
*/
setState: setState,
/**
*
*/
add: add,
/**
*
*/
moveTo: moveTo,
/**
*
*/
resizeTo: resizeTo,
/**
*
*/
remove: remove,
/**
*
*/
insertWidget: insertWidget,
/**
*
*/
getInsertionPoint: getInsertionPoint,
pause: pause,
resume: resume
});
return plugin;
}
function DockableAbsoluteRenderer(){
var plugin = new Plugin("Ajax.org", main.consumes);
// var emit = plugin.getEmitter();
var inited = {};
function init(widget, dockLayout) {
widget.load("plugin" + counter++); // @todo remove after testing
widget.container.style.position = "absolute";
dockLayout.container.appendChild(widget.container);
inited[widget.name] = dockLayout;
}
function render(changeset, dockLayout) {
var change, widget, html;
for (var i = 0; i < changeset.length; i++) {
change = changeset[i];
widget = change.widget;
html = widget.container;
if (!inited[widget.name]
|| dockLayout.container != widget.container.parentNode) // Detect layout moving
init(widget, dockLayout);
if (change.left !== undefined)
html.style.left = change.left + "px";
if (change.top !== undefined)
html.style.top = change.top + "px";
if (change.width !== undefined)
html.style.width = change.width + "px";
if (change.height !== undefined)
html.style.height = change.height + "px";
widget.resize(change);
}
}
/**
*
**/
plugin.freezePublicAPI({
/**
*
*/
render: render
});
plugin.load("DockRenderer" + counter++);
return plugin;
}
var CURSOR = {
"s" : "ns-resize",
"n" : "ns-resize",
"w" : "ew-resize",
"e" : "ew-resize",
"ne" : "nesw-resize",
"nw" : "nwse-resize",
"se" : "nwse-resize",
"sw" : "nesw-resize"
}
function DockableWidget(developer, deps) {
var plugin = new Plugin(developer, deps);
var emit = plugin.getEmitter();
var computed = {};
var row, col, rowspan, colspan, container, handle, layout;
var innerLayout;
var draggable = true;
var resizable = true;
var EDGE_SIZE = 10;
var loaded = false;
function load() {
if (loaded) return false;
loaded = true;
}
function setDragHandle(c, h) {
if (c)
container = c;
else {
container = document.createElement("div");
container.style.boxSizing = "border-box";
}
if (typeof h == "string")
h = c.querySelector(h);
handle = h || container;
handle.decorated = plugin;
event.addListener(container, "mousedown", function(e) {
if (layout.paused) return;
var edge = detectEdge(e);
if (edge && resizable)
startResize(e, edge);
event.stopEvent(e);
});
event.addListener(handle, "mousedown", function(e) {
if (layout.paused) return;
var edge = detectEdge(e);
if ((!edge || !resizable) && draggable)
startDragWatch(e);
event.stopEvent(e);
});
event.addListener(container, "mousemove", function(e) {
if (layout.paused) return;
if (!plugin.innerLayout) {
var edge = detectEdge(e);
var cursor = edge ? CURSOR[edge] : "default";
container.style.cursor = cursor;
setGlobalCursor(edge ? cursor : false);
}
});
event.addListener(container, "mouseout", function(e) {
if (layout.paused) return;
if (e.currentTarget == container && !plugin.innerLayout) {
setGlobalCursor(false);
container.style.cursor = "";
}
});
}
/***** Methods *****/
function resize(e) {
emit("resize", e);
}
function getPosition() {
return {row: row, col: col, rowspan: rowspan, colspan: colspan};
}
function getPreferedSize(availablePos) {
if (this.computePreferredSize)
return this.computePreferredSize(availablePos);
}
function isEdge(value){ return value >= 0 && value < EDGE_SIZE; }
function detectEdge(e) {
var rect = container.getBoundingClientRect();
var hor = "", ver = "";
if (isEdge(e.clientX - rect.left))
hor = "w";
else if (isEdge(rect.left + rect.width - e.clientX))
hor = "e";
if (isEdge(e.clientY - rect.top))
ver = "n";
else if (isEdge(rect.top + rect.height - e.clientY))
ver = "s";
return hor || ver ? ver + hor : false;
}
function startResize(e, edge) {
var el = container;
var drag = getDragOverlay();
// Set Top
drag.style.zIndex = 1000000;
drag.className = "drag resize";
var offsetX = e.clientX - (parseInt(container.style.left, 10) || 0);
var offsetY = e.clientY - (parseInt(container.style.top, 10) || 0);
var moved = false;
var startX = e.clientX - offsetX;
var startY = e.clientY - offsetY;
var rect = layout.container.getBoundingClientRect();
var parentLeft = rect.left;
var parentTop = rect.top;
var startWidth = container.offsetWidth;
var startHeight = container.offsetHeight;
drag.style.left = (startX + parentLeft) + "px";
drag.style.top = (startY + parentTop) + "px";
drag.style.width = startWidth + "px";
drag.style.height = startHeight + "px";
var sizes = { v: layout.rows, h: layout.columns };
var margin = layout.edge;
var padding = layout.padding;
var SNAP_DIST = 20;
function findSnapPos(pos, dir, side) {
var snapPos = margin[dir === "v" ? 0 : 3];
var rows = sizes[dir];
for (var i = 0; i <= rows.length; i++) {
if (side > 0 && i > 1)
snapPos += padding;
if (Math.abs(pos - snapPos) < SNAP_DIST)
return snapPos;
snapPos += rows[i] && rows[i].computed;
if (side < 0)
snapPos += padding;
}
return pos;
}
event.capture(el, function(e) {
var snapToGrid = !e.ctrlKey && !e.altKey && !e.metaKey;
var x = e.clientX - offsetX;
var y = e.clientY - offsetY;
if (!moved && Math.abs(x - startX) + Math.abs(y - startY) > 5)
moved = true;
if (edge.indexOf("w") > -1) {
if (snapToGrid)
x = findSnapPos(x, "h", -1);
drag.style.left = (x + parentLeft) + "px";
drag.style.width = (startWidth + (startX - x)) + "px";
}
else if (edge.indexOf("e") > -1) {
var left = startX + parentLeft;
var w = startWidth + (x - startX);
if (snapToGrid)
w = findSnapPos(w + startX, "h", 1) - startX;
drag.style.left = left + "px";
drag.style.width = w + "px";
}
if (edge.indexOf("n") > -1) {
if (snapToGrid)
y = findSnapPos(y, "v", -1);
drag.style.top = (y + parentTop) + "px";
drag.style.height = (startHeight + (startY - y)) + "px";
}
else if (edge.indexOf("s") > -1) {
var top = startY + parentTop;
var h = startHeight + (y - startY);
if (snapToGrid)
h = findSnapPos(h + startY, "v", 1) - startY;
drag.style.top = top + "px";
drag.style.height = h + "px";
}
drag.style.display = "block";
}, function() {
if (moved)
layout.resizeTo(plugin,
drag.offsetWidth, edge.indexOf("w") > -1,
drag.offsetHeight, edge.indexOf("n") > -1);
drag.style.zIndex = "";
drag.style.display = "none";
});
event.stopEvent(e);
}
function startDragWatch(e) {
var el = handle || container;
var drag = getDragOverlay();
// Set Top
drag.style.zIndex = 1000000;
var lastLayout = layout;
var rect = lastLayout.container.getBoundingClientRect();
var baseX = rect.left;
var baseY = rect.top;
var parentLeft = rect.left;
var parentTop = rect.top;
var offsetX = e.clientX - (parseInt(container.style.left, 10) || 0);
var offsetY = e.clientY - (parseInt(container.style.top, 10) || 0);
var moved = false;
var startX = e.clientX - offsetX;
var startY = e.clientY - offsetY;
var startWidth = container.offsetWidth;
var startHeight = container.offsetHeight;
var lastPos;
event.capture(el, function(e) {
var x = e.clientX - offsetX;
var y = e.clientY - offsetY;
if (!moved && Math.abs(x - startX) + Math.abs(y - startY) > 5)
moved = true;
var pos = lastLayout.getInsertionPoint(plugin,
e.clientX - baseX, e.clientY - baseY) || lastPos;
lastPos = pos;
if (!pos) return;
if (pos.widget == plugin) {
drag.className = "drag";
drag.style.left = (x + parentLeft) + "px";
drag.style.top = (y + parentTop) + "px";
drag.style.width = startWidth + "px";
drag.style.height = startHeight + "px";
}
else {
if (lastLayout != pos.layout) {
lastLayout = pos.layout;
var rect = lastLayout.container.getBoundingClientRect();
baseX = rect.left;
baseY = rect.top;
}
drag.className = "drag " + pos.insertionPoint;
drag.style.left = (baseX + pos.left) + "px";
drag.style.top = (baseY + pos.top) + "px";
drag.style.width = pos.width + "px";
drag.style.height = pos.height + "px";
}
drag.style.display = "block";
}, function() {
if (moved && lastPos.widget != plugin)
lastPos.layout.insertWidget(plugin, lastPos);
drag.style.zIndex = "";
drag.style.display = "none";
});
event.stopEvent(e);
}
var dragOverlay;
function getDragOverlay(){
if (!dragOverlay) {
var el = document.createElement("div");
el.className = "drag";
document.body.appendChild(el);
dragOverlay = el;
}
dragOverlay.style.width = container.offsetWidth + "px";
dragOverlay.style.height = container.offsetHeight + "px";
dragOverlay.style.display = "none";
return dragOverlay;
}
function getState(){
return {
col: col,
row: row,
colspan: colspan,
rowspan: rowspan
}
}
function setState(state) {
col = state.col;
row = state.row;
colspan = state.colspan;
rowspan = state.rowspan;
}
/***** Lifecycle *****/
plugin.on("load", function() {
load();
});
plugin.on("enable", function() {
});
plugin.on("disable", function() {
});
plugin.on("unload", function() {
loaded = false;
});
/***** Register and define API *****/
// This is a baseclass
plugin.freezePublicAPI.baseclass();
/**
*
**/
plugin.freezePublicAPI({
get layout(){ return layout; },
set layout(el) {
layout = el;
if (innerLayout)
innerLayout.parentLayout = layout;
},
get innerLayout(){ return innerLayout; },
set innerLayout(el) {
innerLayout = el;
innerLayout.parentLayout = layout;
},
get container(){ return container; },
get handle(){ return handle || container; },
get row(){ return row; },
set row(v){ row = v; },
get col(){ return col; },
set col(v){ col = v; },
get rowspan(){ return rowspan; },
set rowspan(v){ rowspan = v; },
get colspan(){ return colspan; },
set colspan(v){ colspan = v; },
get computed(){ return computed; },
set computed(v){ computed = v; },
get draggable(){ return draggable; },
set draggable(v){ draggable = v; },
get resizable(){ return resizable; },
set resizable(v){ resizable = v; },
_events: [
/**
* @event draw
*/
"draw",
/**
*
*/
"resize"
],
/**
*
*/
getState: getState,
/**
*
*/
setState: setState,
/**
*
*/
startDragWatch: startDragWatch,
/**
*
*/
setDragHandle: setDragHandle,
/**
*
*/
getPosition: getPosition,
/**
*
*/
getPreferedSize: getPreferedSize,
/**
*
*/
resize: resize
});
return plugin;
}
var setGlobalCursor = (function(){
var done = {};
var sheet;
return function(cursor) {
if (!sheet) {
var style = document.createElement("style");
style.appendChild(document.createTextNode(""));
document.head.appendChild(style);
sheet = style.sheet;
}
if (cursor) {
if (!done[cursor]) {
sheet.addRule("." + cursor + " *", "cursor:" + cursor + " !important;", 0);
done[cursor] = true;
}
document.body.className = cursor;
}
else {
document.body.className = "";
}
}
})();
register(null, {
DockableLayout: DockableLayout,
DockableWidget: DockableWidget,
DockableAbsoluteRenderer: DockableAbsoluteRenderer
});
}
});