I got it
(well.. Claude Grok got it, with Victor’s magic to write to the view’s custom prop)
Markdown component (sorry Ben… I need it to be useable for projects that can’t include 3rd party modules) below:
[
{
"type": "ia.display.markdown",
"version": 0,
"props": {
"markdown": {
"escapeHtml": false
}
},
"meta": {
"name": "JSInjector"
},
"position": {
"x": 600,
"y": -0.375,
"height": 200,
"width": 200
},
"custom": {
"description": "Write to a view custom prop called \"TEST\" => \"bobby\"",
"jsCode": "const tree = document.querySelector('.tree');\nif (tree) {\n const observer = new MutationObserver((mutations) => {\n mutations.forEach((mutation) => {\n // Only process if the target is an expand icon\n if (mutation.target.classList.contains('expand-icon')) {\n const node = mutation.target.closest('[data-item-path]');\n if (!node) return;\n const path = node.dataset.itemPath;\n const isExpanded = mutation.target.dataset.icon === 'material/arrow_drop_down';\n const view = [...window.__client.page.views._data.values()].find(view => view.value.mountPath == this.parentNode.parentNode.parentNode.getAttributeNode('data-component-path').value.split('.')[0]).value;\n view.custom.write('nodePath', path);\n view.custom.write('nodePathChange', isExpanded ? 'expanded' : 'collapsed');\n }\n });\n });\n observer.observe(tree, {\n attributes: true,\n subtree: true,\n attributeFilter: ['data-icon']\n });\n}",
"notes": "Don't use \" in the jsCode. New lines can be funny.."
},
"propConfig": {
"props.source": {
"binding": {
"type": "expr",
"config": {
"expression": "{this.custom.jsCode}"
},
"transforms": [
{
"code": "\tcode = '''<img style=\"display:none\" src=\"/favicon.ico\" onload=\"\n''' + value + '\"/>'\n\treturn code",
"type": "script"
}
]
}
}
}
}
]
Here’s a test view:
{
"custom": {
"nodePath": "0/0/0",
"nodePathChange": "collapsed"
},
"params": {},
"propConfig": {
"custom.nodePath": {
"persistent": true
},
"custom.nodePathChange": {
"persistent": true
}
},
"props": {},
"root": {
"children": [
{
"meta": {
"name": "Tree_0"
},
"position": {
"height": 690,
"width": 530,
"x": 20,
"y": 66
},
"props": {
"appearance": {
"defaultNodeIcons": {
"collapsed": {
"path": "material/folder"
},
"empty": {
"path": "material/stop"
},
"expanded": {
"path": "material/folder_open"
}
},
"expandIcons": {
"collapsed": {
"path": "material/arrow_right"
},
"expanded": {
"path": "material/arrow_drop_down"
}
}
},
"items": [
{
"data": {
"tagPath": "Testing",
"tagType": "Folder"
},
"expanded": true,
"icon": {
"path": "icons/tag-folder"
},
"items": [
{
"data": {
"tagPath": "Testing/Test",
"tagType": "Folder"
},
"expanded": true,
"icon": {
"path": "icons/tag-folder"
},
"items": [
{
"data": {
"Parameters": {
"Description": "",
"Sensor1IPAddress": "",
"Sensor2IPAddress": "",
"Sensor3IPAddress": ""
},
"tagPath": "Testing/Test/WT111",
"tagType": "UdtInstance",
"type": "UdtInstance"
},
"expanded": false,
"icon": {
"path": "icons/tag-udtinstance"
},
"items": [
{
"data": {
"Parameters": {
"Alarm_Area": "",
"Alarm_ParentDevice": "",
"Description": " Sensor 1",
"DeviceName": "",
"Global.": "",
"PLCName": "",
"SMSActivePipeline": ""
},
"tagPath": "Testing/Test/WT111/Displacement Sensor 1",
"tagType": "UdtInstance",
"type": "UdtInstance"
},
"expanded": true,
"icon": {
"path": "icons/tag-udtinstance"
},
"items": [
{
"data": {
"tagPath": "Testing/Test/WT111/Displacement Sensor 1/Sts",
"tagType": "Folder"
},
"expanded": false,
"icon": {
"path": "icons/tag-folder"
},
"items": [
{
"data": {
"dataType": "Float8",
"tagPath": "Testing/Test/WT111/Displacement Sensor 1/Sts/Filtered",
"tagType": "AtomicTag",
"tooltip": "Filtered and zero-offset position value",
"type": "memory",
"valueSource": "memory"
},
"expanded": false,
"icon": {
"path": "icons/tag-notype"
},
"items": [],
"label": "Filtered - Filtered and zero-offset position value"
},
{
"data": {
"dataType": "Float8",
"tagPath": "Testing/Test/WT111/Displacement Sensor 1/Sts/Raw",
"tagType": "AtomicTag",
"tooltip": "The raw position value from the instrument",
"type": "memory",
"valueSource": "memory"
},
"expanded": false,
"icon": {
"path": "icons/tag-notype"
},
"items": [],
"label": "Raw - The raw position value from the instrument"
}
],
"label": "Sts"
}
],
"label": "Displacement Sensor 1 - Sensor 1"
},
{
"data": {
"Parameters": {
"Alarm_Area": "",
"Alarm_ParentDevice": "",
"Description": " Sensor 2",
"DeviceName": "",
"Global.": "",
"PLCName": "",
"SMSActivePipeline": ""
},
"tagPath": "Testing/Test/WT111/Displacement Sensor 2",
"tagType": "UdtInstance",
"type": "UdtInstance"
},
"expanded": true,
"icon": {
"path": "icons/tag-udtinstance"
},
"items": [
{
"data": {
"tagPath": "Testing/Test/WT111/Displacement Sensor 2/Sts",
"tagType": "Folder"
},
"expanded": false,
"icon": {
"path": "icons/tag-folder"
},
"items": [
{
"data": {
"dataType": "Float8",
"tagPath": "Testing/Test/WT111/Displacement Sensor 2/Sts/Filtered",
"tagType": "AtomicTag",
"tooltip": "Filtered and zero-offset position value",
"type": "memory",
"valueSource": "memory"
},
"expanded": false,
"icon": {
"path": "icons/tag-notype"
},
"items": [],
"label": "Filtered - Filtered and zero-offset position value"
},
{
"data": {
"dataType": "Float8",
"tagPath": "Testing/Test/WT111/Displacement Sensor 2/Sts/Raw",
"tagType": "AtomicTag",
"tooltip": "The raw position value from the instrument",
"type": "memory",
"valueSource": "memory"
},
"expanded": false,
"icon": {
"path": "icons/tag-notype"
},
"items": [],
"label": "Raw - The raw position value from the instrument"
}
],
"label": "Sts"
}
],
"label": "Displacement Sensor 2 - Sensor 2"
},
{
"data": {
"Parameters": {
"Alarm_Area": "",
"Alarm_ParentDevice": "",
"Description": " Sensor 3",
"DeviceName": "",
"Global.": "",
"PLCName": "",
"SMSActivePipeline": ""
},
"tagPath": "Testing/Test/WT111/Displacement Sensor 3",
"tagType": "UdtInstance",
"type": "UdtInstance"
},
"expanded": true,
"icon": {
"path": "icons/tag-udtinstance"
},
"items": [
{
"data": {
"tagPath": "Testing/Test/WT111/Displacement Sensor 3/Sts",
"tagType": "Folder"
},
"expanded": false,
"icon": {
"path": "icons/tag-folder"
},
"items": [
{
"data": {
"dataType": "Float8",
"tagPath": "Testing/Test/WT111/Displacement Sensor 3/Sts/Filtered",
"tagType": "AtomicTag",
"tooltip": "Filtered and zero-offset position value",
"type": "memory",
"valueSource": "memory"
},
"expanded": false,
"icon": {
"path": "icons/tag-notype"
},
"items": [],
"label": "Filtered - Filtered and zero-offset position value"
},
{
"data": {
"dataType": "Float8",
"tagPath": "Testing/Test/WT111/Displacement Sensor 3/Sts/Raw",
"tagType": "AtomicTag",
"tooltip": "The raw position value from the instrument",
"type": "memory",
"valueSource": "memory"
},
"expanded": false,
"icon": {
"path": "icons/tag-notype"
},
"items": [],
"label": "Raw - The raw position value from the instrument"
}
],
"label": "Sts"
}
],
"label": "Displacement Sensor 3 - Sensor 3"
}
],
"label": "WT111"
}
],
"label": "Test"
}
],
"label": "Testing"
}
],
"selection": [
"0/0/0/2/0/1"
],
"selectionData": [
{
"itemPath": "0/0/0/2/0/1",
"value": {
"dataType": "Float8",
"tagPath": "Testing/Test/WT111/Displacement Sensor 3/Sts/Raw",
"tagType": "AtomicTag",
"tooltip": "The raw position value from the instrument",
"type": "memory",
"valueSource": "memory"
}
}
],
"style": {
"border": "1px solid #2b2b2b",
"margin": "0.3rem"
}
},
"type": "ia.display.tree"
},
{
"custom": {
"description": "Write to a view custom prop called \"TEST\" \u003d\u003e \"bobby\"",
"jsCode": "const tree \u003d document.querySelector(\u0027.tree\u0027);\nif (tree) {\n const observer \u003d new MutationObserver((mutations) \u003d\u003e {\n mutations.forEach((mutation) \u003d\u003e {\n // Only process if the target is an expand icon\n if (mutation.target.classList.contains(\u0027expand-icon\u0027)) {\n const node \u003d mutation.target.closest(\u0027[data-item-path]\u0027);\n if (!node) return;\n const path \u003d node.dataset.itemPath;\n const isExpanded \u003d mutation.target.dataset.icon \u003d\u003d\u003d \u0027material/arrow_drop_down\u0027;\n const view \u003d [...window.__client.page.views._data.values()].find(view \u003d\u003e view.value.mountPath \u003d\u003d this.parentNode.parentNode.parentNode.getAttributeNode(\u0027data-component-path\u0027).value.split(\u0027.\u0027)[0]).value;\n view.custom.write(\u0027nodePath\u0027, path);\n view.custom.write(\u0027nodePathChange\u0027, isExpanded ? \u0027expanded\u0027 : \u0027collapsed\u0027);\n }\n });\n });\n observer.observe(tree, {\n attributes: true,\n subtree: true,\n attributeFilter: [\u0027data-icon\u0027]\n });\n}",
"notes": "Don\u0027t use \" in the jsCode. New lines can be funny.."
},
"meta": {
"name": "JSInjector"
},
"position": {
"height": 200,
"width": 200,
"x": 600,
"y": -0.375
},
"propConfig": {
"props.source": {
"binding": {
"config": {
"expression": "{this.custom.jsCode}"
},
"transforms": [
{
"code": "\tcode \u003d \u0027\u0027\u0027\u003cimg style\u003d\"display:none\" src\u003d\"/favicon.ico\" onload\u003d\"\n\u0027\u0027\u0027 + value + \u0027\"/\u003e\u0027\n\treturn code",
"type": "script"
}
],
"type": "expr"
}
}
},
"props": {
"markdown": {
"escapeHtml": false
}
},
"type": "ia.display.markdown"
}
],
"custom": {
"TEST": "value"
},
"meta": {
"name": "root"
},
"type": "ia.container.coord"
}
}
The JS:
const tree = document.querySelector('.tree');
if (tree) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// Only process if the target is an expand icon
if (mutation.target.classList.contains('expand-icon')) {
const node = mutation.target.closest('[data-item-path]');
if (!node) return;
const path = node.dataset.itemPath;
const isExpanded = mutation.target.dataset.icon === 'material/arrow_drop_down';
const view = [...window.__client.page.views._data.values()].find(view => view.value.mountPath == this.parentNode.parentNode.parentNode.getAttributeNode('data-component-path').value.split('.')[0]).value;
view.custom.write('nodePath', path);
view.custom.write('nodePathChange', isExpanded ? 'expanded' : 'collapsed');
}
});
});
observer.observe(tree, {
attributes: true,
subtree: true,
attributeFilter: ['data-icon']
});
}
It doesn’t work for multiple trees which I thought was odd considering the query selector… but I don’t need it for multiple so.. 