Skip to content

Commit 533e1fd

Browse files
author
Dmitriy Kubyshkin
committed
Added basic support for 0-width selection (cursor).
1 parent 7578f4f commit 533e1fd

File tree

3 files changed

+193
-2
lines changed

3 files changed

+193
-2
lines changed

demo/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
doc = new Document('Line1\nLine that is little bit longer\nLine4'),
1515
editor = new CanvasTextEditor(doc);
1616
document.body.appendChild(editor.getEl());
17+
editor.focus();
1718
}, false);
1819
</script>
1920
</head>

lib/CanvasTextEditor.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"use strict";
22

33
var FontMetrics = require('FontMetrics'),
4-
Document = require('Document');
4+
Document = require('Document'),
5+
Selection = require('Selection');
56

67
/**
78
* Simple plain-text text editor using html5 canvas.
@@ -13,6 +14,7 @@ var CanvasTextEditor = function(doc) {
1314
this._createWrapper();
1415
this._createCanvas();
1516
this._createInput();
17+
this._selection = new Selection(this);
1618
};
1719

1820
module.exports = CanvasTextEditor;
@@ -31,7 +33,9 @@ CanvasTextEditor.prototype._createWrapper = function() {
3133
this.wrapper = document.createElement('div');
3234
this.wrapper.className = this.className;
3335
this.wrapper.style.display = 'inline-block';
36+
this.wrapper.style.position = 'relative';
3437
this.wrapper.style.backgroundColor = '#eee';
38+
this.wrapper.style.border = '5px solid #eee';
3539
this.wrapper.style.overflow = 'hidden';
3640
this.wrapper.tabIndex = 0; // tabindex is necessary to get focus
3741
this.wrapper.addEventListener('focus', this.focus.bind(this), false);
@@ -58,7 +62,7 @@ CanvasTextEditor.prototype._createCanvas = function() {
5862

5963
for(var i = 0; i < maxHeight; ++i) {
6064
this.context.fillText(
61-
this._document.getLine(i), 2, lineHeight * i + baselineOffset
65+
this._document.getLine(i), 0, lineHeight * i + baselineOffset
6266
);
6367
}
6468

@@ -77,6 +81,7 @@ CanvasTextEditor.prototype._createInput = function() {
7781
this.inputEl.style.width = 0;
7882
this.inputEl.addEventListener('blur', this.blur.bind(this), false);
7983
this.inputEl.addEventListener('focus', this._inputFocus.bind(this), false);
84+
this.inputEl.addEventListener('keydown', this.keydown.bind(this), false);
8085
this.inputEl.tabIndex = -1; // we don't want input to get focus by tabbing
8186
this.wrapper.appendChild(this.inputEl);
8287
};
@@ -87,6 +92,7 @@ CanvasTextEditor.prototype._createInput = function() {
8792
*/
8893
CanvasTextEditor.prototype._inputFocus = function() {
8994
this.wrapper.style.outline = '1px solid #09f';
95+
this._selection.setVisible(true);
9096
};
9197

9298
/**
@@ -97,6 +103,22 @@ CanvasTextEditor.prototype.getEl = function() {
97103
return this.wrapper;
98104
};
99105

106+
/**
107+
* Returns font metrics used in this editor.
108+
* @return {FontMetrics}
109+
*/
110+
CanvasTextEditor.prototype.getFontMetrics = function() {
111+
return this._metrics;
112+
};
113+
114+
/**
115+
* Returns current document.
116+
* @return {Document}
117+
*/
118+
CanvasTextEditor.prototype.getDocument = function() {
119+
return this._document;
120+
};
121+
100122
/**
101123
* Resizes editor to provided dimensions.
102124
* @param {Number} width
@@ -109,12 +131,37 @@ CanvasTextEditor.prototype.resize = function(width, height) {
109131
this.context.font = this._metrics.getSize() + 'px ' + this._metrics.getFamily();
110132
};
111133

134+
/**
135+
* Main keydown handler.
136+
*/
137+
CanvasTextEditor.prototype.keydown = function(e) {
138+
var handled = true;
139+
switch(e.keyCode) {
140+
case 37: // Left arrow
141+
this._selection.moveLeft();
142+
break;
143+
case 38: // Up arrow
144+
this._selection.moveUp();
145+
break;
146+
case 39: // Up arrow
147+
this._selection.moveRight();
148+
break;
149+
case 40: // Down arrow
150+
this._selection.moveDown();
151+
break;
152+
default:
153+
handled = false;
154+
}
155+
return !handled;
156+
};
157+
112158
/**
113159
* Blur handler.
114160
*/
115161
CanvasTextEditor.prototype.blur = function() {
116162
this.inputEl.blur();
117163
this.wrapper.style.outline = 'none';
164+
this._selection.setVisible(false);
118165
};
119166

