Skip to main content

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

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

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

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

import Konva from 'konva';

Konva._fixTextRendering = true;

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);
});
});