Custom text editor

I have this custom text editor. It solves some editing problems but have one bug with TextEdited diagram event. Value of event.parameter is new string instead of previous text like in default. Could you tell me what code in text editor is responsible for updating/storing old string value in diagram.parameter variable?

`‘use strict’;

let BPTextEdtiorFactory = function () {
‘ngInject’;
let textarea;
let topVal = 0;
if (navigator.appVersion.indexOf(“Chrome”) !== -1) topVal = 1;
return {
getTexarea: () => {
return textarea;
},
setTextEditor: (diagram) => {
textarea = document.createElement(‘textarea’);
textarea.id = “BPmGoJSTextEditor”;

  let defaultTextValue = '';

  textarea.addEventListener('input', function (e) {
    let tool = textEditor.tool;
    if (tool.textBlock === null) return;
    let tempText = tool.measureTemporaryTextBlock(this.value);

    let oldskips = diagram.skipsUndoManager;
    diagram.skipsUndoManager = true;
    diagram.startTransaction("Change stroke");

    tool.textBlock.text = textarea.value;

    diagram.commitTransaction("Change stroke");
    diagram.skipsUndoManager = oldskips;

    let loc = tool.textBlock.getDocumentPoint(go.Spot.TopLeft);
    let pos = diagram.position;
    let scale = diagram.scale;

    let maxWidth = parseInt(tool.textBlock._maxWidth);
    let width = tool.textBlock.naturalBounds.width;
    if (width > maxWidth) {
      width = maxWidth;
      tool.textBlock.width = maxWidth;
    }

    this.style.maxwidth = maxWidth + 'px!important;';
    this.style.width = width + 'px';
    this.style.height = tool.textBlock.naturalBounds.height + "px";
    this.style.left = Math.floor((loc.x - pos.x - 1) * scale) + 'px';
    this.style.top = Math.floor((loc.y - pos.y + topVal) * scale) + 'px';
  }, false);

  textarea.addEventListener('keydown', function (e) {
    let tool = textEditor.tool;
    if (tool.textBlock === null) return;
    let key = e.which;
    if (key === 13) { // Enter
      if (tool.textBlock.isMultiline === false) e.preventDefault();
      tool.acceptText(go.TextEditingTool.Enter);
    } else if (key === 9) { // Tab
      tool.acceptText(go.TextEditingTool.Tab);
      e.preventDefault();
    } else if (key === 27) { // Esc
      let oldskips = diagram.skipsUndoManager;
      diagram.skipsUndoManager = true;
      diagram.startTransaction("Change stroke");

      tool.textBlock.text = defaultTextValue;

      diagram.commitTransaction("Change stroke");
      diagram.skipsUndoManager = oldskips;

      tool.doCancel();
      if (tool.diagram !== null) tool.diagram.doFocus();
    }
  }, false);

  textarea.addEventListener('focus', function (e) {
    let tool = textEditor.tool;
    if (!tool || tool.currentTextEditor === null) return;

    if (tool.state === go.TextEditingTool.StateActive) {
      tool.state = go.TextEditingTool.StateEditing;
    }

    if (tool.selectsTextOnActivate) {
      textarea.select();
      textarea.setSelectionRange(0, 9999);
    }
  }, false);

  textarea.addEventListener('blur', function (e) {
    let tool = textEditor.tool;
    if (!tool || tool.currentTextEditor === null) return;

    textarea.focus();

    if (tool.selectsTextOnActivate) {
      textarea.select();
      textarea.setSelectionRange(0, 9999);
    }
  }, false);


  let textEditor = new go.HTMLInfo();

  textEditor.valueFunction = function () {
    let text = textarea.value;
    if (tempTextBlock._trim) text = text.trim();
    if (tempTextBlock._required && text.length === 0) text = defaultTextValue;
    return text;
  };

  textEditor.mainElement = textarea;

  let tempTextBlock;
  textEditor.show = function (textBlock, diagram, tool) {
    if (!(textBlock instanceof go.TextBlock)) return;
    let selnode = diagram.selection.first();

    if (!textBlock.__oldStroke) {
      let oldskips = diagram.skipsUndoManager;
      diagram.skipsUndoManager = true;
      diagram.startTransaction("Change stroke");

      textBlock.__oldStroke = textBlock.stroke;
      textBlock.stroke = 'transparent';
      tempTextBlock = textBlock;

      diagram.commitTransaction("Change stroke");
      diagram.skipsUndoManager = oldskips;
    }

    textEditor.tool = tool;

    if (tool.state === go.TextEditingTool.StateInvalid) {
      textarea.style.border = '1px solid red';
      textarea.focus();
      return;
    }

    defaultTextValue = textBlock.text;
    textarea.value = textBlock.text;
    textarea.rows = textBlock.maxLines;

    diagram.div.style['font'] = textBlock.font;

    let maxLength = parseInt(textBlock._maxLength);
    if (maxLength > 0) {
      textarea.maxLength = maxLength;
    } else {
      textarea.removeAttribute('maxLength');
    }

    let loc = textBlock.getDocumentPoint(go.Spot.TopLeft);
    let pos = diagram.position;
    let scale = diagram.scale;

    let width = textBlock.naturalBounds.width;
    let maxWidth = parseInt(textBlock._maxWidth);
    if (width > maxWidth) {
      width = maxWidth;
      textBlock.width = maxWidth;
    }

    textarea.style.cssText =
      'position: absolute;' +
      'box-shadow: none;' +
      'background: none;' +
      'resize: none;' +
      'transform: scale(' + scale + ');' +
      'transform-origin: 0 0;' +
      'z-index: 60;' +
      'font: inherit;' +
      'max-width: ' + maxWidth + 'px!important;' +
      'width: ' + width + 'px;' +
      'height: ' + textBlock.naturalBounds.height + 'px;' +
      'left: ' + Math.floor((loc.y - pos.y + topVal) * scale) + 'px;' +
      'top: ' + Math.floor((loc.y - pos.y + topVal) * scale) + 'px;' +
      'text-align: ' + textBlock.textAlign + ';' +
      'color: ' + (textBlock.__oldStroke || textBlock.stroke) + ';' +
      'margin: 0;' +
      'padding: 0 1px;' +
      'border: 0;' +
      'outline: none;' +
      // 'white-space: ' + (textBlock.maxLines && textBlock.maxLines > 1 ? 'pre-wrap' : 'nowrap') + ';' +
      'overflow: hidden;';
    if (selnode !== null) {

      // Check event type - End/Start
      if (selnode.data.eventType === 1) {
        textarea.style.cssText = textarea.style.cssText + 'line-height: ;';
      } else if (selnode.data.eventType !== 1) {
        textarea.style.cssText = textarea.style.cssText + 'line-height: 16px;';
      }
    }
    diagram.div.appendChild(textarea);

    textarea.focus();
    if (tool.selectsTextOnActivate) {
      textarea.select();
      textarea.setSelectionRange(0, 9999);
    }
  };

  textEditor.hide = function (diagram, tool) {
    diagram.div.removeChild(textarea);
    textEditor.tool = null;
    if (tempTextBlock && tempTextBlock.__oldStroke) {
      let oldskips = diagram.skipsUndoManager;
      diagram.skipsUndoManager = true;
      diagram.startTransaction("Change stroke");

      tempTextBlock.stroke = tempTextBlock.__oldStroke;
      delete tempTextBlock.__oldStroke;

      diagram.commitTransaction("Change stroke");
      diagram.skipsUndoManager = oldskips;
    }
  };

  let tool = diagram.toolManager.textEditingTool;
  tool.selectsTextOnActivate = true;
  tool.defaultTextEditor = textEditor;

}

}
};

export default BPTextEdtiorFactory;`

