import { HTMLWidget, Platform, PropertyExt, Widget } from "@hpcc-js/common";
import { Grid } from "@hpcc-js/layout";
import { local as d3Local, select as d3Select, selectAll as d3SelectAll } from "d3-selection";
import * as Persist from "./Persist";
import "../src/PropertyEditor.css";
function hasProperties(type) {
switch (type) {
case "widget":
case "widgetArray":
case "propertyArray":
return true;
default:
}
return false;
}
export class PropertyEditor extends HTMLWidget {
_widgetOrig;
_parentPropertyEditor;
_show_settings: boolean;
_selectedItems;
__meta_sorting;
_watch;
private _childPE = d3Local();
constructor() {
super();
this._parentPropertyEditor = null;
this._tag = "div";
this._show_settings = false;
}
parentPropertyEditor(_?: PropertyEditor): PropertyEditor {
if (!arguments.length) return this._parentPropertyEditor;
this._parentPropertyEditor = _;
return this;
}
depth(): number {
let retVal = 0;
let parent = this.parentPropertyEditor();
while (parent) {
++retVal;
parent = parent.parentPropertyEditor();
}
return retVal;
}
_show_header = true;
show_header(): boolean;
show_header(_: boolean): PropertyEditor;
show_header(_?: boolean): boolean | PropertyEditor {
if (!arguments.length) {
return this._show_header;
}
this._show_header = _;
return this;
}
show_settings(): boolean;
show_settings(_: boolean): PropertyEditor;
show_settings(_?: boolean): boolean | PropertyEditor {
if (!arguments.length) {
return this._show_settings;
}
this._show_settings = _;
return this;
}
rootWidgets() {
if (this._selectedItems && this._selectedItems.length) {
return this._selectedItems;
}
return this.show_settings() ? [this] : this.widget() ? [this.widget()] : [];
}
update(domNode, element) {
super.update(domNode, element);
const context = this;
const rootWidgets = this.rootWidgets().filter(function (w) {
if (w._owningWidget && w._owningWidget.excludeObjs instanceof Array) {
if (w._owningWidget.excludeObjs.indexOf(w.classID()) !== -1) {
return false;
}
}
return true;
});
const table = element.selectAll(`table.property-table.table-${this.depth()}`).data(rootWidgets, function (d) {
// We reuse the existing DOM Nodes and this node _might_ have been a regular Input previously ---
if (typeof d.id !== "function") {
return `meta-${d.id}`;
}
return d.id();
});
table.enter().append("table")
.attr("class", `property-table table-${this.depth()}`)
.each(function () {
const tableElement = d3Select(this);
// Header ---
if (context._show_header && context.parentPropertyEditor() === null) {
tableElement.append("thead").append("tr").append("th")// .datum(tableElement)
.attr("colspan", "2")
.each(function () {
context.enterHeader(d3Select(this));
})
;
}
// Body ---
tableElement.append("tbody");
})
.merge(table)
.each(function (tableData) {
const tableElement = d3Select(this);
// Header ---
if (context._show_header && context.parentPropertyEditor() === null) {
context.updateHeader(tableElement.select("thead > tr > th"));
}
// Body ---
context.renderInputs(tableElement.select("tbody"), tableData);
})
;
table.exit()
.each(function () {
context.renderInputs(element.select("tbody"), null);
})
.remove()
;
}
exit(domNode, element) {
super.exit(domNode, element);
this.watchWidget(null);
}
private watchDepth = 0;
watchWidget(widget) {
if (this._watch) {
if ((window as any).__hpcc_debug) {
--this.watchDepth;
console.log("watchDepth: " + this.watchDepth);
}
this._watch.remove();
delete this._watch;
}
if (widget) {
const context = this;
this._watch = widget.monitor(function (_paramId, newVal, oldVal) {
if (oldVal !== newVal) {
const propEditor = context.parentPropertyEditor() || context;
propEditor.lazyRender();
}
});
if ((window as any).__hpcc_debug) {
++this.watchDepth;
console.log("watchDepth: " + this.watchDepth);
}
}
}
enterHeader(th) {
const context = this;
th.append("span");
th.append("i")
.attr("class", "expandIcon fa")
.on("click", function () {
switch (context.peInputIcon()) {
case "fa-caret-up":
case "fa-caret-right":
context.element().selectAll(`.table-${context.depth()} > tbody > tr > .headerRow > .peInput > .property-table-collapsed`)
.classed("property-table-collapsed", false)
;
context.element().selectAll(`.table-${context.depth()} > tbody > tr > .headerRow > .peInput > i`)
.classed("fa-minus-square-o", true)
.classed("fa-plus-square-o", false)
;
break;
case "fa-caret-down":
context.element().selectAll(`.table-${context.depth()} > tbody > tr > .headerRow > .peInput > div`)
.classed("property-table-collapsed", true)
;
context.element().selectAll(`.table-${context.depth()} > tbody > tr > .headerRow > .peInput > i`)
.classed("fa-minus-square-o", false)
.classed("fa-plus-square-o", true)
;
break;
}
context.refreshExpandIcon();
})
;
const sortIcon = th.append("i")
.attr("class", "sortIcon fa")
.on("click", function () {
context.refreshSortIcon(sortIcon, true);
})
;
th.append("i")
.attr("class", "hideParamsIcon fa")
.on("click", function () {
context.hideNonWidgets(!context.hideNonWidgets()).render();
})
;
}
updateHeader(th) {
const widget: any = this.widget();
let spanText = "";
if (widget) {
if (widget.label) {
spanText += widget.label();
}
if (widget.classID) {
if (spanText) {
spanText += " - ";
}
spanText += widget.classID();
}
}
th.select("span")
.text(spanText)
;
this.refreshExpandIcon();
this.refreshSortIcon(th.select(".sortIcon"));
this.refreshHideParamsIcon(th.select(".hideParamsIcon"));
}
peInputCount() {
return this.element().selectAll(`.table-${this.depth()} > tbody > tr > .headerRow > .peInput > div`).size();
}
peInputCollapsedCount() {
return this.element().selectAll(`.table-${this.depth()} > tbody > tr > .headerRow > .peInput > div.property-table-collapsed`).size();
}
peInputIcon(): "fa-caret-down" | "fa-caret-up" | "fa-caret-right" {
const collapsed = this.peInputCollapsedCount();
if (collapsed === 0) {
return "fa-caret-down";
} else if (collapsed === this.peInputCount()) {
return "fa-caret-up";
}
return "fa-caret-right";
}
refreshExpandIcon() {
const newIcon = this.peInputIcon();
this.element().select(`.table-${this.depth()} > thead > tr > th > .expandIcon`)
.classed("fa-caret-up", false)
.classed("fa-caret-right", false)
.classed("fa-caret-down", false)
.classed(newIcon, true)
;
}
refreshSortIcon(sortIcon, increment = false) {
const sort = this.sorting();
const types = this.sorting_options();
const icons = this.__meta_sorting.ext.icons;
if (increment) {
sortIcon.classed(icons[types.indexOf(sort)], false);
this.sorting(types[(types.indexOf(sort) + 1) % types.length]).render();
} else {
sortIcon
.classed(icons[(types.indexOf(sort)) % types.length], true)
.attr("title", sort)
;
}
}
refreshHideParamsIcon(hideParamsIcon) {
hideParamsIcon
.classed("fa-eye", !this.hideNonWidgets())
.classed("fa-eye-slash", this.hideNonWidgets())
;
}
gatherDataTree(widget) {
if (!widget) return null;
const retVal = {
label: widget.id() + " (" + widget.classID() + ")",
children: []
};
const arr2 = Persist.discover(widget);
arr2.forEach(function (prop) {
const node = {
label: prop.id,
children: []
};
switch (prop.type) {
case "widget":
node.children.push(this.gatherDataTree(widget[prop.id]()));
break;
case "widgetArray":
case "propertyArray":
const arr = widget[prop.id]();
if (arr) {
arr.forEach(function (item) {
node.children.push(this.gatherDataTree(item));
}, this);
}
break;
default:
}
retVal.children.push(node);
}, this);
return retVal;
}
getDataTree() {
return this.gatherDataTree(this.widget());
}
_rowSorting(paramArr) {
if (this.sorting() === "type") {
const typeOrder = ["boolean", "number", "string", "html-color", "array", "object", "widget", "widgetArray", "propertyArray"];
paramArr.sort(function (a, b) {
if (a.type === b.type) {
return a.id < b.id ? -1 : 1;
} else {
return typeOrder.indexOf(a.type) < typeOrder.indexOf(b.type) ? -1 : 1;
}
});
} else if (this.sorting() === "A-Z") {
paramArr.sort(function (a, b) { return a.id < b.id ? -1 : 1; });
} else if (this.sorting() === "Z-A") {
paramArr.sort(function (a, b) { return a.id > b.id ? -1 : 1; });
}
}
filterInputs(d) {
const discArr = Persist.discover(d);
if ((this.filterTags() || this.excludeTags().length > 0 || this.excludeParams.length > 0) && d instanceof PropertyEditor === false) {
const context = this;
return discArr.filter(function (param, _idx) {
if (d[param.id + "_hidden"] && d[param.id + "_hidden"]()) return false;
for (const excludeParamItem of context.excludeParams()) {
const arr = excludeParamItem.split(".");
let widgetName;
let excludeParam;
if (arr.length > 2) {
widgetName = arr[0];
excludeParam = arr[2];
} else {
widgetName = arr[0];
excludeParam = arr[1];
}
if (d.class().indexOf(widgetName) !== -1) {
if (param.id === excludeParam) {
return false;
}
return true;
}
}
if (context.excludeTags().length > 0 && param.ext && param.ext.tags && param.ext.tags.some(function (item) { return (context.excludeTags().indexOf(item) > -1); })) {
return false;
}
if ((context.filterTags() && param.ext && param.ext.tags && param.ext.tags.indexOf(context.filterTags()) !== -1) || !context.filterTags()) {
return true;
}
return false;
});
}
return discArr;
}
renderInputs(element, d) {
const context = this;
let discArr = [];
const showFields = !this.show_settings() && this.showFields();
if (d) {
discArr = this.filterInputs(d).filter(function (prop) { return prop.id !== "fields" ? true : showFields; });
if (!this.show_settings() && this.showData() && d.data) {
discArr.push({ id: "data", type: "array" });
}
if (this.hideNonWidgets()) {
discArr = discArr.filter(function (n) {
return hasProperties(n.type);
});
}
this._rowSorting(discArr);
}
const rows = element.selectAll("tr.prop" + this.id()).data(discArr, function (d2) { return d2.id; });
rows.enter().append("tr")
.attr("class", "property-wrapper prop" + this.id())
.each(function (param) {
const tr = d3Select(this);
if (hasProperties(param.type)) {
tr.classed("property-widget-wrapper", true);
tr.append("td")
.attr("colspan", "2")
;
} else {
tr.classed("property-input-wrapper", true);
tr.append("td")
.classed("property-label", true)
.text(param.id)
;
const inputCell = tr.append("td")
.classed("property-input-cell", true)
;
context.enterInputs(d, inputCell, param);
}
}).merge(rows)
.each(function (param) {
const tr = d3Select(this);
tr.classed("disabled", d[param.id + "_disabled"] && d[param.id + "_disabled"]());
tr.classed("invalid", d[param.id + "_valid"] && !d[param.id + "_valid"]());
tr.attr("title", param.description);
if (hasProperties(param.type)) {
context.updateWidgetRow(d, tr.select("td"), param);
} else {
context.updateInputs(d, param);
}
});
rows.exit().each(function (param) {
const tr = d3Select(this);
if (hasProperties(param.type)) {
context.updateWidgetRow(d, tr.select("td"), null);
}
}).remove();
rows.order();
}
updateWidgetRow(widget: PropertyExt, element, param) {
let tmpWidget = [];
if (widget && param) {
tmpWidget = widget[param.id]() || [];
}
let widgetArr = tmpWidget instanceof Array ? tmpWidget : [tmpWidget];
if (param && param.ext && param.ext.autoExpand) {
// remove empties and ensure last row is an empty ---
let lastModified = true;
const noEmpties = widgetArr.filter(function (row, idx) {
lastModified = row.valid();
row._owner = widget;
return lastModified || idx === widgetArr.length - 1;
}, this);
const widgetDisabled = widget[param.id + "_disabled"] && widget[param.id + "_disabled"]();
let changed = !!(widgetArr.length - noEmpties.length);
if (lastModified && !widgetDisabled) {
changed = true;
const autoExpandWidget = new param.ext.autoExpand()
.owner(widget)
;
// autoExpandWidget.monitor((id, newVal, oldVal, source) => {
// widget.broadcast(param.id, newVal, oldVal, source);
// });
noEmpties.push(autoExpandWidget);
}
if (changed) {
widget[param.id](noEmpties);
widgetArr = noEmpties;
}
}
const context = this;
element.classed("headerRow", true);
const peInput = element.selectAll(`div.peInput-${this.depth()}`).data(widgetArr, function (d) { return d.id(); });
peInput.enter().append("div")
.attr("class", `peInput peInput-${this.depth()}`)
.each(function (w) {
const peInputElement = d3Select(this);
// Header ---
peInputElement.append("span");
peInputElement.append("i")
.attr("class", "fa")
.on("click", function (d) {
const clickTarget = peInputElement.select("div");
clickTarget
.classed("property-table-collapsed", !clickTarget.classed("property-table-collapsed"))
;
d3Select(this)
.classed("fa-minus-square-o", !clickTarget.classed("property-table-collapsed"))
.classed("fa-plus-square-o", clickTarget.classed("property-table-collapsed"))
;
context.refreshExpandIcon();
})
;
// Body ---
const peDiv = peInputElement.append("div")
// .attr("class", `property- input - cell propEditor-${context.depth() }`)
;
context._childPE.set(this, new PropertyEditor().label(param.id).target(peDiv.node() as HTMLElement));
})
.merge(peInput)
.each(function (w) {
const peInputElement = d3Select(this);
const clickTarget = peInputElement.select("div");
// Header ---
d3Select(this).select("span")
.text(`${param.id}`)
;
d3Select(this).select("i")
.classed("fa-minus-square-o", !clickTarget.classed("property-table-collapsed"))
.classed("fa-plus-square-o", clickTarget.classed("property-table-collapsed"))
;
// Body ---
context._childPE.get(this)
.parentPropertyEditor(context)
.showFields(context.showFields())
.showData(context.showData())
.sorting(context.sorting())
.filterTags(context.filterTags())
.excludeTags(context.excludeTags())
.excludeParams(context.excludeParams())
.hideNonWidgets(context.hideNonWidgets() && w._class.indexOf("layout_") >= 0)
.widget(w)
.render()
;
})
;
peInput.exit()
.each(function (w) {
context._childPE.get(this)
.widget(null)
.render()
.target(null)
;
context._childPE.remove(this);
})
.remove()
;
}
setProperty(widget, id, value) {
// With PropertyExt not all "widgets" have a render, if not use top most render...
let topWidget: Widget;
let topPropEditor: Widget;
let propEditor: PropertyEditor = this;
let oldValue;
while (propEditor && widget) {
if (propEditor === this) {
oldValue = widget[id]();
widget[id](value);
}
if (propEditor) {
topPropEditor = propEditor;
const w: PropertyExt = propEditor.widget();
if (w instanceof Widget) {
topWidget = w;
}
}
propEditor = propEditor.parentPropertyEditor();
}
if (topWidget) {
topWidget.render();
}
if (topPropEditor) {
topPropEditor.broadcast(id, value, oldValue, widget);
}
}
enterInputs(widget, cell, param) {
cell.classed(param.type + "-cell", true);
const context = this;
if (typeof (param.ext.editor_input) === "function") {
param.ext.editor_input(this, widget, cell, param);
}
switch (param.type) {
case "boolean":
cell.append("input")
.attr("id", this.id() + "_" + param.id)
.classed("property-input", true)
.attr("type", "checkbox")
.on("change", function () {
context.setProperty(widget, param.id, this.checked);
})
;
break;
case "set":
cell.append("select")
.attr("id", this.id() + "_" + param.id)
.classed("property-input", true)
.on("change", function () {
context.setProperty(widget, param.id, this.value);
})
;
break;
case "array":
case "object":
cell.append("textarea")
.attr("id", this.id() + "_" + param.id)
.classed("property-input", true)
.attr("autocomplete", "off")
.attr("autocorrect", "off")
.attr("autocapitalize", "off")
.attr("spellcheck", "false")
.on("change", function () {
let value;
try {
value = JSON.parse(this.value);
} catch (e) {
value = this.value;
}
context.setProperty(widget, param.id, value);
})
;
break;
default:
if (param.ext && param.ext.range) {
cell.append("span")
.classed("property-input-span", true)
.attr("id", this.id() + "_" + param.id + "_currentVal")
.text(param.defaultValue)
;
cell.append("input")
.attr("type", "range")
.attr("step", param.ext.range.step)
.attr("min", param.ext.range.min)
.attr("max", param.ext.range.max)
.attr("id", this.id() + "_" + param.id)
.classed("property-input", true)
.on("input", function () {
context.setProperty(widget, param.id, this.value);
d3Select("#" + this.id + "_currentVal").text("Current Value: " + this.value);
})
.on("change", function () {
context.setProperty(widget, param.id, this.value);
d3Select("#" + this.id + "_currentVal").text("Current Value: " + this.value);
})
;
} else {
cell.append(param.ext && param.ext.multiline ? "textarea" : "input")
.attr("id", this.id() + "_" + param.id)
.classed("property-input", true)
.attr("autocomplete", "off")
.attr("autocorrect", "off")
.attr("autocapitalize", "off")
.attr("spellcheck", "false")
.on("change", function () {
context.setProperty(widget, param.id, this.value);
})
;
if (param.type === "html-color" && !Platform.isIE) {
cell.append("input")
.attr("id", this.id() + "_" + param.id + "_2")
.classed("property-input", true)
.attr("type", "color")
.on("change", function () {
context.setProperty(widget, param.id, this.value);
})
;
}
}
break;
}
}
updateInputs(widget, param) {
const element = d3SelectAll("#" + this.id() + "_" + param.id + ", #" + this.id() + "_" + param.id + "_2");
const val = widget ? widget[param.id]() : "";
element.property("disabled", widget[param.id + "_disabled"] && widget[param.id + "_disabled"]());
element.property("invalid", widget[param.id + "_valid"] && !widget[param.id + "_valid"]());
switch (param.type) {
case "boolean":
element.property("checked", val);
break;
case "set":
const options = element.selectAll("option").data(widget[param.id + "_options"]());
options.enter().append("option")
.merge(options as any)
.attr("value", (d: any) => (d && d.value !== undefined) ? d.value : d)
.text((d: any) => (d && d.text !== undefined) ? d.text : d)
;
options.exit().remove();
element.property("value", val);
break;
case "array":
case "object":
element.property("value", JSON.stringify(val, function replacer(_key, value) {
if (value instanceof Widget) {
return Persist.serialize(value);
}
return value;
}, " "));
break;
default:
if (param.ext && param.ext.range) {
d3Select("#" + this.id() + "_" + param.id + "_currentVal").text("Current Value: " + val);
}
element.property("value", val && val.length && val.length > 100000 ? "...too big to display..." : val);
break;
}
}
showFields: { (): boolean; (_: boolean): PropertyEditor; };
showData: { (): boolean; (_: boolean): PropertyEditor; };
sorting: { (): string; (_: string): PropertyEditor; };
sorting_options: () => string[];
hideNonWidgets: { (): boolean; (_: boolean): PropertyEditor; };
label: { (): string; (_: string): PropertyEditor; };
filterTags: { (): string; (_: string): PropertyEditor; };
excludeTags: { (): string[]; (_: string[]): PropertyEditor; };
excludeParams: { (): string[]; (_: string[]): PropertyEditor; };
widget: { (): PropertyExt; (_: PropertyExt): PropertyEditor };
}
PropertyEditor.prototype._class += " other_PropertyEditor";
PropertyEditor.prototype.publish("showFields", false, "boolean", "If true, widget.fields() will display as if it was a publish parameter.", null, { tags: ["Basic"] });
PropertyEditor.prototype.publish("showData", false, "boolean", "If true, widget.data() will display as if it was a publish parameter.", null, { tags: ["Basic"] });
PropertyEditor.prototype.publish("sorting", "none", "set", "Specify the sorting type", ["none", "A-Z", "Z-A", "type"], { tags: ["Basic"], icons: ["fa-sort", "fa-sort-alpha-asc", "fa-sort-alpha-desc", "fa-sort-amount-asc"] });
PropertyEditor.prototype.publish("hideNonWidgets", false, "boolean", "Hides non-widget params (at this tier only)", null, { tags: ["Basic"] });
PropertyEditor.prototype.publish("label", "", "string", "Label to display in header of property editor table", null, { tags: ["Basic"] });
PropertyEditor.prototype.publish("filterTags", "", "set", "Only show Publish Params of this type", ["Basic", "Intermediate", "Advance", ""], {});
PropertyEditor.prototype.publish("excludeTags", ["Private"], "array", "Exclude this array of tags", null, {});
PropertyEditor.prototype.publish("excludeParams", [], "array", "Exclude this array of params (widget.param)", null, {});
PropertyEditor.prototype.publish("widget", null, "widget", "Widget", null, { tags: ["Basic"], render: false });
const _widgetOrig = PropertyEditor.prototype.widget;
(PropertyEditor.prototype as any).widget = function (_?: Widget): Widget | PropertyEditor {
if (arguments.length && _widgetOrig.call(this) === _) return this;
const retVal = _widgetOrig.apply(this, arguments);
if (arguments.length) {
this.watchWidget(_);
if (_ instanceof Grid) {
const context = this;
_.postSelectionChange = function () {
context._selectedItems = _._selectionBag.get().map(function (item) { return item.widget; });
context.lazyRender();
};
}
}
return retVal;
};