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 }); } });