Text editing in HTML5 canvas with Konva

Get last news, demos, posts from Konva

User can’t directly edit Konva.Text content for many reasons. In fact canvas API is not designed for such purpose.
It is possible to emulate text editing on canvas (by drawing blinking cursor, emulate selection, etc).
Konva has not support for such case. We recommend to edit the user input outside of your canvas with native DOM elements such as input or textarea.

Here we will create two demos. Basic demo for general understanding of the technic. And the more complex one, used in real-word app that cover more edge cases.

If you want to enable full rich text editing features see Rich Text Demo.

Instructions: Double click on text to edit it. Type something. Press Enter.

Simple demo:

Show source code of simple demo!

Konva Editable text Demoview raw
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]/konva.min.js"></script>
<meta charset="utf-8" />
<title>Konva Editable Text on html5 canvas Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f0f0f0;
}
</style>
</head>

<body>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;

var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});

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

var textNode = new Konva.Text({
text: 'Some text here',
x: 50,
y: 50,
fontSize: 20
});

layer.add(textNode);
layer.draw();

textNode.on('dblclick', () => {
// create textarea over canvas with absolute position

// first we need to find position for textarea
// how to find it?

// at first lets find position of text node relative to the stage:
var textPosition = textNode.getAbsolutePosition();

// then lets find position of stage container on the page:
var stageBox = stage.container().getBoundingClientRect();

// so position of textarea will be the sum of positions above:
var areaPosition = {
x: stageBox.left + textPosition.x,
y: stageBox.top + textPosition.y
};

// create textarea and style it
var 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();

textarea.focus();

textarea.addEventListener('keydown', function(e) {
// hide on enter
if (e.keyCode === 13) {
textNode.text(textarea.value);
layer.draw();
document.body.removeChild(textarea);
}
});
});
</script>
</body>
</html>

Complex demo:

Show source code of complex demo!

Canvas Complex Text Demoview raw
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]0.17/konva.min.js"></script>
<meta charset="utf-8" />
<title>Konva Editable Text on html5 canvas Demo</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #f0f0f0;
}
</style>
</head>

<body>
<div id="container"></div>
<script>
var width = window.innerWidth;
var height = window.innerHeight;

var stage = new Konva.Stage({
container: 'container',
width: width,
height: height
});

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

var textNode = new Konva.Text({
text: 'Some text here',
x: 50,
y: 80,
fontSize: 20,
draggable: true,
width: 200
});

layer.add(textNode);

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

textNode.on('transform', function() {
// reset scale, so only with is changing by transformer
textNode.setAttrs({
width: textNode.width() * textNode.scaleX(),
scaleX: 1
});
});

layer.add(tr);

layer.draw();

textNode.on('dblclick', () => {
// hide text node and transformer:
textNode.hide();
tr.hide();
layer.draw();

// create textarea over canvas with absolute position
// first we need to find position for textarea
// how to find it?

// at first lets find position of text node relative to the stage:
var textPosition = textNode.absolutePosition();

// then lets find position of stage container on the page:
var stageBox = stage.container().getBoundingClientRect();

// so position of textarea will be the sum of positions above:
var areaPosition = {
x: stageBox.left + textPosition.x,
y: stageBox.top + textPosition.y
};

// create textarea and style it
var textarea = document.createElement('textarea');
document.body.appendChild(textarea);

// apply many styles to match text on canvas as close as possible
// remember that text rendering on canvas and on the textarea can be different
// and sometimes it is hard to make it 100% the same. But we will try...
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();
textarea.style.fontFamily = textNode.fontFamily();
textarea.style.transformOrigin = 'left top';
textarea.style.textAlign = textNode.align();
textarea.style.color = textNode.fill();
rotation = textNode.rotation();
var transform = '';
if (rotation) {
transform += 'rotateZ(' + rotation + 'deg)';
}

var px = 0;
// also we need to slightly move textarea on firefox
// because it jumps a bit
var isFirefox =
navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isFirefox) {
px += 2 + Math.round(textNode.fontSize() / 20);
}
transform += 'translateY(-' + px + 'px)';

textarea.style.transform = transform;

// reset height
textarea.style.height = 'auto';
// after browsers resized it we can set actual value
textarea.style.height = textarea.scrollHeight + 3 + 'px';

textarea.focus();

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

function setTextareaWidth(newWidth) {
if (!newWidth) {
// set width for placeholder
newWidth = textNode.placeholder.length * textNode.fontSize();
}
// some extra fixes on different browsers
var isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent
);
var isFirefox =
navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
if (isSafari || isFirefox) {
newWidth = Math.ceil(newWidth);
}

var isEdge =
document.documentMode || /Edge/.test(navigator.userAgent);
if (isEdge) {
newWidth += 1;
}
textarea.style.width = newWidth + 'px';
}

textarea.addEventListener('keydown', function(e) {
// hide on enter
// but don't hide on shift + enter
if (e.keyCode === 13 && !e.shiftKey) {
textNode.text(textarea.value);
removeTextarea();
}
// on esc do not set value back to node
if (e.keyCode === 27) {
removeTextarea();
}
});

textarea.addEventListener('keydown', function(e) {
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);
});
});
</script>
</body>
</html>

Next