import { d3Event, HTMLWidget, Platform, select as d3Select, selectAll as d3SelectAll, Utility } from "@hpcc-js/common";
import { drag as d3Drag } from "d3-drag";
import { Cell } from "./Cell.ts";
import "../src/Border.css";
export class Border extends HTMLWidget {
_colCount: number;
_rowCount: number;
_colSize: number;
_rowSize: number;
_shrinkWrapBoxes;
_watch;
_offsetX;
_offsetY;
_dragCell;
_dragCellSize;
_dragCellStartSize;
_handleTop;
_handleLeft;
_dragPrevX;
_dragPrevY;
_cellSizes;
contentDiv;
_scrollBarWidth;
_borderHandles;
_sectionTypeArr;
constructor() {
super();
this._tag = "div";
this._colCount = 0;
this._rowCount = 0;
this._colSize = 0;
this._rowSize = 0;
this._shrinkWrapBoxes = {};
this.content([]);
this.sectionTypes([]);
}
watchWidget(widget) {
if (this._watch === undefined) {
this._watch = {};
}
if (this._watch[widget.id()]) {
this._watch[widget.id()].remove();
delete this._watch[widget.id()];
}
if (widget) {
const context = this;
this._watch[widget.id()] = widget.monitor(function (paramId, newVal, oldVal) {
if (oldVal !== newVal) {
context.lazyPostUpdate();
}
});
}
}
lazyPostUpdate = Utility.debounce(function () {
this.postUpdate();
}, 100);
applyLayoutType() {
const layoutObj = this.borderLayoutObject();
this.content().forEach(function (cell, i) {
cell._fixedLeft = layoutObj[this.sectionTypes()[i]].left;
cell._fixedTop = layoutObj[this.sectionTypes()[i]].top;
cell._fixedWidth = layoutObj[this.sectionTypes()[i]].width;
cell._fixedHeight = layoutObj[this.sectionTypes()[i]].height;
cell._dragHandles = this.cellSpecificDragHandles(this.sectionTypes()[i]);
}, this);
}
cellSpecificDragHandles(sectionType) {
switch (sectionType) {
case "top": return ["s"];
case "right": return ["w"];
case "bottom": return ["n"];
case "left": return ["e"];
case "center": return [];
}
}
borderLayoutObject(layoutType?) {
const retObj = {};
const context = this;
let topSize;
let topPerc;
let bottomSize;
let bottomPerc;
let leftSize;
let leftPerc;
let rightSize;
let rightPerc;
const bcRect = this.target().getBoundingClientRect();
const gridRect: any = {};
gridRect.top = bcRect.top;
gridRect.left = bcRect.left;
gridRect.bottom = bcRect.bottom;
gridRect.right = bcRect.right;
if (this.target() instanceof SVGElement) {
gridRect.width = parseFloat(this.target().getAttribute("width"));
gridRect.height = parseFloat(this.target().getAttribute("height"));
} else {
gridRect.width = bcRect.width;
gridRect.height = bcRect.height;
}
if (this.sectionTypes().indexOf("top") !== -1) {
topSize = this.topSize();
topPerc = this.topPercentage();
if (typeof (this._shrinkWrapBoxes["top"]) !== "undefined") {
topSize = this._shrinkWrapBoxes["top"].height + this.gutter();
topPerc = 0;
}
}
if (this.sectionTypes().indexOf("bottom") !== -1) {
bottomSize = this.bottomSize();
bottomPerc = this.bottomPercentage();
if (typeof (this._shrinkWrapBoxes["bottom"]) !== "undefined") {
bottomSize = this._shrinkWrapBoxes["bottom"].height + this.gutter();
bottomPerc = 0;
}
}
if (this.sectionTypes().indexOf("left") !== -1) {
leftSize = this.leftSize();
leftPerc = this.leftPercentage();
if (typeof (this._shrinkWrapBoxes["left"]) !== "undefined") {
leftSize = this._shrinkWrapBoxes["left"].width + this.gutter();
leftPerc = 0;
}
}
if (this.sectionTypes().indexOf("right") !== -1) {
rightSize = this.rightSize();
rightPerc = this.rightPercentage();
if (typeof (this._shrinkWrapBoxes["right"]) !== "undefined") {
rightSize = this._shrinkWrapBoxes["right"].width + this.gutter();
rightPerc = 0;
}
}
const t = _sectionPlacementObject({
width: { "px": 0, "%": 100 },
height: { "px": topSize, "%": topPerc },
top: { "px": 0, "%": 0 },
left: { "px": 0, "%": 0 }
});
const b = _sectionPlacementObject({
width: { "px": 0, "%": 100 },
height: { "px": bottomSize, "%": bottomPerc },
top: { "px": 0, "%": 100 },
left: { "px": 0, "%": 0 }
});
b.top -= b.height;
const l = _sectionPlacementObject({
width: { "px": leftSize, "%": leftPerc },
height: { "px": -t.height - b.height, "%": 100 },
top: { "px": t.height, "%": 0 },
left: { "px": 0, "%": 0 }
});
const r = _sectionPlacementObject({
width: { "px": rightSize, "%": rightPerc },
height: { "px": -t.height - b.height, "%": 100 },
top: { "px": t.height, "%": 0 },
left: { "px": 0, "%": 100 }
});
r.left -= r.width;
const c = _sectionPlacementObject({
width: { "px": -r.width - l.width, "%": 100 },
height: { "px": -t.height - b.height, "%": 100 },
top: { "px": t.height, "%": 0 },
left: { "px": l.width, "%": 0 }
});
retObj["top"] = t;
retObj["bottom"] = b;
retObj["right"] = r;
retObj["left"] = l;
retObj["center"] = c;
return retObj;
function _sectionPlacementObject(obj) {
obj.width["px"] = typeof (obj.width["px"]) !== "undefined" ? obj.width["px"] : 0;
obj.width["%"] = typeof (obj.width["%"]) !== "undefined" ? obj.width["%"] : 0;
obj.height["px"] = typeof (obj.height["px"]) !== "undefined" ? obj.height["px"] : 0;
obj.height["%"] = typeof (obj.height["%"]) !== "undefined" ? obj.height["%"] : 0;
const ret = {
width: obj.width["px"] + (obj.width["%"] / 100 * context.width()),
height: obj.height["px"] + (obj.height["%"] / 100 * context.height()),
top: obj.top["px"] + (obj.top["%"] / 100 * context.height()) + context.gutter() / 2,
left: obj.left["px"] + (obj.left["%"] / 100 * context.width()) + context.gutter() / 2
};
return ret;
}
}
clearContent(sectionType) {
if (!sectionType) {
this.content().forEach(function (contentWidget) {
contentWidget.target(null);
return false;
});
d3Select("#" + this.id() + " > div.borderHandle")
.classed("borderHandleDisabled", true)
;
delete this._watch;
this.content([]);
this.sectionTypes([]);
} else {
const idx = this.sectionTypes().indexOf(sectionType);
if (idx >= 0) {
if (this._watch && this.content()[idx]) {
delete this._watch[this.content()[idx].id()];
}
this.content()[idx].target(null);
d3Select("#" + this.id() + " > div.borderHandle_" + sectionType)
.classed("borderHandleDisabled", true)
;
this.content().splice(idx, 1);
this.sectionTypes().splice(idx, 1);
}
}
}
hasContent(sectionType, widget, title) {
return this.sectionTypes().indexOf(sectionType) >= 0;
}
setContent(sectionType, widget, title?) {
this.clearContent(sectionType);
title = typeof (title) !== "undefined" ? title : "";
if (widget) {
const cell = new Cell()
.surfaceBorderWidth(0)
.widget(widget)
.title(title)
;
this.watchWidget(widget);
this.content().push(cell);
this.sectionTypes().push(sectionType);
}
return this;
}
getCell(id) {
const idx = this.sectionTypes().indexOf(id);
if (idx >= 0) {
return this.content()[idx];
}
return null;
}
getContent(id) {
const idx = this.sectionTypes().indexOf(id);
if (idx >= 0) {
return this.content()[idx].widget();
}
return null;
}
setLayoutOffsets() {
this._offsetX = this._element.node().getBoundingClientRect().left + (this.gutter() / 2);
this._offsetY = this._element.node().getBoundingClientRect().top + (this.gutter() / 2);
}
dragStart(handle) {
const event = d3Event();
event.sourceEvent.stopPropagation();
const context = this;
this._dragCell = handle;
this._dragCellStartSize = this[handle + "Size"]();
if (this[handle + "ShrinkWrap"]()) {
this[handle + "Percentage"](0);
this[handle + "ShrinkWrap"](false);
}
const handleElm = d3Select("#" + context.id() + " > div.borderHandle_" + handle);
context._handleTop = parseFloat(handleElm.style("top").split("px")[0]);
context._handleLeft = parseFloat(handleElm.style("left").split("px")[0]);
this._dragPrevX = event.sourceEvent.clientX;
this._dragPrevY = event.sourceEvent.clientY;
}
dragTick(handle) {
const context = this;
const event = d3Event();
const xDelta = this._dragPrevX - event.sourceEvent.clientX;
const yDelta = this._dragPrevY - event.sourceEvent.clientY;
switch (handle) {
case "top":
case "bottom":
_moveHandles(handle, yDelta);
break;
case "right":
case "left":
_moveHandles(handle, xDelta);
break;
}
function _moveHandles(handle2, delta) {
if (delta === 0) return;
const handles = d3SelectAll("#" + context.id() + " > div.borderHandle");
const grabbedHandle = d3Select("#" + context.id() + " > div.borderHandle_" + handle2);
if (grabbedHandle.classed("borderHandle_top")) {
grabbedHandle.style("top", (context._handleTop - delta) + "px");
context._cellSizes.topHeight = context._handleTop - delta;
context._cellSizes.leftHeight = context._cellSizes.height;
context._cellSizes.leftHeight -= context._cellSizes.topHeight;
context._cellSizes.leftHeight -= context._cellSizes.bottomHeight;
context._cellSizes.rightHeight = context._cellSizes.leftHeight;
} else if (grabbedHandle.classed("borderHandle_right")) {
grabbedHandle.style("left", (context._handleLeft - delta) + "px");
context._cellSizes.rightWidth = context._cellSizes.width - context._handleLeft + delta;
} else if (grabbedHandle.classed("borderHandle_bottom")) {
grabbedHandle.style("top", (context._handleTop - delta) + "px");
context._cellSizes.bottomHeight = context._cellSizes.height - context._handleTop + delta;
context._cellSizes.leftHeight = context._cellSizes.height;
context._cellSizes.leftHeight -= context._cellSizes.bottomHeight;
context._cellSizes.leftHeight -= context._cellSizes.topHeight;
context._cellSizes.rightHeight = context._cellSizes.leftHeight;
} else if (grabbedHandle.classed("borderHandle_left")) {
grabbedHandle.style("left", (context._handleLeft - delta) + "px");
context._cellSizes.leftWidth = context._handleLeft - delta;
}
handles.each(function () {
const handle3 = d3Select(this);
if (handle3.classed("borderHandle_top")) {
handle3.style("width", context._cellSizes.width + "px");
handle3.style("top", (context._cellSizes.topHeight - 3) + "px");
} else if (handle3.classed("borderHandle_right")) {
handle3.style("left", (context._cellSizes.width - context._cellSizes.rightWidth) + "px");
handle3.style("top", (context._cellSizes.topHeight + 3) + "px");
handle3.style("height", context._cellSizes.rightHeight + "px");
} else if (handle3.classed("borderHandle_bottom")) {
handle3.style("width", context._cellSizes.width + "px");
handle3.style("top", (context._cellSizes.height - context._cellSizes.bottomHeight - 3) + "px");
} else if (handle3.classed("borderHandle_left")) {
handle3.style("left", context._cellSizes.leftWidth + "px");
handle3.style("height", context._cellSizes.leftHeight + "px");
handle3.style("top", (context._cellSizes.topHeight + 3) + "px");
}
});
}
}
dragEnd(handle) {
if (handle) {
const event = d3Event();
const xDelta = this._dragPrevX - event.sourceEvent.clientX;
const yDelta = this._dragPrevY - event.sourceEvent.clientY;
switch (handle) {
case "top":
if (yDelta !== 0) {
this.topPercentage(0);
this.topSize(this.topSize() === 0 ? this.getContent("top").getBBox().height - yDelta : this.topSize() - yDelta);
}
break;
case "right":
if (xDelta !== 0) {
this.rightPercentage(0);
this.rightSize(this.rightSize() === 0 ? this.getContent("right").getBBox().width + xDelta : this.rightSize() + xDelta);
}
break;
case "bottom":
if (yDelta !== 0) {
this.bottomPercentage(0);
this.bottomSize(this.bottomSize() === 0 ? this.getContent("bottom").getBBox().height + yDelta : this.bottomSize() + yDelta);
}
break;
case "left":
if (xDelta !== 0) {
this.leftPercentage(0);
this.leftSize(this.leftSize() === 0 ? this.getContent("left").getBBox().width - xDelta : this.leftSize() - xDelta);
}
break;
}
this._dragPrevX = event.sourceEvent.clientX;
this._dragPrevY = event.sourceEvent.clientY;
}
this.render();
}
size(_?) {
const retVal = HTMLWidget.prototype.size.apply(this, arguments);
if (arguments.length && this.contentDiv) {
this.contentDiv
.style("width", this._size.width + "px")
.style("height", this._size.height + "px")
;
}
return retVal;
}
enter(domNode, element) {
super.enter(domNode, element);
const context = this;
element.style("position", "relative");
this.contentDiv = element.append("div").classed("border-content", true);
this._scrollBarWidth = Platform.getScrollbarWidth();
this._borderHandles = ["top", "left", "right", "bottom"];
const handles = element.selectAll("div.borderHandle").data(this._borderHandles);
handles.enter().append("div")
.classed("borderHandle", true)
.each(function (handle) {
const h = d3Select(this);
h.classed("borderHandle_" + handle, true)
.classed("borderHandleDisabled", context.getContent(handle) === null)
;
});
}
update(domNode, element) {
super.update(domNode, element);
this._sectionTypeArr = this.sectionTypes();
const context = this;
element.classed("design-mode", this.designMode());
this.setLayoutOffsets();
const rows = this.contentDiv.selectAll(".cell_" + this._id).data(this.content(), function (d) { return d._id; });
const rowsUpdate = rows.enter().append("div")
.classed("cell_" + this._id, true)
.style("position", "absolute")
.each(function (d, i) {
d3Select(this).classed("border-cell border-cell-" + context._sectionTypeArr[i], true);
d.target(this);
d3Select("#" + context.id() + " > div.borderHandle_" + context._sectionTypeArr[i])
.classed("borderHandleDisabled", false);
}).merge(rows);
rowsUpdate
.each(function (d, idx) {
const sectionType = context.sectionTypes()[idx];
if (typeof (context[sectionType + "ShrinkWrap"]) !== "undefined" && context[sectionType + "ShrinkWrap"]()) {
d.render();
context._shrinkWrapBoxes[sectionType] = d.widget().getBBox(true);
} else {
delete context._shrinkWrapBoxes[sectionType];
}
});
const drag = d3Drag()
.on("start", function (d, i) { context.dragStart.call(context, d, i); })
.on("drag", function (d, i) { context.dragTick.call(context, d, i); })
.on("end", function (d, i) { context.dragEnd.call(context, d, i); })
;
if (this.designMode()) {
element.selectAll("#" + this.id() + " > div.borderHandle").call(drag);
} else {
element.selectAll("#" + this.id() + " > div.borderHandle").on(".drag", null);
}
const layoutObj = this.borderLayoutObject();
this.content().forEach(function (cell, i) {
cell._fixedLeft = layoutObj[this.sectionTypes()[i]].left;
cell._fixedTop = layoutObj[this.sectionTypes()[i]].top;
cell._fixedWidth = layoutObj[this.sectionTypes()[i]].width;
cell._fixedHeight = layoutObj[this.sectionTypes()[i]].height;
cell._dragHandles = [];
}, this);
rowsUpdate
.style("left", function (d) { return d._fixedLeft + "px"; })
.style("top", function (d) { return d._fixedTop + "px"; })
.style("width", function (d) { return d._fixedWidth - context.gutter() + "px"; })
.style("height", function (d) { return d._fixedHeight - context.gutter() + "px"; })
.each(function (d) {
d._placeholderElement
.attr("draggable", context.designMode())
.selectAll(".dragHandle")
.attr("draggable", context.designMode())
;
d
.surfacePadding(context.surfacePadding())
.resize()
;
});
rows.exit().each(function (d) {
d.target(null);
}).remove();
this.getCellSizes();
element
.selectAll("#" + this.id() + " > div.borderHandle")
.each(function () {
const handle = d3Select(this);
if (handle.classed("borderHandle_top")) {
handle.style("width", context._cellSizes.width + "px");
handle.style("top", (context._cellSizes.topHeight - 3) + "px");
} else if (handle.classed("borderHandle_right")) {
handle.style("left", (context._cellSizes.width - context._cellSizes.rightWidth) + "px");
handle.style("top", (context._cellSizes.topHeight + 3) + "px");
handle.style("height", context._cellSizes.rightHeight + "px");
} else if (handle.classed("borderHandle_bottom")) {
handle.style("width", context._cellSizes.width + "px");
handle.style("top", (context._cellSizes.height - context._cellSizes.bottomHeight - 3) + "px");
} else if (handle.classed("borderHandle_left")) {
handle.style("left", context._cellSizes.leftWidth + "px");
handle.style("height", context._cellSizes.leftHeight + "px");
handle.style("top", (context._cellSizes.topHeight + 3) + "px");
}
})
;
}
getCellSizes() {
const context = this;
context._cellSizes = {};
const contentRect = this.element().node().getBoundingClientRect();
context._cellSizes.width = contentRect.width;
context._cellSizes.height = contentRect.height;
this.element()
.selectAll("#" + this.id() + " > div > div.border-cell")
.each(function () {
const cell = d3Select(this);
if (typeof cell.node === "function") {
const rect = cell.node().getBoundingClientRect();
if (cell.classed("border-cell-top")) {
context._cellSizes.topHeight = rect.height;
} else if (cell.classed("border-cell-left")) {
context._cellSizes.leftWidth = rect.width;
context._cellSizes.leftHeight = rect.height;
} else if (cell.classed("border-cell-right")) {
context._cellSizes.rightWidth = rect.width;
context._cellSizes.rightHeight = rect.height;
} else if (cell.classed("border-cell-bottom")) {
context._cellSizes.bottomHeight = rect.height;
}
}
});
const sizes = ["height", "width", "topHeight", "bottomHeight", "leftHeight", "rightHeight", "leftWidth", "rightWidth"];
sizes.forEach(function (size) {
context._cellSizes[size] = context._cellSizes[size] === undefined ? 0 : context._cellSizes[size];
});
}
postUpdate(domNode, element) {
const context = this;
this.content().forEach(function (n) {
if (n._element.node() !== null && n.widget()) {
const prevBox = n.widget().getBBox(false, true);
const currBox = n.widget().getBBox(true, true);
if (prevBox.width !== currBox.width || prevBox.height !== currBox.height) {
context.lazyRender();
}
}
});
}
exit(domNode, element) {
this.content().forEach(w => w.target(null));
super.exit(domNode, element);
}
}
Border.prototype._class += " layout_Border";
export interface Border {
designMode(): boolean;
designMode(_: boolean): this;
content(): any[];
content(_: any[]): this;
gutter(): number;
gutter(_: number): this;
topShrinkWrap(): boolean;
topShrinkWrap(_: boolean): this;
leftShrinkWrap(): boolean;
leftShrinkWrap(_: boolean): this;
rightShrinkWrap(): boolean;
rightShrinkWrap(_: boolean): this;
bottomShrinkWrap(): boolean;
bottomShrinkWrap(_: boolean): this;
topSize(): number;
topSize(_: number): this;
leftSize(): number;
leftSize(_: number): this;
rightSize(): number;
rightSize(_: number): this;
bottomSize(): number;
bottomSize(_: number): this;
topPercentage(): number;
topPercentage(_: number): this;
leftPercentage(): number;
leftPercentage(_: number): this;
rightPercentage(): number;
rightPercentage(_: number): this;
bottomPercentage(): number;
bottomPercentage(_: number): this;
surfacePadding(): number;
surfacePadding(_: number): this;
sectionTypes(): any[];
sectionTypes(_: any[]): this;
}
Border.prototype.publish("designMode", false, "boolean", "Design Mode", null, { tags: ["Basic"] });
Border.prototype.publish("content", [], "widgetArray", "widgets", null, { tags: ["Intermediate"] });
Border.prototype.publish("gutter", 0, "number", "Gap Between Widgets", null, { tags: ["Basic"] });
Border.prototype.publish("topShrinkWrap", false, "boolean", "'Top' Cell shrinks to fit content", null, { tags: ["Intermediate"] });
Border.prototype.publish("leftShrinkWrap", false, "boolean", "'Left' Cell shrinks to fit content", null, { tags: ["Intermediate"] });
Border.prototype.publish("rightShrinkWrap", false, "boolean", "'Right' Cell shrinks to fit content", null, { tags: ["Intermediate"] });
Border.prototype.publish("bottomShrinkWrap", false, "boolean", "'Bottom' Cell shrinks to fit content", null, { tags: ["Intermediate"] });
Border.prototype.publish("topSize", 0, "number", "Height of the 'Top' Cell (px)", null, { tags: ["Private"] });
Border.prototype.publish("leftSize", 0, "number", "Width of the 'Left' Cell (px)", null, { tags: ["Private"] });
Border.prototype.publish("rightSize", 0, "number", "Width of the 'Right' Cell (px)", null, { tags: ["Private"] });
Border.prototype.publish("bottomSize", 0, "number", "Height of the 'Bottom' Cell (px)", null, { tags: ["Private"] });
Border.prototype.publish("topPercentage", 20, "number", "Percentage (of parent) Height of the 'Top' Cell", null, { tags: ["Private"] });
Border.prototype.publish("leftPercentage", 20, "number", "Percentage (of parent) Width of the 'Left' Cell", null, { tags: ["Private"] });
Border.prototype.publish("rightPercentage", 20, "number", "Percentage (of parent) Width of the 'Right' Cell", null, { tags: ["Private"] });
Border.prototype.publish("bottomPercentage", 20, "number", "Percentage (of parent) Height of the 'Bottom' Cell", null, { tags: ["Private"] });
Border.prototype.publish("surfacePadding", 0, "number", "Cell Padding (px)", null, { tags: ["Intermediate"] });
Border.prototype.publish("sectionTypes", [], "array", "Section Types sharing an index with 'content' - Used to determine position/size.", null, { tags: ["Private"] });