/* * Copyright (C) 1998-2023 by Northwoods Software Corporation. All Rights Reserved. */ /* * This is an extension and not part of the main GoJS library. * Note that the API for this class may change with any version, even point releases. * If you intend to use an extension in production, you should copy the code to your own source directory. * Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders. * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information. */ import * as go from '../release/go-module.js'; // A "ScrollingTable" Panel // This defines an "AutoRepeatButton" Panel, // which is used by the scrollbar in the "ScrollingTable" Panel. // It is basically a custom "Button" that automatically repeats its click // action when the user holds down the mouse. // The first optional argument may be a number indicating the number of milliseconds // to wait between calls to the click function. Default is 50. // The second optional argument may be a number indicating the number of milliseconds // to delay before starting calls to the click function. Default is 500. // Example: // $("AutoRepeatButton", 150, // slower than the default 50 milliseconds between calls // { // click: (e, button) => doSomething(button.part) // }, // $(go.Shape, "Circle", { width: 8, height: 8 }) // ) go.GraphObject.defineBuilder('AutoRepeatButton', args => { const repeat = go.GraphObject.takeBuilderArgument(args, 50, x => typeof x === "number"); const delay = go.GraphObject.takeBuilderArgument(args, 500, x => typeof x === "number"); const $ = go.GraphObject.make; // some internal helper functions for auto-repeating function delayClicking(e: go.InputEvent, obj: any) { endClicking(e, obj); if (obj.click) { // wait milliseconds before starting clicks obj._timer = setTimeout(() => repeatClicking(e, obj), delay); } } function repeatClicking(e: go.InputEvent, obj: any) { if (obj._timer) clearTimeout(obj._timer); if (obj.click) { obj._timer = setTimeout(() => { if (obj.click) { (obj.click)(e, obj); repeatClicking(e, obj); } }, repeat); // milliseconds between clicks } } function endClicking(e: go.InputEvent, obj: any) { if (obj._timer) { clearTimeout(obj._timer); obj._timer = undefined; } } const button = $("Button", { "ButtonBorder.figure": "Rectangle", "ButtonBorder.fill": "transparent", "ButtonBorder.stroke": null, "_buttonFillOver": "rgba(0, 0, 0, .25)", "_buttonStrokeOver": null, cursor: "auto" }); // override the normal button actions const btndown = button.actionDown; const btnup = button.actionUp; const btncancel = button.actionCancel; button.actionDown = (e, btn) => { delayClicking(e, btn); if (btndown) btndown(e, btn); }; button.actionUp = (e, btn) => { endClicking(e, btn); if (btnup) btnup(e, btn); }; button.actionCancel = (e, btn) => { endClicking(e, btn); if (btncancel) btncancel(e, btn); }; return button; }); // Create a "Table" Panel that supports scrolling. // This creates a Panel that contains the "Table" Panel whose topIndex is modified plus a scroll bar panel. // That "Table" Panel is given a name that is given as the optional first argument. // If not given the name defaults to "TABLE". // The scroll bar panel is named "SCROLLBAR". // It has three pieces, the "UP" "AutoRepeatButton", the "THUMB", and the "DOWN" "AutoRepeatButton". // The scroll bar can be on either side of the "Table" Panel; it defaults to being on the right side. // The side is controlled by whether the column of the "Table" Panel is 0 (the default) or 2. // Example use: // $("ScrollingTable", "TABLE", // new go.Binding("TABLE.itemArray", "someArrayProperty"), // ...) // Note that if you have more than one of these in a Part, // you'll want to make sure each one has a unique name. go.GraphObject.defineBuilder("ScrollingTable", args => { const $ = go.GraphObject.make; const tablename = go.GraphObject.takeBuilderArgument(args, "TABLE"); // an internal helper function used by the THUMB for scrolling to a Y-axis point in local coordinates function setScrollIndexLocal(bar: go.GraphObject | null, y: number) { // may be called with the "SCROLLBAR" panel or any element within it while (bar && bar.name !== "SCROLLBAR") bar = bar.panel; if (!(bar instanceof go.Panel)) return; const table = bar.panel!.findObject(tablename); if (!(table instanceof go.Panel)) return; const up = bar.findObject("UP"); const uph = up ? up.actualBounds.height : 0; const down = bar.findObject("DOWN"); const downh = down ? down.actualBounds.height : 0; const tabh = bar.actualBounds.height; const idx = Math.round(Math.max(0, Math.min(1, (y - uph) / (tabh - uph - downh))) * table.rowCount); incrTableIndex(bar, idx - table.topIndex); } // an internal helper function used by the UP and DOWN buttons for relative scrolling // the OBJ may be the "SCROLLBAR" panel or any element within it function incrTableIndex(obj: go.GraphObject, i: number) { const diagram = obj.diagram; let table: go.GraphObject | null = obj; while (table && table.name !== "SCROLLBAR") table = table.panel; if (table) table = table.panel!.findObject(tablename); if (!(table instanceof go.Panel)) return; if (i === +Infinity || i === -Infinity) { // page up or down const tabh = table.actualBounds.height; const rowh = table.elt(table.topIndex).actualBounds.height; // assume each row has same height? if (i === +Infinity) { i = Math.max(1, Math.ceil(tabh / rowh) - 1); } else { i = -Math.max(1, Math.ceil(tabh / rowh) - 1); } } let idx = table.topIndex + i; if (idx >= table.rowCount - 1) idx = table.rowCount - 1; if (idx < 0) idx = 0; if (table.topIndex !== idx) { if (diagram !== null) diagram.startTransaction("scroll"); table.topIndex = idx; const node = table.part; // may need to reroute links if the table contains any ports if (node instanceof go.Node) node.invalidateConnectedLinks(); updateScrollBar(table); if (diagram !== null) diagram.commitTransaction("scroll"); } } // must be passed either the "ScrollingTable" Panel, or the "Table" Panel that holds the rows // that are scrolled (i.e. adjusting topIndex), or the "SCROLLBAR" Panel function updateScrollBar(table: go.Panel) { if (!(table instanceof go.Panel) || table.type !== go.Panel.Table) return; if (table.part) table.part.ensureBounds(); if (table.name !== tablename) { let tab: go.Panel | null = table; while (tab && !(tab as any)._updateScrollBar) tab = tab.panel; if (!tab) return; table = tab.findObject(tablename) as go.Panel; } // the scrollbar is a sibling of the table const bar = table.panel!.findObject("SCROLLBAR") as go.Panel; if (!bar) return; const idx = table.topIndex; const up = bar.findObject("UP"); let uph = 0; if (up) { up.opacity = (idx > 0) ? 1.0 : 0.3; uph = up.actualBounds.height; } const down = bar.findObject("DOWN"); let downh = 0; if (down) { down.opacity = (idx < table.rowCount - 1) ? 1.0 : 0.3; downh = down.actualBounds.height; } const thumb = bar.findObject("THUMB"); const tabh = bar.actualBounds.height; const availh = Math.max(0, (tabh - uph - downh)); if (table.rowCount <= 0) { if (thumb) thumb.height = Math.min(availh, 10); return; } let rows = 0; let last = idx; for (var i = idx; i < table.rowCount; i++) { var h = table.elt(i).actualBounds.height; if (h > 0) { rows++; last = i; } } const needed = idx > 0 || last < table.rowCount-1; bar.opacity = needed ? 1.0 : 0.5; if (thumb) { thumb.height = Math.max((rows / table.rowCount) * availh, Math.min(availh, 10)) - (thumb instanceof go.Shape ? thumb.strokeWidth : 0); thumb.alignment = new go.Spot(0.5, (Math.min(table.rowCount, (idx+0.5)) / table.rowCount), 0, 0); } } // must be called with the "SCROLLBAR" panel function showScrollButtons(bar: go.Panel, show: boolean) { if (!bar || bar.name !== "SCROLLBAR") return; const table = bar.panel!.findObject(tablename) as go.Panel; if (!table) return; const idx = table.topIndex; const up = bar.findObject("UP"); if (up) up.opacity = show ? ((idx > 0) ? 1.0 : 0.3) : 0.0; const down = bar.findObject("DOWN"); if (down) down.opacity = show ? ((idx < table.rowCount - 1) ? 1.0 : 0.3) : 0.0; const thumb = bar.findObject("THUMB"); if (thumb) thumb.opacity = table.rowCount > 0 ? 1 : 0; } return $(go.Panel, "Table", { // in case external code wants to update the scrollbar _updateScrollBar: updateScrollBar, mouseEnter: (e, table) => (table as any)._updateScrollBar(table) }, // this actually holds the item elements $(go.Panel, "Table", { name: tablename, column: 0, stretch: go.GraphObject.Fill, background: "whitesmoke", rowSizing: go.RowColumnDefinition.None, defaultAlignment: go.Spot.Top }), // this is the scrollbar $(go.RowColumnDefinition, { column: 1, sizing: go.RowColumnDefinition.None }), $(go.Panel, "Table", { name: "SCROLLBAR", column: 1, stretch: go.GraphObject.Vertical, background: "#DDDDDD", mouseEnter: (e, bar: go.GraphObject) => showScrollButtons(bar as go.Panel, true), mouseLeave: (e, bar: go.GraphObject) => showScrollButtons(bar as go.Panel, false) }, // the scroll up button $("AutoRepeatButton", { name: "UP", row: 0, opacity: 0, click: (e, obj) => { e.handled = true; incrTableIndex(obj, -1); } }, $(go.Shape, "TriangleUp", { stroke: null, desiredSize: new go.Size(6, 6) })), $(go.RowColumnDefinition, { row: 0, sizing: go.RowColumnDefinition.None }), { // clicking in the bar scrolls directly to that point in the list of items click: (e, bar) => { e.handled = true; const local = bar.getLocalPoint(e.documentPoint); setScrollIndexLocal(bar, local.y); } }, // the scroll thumb, gets all available extra height $(go.Shape, { name: "THUMB", row: 1, stretch: go.GraphObject.Horizontal, height: 10, margin: new go.Margin(0, 2), fill: "gray", stroke: "transparent", alignment: go.Spot.Top, alignmentFocus: go.Spot.Top, mouseEnter: (e: go.InputEvent, thumb: go.GraphObject) => (thumb as go.Shape).stroke = "gray", mouseLeave: (e: go.InputEvent, thumb: go.GraphObject) => (thumb as go.Shape).stroke = "transparent", isActionable: true, actionMove: (e: go.InputEvent, thumb: go.GraphObject) => { const local = thumb.panel!.getLocalPoint(e.documentPoint); setScrollIndexLocal(thumb, local.y); } }), $(go.RowColumnDefinition, { row: 1, stretch: go.GraphObject.Vertical }), // the scroll down button $("AutoRepeatButton", { name: "DOWN", row: 2, opacity: 0, click: (e, obj) => { e.handled = true; incrTableIndex(obj, +1); } }, $(go.Shape, "TriangleDown", { stroke: null, desiredSize: new go.Size(6, 6) })), $(go.RowColumnDefinition, { row: 2, sizing: go.RowColumnDefinition.None }) ) ); });