Let me rephrase this question. In a forum post https://forum.nwoods.com/t/how-to-expand-nodes-size-dynamically-when-the-text-in-its-textblock-is-being-entered/8253/3?u=mickey you gave us solution for text editing but that solution has bug with keeping previous text in TextEdited event. No matter what event we use (diagram or textblock) the old value is the same as new one. So I can’t check if value has been changed or not. I want to avoid call to backend to update value when it is not changed and that’s why I need to compare previous text with current text. But we also need editor to behave in a way that has been described in post I’m referring. And looking at the custom editor code i don’t have a clue what is responsible for that. I don’t have knowledge of GoJS internals so I need your help to find out.

Thank you.

Hm I just tried to use this custom editor https://gojs.net/latest/extensions/TextEditor.js to check how it behave and found the same problem. It seems this is bug in custom editor examples in general.

We’re looking into this now.

Now I see what is happening. Because you are using the realtime text editing tool, the actual TextBlock element of the node is being updated as you type in the text editor. Then, when the keydown listener calls tool.acceptText, the TextBlock is already updated, so that’s what it thinks the old text was.

In your textEditor.show function, you could save the original string:

textEditor.originalString = textBlock.text;

Then in your keydown listener, you can restore the original text before accepting the new text:

...
if (keynum === 13) { // Enter
    if (tool.textBlock.isMultiline === false) e.preventDefault();
    tool.diagram.startTransaction();
    tool.textBlock.text = textEditor.originalString;
    tool.diagram.commitTransaction("orig text");
    tool.acceptText(go.TextEditingTool.Enter);
    return;
  } else if (keynum === 9) { // Tab
    tool.diagram.startTransaction();
    tool.textBlock.text = TextEditor.originalString;
    tool.diagram.commitTransaction("orig text");
    tool.acceptText(go.TextEditingTool.Tab);
    e.preventDefault();
    return;
  }
...

Well this solution works if we use only keyboard. But finishing of editing is possible to do also by clicking with mouse anywhere out of textarea. How to catch such situation and reset content of originalString so diagram event can contain that value in its paramater variable?

Rather than modifying the keydown listener, override the TextEditingTool.acceptText function after setting the TextEditingTool.defaultTextEditor. You’ll still need to save the original string. It will look similar to this:

myDiagram.toolManager.textEditingTool.defaultTextEditor = window.TextEditor;
myDiagram.toolManager.textEditingTool.acceptText = function (reason) {
  var editor = this.defaultTextEditor;
  var tool = editor.tool;
  tool.diagram.startTransaction("original text");
  tool.textBlock.text = editor.originalString;
  tool.diagram.commitTransaction("original text");
  go.TextEditingTool.prototype.acceptText.call(this, reason);
}

Yes that is final solution. It solves all our problems with custom text editor. Thank you.

After latest GoJS update our custom text editor can’t be activated on mouse click. Any idea?

Is there some other object over the TextBlock? We fixed an issue where the TextEditingTool sometimes ignored objects in front of a TextBlock. See the changelog: GoJS Change Log

Not as I know. It should not be objects over text blocks in our case.

Well it’s hard to say what might be happening without a screenshot, template, or reproducible example.

Ok I will try to investigate when have more time and let you know more about it tomorrow.

I just discovered that problem occurs because of Adornment. Adornment had a height over node with transparent background. That was the reason why i couldn’t activate text editing. I corrected that and now everything is as should be.

Did you just replace background: "transparent" with background: null in the Adornment?

(I’m saying this to make the “fix” clear to anyone else who might read this.)

No but that is good idea. I put maxSize to (NaN, 48) in my case to keep it to not overlap everything below. Because it is toolbar width fixed height. But putting background to null is actually better practice. Especially for second adornment I have on links. It covers much bigger rectangle in some cases and using null for background solves this easily.