import { HTMLWidget, Palette, Platform, select as d3Select, Utility, } from "@hpcc-js/common"; import { max as d3Max} from "d3-array"; import { hierarchy as d3Hierarchy } from "d3-hierarchy"; interface DirectoryItem { color?: string; iconClass?: string; label: string; depth: number; content?: string; markers?: any; isFolder: boolean; bold?: boolean; selected?: boolean; weightValue?: string; weightColor?: string; } export class DirectoryTree extends HTMLWidget { _palette; constructor() { super(); } flattenData(json): DirectoryItem[] { const context = this; const root = d3Hierarchy(json); const ret = []; if (!this.omitRoot()) { visitNode(root); } else if (root.children) { root.children.forEach(visitNode); } return ret; function visitNode(node) { const weightValue = node.data.markers && node.data.markers.length ? node.data.markers.length : ""; ret.push({ label: node.data.label, depth: node.depth - (context.omitRoot() ? 1 : 0), content: node.data.content, isFolder: !!node.data.children, iconClass: node.data.iconClass, color: node.data.color, bold: node.data.bold, weightValue, markers: node.data.markers, selected: node.data.selected }); if (node.children) { node.children.forEach(visitNode); } } } protected iconClass(d) { if (d.label === "error") { return "fa fa-exclamation"; } if (d.isFolder) { return this.folderIconOpen(); } return this.textFileIcon(); } protected calcRequiredWidth() { const flatData = this.flattenData(this.data()); let widest = 0; const padding = this.rowItemPadding(); const iconWidth = this.iconSize() + (padding * 2); const scrollbarWidth = Platform.getScrollbarWidth(); flatData.forEach(row => { const offsetWidth = (row.depth * iconWidth) + (padding * 2); const textWidth = Utility.textSize( row.label, this.fontFamily(), this.fontSize(), !!row.bold ).width + (padding * 2); const totalWidth = textWidth + iconWidth + offsetWidth + scrollbarWidth; if (widest < totalWidth) { widest = totalWidth; } }); return widest; } rowClick(str, markers) {} enter(domNode, element) { super.enter(domNode, element); element .style("width", "100%") .style("height", "100%") ; } update(domNode, element) { super.update(domNode, element); this._palette = this._palette.switch(this.paletteID()); element .style("overflow-y", this.verticalScroll() ? "scroll" : null) ; const flatData = this.flattenData(this.data()); const maxWeightValue = d3Max(flatData, n=>Number(n.weightValue)); flatData.forEach(d=>{ if(!d.weightValue){ d.weightColor = "transparent"; } else { d.weightColor = this._palette(d.weightValue, 1, maxWeightValue); } }); const context = this; const padding = this.rowItemPadding(); const iconWidth = this.iconSize() + padding; const lineHeight = Math.max(context.iconSize(), context.fontSize()); const rowSelection = element.selectAll(".directory-row").data(flatData); const fontFamily = this.fontFamily(); const fontSize = this.fontSize(); const maxWeightWidth = d3Max(flatData, d=>this.textSize(d.weightValue, fontFamily, fontSize).width); const rowItemPadding = `${padding}px ${padding}px ${padding / 2}px ${padding}px`; const rowEnter = rowSelection.enter().append("div") .attr("class", d => `directory-row directory-row-depth-${d.depth}`) .style("display", "flex") .style("cursor", "pointer") .each(function (d: DirectoryItem) { const rowDiv = d3Select(this); const fontColor = d.color ? d.color : context.fontColor(); const weightColor = d.weightColor ? d.weightColor : "transparent"; const weightFontColor = Palette.textColor(weightColor); const weightDiv = rowDiv.append("div") .attr("class", "row-weight") .style("padding", rowItemPadding) .style("color", weightFontColor) .style("box-shadow", `inset 0 0 100px ${weightColor}`) .style("font-weight", d.bold ? "bold" : "normal") .style("font-family", fontFamily) .style("font-size", fontSize + "px") .text(d.weightValue) .attr("title", d.weightValue) .style("overflow", "hidden") .style("width", (maxWeightWidth + (padding * 2)) + "px") .style("text-overflow", "ellipsis") .style("text-align", "right") .style("line-height", lineHeight + "px") ; rowDiv.append("div") .attr("class", "row-depth") .style("width", (context.depthSize() * d.depth) + "px") .style("opacity", 1) .style("line-height", lineHeight + "px") ; const iconDiv = rowDiv.append("div") .attr("class", "row-icon " + (d.iconClass ? d.iconClass : context.iconClass(d))) .style("width", iconWidth + "px") .style("height", lineHeight + "px") .style("color", fontColor) .style("background-color", d.selected ? context.selectionBackgroundColor() : "transparent") .style("font-size", context.iconSize() + "px") .style("padding", rowItemPadding) .style("line-height", lineHeight + "px") ; const labelDiv = rowDiv.append("div") .attr("class", "row-label") .style("padding", rowItemPadding) .style("color", fontColor) .style("background-color", d.selected ? context.selectionBackgroundColor() : "transparent") .style("font-weight", d.bold ? "bold" : "normal") .style("font-family", context.fontFamily()) .style("font-size", context.fontSize() + "px") .text(d.label) .attr("title", d.label) .style("flex", 1) .style("overflow", "hidden") .style("text-overflow", "ellipsis") .style("line-height", lineHeight + "px") ; rowDiv .on("mouseenter", () => { labelDiv.style("font-weight", "bold"); }) .on("mouseleave", () => { labelDiv.style("font-weight", d.bold ? "bold" : "normal"); }) ; weightDiv .on("mouseenter", () => { context.weight_mouseenter(d); }) .on("mouseleave", () => { context.weight_mouseleave(d); }) ; if (d.isFolder) { rowDiv.on("click", function (d: any) { let next = this.nextSibling; const wasClosed = rowDiv.classed("folder-closed"); if (wasClosed) { rowDiv.classed("folder-closed", false); rowDiv.classed("folder-open", true); iconDiv.attr("class", "row-icon " + context.folderIconOpen()); } else { rowDiv.classed("folder-closed", true); rowDiv.classed("folder-open", false); iconDiv.attr("class", "row-icon " + context.folderIconClosed()); } while (next !== null) { const nextDepth = (d3Select(next).datum() as any).depth; if (nextDepth > d.depth) { next.style.display = wasClosed ? "flex" : "none"; next = next.nextSibling; } else { next = null; } } }); } else { rowDiv.on("click", () => { element.selectAll(".row-label").style("background-color", "transparent"); element.selectAll(".row-icon").style("background-color", "transparent"); iconDiv.style("background-color", context.selectionBackgroundColor()); labelDiv.style("background-color", context.selectionBackgroundColor()); const ext = d.label.split(".").pop().toLowerCase(); context.rowClick(ext === "json" ? JSON.stringify(JSON.parse(d.content), null, 4) : d.content, d.markers); }); } }) ; rowEnter .merge(rowSelection) .style("background-color", context.backgroundColor()) ; rowSelection.exit().remove(); } weight_mouseenter(d){ } weight_mouseleave(d){ } } DirectoryTree.prototype._class += " tree_DirectoryTree"; export interface DirectoryTree { backgroundColor(): string; backgroundColor(_: string): this; fontColor(): string; fontColor(_: string): this; fontFamily(): string; fontFamily(_: string): this; omitRoot(): boolean; omitRoot(_: boolean): this; fontSize(): number; fontSize(_: number): this; iconSize(): number; iconSize(_: number): this; fileIconSize(): number; fileIconSize(_: number): this; folderIconOpen(): string; folderIconOpen(_: string): this; folderIconClosed(): string; folderIconClosed(_: string): this; hoverBackgroundColor(): string; hoverBackgroundColor(_: string): this; selectionBackgroundColor(): string; selectionBackgroundColor(_: string): this; rowItemPadding(): number; rowItemPadding(_: number): this; textFileIcon(): string; textFileIcon(_: string): this; verticalScroll(): boolean; verticalScroll(_: boolean): this; paletteID(): string; paletteID(_: string): this; depthSize(): number; depthSize(_: number): this; } DirectoryTree.prototype._palette = Palette.rainbow("Blues"); DirectoryTree.prototype.publish("depthSize", 14, "number", "Width of indentation per file or folder depth (pixels)"); DirectoryTree.prototype.publish("paletteID", "Blues", "set", "Color palette for the weight backgrounds", DirectoryTree.prototype._palette.switch(), { tags: ["Basic"] }); DirectoryTree.prototype.publish("omitRoot", false, "boolean", "If true, root node will not display"); DirectoryTree.prototype.publish("rowItemPadding", 2, "number", "Top, bottom, left and right row item padding"); DirectoryTree.prototype.publish("selectionBackgroundColor", "#CCC", "html-color", "Background color of selected directory rows"); DirectoryTree.prototype.publish("backgroundColor", "#FFF", "html-color", "Directory item background color"); DirectoryTree.prototype.publish("fontColor", "#000", "html-color", "Directory item font color"); DirectoryTree.prototype.publish("fontFamily", "Arial", "string", "Directory item font family"); DirectoryTree.prototype.publish("fontSize", 12, "number", "Directory item font size (pixels)"); DirectoryTree.prototype.publish("iconSize", 12, "number", "Directory folder and file icon size (pixels)"); DirectoryTree.prototype.publish("folderIconOpen", "fa fa-folder-open", "string", "Open folder icon class"); DirectoryTree.prototype.publish("folderIconClosed", "fa fa-folder", "string", "Closed folder icon class"); DirectoryTree.prototype.publish("textFileIcon", "fa fa-file-text-o", "string", "Text file icon class"); DirectoryTree.prototype.publish("verticalScroll", true, "boolean", "If true, vertical scroll bar will be shown");