Skip to content

Commit 14fc582

Browse files
author
Dmitriy Kubyshkin
committed
Added simple scroll that always keeps cursor visible.
Added some options to customize the editor.
1 parent 227e626 commit 14fc582

File tree

4 files changed

+115
-32
lines changed

4 files changed

+115
-32
lines changed

demo/index.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,19 @@
99
<script type="text/javascript" src="http://localhost:4000/lib.js"></script>
1010
<script type="text/javascript" charset="utf-8">
1111
document.addEventListener('DOMContentLoaded', function(){
12+
var text = '',
13+
characterCount = 0,
14+
aCharCode = 'a'.charCodeAt(0);
15+
for (var i = 0; i < 100; i++) {
16+
characterCount = Math.floor(Math.random() * 120);
17+
for (var j = 0; j < characterCount; j++) {
18+
text += String.fromCharCode(aCharCode + Math.floor(Math.random() * 26));
19+
};
20+
text += '\n';
21+
};
1222
var CanvasTextEditor = require('CanvasTextEditor'),
1323
Document = require('Document'),
14-
doc = new Document('Line1\nLine that is little bit longer\nLine4'),
24+
doc = new Document(text),
1525
editor = new CanvasTextEditor(doc);
1626
document.body.appendChild(editor.getEl());
1727
editor.focus();

lib/CanvasTextEditor.js

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,30 @@ var FontMetrics = require('FontMetrics'),
88
* Simple plain-text text editor using html5 canvas.
99
* @constructor
1010
*/
11-
var CanvasTextEditor = function(doc) {
11+
var CanvasTextEditor = function(doc, options) {
1212
this._document = doc || (new Document);
13-
this._metrics = new FontMetrics('"Courier New", Courier, monospace', 14);
13+
14+
this.options = {
15+
textColor: 'WindowText',
16+
backgroundColor: 'Window',
17+
selectionColor: 'Highlight',
18+
focusColor: '#09f',
19+
fontFamily: '"Courier New", Courier, monospace',
20+
fontSize: 14,
21+
padding: 5,
22+
width: 640,
23+
height: 480
24+
};
25+
26+
if (typeof options === 'object') {
27+
for(key in options) {
28+
this.options[key] = options[key];
29+
}
30+
}
31+
32+
this._metrics = new FontMetrics(this.options.fontFamily, this.options.fontSize);
1433
this._createWrapper();
15-
this._selection = new Selection(this);
34+
this._selection = new Selection(this, this.options.textColor);
1635
this._selection.onchange = this.selectionChange.bind(this);
1736
this._createCanvas();
1837
this._createInput();
@@ -24,6 +43,18 @@ var CanvasTextEditor = function(doc) {
2443

2544
module.exports = CanvasTextEditor;
2645

46+
/**
47+
* Top offset in lines
48+
* @type {Number}
49+
*/
50+
CanvasTextEditor.prototype._scrollTop = 0;
51+
52+
/**
53+
* Left offset in characters
54+
* @type {Number}
55+
*/
56+
CanvasTextEditor.prototype._scrollLeft = 0;
57+
2758
/**
2859
* Determines if current browser is Opera
2960
* @type {Boolean}
@@ -77,6 +108,22 @@ CanvasTextEditor.prototype.getSelection = function() {
77108
return this._selection;
78109
};
79110

111+
/**
112+
* Returns current top offset
113+
* @return {number}
114+
*/
115+
CanvasTextEditor.prototype.scrollTop = function() {
116+
return this._scrollTop;
117+
};
118+
119+
/**
120+
* Returns current left offset
121+
* @return {number}
122+
*/
123+
CanvasTextEditor.prototype.scrollLeft = function() {
124+
return this._scrollLeft;
125+
};
126+
80127
/**
81128
* Handles selection change
82129
*/
@@ -95,6 +142,7 @@ CanvasTextEditor.prototype.selectionChange = function() {
95142
}
96143
}
97144

145+
this._checkScroll();
98146
this.setInputText(selectedText, true);
99147

100148
// Updating canvas to show selection
@@ -110,8 +158,8 @@ CanvasTextEditor.prototype._createWrapper = function() {
110158
this.wrapper.className = this.className;
111159
this.wrapper.style.display = 'inline-block';
112160
this.wrapper.style.position = 'relative';
113-
this.wrapper.style.backgroundColor = '#eee';
114-
this.wrapper.style.border = '5px solid #eee';
161+
this.wrapper.style.backgroundColor = this.options.backgroundColor;
162+
this.wrapper.style.border = this.options.padding + 'px solid ' + this.options.backgroundColor;
115163
this.wrapper.style.overflow = 'hidden';
116164
this.wrapper.tabIndex = 0; // tabindex is necessary to get focus
117165
this.wrapper.addEventListener('focus', this.focus.bind(this), false);
@@ -125,11 +173,32 @@ CanvasTextEditor.prototype._createCanvas = function() {
125173
this.canvas = document.createElement('canvas');
126174
this.canvas.style.display = 'block';
127175
this.context = this.canvas.getContext('2d');
128-
this.resize(640, 480);
176+
this.resize(this.options.width, this.options.height);
129177
this.render();
130178
this.wrapper.appendChild(this.canvas);
131179
};
132180

181+
/**
182+
* Makes sure that cursor is visible
183+
* @return {[type]} [description]
184+
*/
185+
CanvasTextEditor.prototype._checkScroll = function() {
186+
var maxHeight = Math.ceil(this.canvas.height / this._metrics.getHeight()) - 1,
187+
maxWidth = Math.ceil(this.canvas.width / this._metrics.getWidth()) - 1,
188+
cursorPosition = this._selection.getPosition();
189+
if (cursorPosition[0] > this._scrollLeft + maxWidth ) {
190+
this._scrollLeft = cursorPosition[0] - maxWidth;
191+
} else if (cursorPosition[0] < this._scrollLeft) {
192+
this._scrollLeft = cursorPosition[0];
193+
}
194+
if (cursorPosition[1] > this._scrollTop + maxHeight) {
195+
this._scrollTop = cursorPosition[1] - maxHeight;
196+
} else if (cursorPosition[1] < this._scrollTop) {
197+
this._scrollTop = cursorPosition[1];
198+
}
199+
this._selection.updateCursorStyle();
200+
};
201+
133202
/**
134203
* Renders document onto the canvas
135204
* @return {[type]} [description]
@@ -138,7 +207,7 @@ CanvasTextEditor.prototype.render = function() {
138207
var baselineOffset = this._metrics.getBaseline(),
139208
lineHeight = this._metrics.getHeight(),
140209
characterWidth = this._metrics.getWidth(),
141-
maxHeight = Math.ceil(640 / lineHeight),
210+
maxHeight = Math.ceil(this.canvas.height / lineHeight),
142211
lineCount = this._document.getLineCount(),
143212
selectionRanges = this._selection.lineRanges(),
144213
selectionWidth = 0;
@@ -147,17 +216,17 @@ CanvasTextEditor.prototype.render = function() {
147216
if (lineCount < maxHeight) maxHeight = lineCount;
148217

149218
// Clearing previous iteration
150-
this.context.fillStyle = '#eee';
219+
this.context.fillStyle = this.options.backgroundColor;
151220
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
152-
this.context.fillStyle = '#000';
221+
this.context.fillStyle = this.options.textColor;
153222

154223
// Looping over document lines
155-
for(var i = 0; i < maxHeight; ++i) {
156-
var topOffset = lineHeight * i;
224+
for(var i = this._scrollTop; i < maxHeight + this._scrollTop; ++i) {
225+
var topOffset = lineHeight * (i - this._scrollTop);
157226

158227
// Rendering selection for this line if one is present
159228
if (selectionRanges[i]) {
160-
this.context.fillStyle = '#cce6ff';
229+
this.context.fillStyle = this.options.selectionColor;
161230

162231
// Check whether we should select to the end of the line or not
163232
if(selectionRanges[i][1] === true) {
@@ -168,19 +237,19 @@ CanvasTextEditor.prototype.render = function() {
168237

169238
// Drawing selection
170239
this.context.fillRect(
171-
selectionRanges[i][0] * characterWidth,
172-
i * lineHeight,
240+
(selectionRanges[i][0] - this._scrollLeft) * characterWidth,
241+
topOffset,
173242
selectionWidth,
174243
lineHeight
175244
)
176245

177246
// Restoring fill color for the text
178-
this.context.fillStyle = '#000';
247+
this.context.fillStyle = this.options.textColor;
179248
}
180249

181250
// Drawing text
182251
this.context.fillText(
183-
this._document.getLine(i), 0, topOffset + baselineOffset
252+
this._document.getLine(i).slice(this._scrollLeft), 0, topOffset + baselineOffset
184253
);
185254
}
186255
};
@@ -291,7 +360,7 @@ CanvasTextEditor.prototype.deleteCharAtCurrentPosition = function(forward) {
291360
* @private
292361
*/
293362
CanvasTextEditor.prototype._inputFocus = function() {
294-
this.wrapper.style.outline = '1px solid #09f';
363+
this.wrapper.style.outline = '1px solid ' + this.options.focusColor;
295364
this._selection.setVisible(true);
296365
};
297366

@@ -338,10 +407,10 @@ CanvasTextEditor.prototype.resize = function(width, height) {
338407
CanvasTextEditor.prototype.keydown = function(e) {
339408
var handled = true;
340409
switch(e.keyCode) {
341-
case 8: // backspace
410+
case 8: // Backspace
342411
this.deleteCharAtCurrentPosition(false);
343412
break;
344-
case 46: // delete
413+
case 46: // Delete
345414
this.deleteCharAtCurrentPosition(true);
346415
break;
347416
case 13: // Enter
@@ -353,7 +422,7 @@ CanvasTextEditor.prototype.keydown = function(e) {
353422
case 38: // Up arrow
354423
this._selection.moveUp(1, this.shiftPressed);
355424
break;
356-
case 39: // Up arrow
425+
case 39: // Right arrow
357426
this._selection.moveRight(1, this.shiftPressed);
358427
break;
359428
case 40: // Down arrow

lib/Selection.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
* @param {Editor} editor.
44
* @constructor
55
*/
6-
Selection = function(editor) {
6+
Selection = function(editor, color) {
77
this.editor = editor;
8+
color || (color = '#000');
89

910
this.start = {
1011
line: 0,
@@ -20,7 +21,7 @@ Selection = function(editor) {
2021
this.el.style.position = 'absolute';
2122
this.el.style.width = '1px';
2223
this.el.style.height = this.editor.getFontMetrics().getHeight() + 'px';
23-
this.el.style.backgroundColor = '#000';
24+
this.el.style.backgroundColor = color;
2425

2526
this.editor.getEl().appendChild(this.el);
2627
this.setPosition(0, 0);
@@ -111,7 +112,6 @@ Selection.prototype.setPosition = function(character, line, keepSelection) {
111112

112113
// Calling private setter that does the heavy lifting
113114
this._doSetPosition(position[0], position[1], keepSelection);
114-
this._updateCursorStyle();
115115

116116
// Making a callback if necessary
117117
if (typeof this.onchange === 'function') {
@@ -143,6 +143,7 @@ Selection.prototype._forceBounds = function(character, line) {
143143
line < lineCount || (line = lineCount - 1);
144144
var characterCount = this.editor.getDocument().getLine(line).trim('\n').length;
145145
if (character > characterCount) {
146+
// Wraparound for lines
146147
if (line === position[1] && line < this.editor.getDocument().getLineCount() - 1) {
147148
++line;
148149
character = 0;
@@ -156,12 +157,12 @@ Selection.prototype._forceBounds = function(character, line) {
156157
/**
157158
* Updates cursor styles so it matches current position
158159
*/
159-
Selection.prototype._updateCursorStyle = function() {
160+
Selection.prototype.updateCursorStyle = function() {
160161
// Calculating new position on the screen
161162
var metrics = this.editor.getFontMetrics(),
162163
position = this.getPosition(),
163-
offsetX = position[0] * metrics.getWidth(),
164-
offsetY = position[1] * metrics.getHeight();
164+
offsetX = (position[0] - this.editor.scrollLeft()) * metrics.getWidth(),
165+
offsetY = (position[1] - this.editor.scrollTop()) * metrics.getHeight();
165166
this.el.style.left = offsetX + 'px';
166167
this.el.style.top = offsetY + 'px';
167168

@@ -182,19 +183,22 @@ Selection.prototype._updateCursorStyle = function() {
182183
* @param {boolean} keepSelection
183184
*/
184185
Selection.prototype._doSetPosition = function(character, line, keepSelection) {
185-
// Saving new value
186+
// If this is a selection range
186187
if (keepSelection) {
187188

188189
compare = this.comparePosition({
189190
line: line,
190191
character: character
191192
}, this.start);
192193

193-
// If selection is empty and we are moving left we set active side to start
194+
// Determining whether we should make the start side of the range active
195+
// (have a cursor). This happens when we start the selection be moving
196+
// left, or moving up.
194197
if (compare === -1 && (this.isEmpty() || line < this.start.line)) {
195198
this.activeEndSide = false;
196199
}
197200

201+
// Assign new value to the side that is active
198202
if (this.activeEndSide) {
199203
this.end.line = line;
200204
this.end.character = character;
@@ -203,7 +207,7 @@ Selection.prototype._doSetPosition = function(character, line, keepSelection) {
203207
this.start.character = character;
204208
}
205209

206-
// Making sure that end is further than start
210+
// Making sure that end is further than start and swap if necessary
207211
if (this.comparePosition(this.start, this.end) > 0) {
208212
this.activeEndSide = !this.activeEndSide;
209213
var temp = {
@@ -215,7 +219,7 @@ Selection.prototype._doSetPosition = function(character, line, keepSelection) {
215219
this.end.line = temp.line;
216220
this.end.character = temp.character;
217221
}
218-
} else {
222+
} else { // Simple cursor move
219223
this.activeEndSide = true;
220224
this.start.line = this.end.line = line;
221225
this.start.character = this.end.character = character;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"author": "Dmitriy Kubyshkin <dmitriy@kubyshkin.ru>",
33
"name": "canvas-text-editor",
44
"description": "Simple text editor using html5 canvas",
5-
"version": "0.0.1",
5+
"version": "0.1.0",
66
"engines": {
77
"node": ">= 0.4.x < 0.7.0"
88
},

0 commit comments

Comments
 (0)