Custom edit on TextBlock hangs when text is large

I have a custom editor, which invokes when I click a button.

I have found that when I put lots of text in that custom editor and then save the model.
Then when I open the model and click on the button to invoke the editor, it hangs when theres about 100K of text in the editor. I’ve tested when its about 50 or 60K and that works OK.

Here is a simplified version of the code. If you put lots of text (>100K) in the custom text editor, save the model to file. then own from that file, and click on the button, you should see the issue. (I am using the edge browser):

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Large File Test</title>    
    <script src="https://unpkg.com/gojs/release/go-debug.js"></script>
<script>

var myDiagram;

function init() {

  var $ = go.GraphObject.make;  

  myDiagram = $(go.Diagram, "myDiagramDiv");

  // custom editor #####################
  var customEditor = new go.HTMLInfo();  // Create an HTMLInfo and dynamically create HTML to show/hide
  var documentContainerDiv = document.getElementById("documentContainer");

  customEditor.show = function (textBlock, diagram, tool) {
    if (!(textBlock instanceof go.TextBlock)) return;

    const buttonDoc = textBlock.part.findObject('buttonDocumentation'); // doesnt work for button panels

    var loc = buttonDoc.getDocumentPoint(go.Spot.BottomLeft);
    var pos = diagram.transformDocToView(loc);

    documentContainerDiv.style.display = 'block';
    documentContainerDiv.style.left = (pos.x) + "px";
    documentContainerDiv.style.top = (pos.y + 20) + "px";

    documentContainerDiv.style.display = 'block';             // Unhide div
    var textContentDiv = document.getElementById("documentEditor")
    textContentDiv.value = textBlock.text;
  }

  customEditor.hide = function (diagram, tool) {
    documentContainerDiv.style.display = 'none';
  }

  customEditor.valueFunction = function () {
    var textContentDiv = document.getElementById("documentEditor")
    return textContentDiv.value;
  }

  // end of custom editor
  // ####################################




  function editDoc(e, obj) {

    var nodeObj;
    if (obj.name === "Add Comment") 
    {
      nodeObj = myDiagram.findNodeForKey(obj.part.data.key); // if called from right mouse click,  then add need to get node associated with the context
      // Initialize comment
      const docoTemplate =
        '<h1>' + obj.part.data.key + '</h1>\n' +
        '<h3>Definition:</h3>\n<p style=\"padding-left: 40px;\">enter definition here...</p>\n';
      myDiagram.startTransaction("set init doc");
      myDiagram.model.setDataProperty(obj.part.data, "documentation", docoTemplate);
      myDiagram.commitTransaction("set init doc");
    }
    else
      nodeObj = obj;


    var textBlock = nodeObj.part.findObject('textBlockDocumentation');
    if (myDiagram.commandHandler.canEditTextBlock(textBlock))
      myDiagram.commandHandler.editTextBlock(textBlock);

  }

  function removeComment(e, obj) {
    myDiagram.commit(function (d) {
      d.model.setDataProperty(obj.part.data, "documentation", "");
    }, "remove comment");
  }


  var contextMenuButtonArray = [];

  // This menu item calls editDoc which opens up the editor to allow the user to start creating a comment for th enode
  contextMenuButtonArray.push($("ContextMenuButton", { name: "Add Comment" }
    , new go.Binding("visible", "key", function (v){ return !myDiagram.findNodeForKey(v).part.findObject('buttonDocumentation').visible; } )
    , $(go.TextBlock, "Add Comment", { margin: 4 }), { click: editDoc })
  );

  // This menu item removes the comment
  contextMenuButtonArray.push($("ContextMenuButton", { name: "Remove Comment" }
    , new go.Binding("visible", "key", function (v){ return myDiagram.findNodeForKey(v).part.findObject('buttonDocumentation').visible; } )
    , $(go.TextBlock, "Remove Comment", { margin: 4 }), { click: removeComment }));

  var entityTemplate =
    $(go.Node, "Auto", { resizable: true, minSize: new go.Size(100, 100) },
      $(go.Shape, { figure: "Rectangle", strokeWidth: 2, stretch: go.GraphObject.Fill, fill: "lightgray" }),
      $(go.TextBlock,
        {
          name: "textBlockTitle"
          , cursor: "default"
          , textAlign: "center"
          , margin: 10
          , minSize: new go.Size(100, 100)
          , stretch: go.GraphObject.Fill
          , font: "18px sans-serif"
          , editable: true
        }
        , new go.Binding("text", "title").makeTwoWay() 
      ),

      $(go.TextBlock   // Text block for the node documentation, this is always hidden because I have a custom editor
        , {
            name: "textBlockDocumentation"
          , height: 500, width: 200 // this line should be irrelevant but if I remove, the location changes when the editor is invoked
          , textEditor: customEditor
          , visible: false
        }
        , new go.Binding("text", "documentation").makeTwoWay()
      ),
      $("Button"
        , {
          name: "buttonDocumentation"
          , alignment: new go.Spot(0, 1, 6, -6)
          , alignmentFocus: go.Spot.BottomLeft
          , "ButtonBorder.fill": "transparent"
          , "ButtonBorder.strokeWidth": 0
          , "ButtonBorder.margin": 0
          , click: editDoc
          , visible: false
        }
        , new go.Binding("visible", "documentation", function (v) { return v.length > 0; })

        , $(go.Shape,  // the button picture
          {
            width: 16, height: 22, strokeWidth: 2
          , geometryString: "M0,0 h14 L22,8 v20 h-22 z M5,14 h11.5 M5,18 h11.5 M5,22 h11.5 M14,0 v8h8"
          }
        )
      )

      , { contextMenu: $("ContextMenu", contextMenuButtonArray) }
    );

  myDiagram.nodeTemplate = entityTemplate;

  var nodeDataArray = [
    { "key": "NodeKey", "title": "Node Title", "documentation": "initial documentation..." },
  ];


  // listen for changes to the openFileButtonInput
  const fileInput = document.getElementById('openFileInputButton');
  fileInput.onchange = () => {
    const selectedFile = fileInput.files[0];
    DisplayContent(selectedFile);
  }

  myDiagram.model = new go.GraphLinksModel(nodeDataArray, []);

} // end init()