120167
/**

lib/Selection.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Creates new selection for the editor.
3+
* @param {Editor} editor.
4+
* @constructor
5+
*/
6+
Selection = function(editor) {
7+
this.editor = editor;
8+
this.blinkInterval = 500;
9+
10+
this.start = {
11+
line: 0,
12+
character: 0
13+
};
14+
15+
this.end = {
16+
line: 0,
17+
character: 0
18+
};
19+
20+
this.el = document.createElement('div');
21+
this.el.style.position = 'absolute';
22+
this.el.style.width = '1px';
23+
this.el.style.height = this.editor.getFontMetrics().getHeight() + 'px';
24+
this.el.style.backgroundColor = '#000';
25+
26+
this.editor.getEl().appendChild(this.el);
27+
this.setPosition(0, 0);
28+
};
29+
30+
/**
31+
* Responsible for blinking
32+
* @return {void}
33+
*/
34+
Selection.prototype.blink = function() {
35+
if (parseInt(this.el.style.opacity, 10)) {
36+
this.el.style.opacity = 0;
37+
} else {
38+
this.el.style.opacity = 1;
39+
}
40+
};
41+
42+
/**
43+
* Moves both start and end to a specified position inside document.
44+
* @param {number?} line
45+
* @param {number?} character
46+
*/
47+
Selection.prototype.setPosition = function(line, character) {
48+
// Providing defaults for both line and character parts of position
49+
if (typeof line === 'undefined') line = this.end.line
50+
if (typeof character === 'undefined') character = this.end.character
51+
52+
// Checking lower bounds
53+
line >= 0 || (line = 0);
54+
character >= 0 || (character = 0);
55+
56+
// Checking upper bounds
57+
var lineCount = this.editor.getDocument().getLineCount();
58+
line < lineCount || (line = lineCount - 1);
59+
var characterCount = this.editor.getDocument().getLine(line).trim('\n').length;
60+
character <= characterCount || (character = characterCount);
61+
62+
// Saving new value
63+
this.start.line = this.end.line = line;
64+
this.start.character = this.end.character = character;
65+
66+
// Calculating new position on the screen
67+
var metrics = this.editor.getFontMetrics(),
68+
offsetX = character * metrics.getWidth(),
69+
offsetY = line * metrics.getHeight();
70+
this.el.style.left = offsetX + 'px';
71+
this.el.style.top = offsetY + 'px';
72+
73+
// This helps to see moving cursor when it is always in blink on
74+
// state on a new position. Try to move cursror in any editor and you
75+
// will see this in action.
76+
if(this.isVisible()) {
77+
this.el.style.opacity = 1;
78+
clearInterval(this.interval);
79+
this.interval = setInterval(this.blink.bind(this), this.blinkInterval);
80+
}
81+
};
82+
83+
/**
84+
* Moves up specified amount of lines.
85+
* @param {number} length
86+
*/
87+
Selection.prototype.moveUp = function(length) {
88+
arguments.length || (length = 1);
89+
this.setPosition(this.end.line - length);
90+
};
91+
92+
/**
93+
* Moves down specified amount of lines.
94+
* @param {number} length
95+
*/
96+
Selection.prototype.moveDown = function(length) {
97+
arguments.length || (length = 1);
98+
this.setPosition(this.end.line + length);
99+
};
100+
101+
/**
102+
* Moves up specified amount of lines.
103+
* @param {number} length
104+
*/
105+
Selection.prototype.moveLeft = function(length) {
106+
arguments.length || (length = 1);
107+
this.setPosition(undefined, this.end.character - length);
108+
};
109+
110+
/**
111+
* Moves down specified amount of lines.
112+
* @param {number} length
113+
*/
114+
Selection.prototype.moveRight = function(length) {
115+
arguments.length || (length = 1);
116+
this.setPosition(undefined, this.end.character + length);
117+
};
118+
119+
/**
120+
* Shows or hides cursor.
121+
* @param {void} visible Whether cursor should be visible
122+
*/
123+
Selection.prototype.setVisible = function(visible) {
124+
clearInterval(this.interval);
125+
if(visible) {
126+
this.el.style.display = 'block';
127+
this.el.style.opacity = 1;
128+
this.interval = setInterval(this.blink.bind(this), this.blinkInterval);
129+
} else {
130+
this.el.style.display = 'none';
131+
}
132+
this.visible = visible;
133+
};
134+
135+
/**
136+
* Returns visibility of the cursor.
137+
* @return {Boolean}
138+
*/
139+
Selection.prototype.isVisible = function() {
140+
return this.visible;
141+
};
142+
143+
module.exports = Selection;

0 commit comments

Comments
 (0)