Skip to main content

在 HTML5 canvas 中使用 Konva 进行文本编辑

用户无法直接编辑 Konva.Text 内容,原因有很多。实际上,canvas API 并不为此目的而设计。 可以在 canvas 上模拟文本编辑(通过绘制闪烁光标、模拟选择等)。 Konva 并不支持这种情况。我们建议使用本地 DOM 元素(如 inputtextarea)在 canvas 外部编辑用户输入。

如果您想启用完整的富文本编辑功能,请参见 富文本演示

说明:双击文本以编辑。输入内容。按 Enter 或点击外部以保存更改。

import Konva from 'konva';

const stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight,
});

const layer = new Konva.Layer();
stage.add(layer);

const textNode = new Konva.Text({
  text: '一些文本',
  x: 50,
  y: 80,
  fontSize: 20,
  draggable: true,
  width: 200,
});

layer.add(textNode);

const tr = new Konva.Transformer({
  node: textNode,
  enabledAnchors: ['middle-left', 'middle-right'],
  boundBoxFunc: function (oldBox, newBox) {
    newBox.width = Math.max(30, newBox.width);
    return newBox;
  },
});

textNode.on('transform', function () {
  textNode.setAttrs({
    width: textNode.width() * textNode.scaleX(),
    scaleX: 1,
  });
});

layer.add(tr);

textNode.on('dblclick dbltap', () => {
  textNode.hide();
  tr.hide();

  const textPosition = textNode.absolutePosition();
  const stageBox = stage.container().getBoundingClientRect();

  const areaPosition = {
    x: stageBox.left + textPosition.x,
    y: stageBox.top + textPosition.y,
  };

  const textarea = document.createElement('textarea');
  document.body.appendChild(textarea);

  textarea.value = textNode.text();
  textarea.style.position = 'absolute';
  textarea.style.top = areaPosition.y + 'px';
  textarea.style.left = areaPosition.x + 'px';
  textarea.style.width = textNode.width() - textNode.padding() * 2 + 'px';
  textarea.style.height = textNode.height() - textNode.padding() * 2 + 5 + 'px';
  textarea.style.fontSize = textNode.fontSize() + 'px';
  textarea.style.border = 'none';
  textarea.style.padding = '0px';
  textarea.style.margin = '0px';
  textarea.style.overflow = 'hidden';
  textarea.style.background = 'none';
  textarea.style.outline = 'none';
  textarea.style.resize = 'none';
  textarea.style.lineHeight = textNode.lineHeight().toString();
  textarea.style.fontFamily = textNode.fontFamily();
  textarea.style.transformOrigin = 'left top';
  textarea.style.textAlign = textNode.align();
  textarea.style.color = textNode.fill().toString();

  const rotation = textNode.rotation();
  let transform = '';
  if (rotation) {
    transform += 'rotateZ(' + rotation + 'deg)';
  }
  transform += 'translateY(-' + 2 + 'px)';
  textarea.style.transform = transform;

  textarea.style.height = 'auto';
  textarea.style.height = textarea.scrollHeight + 3 + 'px';

  textarea.focus();

  function removeTextarea() {
    textarea.parentNode.removeChild(textarea);
    window.removeEventListener('click', handleOutsideClick);
    window.removeEventListener('touchstart', handleOutsideClick);
    textNode.show();
    tr.show();
    tr.forceUpdate();
  }

  function setTextareaWidth(newWidth = 0) {
    if (!newWidth) {
      newWidth = textNode.placeholder.length * textNode.fontSize();
    }
    textarea.style.width = newWidth + 'px';
  }

  textarea.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey) {
      textNode.text(textarea.value);
      removeTextarea();
    }
    if (e.key === 'Escape') {
      removeTextarea();
    }
  });

  textarea.addEventListener('keydown', function () {
    const scale = textNode.getAbsoluteScale().x;
    setTextareaWidth(textNode.width() * scale);
    textarea.style.height = 'auto';
    textarea.style.height = textarea.scrollHeight + textNode.fontSize() + 'px';
  });

  function handleOutsideClick(e) {
    if (e.target !== textarea) {
      textNode.text(textarea.value);
      removeTextarea();
    }
  }
  setTimeout(() => {
    window.addEventListener('click', handleOutsideClick);
  });
});