function saveContent() {

  let filename = "TestLargeFile.txt";
  let fileContent = document.getElementById("documentEditor").value;

  var diagramModelJSON = myDiagram.model.toJson();
  var link = document.createElement('a');
  const mimeType = 'text/plain';

  link.setAttribute('download', filename);  
  link.setAttribute('href', 'data:' + mimeType + ';charset=utf-8,' + encodeURIComponent(diagramModelJSON));
  link.click();
}


// called from Choose File (openFileButtonInput) and select from directory
async function DisplayContent(file)
{
  var fileContent = await loadFile(file);
  myDiagram.model = go.Model.fromJson(fileContent);
}
   
async function loadFile(file) {

  let myPromise = new Promise(function (resolve) {
    var reader = new FileReader();
    reader.readAsText(file);  // invokes reader.onload        
    reader.onload = function (e) {
      resolve(e.target.result); 
    };
  });

  var fileresult = await myPromise;
  return fileresult;

} // end loadFile(file)


window.addEventListener('DOMContentLoaded', init);

</script>

</head>

<body>

  
  <div id="myDiagramDiv" style="border: solid 1px blue; width:1000px; height:1000px"></div>
  <div id="documentContainer" style="position: absolute; display: none; z-index: 1000;">
    <textarea id="documentEditor" style="height: 300px; width:500px"></textarea>
  </div>

  <input id="openFileInputButton" type="file" accept=".txt" />
  <button id="saveContentButton"     onclick="saveContent()">Save</button>

</body>

</html>

I’m not sure what’s wrong, but the problem goes away when the TextBlock is visible. It’s related to ContextMenuButton Data Binding to Node property. Here’s what I suggest that you use in your node template:

      $(go.TextBlock   // Text block for the node documentation
        , {
            name: "textBlockDocumentation"
          , height: 0, width: 0
          , isMultiline: false
          , textEditor: customEditor
        }
        , new go.Binding("text", "documentation").makeTwoWay()
      ),

Perhaps even more efficient would be to have it be not visible normally, but visible just when editing. So then the TextBlock could be:

      $(go.TextBlock   // Text block for the node documentation, this is always hidden because I have a custom editor
        , {
            name: "textBlockDocumentation"
          , height: 0, width: 0
          , isMultiline: false
          , textEditor: customEditor
          , visible: false
          , textEdited: (tb, olds, news) => tb.visible = false
        }
        , new go.Binding("text", "documentation").makeTwoWay()
      ),

And you would need to change your editDoc function:

    var textBlock = nodeObj.part.findObject('textBlockDocumentation');
    if (myDiagram.commandHandler.canEditTextBlock(textBlock)) {
      textBlock.visible = true;
      textBlock.part.ensureBounds();  // needed to avoid the problem with editing not-visible TextBlocks
      myDiagram.commandHandler.editTextBlock(textBlock);
    }