Here’s the final sample that I’ll give you involving this issue. I’ve customized the text editor used for the editable “FILTER” TextBlock in the ChoicesAdornment so that as the user types, each of the list’s item panels are made visible or not depending on whether their TextBlock.text string includes the filter string.
Note that when that “FILTER” TextBlock is being edited, the user has to leave the text editor before the user can do anything else in the diagram, such as selecting a choice, scrolling the choices list, selecting another node, or even just hiding the ChoicesAdornment.
<!DOCTYPE html>
<html>
<head>
<title>Simple Choices Selector Adornment</title>
<!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<textarea id="mySavedModel" style="width:100%;height:150px"></textarea>
<script src="https://unpkg.com/gojs"></script>
<!-- <script src="https://unpkg.com/create-gojs-kit/dist/extensions/ScrollingTable.js"></script> -->
<script src="../latest/extensions/ScrollingTable.js"></script>
<script id="code">
// This is copied and adapted from extensions/TextEditor.js
// in order to implement filtering in the ChoicesAdornment in this sample.
// See the additional code in the textarea's "input" listener.
/*
* Copyright (C) 1998-2024 by Northwoods Software Corporation. All Rights Reserved.
*/
// This is the definitions of the predefined text editor used by TextEditingTool
// when you set or bind TextBlock.editable to true.
// You do not need to load this file in order to use in-place text editing.
// HTML + JavaScript text editor menu, made with HTMLInfo
// This is a re-implementation of the default text editor
// This file exposes one instance of HTMLInfo, window.TextEditor
// Typical usage is:
// ```js
// new go.Diagram(...,
// {
// 'textEditingTool.defaultTextEditor': window.TextEditor,
// . . .
// })
// ```
// or:
// ```js
// myDiagram.toolManager.textEditingTool.defaultTextEditor = window.TextEditor;
// ```
// ```js
// $(go.Node, . . .,
// . . .
// $(go.TextBlock, { textEditor: window.TextEditor, . . . })
// . . .
// )
// ```
// If you do use this code, copy it into your project and modify it there.
// See also TextEditor.html
((window) => {
const TextEditor = new go.HTMLInfo();
const textarea = document.createElement('textarea');
textarea.id = 'myTextArea';
textarea.addEventListener('input', (e) => {
const tool = TextEditor.tool;
if (tool.textBlock === null)
return;
const tempText = tool.measureTemporaryTextBlock(textarea.value);
const scale = textarea.textScale;
textarea.style.width =
20 +
Math.max(tool.textBlock.measuredBounds.width, tempText.measuredBounds.width) * scale +
'px';
textarea.rows = Math.max(tool.textBlock.lineCount, tempText.lineCount);
// filter the choices list held in the "TABLE" panel of the Adornment
const ad = tool.textBlock.part;
const table = ad.findObject("TABLE");
if (!table) return;
const filter = textarea.value.toLowerCase();
table.elements.each(itempanel => {
const itemtext = itempanel.elt(0).text;
itempanel.visible = itemtext.toLowerCase().includes(filter);
});
const scroller = ad.findObject("SCROLLER");
if (scroller && scroller._updateScrollBar) scroller._updateScrollBar(table);
}, false);
textarea.addEventListener('keydown', (e) => {
if (e.isComposing)
return;
const tool = TextEditor.tool;
if (tool.textBlock === null)
return;
const code = e.code;
if (code === 'Enter') {
// Enter
if (tool.textBlock.isMultiline === false)
e.preventDefault();
tool.acceptText(go.TextEditingAccept.Enter);
}
else if (code === 'Tab') {
// Tab
tool.acceptText(go.TextEditingAccept.Tab);
e.preventDefault();
}
else if (code === 'Escape') {
// Esc
tool.doCancel();
if (tool.diagram !== null)
tool.diagram.doFocus();
}
}, false);
// handle focus:
textarea.addEventListener('focus', (e) => {
const tool = TextEditor.tool;
if (!tool || tool.currentTextEditor === null || tool.state === go.TextEditingState.None)
return;
if (tool.state === go.TextEditingState.Active) {
tool.state = go.TextEditingState.Editing;
}
if (tool.selectsTextOnActivate) {
textarea.select();
textarea.setSelectionRange(0, 9999);
}
}, false);
// Disallow blur.
// If the textEditingTool blurs and the text is not valid,
// we do not want focus taken off the element just because a user clicked elsewhere.
textarea.addEventListener('blur', (e) => {
const tool = TextEditor.tool;
if (!tool || tool.currentTextEditor === null || tool.state === go.TextEditingState.None)
return;
textarea.focus();
if (tool.selectsTextOnActivate) {
textarea.select();
textarea.setSelectionRange(0, 9999);
}
}, false);
TextEditor.valueFunction = () => textarea.value;
TextEditor.mainElement = textarea; // to reference it more easily
TextEditor.tool = null; // Initialize
// used to be in doActivate
TextEditor.show = (textBlock, diagram, tool) => {
if (!diagram || !diagram.div)
return;
if (!(textBlock instanceof go.TextBlock))
return;
if (TextEditor.tool !== null)
return; // Only one at a time.
TextEditor.tool = tool; // remember the TextEditingTool for use by listeners
// This is called during validation, if validation failed:
if (tool.state === go.TextEditingState.Invalid) {
textarea.style.border = '3px solid red';
textarea.focus();
return;
}
// This part is called during initalization:
const loc = textBlock.getDocumentPoint(go.Spot.Center);
const pos = diagram.position;
const sc = diagram.scale;
let textscale = textBlock.getDocumentScale() * sc;
if (textscale < tool.minimumEditorScale)
textscale = tool.minimumEditorScale;
// Add slightly more width/height to stop scrollbars and line wrapping on some browsers
// +6 is firefox minimum, otherwise lines will be wrapped improperly
const textwidth = textBlock.naturalBounds.width * textscale + 6;
const textheight = textBlock.naturalBounds.height * textscale + 2;
const left = (loc.x - pos.x) * sc;
const yCenter = (loc.y - pos.y) * sc; // this is actually the center, used to set style.top
const valign = textBlock.verticalAlignment;
const oneLineHeight = textBlock.lineHeight + textBlock.spacingAbove + textBlock.spacingBelow;
const allLinesHeight = oneLineHeight * textBlock.lineCount * textscale;
const center = 0.5 * textheight - 0.5 * allLinesHeight;
// add offset to yCenter to get the appropriate position:
const yOffset = valign.y * textheight -
valign.y * allLinesHeight +
valign.offsetY -
center -
allLinesHeight / 2;
textarea.value = textBlock.text;
// the only way you can mix font and fontSize is if the font inherits and the fontSize overrides
// in the future maybe have textarea contained in its own div
diagram.div.style['font'] = textBlock.font;
const paddingsize = 1;
textarea.style['position'] = 'absolute';
textarea.style['zIndex'] = '100';
textarea.style['font'] = 'inherit';
textarea.style['fontSize'] = textscale * 100 + '%';
textarea.style['lineHeight'] = 'normal';
textarea.style['width'] = textwidth + 'px';
textarea.style['left'] = ((left - textwidth / 2) | 0) - paddingsize + 'px';
textarea.style['top'] = ((yCenter + yOffset) | 0) - paddingsize + 'px';
textarea.style['textAlign'] = textBlock.textAlign;
textarea.style['margin'] = '0';
textarea.style['padding'] = paddingsize + 'px';
textarea.style['border'] = '0';
textarea.style['outline'] = 'none';
textarea.style['whiteSpace'] = 'pre-wrap';
textarea.style['overflow'] = 'hidden'; // for proper IE wrap
textarea.rows = textBlock.lineCount;
textarea.textScale = textscale; // attach a value to the textarea, for convenience
textarea.className = 'goTXarea';
// Show:
diagram.div.appendChild(textarea);
// After adding, focus:
textarea.focus();
if (tool.selectsTextOnActivate) {
textarea.select();
textarea.setSelectionRange(0, 9999);
}
};
TextEditor.hide = (diagram, tool) => {
TextEditor.tool = null; // forget reference to TextEditingTool
if (diagram.div)
diagram.div.removeChild(textarea);
};
window.TextEditor = TextEditor;
})(window);
const myDiagram =
new go.Diagram("myDiagramDiv", {
"BackgroundSingleClicked": e => {
if (ChoicesAdornment.diagram &&
ChoicesAdornment.actualBounds.containsPoint(e.diagram.lastInput.documentPoint)) return;
showChoices(null)
},
"textEditingTool.starting": go.TextEditingStarting.SingleClick,
"undoManager.isEnabled": true,
"ModelChanged": e => { // just for demonstration purposes,
if (e.isTransactionFinished) { // show the model data in the page's TextArea
document.getElementById("mySavedModel").textContent = e.model.toJson();
}
}
});
// the Adornment holding the list of choices
const ChoicesAdornment =
new go.Adornment("Spot")
.add(
new go.Placeholder(), // placeholder for the TextBlock in the Node
new go.Panel("Auto", {
alignment: go.Spot.BottomLeft,
alignmentFocus: go.Spot.TopLeft,
stretch: go.Stretch.Horizontal
})
.add(
// border around whole filter + list
new go.Shape({ fill: null, stroke: "lightgray", strokeWidth: 4 }),
new go.Panel("Vertical", {
stretch: go.Stretch.Horizontal,
defaultStretch: go.Stretch.Horizontal
})
.add(
new go.Panel("Auto")
.add(
new go.Shape({ fill: "lightyellow" }),
new go.TextBlock("filter", {
name: "FILTER",
stretch: go.Stretch.Horizontal,
margin: new go.Margin(2, 2, 1, 2),
font: "italic 10pt sans-serif",
isMultiline: false,
editable: true,
textEditor: window.TextEditor
})
),
new go.Panel("Auto")
.add(
new go.Shape({ fill: "white", stroke: "gray", strokeWidth: 2 }),
go.GraphObject.build("ScrollingTable", {
name: "SCROLLER",
maxSize: new go.Size(NaN, 65),
stretch: go.Stretch.Fill,
"TABLE.rowSizing": go.Sizing.None,
"TABLE.itemTemplate":
new go.Panel("TableRow", {
isActionable: true, // to allow a click event in an Adornment
click: (e, item) => {
if (e.diagram.currentTool instanceof go.TextEditingTool) {
e.diagram.currentTool.stopTool();
}
const tb = item.part.adornedPart.findObject("TB");
if (!tb) return;
e.diagram.commit(diag => {
tb.text = item.data;
showChoices(null);
});
},
// for mouse-over highlighting
background: "transparent",
mouseEnter: (e, item) => item.background = "cyan",
mouseLeave: (e, item) => item.background = "transparent"
})
.add(
new go.TextBlock({ margin: 1 })
.bind("text", "") // TextBlock.text gets the whole Array item value
)
})
.bind("TABLE.itemArray", "choices")
.bind("SCROLLBAR.visible", "choices", arr => arr.length > 6)
)
)
)
);
function showChoices(node) {
const ad = ChoicesAdornment;
const currentnode = ad.adornedPart;
// if ChoicesAdornment is shown, when no node or when toggling visibility on currentnode,
// remove the ChoicesAdornment
if (currentnode && (!node || currentnode === node)) {
const oldshp = currentnode.findObject("SHP");
if (oldshp) oldshp.figure = "LineDown";
currentnode.removeAdornment("Choices");
ad.adornedObject = null;
if (currentnode === node) return;
ad._key = undefined;
}
if (!node) return;
// maybe showing on a different node, so any filter no longer applies
if (ad._key !== node.key) {
const filter = ad.findObject("FILTER");
if (filter) filter.text = "";
const choices = ad.findObject("TABLE");
if (choices) {
choices.itemArray = [];
choices.topIndex = 0;
}
}
const tb = node.findObject("TB");
const shp = node.findObject("SHP");
if (!tb || !shp) return;
shp.figure = "LineUp";
ad.adornedObject = tb;
node.addAdornment("Choices", ad);
// if there's no "choices" data Array, use a default one
if (!Array.isArray(node.data.choices) || node.data.choices.length === 0) {
ad.data = { choices: ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta", "Eta", "Theta"] }; // default choices Array
}
// remember which node it last showed for
ad._key = node.key;
}
myDiagram.nodeTemplate =
new go.Node("Auto")
.add(
new go.Shape({ fill: "white" }),
new go.Panel("Vertical")
.add(
new go.TextBlock({ margin: 4, font: "bold 12pt sans-serif" })
.bind("text", "title"),
new go.Panel("Horizontal", {
click: (e, pnl) => { // show or hide the ChoicesAdornment
e.handled = true;
showChoices(pnl.part);
},
// for mouse-over highlighting
background: "transparent",
mouseEnter: (e, pnl) => pnl.background = "lightgray",
mouseLeave: (e, pnl) => pnl.background = "transparent"
})
.add(
new go.TextBlock("(choose)", {
name: "TB",
width: 100,
margin: new go.Margin(4, 4, 2, 4),
font: "italic 10pt sans-serif",
stroke: "blue"
})
.bindTwoWay("text", "value")
.bind("font", "value", v => v ? "bold 10pt sans-serif" : "italic 10pt sans-serif")
.bind("stroke", "value", v => v ? "black" : "blue"),
new go.Shape("LineDown", {
name: "SHP",
width: 14, height: 12,
margin: 2,
strokeWidth: 2,
})
)
)
);
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, title: "Alpha", choices: ["one", "two", "three", "four", "five", "six", "seven"], value: "one" },
{ key: 2, title: "Beta", choices: ["hello", "goodbye"] },
{ key: 3, title: "Gamma", choices: ["only one"] },
{ key: 4, title: "Delta" }, // use a default choices Array
]);
</script>
</body>
</html>