跳到主要内容

使用 Konva 在 HTML5 Canvas 中编辑文本

由于多种原因,用户无法直接编辑 Konva.Text 内容。Canvas API 的设计目的不是编辑文本。 可以在 Canvas 上模拟文本编辑,例如绘制闪烁光标和模拟选择。 Konva 不支持此功能。建议使用 inputtextarea 等原生 DOM 元素,在 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: 'Some text here',
x: 50,
y: 80,
fontSize: 20,
draggable: true,
width: 200,
});

layer.add(textNode);

const tr = new Konva.Transformer({
nodes: [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);
});
});

textarea 叠加层无法处理的内容

当访问者一次编辑一个字段时,上述方法效果良好。如果文本需要像文档一样工作, 此方法就不再适用:

  • **重排。**调整文本框大小时,叠加层不会重新换行。因此,在变换过程中, 编辑视图与绘制视图不一致。
  • **字体度量。**如果 Web 字体在首次绘制后加载,行高会发生变化。因此, 缓存的度量数据和插入符位置会失效。
  • 单字符样式。Konva.Text 对整个节点应用一种样式。 要在一个段落中混合使用粗体、颜色或字号,需要多个节点和自定义 布局过程。
  • **历史记录。**必须将每次按键操作合并到 Canvas 操作使用的同一个撤销堆栈中。 否则,撤销会在两个不同的时间线之间跳转。
  • **导出保真度。**导出的图像由 Canvas 而不是 textarea 绘制。 因此,两者之间的任何差异都会出现在输出中。

此时,你实际上是在编写文本引擎,而不是编辑器功能。如果不想自行编写,可以使用 Polotno。这是一个使用 Konva 构建的商业设计编辑器 SDK,并且已经解决这些问题。