From ab8a7d4aad26d6f0ba62eaaea6d3b4a03773e0d7 Mon Sep 17 00:00:00 2001 From: Dmitriy Kubyshkin Date: Sat, 17 Mar 2012 17:47:52 +0700 Subject: [PATCH 1/6] First live demo. --- Makefile | 10 - README.md | 3 - canvas-text-editor.js | 519 ++++ demo/index.html => index.html | 9 +- lib/CanvasTextEditor.js | 173 -- lib/Document.js | 72 - lib/FontMetrics.js | 81 - lib/Selection.js | 143 -- package.json | 15 - scripts/build.js | 25 - scripts/common.js | 15 - scripts/serve.js | 11 - test/lib/jasmine-1.1.0/MIT.LICENSE | 20 - test/lib/jasmine-1.1.0/jasmine-html.js | 190 -- test/lib/jasmine-1.1.0/jasmine.css | 166 -- test/lib/jasmine-1.1.0/jasmine.js | 2476 -------------------- test/lib/jasmine-1.1.0/jasmine_favicon.png | Bin 905 -> 0 bytes test/runner.html | 54 - test/spec/DocumentSpec.js | 26 - test/spec/EditorSpec.js | 7 - test/spec/FontMetricsSpec.js | 27 - 21 files changed, 527 insertions(+), 3515 deletions(-) delete mode 100644 Makefile delete mode 100644 README.md create mode 100644 canvas-text-editor.js rename demo/index.html => index.html (70%) delete mode 100644 lib/CanvasTextEditor.js delete mode 100755 lib/Document.js delete mode 100644 lib/FontMetrics.js delete mode 100644 lib/Selection.js delete mode 100644 package.json delete mode 100644 scripts/build.js delete mode 100644 scripts/common.js delete mode 100644 scripts/serve.js delete mode 100644 test/lib/jasmine-1.1.0/MIT.LICENSE delete mode 100644 test/lib/jasmine-1.1.0/jasmine-html.js delete mode 100644 test/lib/jasmine-1.1.0/jasmine.css delete mode 100644 test/lib/jasmine-1.1.0/jasmine.js delete mode 100644 test/lib/jasmine-1.1.0/jasmine_favicon.png delete mode 100755 test/runner.html delete mode 100644 test/spec/DocumentSpec.js delete mode 100644 test/spec/EditorSpec.js delete mode 100644 test/spec/FontMetricsSpec.js diff --git a/Makefile b/Makefile deleted file mode 100644 index de894d5..0000000 --- a/Makefile +++ /dev/null @@ -1,10 +0,0 @@ -# Builds library for browser usage -build: - node ./scripts/build.js - -# Starts express server that serves stitched library -serve: - node ./scripts/serve.js - -# These aren't real targets so we need to list them here -.PHONY: build serve \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 8521ecf..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Canvas Text Editor - -A simple text editor using html5 canvas that is being written as a result of [set of tutorials](http://kubyshkin.ru/tag/text-editor/) on text editor inner workings. \ No newline at end of file diff --git a/canvas-text-editor.js b/canvas-text-editor.js new file mode 100644 index 0000000..ad7b3ab --- /dev/null +++ b/canvas-text-editor.js @@ -0,0 +1,519 @@ + +(function(/*! Stitch !*/) { + if (!this.require) { + var modules = {}, cache = {}, require = function(name, root) { + var path = expand(root, name), module = cache[path], fn; + if (module) { + return module.exports; + } else if (fn = modules[path] || modules[path = expand(path, './index')]) { + module = {id: path, exports: {}}; + try { + cache[path] = module; + fn(module.exports, function(name) { + return require(name, dirname(path)); + }, module); + return module.exports; + } catch (err) { + delete cache[path]; + throw err; + } + } else { + throw 'module \'' + name + '\' not found'; + } + }, expand = function(root, name) { + var results = [], parts, part; + if (/^\.\.?(\/|$)/.test(name)) { + parts = [root, name].join('/').split('/'); + } else { + parts = name.split('/'); + } + for (var i = 0, length = parts.length; i < length; i++) { + part = parts[i]; + if (part == '..') { + results.pop(); + } else if (part != '.' && part != '') { + results.push(part); + } + } + return results.join('/'); + }, dirname = function(path) { + return path.split('/').slice(0, -1).join('/'); + }; + this.require = function(name) { + return require(name, ''); + } + this.require.define = function(bundle) { + for (var key in bundle) + modules[key] = bundle[key]; + }; + } + return this.require.define; +}).call(this)({"CanvasTextEditor": function(exports, require, module) {"use strict"; + +var FontMetrics = require('FontMetrics'), + Document = require('Document'), + Cursor = require('Cursor'); + +/** + * Simple plain-text text editor using html5 canvas. + * @constructor + */ +var CanvasTextEditor = function(doc) { + this._document = doc || (new Document); + this._metrics = new FontMetrics('"Courier New", Courier, monospace', 14); + this._createWrapper(); + this._createCanvas(); + this._createInput(); + this._cursor = new Cursor(this); +}; + +module.exports = CanvasTextEditor; + +/** + * CSS class that is assigned to the wrapper. + * @type {String} + */ +CanvasTextEditor.prototype.className = 'canvas-text-editor'; + +/** + * Creates wrapper element for all parts of the editor + * @private + */ +CanvasTextEditor.prototype._createWrapper = function() { + this.wrapper = document.createElement('div'); + this.wrapper.className = this.className; + this.wrapper.style.display = 'inline-block'; + this.wrapper.style.position = 'relative'; + this.wrapper.style.backgroundColor = '#eee'; + this.wrapper.style.border = '5px solid #eee'; + this.wrapper.style.overflow = 'hidden'; + this.wrapper.tabIndex = 0; // tabindex is necessary to get focus + this.wrapper.addEventListener('focus', this.focus.bind(this), false); +}; + +/** + * Creates canvas for drawing + * @private + */ +CanvasTextEditor.prototype._createCanvas = function() { + this.canvas = document.createElement('canvas'); + this.canvas.style.display = 'block'; + this.context = this.canvas.getContext('2d'); + this.resize(640, 480); + + // For now just very dumb implementation of rendering + var baselineOffset = this._metrics.getBaseline(), + lineHeight = this._metrics.getHeight(), + characterWidth = this._metrics.getWidth(), + maxHeight = Math.ceil(640 / lineHeight), + lineCount = this._document.getLineCount(); + + if (lineCount < maxHeight) maxHeight = lineCount; + + for(var i = 0; i < maxHeight; ++i) { + this.context.fillText( + this._document.getLine(i), 0, lineHeight * i + baselineOffset + ); + } + + this.wrapper.appendChild(this.canvas); +}; + +/** + * Creates textarea that will handle user input and copy-paste actions + * @private + */ +CanvasTextEditor.prototype._createInput = function() { + this.inputEl = document.createElement('textarea'); + this.inputEl.style.position = 'absolute'; + this.inputEl.style.top = '-100px'; + this.inputEl.style.height = 0; + this.inputEl.style.width = 0; + this.inputEl.addEventListener('blur', this.blur.bind(this), false); + this.inputEl.addEventListener('focus', this._inputFocus.bind(this), false); + this.inputEl.addEventListener('keydown', this.keydown.bind(this), false); + this.inputEl.tabIndex = -1; // we don't want input to get focus by tabbing + this.wrapper.appendChild(this.inputEl); +}; + +/** + * Real handler code for editor gaining focus. + * @private + */ +CanvasTextEditor.prototype._inputFocus = function() { + this.wrapper.style.outline = '1px solid #09f'; + this._cursor.setVisible(true); +}; + +/** + * Returns main editor node so it can be inserted into document. + * @return {HTMLElement} + */ +CanvasTextEditor.prototype.getEl = function() { + return this.wrapper; +}; + +/** + * Returns font metrics used in this editor. + * @return {FontMetrics} + */ +CanvasTextEditor.prototype.getFontMetrics = function() { + return this._metrics; +}; + +/** + * Returns current document. + * @return {Document} + */ +CanvasTextEditor.prototype.getDocument = function() { + return this._document; +}; + +/** + * Resizes editor to provided dimensions. + * @param {Number} width + * @param {Number} height + */ +CanvasTextEditor.prototype.resize = function(width, height) { + this.canvas.width = width; + this.canvas.height = height; + // We need to update context settings every time we resize + this.context.font = this._metrics.getSize() + 'px ' + this._metrics.getFamily(); +}; + +/** + * Main keydown handler. + */ +CanvasTextEditor.prototype.keydown = function(e) { + var handled = true; + switch(e.keyCode) { + case 37: // Left arrow + this._cursor.moveLeft(); + break; + case 38: // Up arrow + this._cursor.moveUp(); + break; + case 39: // Up arrow + this._cursor.moveRight(); + break; + case 40: // Down arrow + this._cursor.moveDown(); + break; + default: + handled = false; + } + return !handled; +}; + +/** + * Blur handler. + */ +CanvasTextEditor.prototype.blur = function() { + this.inputEl.blur(); + this.wrapper.style.outline = 'none'; + this._cursor.setVisible(false); +}; + +/** + * Focus handler. Acts as a proxy to input focus. + */ +CanvasTextEditor.prototype.focus = function() { + this.inputEl.focus(); +}; + +}, "Cursor": function(exports, require, module) {/** + * Creates new cursor for the editor. + * @param {Editor} editor. + * @constructor + */ +Selection = function(editor) { + this.editor = editor; + this.blinkInterval = 500; + + this.start = { + line: 0, + character: 0 + }; + + this.end = { + line: 0, + character: 0 + }; + + this.el = document.createElement('div'); + this.el.style.position = 'absolute'; + this.el.style.width = '1px'; + this.el.style.height = this.editor.getFontMetrics().getHeight() + 'px'; + this.el.style.backgroundColor = '#000'; + + this.editor.getEl().appendChild(this.el); + this.setPosition(0, 0); +}; + +/** + * Responsible for cursor blinking + * @return {void} + */ +Selection.prototype.blink = function() { + if (parseInt(this.el.style.opacity, 10)) { + this.el.style.opacity = 0; + } else { + this.el.style.opacity = 1; + } +}; + +/** + * Moves cursor to a specified position inside document. + * @param {number} position Offset from the start of the document. + */ +Selection.prototype.setPosition = function(line, character) { + // Providing defaults for both line and character parts of position + if (typeof line === 'undefined') line = this.end.line + if (typeof character === 'undefined') character = this.end.character + + // Checking lower bounds + line >= 0 || (line = 0); + character >= 0 || (character = 0); + + // Checking upper bounds + var lineCount = this.editor.getDocument().getLineCount(); + line < lineCount || (line = lineCount - 1); + var characterCount = this.editor.getDocument().getLine(line).trim('\n').length; + character <= characterCount || (character = characterCount); + + // Saving new value + this.start.line = this.end.line = line; + this.start.character = this.end.character = character; + + // Calculating new position on the screen + var metrics = this.editor.getFontMetrics(), + offsetX = character * metrics.getWidth(), + offsetY = line * metrics.getHeight(); + this.el.style.left = offsetX + 'px'; + this.el.style.top = offsetY + 'px'; + + // This helps to see moving cursor when it is always in blink on + // state on a new position. Try to move cursror in any editor and you + // will see this in action. + if(this.isVisible()) { + this.el.style.opacity = 1; + clearInterval(this.interval); + this.interval = setInterval(this.blink.bind(this), this.blinkInterval); + } +}; + +/** + * Moves cursor up specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveUp = function(length) { + arguments.length || (length = 1); + var line = this.end.line - length; + this.setPosition(line); +}; + +/** + * Moves cursor down specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveDown = function(length) { + arguments.length || (length = 1); + this.setPosition(this.end.line + length); +}; + +/** + * Moves cursor up specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveLeft = function(length) { + arguments.length || (length = 1); + this.setPosition(undefined, this.end.character - length); +}; + +/** + * Moves cursor down specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveRight = function(length) { + arguments.length || (length = 1); + this.setPosition(undefined, this.end.character + length); +}; + +/** + * Shows or hides cursor. + * @param {void} visible Whether cursor should be visible + */ +Selection.prototype.setVisible = function(visible) { + clearInterval(this.interval); + if(visible) { + this.el.style.display = 'block'; + this.el.style.opacity = 1; + this.interval = setInterval(this.blink.bind(this), this.blinkInterval); + } else { + this.el.style.display = 'none'; + } + this.visible = visible; +}; + +/** + * Returns visibility of the cursor. + * @return {Boolean} + */ +Selection.prototype.isVisible = function() { + return this.visible; +}; + +module.exports = Selection; +}, "Document": function(exports, require, module) { +/** + * Creates new document from provided text. + * @param {string} text Full document text. + * @constructor + */ +Document = function(text) { + text || (text = ''); + this.storage = Document.prepareText(text); +}; + +module.exports = Document; + +/** + * Splits text into array of lines. Can't use .split('\n') because + * we want to keep trailing \n at the ends of lines. + * @param {string} text + * @return {Array.{string}} + */ +Document.prepareText = function(text) { + var lines = [], + index = 0, + newIndex; + do { + newIndex = text.indexOf('\n', index); + // Adding from previous index to new one or to the end of the string + lines.push(text.substr(index, newIndex !== -1 ? newIndex - index + 1 : void 0)); + // next search will be after found newline + index = newIndex + 1; + } while (newIndex !== -1); + + return lines; +}; + +/** + * Returns line count for the document + * @return {number} + */ +Document.prototype.getLineCount = function() { + return this.storage.length; +}; + +/** + * Returns line on the corresponding index. + * @param {number} 0-based index of the line + * @return {string} + */ +Document.prototype.getLine = function(index) { + return this.storage[index]; +}; + +/** + * Returns linear length of the document. + * @return {number} + */ +Document.prototype.getLength = function() { + var sum = 0; + for (var i = this.storage.length - 1; i >= 0; --i) { + sum += this.storage[i].length + }; + return sum; +}; + +/** + * Returns char at specified offset. + * @param {number} offset + * @return {string|undefined} + */ +Document.prototype.charAt = function(column, row) { + var row = this.storage[row]; + if (row) return row.charAt(column); +};}, "FontMetrics": function(exports, require, module) {"use strict"; + +/** + * A simple wrapper for system fonts to provide + * @param {String} family Font Family (same as in CSS) + * @param {Number} size Size in px + * @constructor + */ +var FontMetrics = function(family, size) { + this._family = family || (family = "Monaco, 'Courier New', Courier, monospace"); + this._size = parseInt(size) || (size = 12); + + // Preparing container + var line = document.createElement('div'), + body = document.body; + line.style.position = 'absolute'; + line.style.whiteSpace = 'nowrap'; + line.style.font = size + 'px ' + family; + body.appendChild(line); + + // Now we can measure width and height of the letter + line.innerHTML = 'm'; // It doesn't matter what text goes here + this._width = line.offsetWidth; + this._height = line.offsetHeight; + + // Now creating 1px sized item that will be aligned to baseline + // to calculate baseline shift + var span = document.createElement('span'); + span.style.display = 'inline-block'; + span.style.overflow = 'hidden'; + span.style.width = '1px'; + span.style.height = '1px'; + line.appendChild(span); + + // Baseline is important for positioning text on canvas + this._baseline = span.offsetTop + span.offsetHeight; + + document.body.removeChild(line); +}; + +module.exports = FontMetrics; + +/** + * Returns font family + * @return {String} + */ +FontMetrics.prototype.getFamily = function() { + return this._family; +}; + +/** + * Returns font family + * @return {Number} + */ +FontMetrics.prototype.getSize = function() { + return this._size; +}; + +/** + * Returns line height in px + * @return {Number} + */ +FontMetrics.prototype.getHeight = function() { + return this._height; +}; + +/** + * Returns line height in px + * @return {Number} + */ +FontMetrics.prototype.getWidth = function() { + return this._width; +}; + +/** + * Returns line height in px + * @return {Number} + */ +FontMetrics.prototype.getBaseline = function() { + return this._baseline; +}; +}}); diff --git a/demo/index.html b/index.html similarity index 70% rename from demo/index.html rename to index.html index d2c3120..fd08211 100755 --- a/demo/index.html +++ b/index.html @@ -6,7 +6,12 @@ Canvas Text Editor - + + +

Canvas Text Editor Demo

+

More info here

diff --git a/lib/CanvasTextEditor.js b/lib/CanvasTextEditor.js deleted file mode 100644 index 0db557b..0000000 --- a/lib/CanvasTextEditor.js +++ /dev/null @@ -1,173 +0,0 @@ -"use strict"; - -var FontMetrics = require('FontMetrics'), - Document = require('Document'), - Selection = require('Selection'); - -/** - * Simple plain-text text editor using html5 canvas. - * @constructor - */ -var CanvasTextEditor = function(doc) { - this._document = doc || (new Document); - this._metrics = new FontMetrics('"Courier New", Courier, monospace', 14); - this._createWrapper(); - this._createCanvas(); - this._createInput(); - this._selection = new Selection(this); -}; - -module.exports = CanvasTextEditor; - -/** - * CSS class that is assigned to the wrapper. - * @type {String} - */ -CanvasTextEditor.prototype.className = 'canvas-text-editor'; - -/** - * Creates wrapper element for all parts of the editor - * @private - */ -CanvasTextEditor.prototype._createWrapper = function() { - this.wrapper = document.createElement('div'); - this.wrapper.className = this.className; - this.wrapper.style.display = 'inline-block'; - this.wrapper.style.position = 'relative'; - this.wrapper.style.backgroundColor = '#eee'; - this.wrapper.style.border = '5px solid #eee'; - this.wrapper.style.overflow = 'hidden'; - this.wrapper.tabIndex = 0; // tabindex is necessary to get focus - this.wrapper.addEventListener('focus', this.focus.bind(this), false); -}; - -/** - * Creates canvas for drawing - * @private - */ -CanvasTextEditor.prototype._createCanvas = function() { - this.canvas = document.createElement('canvas'); - this.canvas.style.display = 'block'; - this.context = this.canvas.getContext('2d'); - this.resize(640, 480); - - // For now just very dumb implementation of rendering - var baselineOffset = this._metrics.getBaseline(), - lineHeight = this._metrics.getHeight(), - characterWidth = this._metrics.getWidth(), - maxHeight = Math.ceil(640 / lineHeight), - lineCount = this._document.getLineCount(); - - if (lineCount < maxHeight) maxHeight = lineCount; - - for(var i = 0; i < maxHeight; ++i) { - this.context.fillText( - this._document.getLine(i), 0, lineHeight * i + baselineOffset - ); - } - - this.wrapper.appendChild(this.canvas); -}; - -/** - * Creates textarea that will handle user input and copy-paste actions - * @private - */ -CanvasTextEditor.prototype._createInput = function() { - this.inputEl = document.createElement('textarea'); - this.inputEl.style.position = 'absolute'; - this.inputEl.style.top = '-100px'; - this.inputEl.style.height = 0; - this.inputEl.style.width = 0; - this.inputEl.addEventListener('blur', this.blur.bind(this), false); - this.inputEl.addEventListener('focus', this._inputFocus.bind(this), false); - this.inputEl.addEventListener('keydown', this.keydown.bind(this), false); - this.inputEl.tabIndex = -1; // we don't want input to get focus by tabbing - this.wrapper.appendChild(this.inputEl); -}; - -/** - * Real handler code for editor gaining focus. - * @private - */ -CanvasTextEditor.prototype._inputFocus = function() { - this.wrapper.style.outline = '1px solid #09f'; - this._selection.setVisible(true); -}; - -/** - * Returns main editor node so it can be inserted into document. - * @return {HTMLElement} - */ -CanvasTextEditor.prototype.getEl = function() { - return this.wrapper; -}; - -/** - * Returns font metrics used in this editor. - * @return {FontMetrics} - */ -CanvasTextEditor.prototype.getFontMetrics = function() { - return this._metrics; -}; - -/** - * Returns current document. - * @return {Document} - */ -CanvasTextEditor.prototype.getDocument = function() { - return this._document; -}; - -/** - * Resizes editor to provided dimensions. - * @param {Number} width - * @param {Number} height - */ -CanvasTextEditor.prototype.resize = function(width, height) { - this.canvas.width = width; - this.canvas.height = height; - // We need to update context settings every time we resize - this.context.font = this._metrics.getSize() + 'px ' + this._metrics.getFamily(); -}; - -/** - * Main keydown handler. - */ -CanvasTextEditor.prototype.keydown = function(e) { - var handled = true; - switch(e.keyCode) { - case 37: // Left arrow - this._selection.moveLeft(); - break; - case 38: // Up arrow - this._selection.moveUp(); - break; - case 39: // Up arrow - this._selection.moveRight(); - break; - case 40: // Down arrow - this._selection.moveDown(); - break; - default: - handled = false; - } - return !handled; -}; - -/** - * Blur handler. - */ -CanvasTextEditor.prototype.blur = function() { - this.inputEl.blur(); - this.wrapper.style.outline = 'none'; - this._selection.setVisible(false); -}; - -/** - * Focus handler. Acts as a proxy to input focus. - */ -CanvasTextEditor.prototype.focus = function() { - this.inputEl.focus(); -}; - diff --git a/lib/Document.js b/lib/Document.js deleted file mode 100755 index 0fa7e72..0000000 --- a/lib/Document.js +++ /dev/null @@ -1,72 +0,0 @@ - -/** - * Creates new document from provided text. - * @param {string} text Full document text. - * @constructor - */ -Document = function(text) { - text || (text = ''); - this.storage = Document.prepareText(text); -}; - -module.exports = Document; - -/** - * Splits text into array of lines. Can't use .split('\n') because - * we want to keep trailing \n at the ends of lines. - * @param {string} text - * @return {Array.{string}} - */ -Document.prepareText = function(text) { - var lines = [], - index = 0, - newIndex; - do { - newIndex = text.indexOf('\n', index); - // Adding from previous index to new one or to the end of the string - lines.push(text.substr(index, newIndex !== -1 ? newIndex - index + 1 : void 0)); - // next search will be after found newline - index = newIndex + 1; - } while (newIndex !== -1); - - return lines; -}; - -/** - * Returns line count for the document - * @return {number} - */ -Document.prototype.getLineCount = function() { - return this.storage.length; -}; - -/** - * Returns line on the corresponding index. - * @param {number} 0-based index of the line - * @return {string} - */ -Document.prototype.getLine = function(index) { - return this.storage[index]; -}; - -/** - * Returns linear length of the document. - * @return {number} - */ -Document.prototype.getLength = function() { - var sum = 0; - for (var i = this.storage.length - 1; i >= 0; --i) { - sum += this.storage[i].length - }; - return sum; -}; - -/** - * Returns char at specified offset. - * @param {number} offset - * @return {string|undefined} - */ -Document.prototype.charAt = function(column, row) { - var row = this.storage[row]; - if (row) return row.charAt(column); -}; \ No newline at end of file diff --git a/lib/FontMetrics.js b/lib/FontMetrics.js deleted file mode 100644 index 6794656..0000000 --- a/lib/FontMetrics.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; - -/** - * A simple wrapper for system fonts to provide - * @param {String} family Font Family (same as in CSS) - * @param {Number} size Size in px - * @constructor - */ -var FontMetrics = function(family, size) { - this._family = family || (family = "Monaco, 'Courier New', Courier, monospace"); - this._size = parseInt(size) || (size = 12); - - // Preparing container - var line = document.createElement('div'), - body = document.body; - line.style.position = 'absolute'; - line.style.whiteSpace = 'nowrap'; - line.style.font = size + 'px ' + family; - body.appendChild(line); - - // Now we can measure width and height of the letter - line.innerHTML = 'm'; // It doesn't matter what text goes here - this._width = line.offsetWidth; - this._height = line.offsetHeight; - - // Now creating 1px sized item that will be aligned to baseline - // to calculate baseline shift - var span = document.createElement('span'); - span.style.display = 'inline-block'; - span.style.overflow = 'hidden'; - span.style.width = '1px'; - span.style.height = '1px'; - line.appendChild(span); - - // Baseline is important for positioning text on canvas - this._baseline = span.offsetTop + span.offsetHeight; - - document.body.removeChild(line); -}; - -module.exports = FontMetrics; - -/** - * Returns font family - * @return {String} - */ -FontMetrics.prototype.getFamily = function() { - return this._family; -}; - -/** - * Returns font family - * @return {Number} - */ -FontMetrics.prototype.getSize = function() { - return this._size; -}; - -/** - * Returns line height in px - * @return {Number} - */ -FontMetrics.prototype.getHeight = function() { - return this._height; -}; - -/** - * Returns line height in px - * @return {Number} - */ -FontMetrics.prototype.getWidth = function() { - return this._width; -}; - -/** - * Returns line height in px - * @return {Number} - */ -FontMetrics.prototype.getBaseline = function() { - return this._baseline; -}; diff --git a/lib/Selection.js b/lib/Selection.js deleted file mode 100644 index 8909d54..0000000 --- a/lib/Selection.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Creates new selection for the editor. - * @param {Editor} editor. - * @constructor - */ -Selection = function(editor) { - this.editor = editor; - this.blinkInterval = 500; - - this.start = { - line: 0, - character: 0 - }; - - this.end = { - line: 0, - character: 0 - }; - - this.el = document.createElement('div'); - this.el.style.position = 'absolute'; - this.el.style.width = '1px'; - this.el.style.height = this.editor.getFontMetrics().getHeight() + 'px'; - this.el.style.backgroundColor = '#000'; - - this.editor.getEl().appendChild(this.el); - this.setPosition(0, 0); -}; - -/** - * Responsible for blinking - * @return {void} - */ -Selection.prototype.blink = function() { - if (parseInt(this.el.style.opacity, 10)) { - this.el.style.opacity = 0; - } else { - this.el.style.opacity = 1; - } -}; - -/** - * Moves both start and end to a specified position inside document. - * @param {number?} line - * @param {number?} character - */ -Selection.prototype.setPosition = function(line, character) { - // Providing defaults for both line and character parts of position - if (typeof line === 'undefined') line = this.end.line - if (typeof character === 'undefined') character = this.end.character - - // Checking lower bounds - line >= 0 || (line = 0); - character >= 0 || (character = 0); - - // Checking upper bounds - var lineCount = this.editor.getDocument().getLineCount(); - line < lineCount || (line = lineCount - 1); - var characterCount = this.editor.getDocument().getLine(line).trim('\n').length; - character <= characterCount || (character = characterCount); - - // Saving new value - this.start.line = this.end.line = line; - this.start.character = this.end.character = character; - - // Calculating new position on the screen - var metrics = this.editor.getFontMetrics(), - offsetX = character * metrics.getWidth(), - offsetY = line * metrics.getHeight(); - this.el.style.left = offsetX + 'px'; - this.el.style.top = offsetY + 'px'; - - // This helps to see moving cursor when it is always in blink on - // state on a new position. Try to move cursror in any editor and you - // will see this in action. - if(this.isVisible()) { - this.el.style.opacity = 1; - clearInterval(this.interval); - this.interval = setInterval(this.blink.bind(this), this.blinkInterval); - } -}; - -/** - * Moves up specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveUp = function(length) { - arguments.length || (length = 1); - this.setPosition(this.end.line - length); -}; - -/** - * Moves down specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveDown = function(length) { - arguments.length || (length = 1); - this.setPosition(this.end.line + length); -}; - -/** - * Moves up specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveLeft = function(length) { - arguments.length || (length = 1); - this.setPosition(undefined, this.end.character - length); -}; - -/** - * Moves down specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveRight = function(length) { - arguments.length || (length = 1); - this.setPosition(undefined, this.end.character + length); -}; - -/** - * Shows or hides cursor. - * @param {void} visible Whether cursor should be visible - */ -Selection.prototype.setVisible = function(visible) { - clearInterval(this.interval); - if(visible) { - this.el.style.display = 'block'; - this.el.style.opacity = 1; - this.interval = setInterval(this.blink.bind(this), this.blinkInterval); - } else { - this.el.style.display = 'none'; - } - this.visible = visible; -}; - -/** - * Returns visibility of the cursor. - * @return {Boolean} - */ -Selection.prototype.isVisible = function() { - return this.visible; -}; - -module.exports = Selection; diff --git a/package.json b/package.json deleted file mode 100644 index 01ab650..0000000 --- a/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "author": "Dmitriy Kubyshkin ", - "name": "canvas-text-editor", - "description": "Simple text editor using html5 canvas", - "version": "0.0.1", - "engines": { - "node": ">= 0.4.x < 0.7.0" - }, - "devDependencies": { - "coffee-script": "latest", - "stitch": "latest", - "uglify-js": "latest", - "express": "latest" - } -} diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100644 index 2075911..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,25 +0,0 @@ -var common = require('./common'); -var fs = require('fs'); -var jsp = require("uglify-js").parser; -var pro = require("uglify-js").uglify; - -common.package.compile(function (err, source){ - var dir = __dirname + '/../build'; - - // Making sure build dir exists - try { fs.statSync(dir); } - catch (e) { fs.mkdirSync(dir, 0755); } - - // Generating developer (unminified) version - var path = dir + '/' + common.name + '.js'; - fs.writeFileSync(path, source); - console.log('Developer version: ' + path.replace(__dirname + '/../', '')); - - // And production one - var minPath = dir + '/' + common.name + '.min.js'; - var ast = jsp.parse(source); // parse code and get the initial AST - ast = pro.ast_mangle(ast); // get a new AST with mangled names - ast = pro.ast_squeeze(ast); // get an AST with compression optimizations - fs.writeFile(minPath, pro.gen_code(ast)); - console.log('Minified version: ' + minPath.replace(__dirname + '/../', '')); -}) \ No newline at end of file diff --git a/scripts/common.js b/scripts/common.js deleted file mode 100644 index 0affb3a..0000000 --- a/scripts/common.js +++ /dev/null @@ -1,15 +0,0 @@ -var stitch = require('stitch'); -var fs = require('fs'); - -// Loading project information to get name for building -var project = JSON.parse(fs.readFileSync(__dirname + '/../package.json')); - -// Stitch everything together -var package = stitch.createPackage({ - paths: [__dirname + '/../lib', __dirname + '/../vendor'] -}); - -module.exports = { - name: project.name, - package: package -}; \ No newline at end of file diff --git a/scripts/serve.js b/scripts/serve.js deleted file mode 100644 index 10504c1..0000000 --- a/scripts/serve.js +++ /dev/null @@ -1,11 +0,0 @@ -var common = require('./common'); -var express = require('express'); - -var app = express.createServer(); -var url = '/lib.js'; -var port = 4000; -app.get(url, common.package.createServer()); -app.listen(port); - -console.log('\nYou can load stitched file from:'); -console.log('http://localhost:' + port + url); \ No newline at end of file diff --git a/test/lib/jasmine-1.1.0/MIT.LICENSE b/test/lib/jasmine-1.1.0/MIT.LICENSE deleted file mode 100644 index 7c435ba..0000000 --- a/test/lib/jasmine-1.1.0/MIT.LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2008-2011 Pivotal Labs - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/test/lib/jasmine-1.1.0/jasmine-html.js b/test/lib/jasmine-1.1.0/jasmine-html.js deleted file mode 100644 index 7383401..0000000 --- a/test/lib/jasmine-1.1.0/jasmine-html.js +++ /dev/null @@ -1,190 +0,0 @@ -jasmine.TrivialReporter = function(doc) { - this.document = doc || document; - this.suiteDivs = {}; - this.logRunningSpecs = false; -}; - -jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { - var el = document.createElement(type); - - for (var i = 2; i < arguments.length; i++) { - var child = arguments[i]; - - if (typeof child === 'string') { - el.appendChild(document.createTextNode(child)); - } else { - if (child) { el.appendChild(child); } - } - } - - for (var attr in attrs) { - if (attr == "className") { - el[attr] = attrs[attr]; - } else { - el.setAttribute(attr, attrs[attr]); - } - } - - return el; -}; - -jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { - var showPassed, showSkipped; - - this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' }, - this.createDom('div', { className: 'banner' }, - this.createDom('div', { className: 'logo' }, - this.createDom('span', { className: 'title' }, "Jasmine"), - this.createDom('span', { className: 'version' }, runner.env.versionString())), - this.createDom('div', { className: 'options' }, - "Show ", - showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), - this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), - showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), - this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") - ) - ), - - this.runnerDiv = this.createDom('div', { className: 'runner running' }, - this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), - this.runnerMessageSpan = this.createDom('span', {}, "Running..."), - this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) - ); - - this.document.body.appendChild(this.outerDiv); - - var suites = runner.suites(); - for (var i = 0; i < suites.length; i++) { - var suite = suites[i]; - var suiteDiv = this.createDom('div', { className: 'suite' }, - this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), - this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); - this.suiteDivs[suite.id] = suiteDiv; - var parentDiv = this.outerDiv; - if (suite.parentSuite) { - parentDiv = this.suiteDivs[suite.parentSuite.id]; - } - parentDiv.appendChild(suiteDiv); - } - - this.startedAt = new Date(); - - var self = this; - showPassed.onclick = function(evt) { - if (showPassed.checked) { - self.outerDiv.className += ' show-passed'; - } else { - self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); - } - }; - - showSkipped.onclick = function(evt) { - if (showSkipped.checked) { - self.outerDiv.className += ' show-skipped'; - } else { - self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); - } - }; -}; - -jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { - var results = runner.results(); - var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; - this.runnerDiv.setAttribute("class", className); - //do it twice for IE - this.runnerDiv.setAttribute("className", className); - var specs = runner.specs(); - var specCount = 0; - for (var i = 0; i < specs.length; i++) { - if (this.specFilter(specs[i])) { - specCount++; - } - } - var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); - message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; - this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); - - this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); -}; - -jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { - var results = suite.results(); - var status = results.passed() ? 'passed' : 'failed'; - if (results.totalCount === 0) { // todo: change this to check results.skipped - status = 'skipped'; - } - this.suiteDivs[suite.id].className += " " + status; -}; - -jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { - if (this.logRunningSpecs) { - this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); - } -}; - -jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { - var results = spec.results(); - var status = results.passed() ? 'passed' : 'failed'; - if (results.skipped) { - status = 'skipped'; - } - var specDiv = this.createDom('div', { className: 'spec ' + status }, - this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), - this.createDom('a', { - className: 'description', - href: '?spec=' + encodeURIComponent(spec.getFullName()), - title: spec.getFullName() - }, spec.description)); - - - var resultItems = results.getItems(); - var messagesDiv = this.createDom('div', { className: 'messages' }); - for (var i = 0; i < resultItems.length; i++) { - var result = resultItems[i]; - - if (result.type == 'log') { - messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); - } else if (result.type == 'expect' && result.passed && !result.passed()) { - messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); - - if (result.trace.stack) { - messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); - } - } - } - - if (messagesDiv.childNodes.length > 0) { - specDiv.appendChild(messagesDiv); - } - - this.suiteDivs[spec.suite.id].appendChild(specDiv); -}; - -jasmine.TrivialReporter.prototype.log = function() { - var console = jasmine.getGlobal().console; - if (console && console.log) { - if (console.log.apply) { - console.log.apply(console, arguments); - } else { - console.log(arguments); // ie fix: console.log.apply doesn't exist on ie - } - } -}; - -jasmine.TrivialReporter.prototype.getLocation = function() { - return this.document.location; -}; - -jasmine.TrivialReporter.prototype.specFilter = function(spec) { - var paramMap = {}; - var params = this.getLocation().search.substring(1).split('&'); - for (var i = 0; i < params.length; i++) { - var p = params[i].split('='); - paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); - } - - if (!paramMap.spec) { - return true; - } - return spec.getFullName().indexOf(paramMap.spec) === 0; -}; diff --git a/test/lib/jasmine-1.1.0/jasmine.css b/test/lib/jasmine-1.1.0/jasmine.css deleted file mode 100644 index 6583fe7..0000000 --- a/test/lib/jasmine-1.1.0/jasmine.css +++ /dev/null @@ -1,166 +0,0 @@ -body { - font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; -} - - -.jasmine_reporter a:visited, .jasmine_reporter a { - color: #303; -} - -.jasmine_reporter a:hover, .jasmine_reporter a:active { - color: blue; -} - -.run_spec { - float:right; - padding-right: 5px; - font-size: .8em; - text-decoration: none; -} - -.jasmine_reporter { - margin: 0 5px; -} - -.banner { - color: #303; - background-color: #fef; - padding: 5px; -} - -.logo { - float: left; - font-size: 1.1em; - padding-left: 5px; -} - -.logo .version { - font-size: .6em; - padding-left: 1em; -} - -.runner.running { - background-color: yellow; -} - - -.options { - text-align: right; - font-size: .8em; -} - - - - -.suite { - border: 1px outset gray; - margin: 5px 0; - padding-left: 1em; -} - -.suite .suite { - margin: 5px; -} - -.suite.passed { - background-color: #dfd; -} - -.suite.failed { - background-color: #fdd; -} - -.spec { - margin: 5px; - padding-left: 1em; - clear: both; -} - -.spec.failed, .spec.passed, .spec.skipped { - padding-bottom: 5px; - border: 1px solid gray; -} - -.spec.failed { - background-color: #fbb; - border-color: red; -} - -.spec.passed { - background-color: #bfb; - border-color: green; -} - -.spec.skipped { - background-color: #bbb; -} - -.messages { - border-left: 1px dashed gray; - padding-left: 1em; - padding-right: 1em; -} - -.passed { - background-color: #cfc; - display: none; -} - -.failed { - background-color: #fbb; -} - -.skipped { - color: #777; - background-color: #eee; - display: none; -} - - -/*.resultMessage {*/ - /*white-space: pre;*/ -/*}*/ - -.resultMessage span.result { - display: block; - line-height: 2em; - color: black; -} - -.resultMessage .mismatch { - color: black; -} - -.stackTrace { - white-space: pre; - font-size: .8em; - margin-left: 10px; - max-height: 5em; - overflow: auto; - border: 1px inset red; - padding: 1em; - background: #eef; -} - -.finished-at { - padding-left: 1em; - font-size: .6em; -} - -.show-passed .passed, -.show-skipped .skipped { - display: block; -} - - -#jasmine_content { - position:fixed; - right: 100%; -} - -.runner { - border: 1px solid gray; - display: block; - margin: 5px 0; - padding: 2px 0 2px 10px; -} diff --git a/test/lib/jasmine-1.1.0/jasmine.js b/test/lib/jasmine-1.1.0/jasmine.js deleted file mode 100644 index c3d2dc7..0000000 --- a/test/lib/jasmine-1.1.0/jasmine.js +++ /dev/null @@ -1,2476 +0,0 @@ -var isCommonJS = typeof window == "undefined"; - -/** - * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. - * - * @namespace - */ -var jasmine = {}; -if (isCommonJS) exports.jasmine = jasmine; -/** - * @private - */ -jasmine.unimplementedMethod_ = function() { - throw new Error("unimplemented method"); -}; - -/** - * Use jasmine.undefined instead of undefined, since undefined is just - * a plain old variable and may be redefined by somebody else. - * - * @private - */ -jasmine.undefined = jasmine.___undefined___; - -/** - * Show diagnostic messages in the console if set to true - * - */ -jasmine.VERBOSE = false; - -/** - * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. - * - */ -jasmine.DEFAULT_UPDATE_INTERVAL = 250; - -/** - * Default timeout interval in milliseconds for waitsFor() blocks. - */ -jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; - -jasmine.getGlobal = function() { - function getGlobal() { - return this; - } - - return getGlobal(); -}; - -/** - * Allows for bound functions to be compared. Internal use only. - * - * @ignore - * @private - * @param base {Object} bound 'this' for the function - * @param name {Function} function to find - */ -jasmine.bindOriginal_ = function(base, name) { - var original = base[name]; - if (original.apply) { - return function() { - return original.apply(base, arguments); - }; - } else { - // IE support - return jasmine.getGlobal()[name]; - } -}; - -jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); -jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); -jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); -jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); - -jasmine.MessageResult = function(values) { - this.type = 'log'; - this.values = values; - this.trace = new Error(); // todo: test better -}; - -jasmine.MessageResult.prototype.toString = function() { - var text = ""; - for (var i = 0; i < this.values.length; i++) { - if (i > 0) text += " "; - if (jasmine.isString_(this.values[i])) { - text += this.values[i]; - } else { - text += jasmine.pp(this.values[i]); - } - } - return text; -}; - -jasmine.ExpectationResult = function(params) { - this.type = 'expect'; - this.matcherName = params.matcherName; - this.passed_ = params.passed; - this.expected = params.expected; - this.actual = params.actual; - this.message = this.passed_ ? 'Passed.' : params.message; - - var trace = (params.trace || new Error(this.message)); - this.trace = this.passed_ ? '' : trace; -}; - -jasmine.ExpectationResult.prototype.toString = function () { - return this.message; -}; - -jasmine.ExpectationResult.prototype.passed = function () { - return this.passed_; -}; - -/** - * Getter for the Jasmine environment. Ensures one gets created - */ -jasmine.getEnv = function() { - var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); - return env; -}; - -/** - * @ignore - * @private - * @param value - * @returns {Boolean} - */ -jasmine.isArray_ = function(value) { - return jasmine.isA_("Array", value); -}; - -/** - * @ignore - * @private - * @param value - * @returns {Boolean} - */ -jasmine.isString_ = function(value) { - return jasmine.isA_("String", value); -}; - -/** - * @ignore - * @private - * @param value - * @returns {Boolean} - */ -jasmine.isNumber_ = function(value) { - return jasmine.isA_("Number", value); -}; - -/** - * @ignore - * @private - * @param {String} typeName - * @param value - * @returns {Boolean} - */ -jasmine.isA_ = function(typeName, value) { - return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; -}; - -/** - * Pretty printer for expecations. Takes any object and turns it into a human-readable string. - * - * @param value {Object} an object to be outputted - * @returns {String} - */ -jasmine.pp = function(value) { - var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); - stringPrettyPrinter.format(value); - return stringPrettyPrinter.string; -}; - -/** - * Returns true if the object is a DOM Node. - * - * @param {Object} obj object to check - * @returns {Boolean} - */ -jasmine.isDomNode = function(obj) { - return obj.nodeType > 0; -}; - -/** - * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. - * - * @example - * // don't care about which function is passed in, as long as it's a function - * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); - * - * @param {Class} clazz - * @returns matchable object of the type clazz - */ -jasmine.any = function(clazz) { - return new jasmine.Matchers.Any(clazz); -}; - -/** - * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. - * - * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine - * expectation syntax. Spies can be checked if they were called or not and what the calling params were. - * - * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). - * - * Spies are torn down at the end of every spec. - * - * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. - * - * @example - * // a stub - * var myStub = jasmine.createSpy('myStub'); // can be used anywhere - * - * // spy example - * var foo = { - * not: function(bool) { return !bool; } - * } - * - * // actual foo.not will not be called, execution stops - * spyOn(foo, 'not'); - - // foo.not spied upon, execution will continue to implementation - * spyOn(foo, 'not').andCallThrough(); - * - * // fake example - * var foo = { - * not: function(bool) { return !bool; } - * } - * - * // foo.not(val) will return val - * spyOn(foo, 'not').andCallFake(function(value) {return value;}); - * - * // mock example - * foo.not(7 == 7); - * expect(foo.not).toHaveBeenCalled(); - * expect(foo.not).toHaveBeenCalledWith(true); - * - * @constructor - * @see spyOn, jasmine.createSpy, jasmine.createSpyObj - * @param {String} name - */ -jasmine.Spy = function(name) { - /** - * The name of the spy, if provided. - */ - this.identity = name || 'unknown'; - /** - * Is this Object a spy? - */ - this.isSpy = true; - /** - * The actual function this spy stubs. - */ - this.plan = function() { - }; - /** - * Tracking of the most recent call to the spy. - * @example - * var mySpy = jasmine.createSpy('foo'); - * mySpy(1, 2); - * mySpy.mostRecentCall.args = [1, 2]; - */ - this.mostRecentCall = {}; - - /** - * Holds arguments for each call to the spy, indexed by call count - * @example - * var mySpy = jasmine.createSpy('foo'); - * mySpy(1, 2); - * mySpy(7, 8); - * mySpy.mostRecentCall.args = [7, 8]; - * mySpy.argsForCall[0] = [1, 2]; - * mySpy.argsForCall[1] = [7, 8]; - */ - this.argsForCall = []; - this.calls = []; -}; - -/** - * Tells a spy to call through to the actual implemenatation. - * - * @example - * var foo = { - * bar: function() { // do some stuff } - * } - * - * // defining a spy on an existing property: foo.bar - * spyOn(foo, 'bar').andCallThrough(); - */ -jasmine.Spy.prototype.andCallThrough = function() { - this.plan = this.originalValue; - return this; -}; - -/** - * For setting the return value of a spy. - * - * @example - * // defining a spy from scratch: foo() returns 'baz' - * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); - * - * // defining a spy on an existing property: foo.bar() returns 'baz' - * spyOn(foo, 'bar').andReturn('baz'); - * - * @param {Object} value - */ -jasmine.Spy.prototype.andReturn = function(value) { - this.plan = function() { - return value; - }; - return this; -}; - -/** - * For throwing an exception when a spy is called. - * - * @example - * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' - * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); - * - * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' - * spyOn(foo, 'bar').andThrow('baz'); - * - * @param {String} exceptionMsg - */ -jasmine.Spy.prototype.andThrow = function(exceptionMsg) { - this.plan = function() { - throw exceptionMsg; - }; - return this; -}; - -/** - * Calls an alternate implementation when a spy is called. - * - * @example - * var baz = function() { - * // do some stuff, return something - * } - * // defining a spy from scratch: foo() calls the function baz - * var foo = jasmine.createSpy('spy on foo').andCall(baz); - * - * // defining a spy on an existing property: foo.bar() calls an anonymnous function - * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); - * - * @param {Function} fakeFunc - */ -jasmine.Spy.prototype.andCallFake = function(fakeFunc) { - this.plan = fakeFunc; - return this; -}; - -/** - * Resets all of a spy's the tracking variables so that it can be used again. - * - * @example - * spyOn(foo, 'bar'); - * - * foo.bar(); - * - * expect(foo.bar.callCount).toEqual(1); - * - * foo.bar.reset(); - * - * expect(foo.bar.callCount).toEqual(0); - */ -jasmine.Spy.prototype.reset = function() { - this.wasCalled = false; - this.callCount = 0; - this.argsForCall = []; - this.calls = []; - this.mostRecentCall = {}; -}; - -jasmine.createSpy = function(name) { - - var spyObj = function() { - spyObj.wasCalled = true; - spyObj.callCount++; - var args = jasmine.util.argsToArray(arguments); - spyObj.mostRecentCall.object = this; - spyObj.mostRecentCall.args = args; - spyObj.argsForCall.push(args); - spyObj.calls.push({object: this, args: args}); - return spyObj.plan.apply(this, arguments); - }; - - var spy = new jasmine.Spy(name); - - for (var prop in spy) { - spyObj[prop] = spy[prop]; - } - - spyObj.reset(); - - return spyObj; -}; - -/** - * Determines whether an object is a spy. - * - * @param {jasmine.Spy|Object} putativeSpy - * @returns {Boolean} - */ -jasmine.isSpy = function(putativeSpy) { - return putativeSpy && putativeSpy.isSpy; -}; - -/** - * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something - * large in one call. - * - * @param {String} baseName name of spy class - * @param {Array} methodNames array of names of methods to make spies - */ -jasmine.createSpyObj = function(baseName, methodNames) { - if (!jasmine.isArray_(methodNames) || methodNames.length === 0) { - throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); - } - var obj = {}; - for (var i = 0; i < methodNames.length; i++) { - obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); - } - return obj; -}; - -/** - * All parameters are pretty-printed and concatenated together, then written to the current spec's output. - * - * Be careful not to leave calls to jasmine.log in production code. - */ -jasmine.log = function() { - var spec = jasmine.getEnv().currentSpec; - spec.log.apply(spec, arguments); -}; - -/** - * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. - * - * @example - * // spy example - * var foo = { - * not: function(bool) { return !bool; } - * } - * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops - * - * @see jasmine.createSpy - * @param obj - * @param methodName - * @returns a Jasmine spy that can be chained with all spy methods - */ -var spyOn = function(obj, methodName) { - return jasmine.getEnv().currentSpec.spyOn(obj, methodName); -}; -if (isCommonJS) exports.spyOn = spyOn; - -/** - * Creates a Jasmine spec that will be added to the current suite. - * - * // TODO: pending tests - * - * @example - * it('should be true', function() { - * expect(true).toEqual(true); - * }); - * - * @param {String} desc description of this specification - * @param {Function} func defines the preconditions and expectations of the spec - */ -var it = function(desc, func) { - return jasmine.getEnv().it(desc, func); -}; -if (isCommonJS) exports.it = it; - -/** - * Creates a disabled Jasmine spec. - * - * A convenience method that allows existing specs to be disabled temporarily during development. - * - * @param {String} desc description of this specification - * @param {Function} func defines the preconditions and expectations of the spec - */ -var xit = function(desc, func) { - return jasmine.getEnv().xit(desc, func); -}; -if (isCommonJS) exports.xit = xit; - -/** - * Starts a chain for a Jasmine expectation. - * - * It is passed an Object that is the actual value and should chain to one of the many - * jasmine.Matchers functions. - * - * @param {Object} actual Actual value to test against and expected value - */ -var expect = function(actual) { - return jasmine.getEnv().currentSpec.expect(actual); -}; -if (isCommonJS) exports.expect = expect; - -/** - * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. - * - * @param {Function} func Function that defines part of a jasmine spec. - */ -var runs = function(func) { - jasmine.getEnv().currentSpec.runs(func); -}; -if (isCommonJS) exports.runs = runs; - -/** - * Waits a fixed time period before moving to the next block. - * - * @deprecated Use waitsFor() instead - * @param {Number} timeout milliseconds to wait - */ -var waits = function(timeout) { - jasmine.getEnv().currentSpec.waits(timeout); -}; -if (isCommonJS) exports.waits = waits; - -/** - * Waits for the latchFunction to return true before proceeding to the next block. - * - * @param {Function} latchFunction - * @param {String} optional_timeoutMessage - * @param {Number} optional_timeout - */ -var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { - jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); -}; -if (isCommonJS) exports.waitsFor = waitsFor; - -/** - * A function that is called before each spec in a suite. - * - * Used for spec setup, including validating assumptions. - * - * @param {Function} beforeEachFunction - */ -var beforeEach = function(beforeEachFunction) { - jasmine.getEnv().beforeEach(beforeEachFunction); -}; -if (isCommonJS) exports.beforeEach = beforeEach; - -/** - * A function that is called after each spec in a suite. - * - * Used for restoring any state that is hijacked during spec execution. - * - * @param {Function} afterEachFunction - */ -var afterEach = function(afterEachFunction) { - jasmine.getEnv().afterEach(afterEachFunction); -}; -if (isCommonJS) exports.afterEach = afterEach; - -/** - * Defines a suite of specifications. - * - * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared - * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization - * of setup in some tests. - * - * @example - * // TODO: a simple suite - * - * // TODO: a simple suite with a nested describe block - * - * @param {String} description A string, usually the class under test. - * @param {Function} specDefinitions function that defines several specs. - */ -var describe = function(description, specDefinitions) { - return jasmine.getEnv().describe(description, specDefinitions); -}; -if (isCommonJS) exports.describe = describe; - -/** - * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. - * - * @param {String} description A string, usually the class under test. - * @param {Function} specDefinitions function that defines several specs. - */ -var xdescribe = function(description, specDefinitions) { - return jasmine.getEnv().xdescribe(description, specDefinitions); -}; -if (isCommonJS) exports.xdescribe = xdescribe; - - -// Provide the XMLHttpRequest class for IE 5.x-6.x: -jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { - function tryIt(f) { - try { - return f(); - } catch(e) { - } - return null; - } - - var xhr = tryIt(function() { - return new ActiveXObject("Msxml2.XMLHTTP.6.0"); - }) || - tryIt(function() { - return new ActiveXObject("Msxml2.XMLHTTP.3.0"); - }) || - tryIt(function() { - return new ActiveXObject("Msxml2.XMLHTTP"); - }) || - tryIt(function() { - return new ActiveXObject("Microsoft.XMLHTTP"); - }); - - if (!xhr) throw new Error("This browser does not support XMLHttpRequest."); - - return xhr; -} : XMLHttpRequest; -/** - * @namespace - */ -jasmine.util = {}; - -/** - * Declare that a child class inherit it's prototype from the parent class. - * - * @private - * @param {Function} childClass - * @param {Function} parentClass - */ -jasmine.util.inherit = function(childClass, parentClass) { - /** - * @private - */ - var subclass = function() { - }; - subclass.prototype = parentClass.prototype; - childClass.prototype = new subclass(); -}; - -jasmine.util.formatException = function(e) { - var lineNumber; - if (e.line) { - lineNumber = e.line; - } - else if (e.lineNumber) { - lineNumber = e.lineNumber; - } - - var file; - - if (e.sourceURL) { - file = e.sourceURL; - } - else if (e.fileName) { - file = e.fileName; - } - - var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); - - if (file && lineNumber) { - message += ' in ' + file + ' (line ' + lineNumber + ')'; - } - - return message; -}; - -jasmine.util.htmlEscape = function(str) { - if (!str) return str; - return str.replace(/&/g, '&') - .replace(//g, '>'); -}; - -jasmine.util.argsToArray = function(args) { - var arrayOfArgs = []; - for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); - return arrayOfArgs; -}; - -jasmine.util.extend = function(destination, source) { - for (var property in source) destination[property] = source[property]; - return destination; -}; - -/** - * Environment for Jasmine - * - * @constructor - */ -jasmine.Env = function() { - this.currentSpec = null; - this.currentSuite = null; - this.currentRunner_ = new jasmine.Runner(this); - - this.reporter = new jasmine.MultiReporter(); - - this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; - this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; - this.lastUpdate = 0; - this.specFilter = function() { - return true; - }; - - this.nextSpecId_ = 0; - this.nextSuiteId_ = 0; - this.equalityTesters_ = []; - - // wrap matchers - this.matchersClass = function() { - jasmine.Matchers.apply(this, arguments); - }; - jasmine.util.inherit(this.matchersClass, jasmine.Matchers); - - jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); -}; - - -jasmine.Env.prototype.setTimeout = jasmine.setTimeout; -jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; -jasmine.Env.prototype.setInterval = jasmine.setInterval; -jasmine.Env.prototype.clearInterval = jasmine.clearInterval; - -/** - * @returns an object containing jasmine version build info, if set. - */ -jasmine.Env.prototype.version = function () { - if (jasmine.version_) { - return jasmine.version_; - } else { - throw new Error('Version not set'); - } -}; - -/** - * @returns string containing jasmine version build info, if set. - */ -jasmine.Env.prototype.versionString = function() { - if (!jasmine.version_) { - return "version unknown"; - } - - var version = this.version(); - var versionString = version.major + "." + version.minor + "." + version.build; - if (version.release_candidate) { - versionString += ".rc" + version.release_candidate; - } - versionString += " revision " + version.revision; - return versionString; -}; - -/** - * @returns a sequential integer starting at 0 - */ -jasmine.Env.prototype.nextSpecId = function () { - return this.nextSpecId_++; -}; - -/** - * @returns a sequential integer starting at 0 - */ -jasmine.Env.prototype.nextSuiteId = function () { - return this.nextSuiteId_++; -}; - -/** - * Register a reporter to receive status updates from Jasmine. - * @param {jasmine.Reporter} reporter An object which will receive status updates. - */ -jasmine.Env.prototype.addReporter = function(reporter) { - this.reporter.addReporter(reporter); -}; - -jasmine.Env.prototype.execute = function() { - this.currentRunner_.execute(); -}; - -jasmine.Env.prototype.describe = function(description, specDefinitions) { - var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); - - var parentSuite = this.currentSuite; - if (parentSuite) { - parentSuite.add(suite); - } else { - this.currentRunner_.add(suite); - } - - this.currentSuite = suite; - - var declarationError = null; - try { - specDefinitions.call(suite); - } catch(e) { - declarationError = e; - } - - if (declarationError) { - this.it("encountered a declaration exception", function() { - throw declarationError; - }); - } - - this.currentSuite = parentSuite; - - return suite; -}; - -jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { - if (this.currentSuite) { - this.currentSuite.beforeEach(beforeEachFunction); - } else { - this.currentRunner_.beforeEach(beforeEachFunction); - } -}; - -jasmine.Env.prototype.currentRunner = function () { - return this.currentRunner_; -}; - -jasmine.Env.prototype.afterEach = function(afterEachFunction) { - if (this.currentSuite) { - this.currentSuite.afterEach(afterEachFunction); - } else { - this.currentRunner_.afterEach(afterEachFunction); - } - -}; - -jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { - return { - execute: function() { - } - }; -}; - -jasmine.Env.prototype.it = function(description, func) { - var spec = new jasmine.Spec(this, this.currentSuite, description); - this.currentSuite.add(spec); - this.currentSpec = spec; - - if (func) { - spec.runs(func); - } - - return spec; -}; - -jasmine.Env.prototype.xit = function(desc, func) { - return { - id: this.nextSpecId(), - runs: function() { - } - }; -}; - -jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { - if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { - return true; - } - - a.__Jasmine_been_here_before__ = b; - b.__Jasmine_been_here_before__ = a; - - var hasKey = function(obj, keyName) { - return obj !== null && obj[keyName] !== jasmine.undefined; - }; - - for (var property in b) { - if (!hasKey(a, property) && hasKey(b, property)) { - mismatchKeys.push("expected has key '" + property + "', but missing from actual."); - } - } - for (property in a) { - if (!hasKey(b, property) && hasKey(a, property)) { - mismatchKeys.push("expected missing key '" + property + "', but present in actual."); - } - } - for (property in b) { - if (property == '__Jasmine_been_here_before__') continue; - if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { - mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); - } - } - - if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { - mismatchValues.push("arrays were not the same length"); - } - - delete a.__Jasmine_been_here_before__; - delete b.__Jasmine_been_here_before__; - return (mismatchKeys.length === 0 && mismatchValues.length === 0); -}; - -jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { - mismatchKeys = mismatchKeys || []; - mismatchValues = mismatchValues || []; - - for (var i = 0; i < this.equalityTesters_.length; i++) { - var equalityTester = this.equalityTesters_[i]; - var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); - if (result !== jasmine.undefined) return result; - } - - if (a === b) return true; - - if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { - return (a == jasmine.undefined && b == jasmine.undefined); - } - - if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { - return a === b; - } - - if (a instanceof Date && b instanceof Date) { - return a.getTime() == b.getTime(); - } - - if (a instanceof jasmine.Matchers.Any) { - return a.matches(b); - } - - if (b instanceof jasmine.Matchers.Any) { - return b.matches(a); - } - - if (jasmine.isString_(a) && jasmine.isString_(b)) { - return (a == b); - } - - if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { - return (a == b); - } - - if (typeof a === "object" && typeof b === "object") { - return this.compareObjects_(a, b, mismatchKeys, mismatchValues); - } - - //Straight check - return (a === b); -}; - -jasmine.Env.prototype.contains_ = function(haystack, needle) { - if (jasmine.isArray_(haystack)) { - for (var i = 0; i < haystack.length; i++) { - if (this.equals_(haystack[i], needle)) return true; - } - return false; - } - return haystack.indexOf(needle) >= 0; -}; - -jasmine.Env.prototype.addEqualityTester = function(equalityTester) { - this.equalityTesters_.push(equalityTester); -}; -/** No-op base class for Jasmine reporters. - * - * @constructor - */ -jasmine.Reporter = function() { -}; - -//noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { -}; - -//noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.reportRunnerResults = function(runner) { -}; - -//noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.reportSuiteResults = function(suite) { -}; - -//noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.reportSpecStarting = function(spec) { -}; - -//noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.reportSpecResults = function(spec) { -}; - -//noinspection JSUnusedLocalSymbols -jasmine.Reporter.prototype.log = function(str) { -}; - -/** - * Blocks are functions with executable code that make up a spec. - * - * @constructor - * @param {jasmine.Env} env - * @param {Function} func - * @param {jasmine.Spec} spec - */ -jasmine.Block = function(env, func, spec) { - this.env = env; - this.func = func; - this.spec = spec; -}; - -jasmine.Block.prototype.execute = function(onComplete) { - try { - this.func.apply(this.spec); - } catch (e) { - this.spec.fail(e); - } - onComplete(); -}; -/** JavaScript API reporter. - * - * @constructor - */ -jasmine.JsApiReporter = function() { - this.started = false; - this.finished = false; - this.suites_ = []; - this.results_ = {}; -}; - -jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { - this.started = true; - var suites = runner.topLevelSuites(); - for (var i = 0; i < suites.length; i++) { - var suite = suites[i]; - this.suites_.push(this.summarize_(suite)); - } -}; - -jasmine.JsApiReporter.prototype.suites = function() { - return this.suites_; -}; - -jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { - var isSuite = suiteOrSpec instanceof jasmine.Suite; - var summary = { - id: suiteOrSpec.id, - name: suiteOrSpec.description, - type: isSuite ? 'suite' : 'spec', - children: [] - }; - - if (isSuite) { - var children = suiteOrSpec.children(); - for (var i = 0; i < children.length; i++) { - summary.children.push(this.summarize_(children[i])); - } - } - return summary; -}; - -jasmine.JsApiReporter.prototype.results = function() { - return this.results_; -}; - -jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { - return this.results_[specId]; -}; - -//noinspection JSUnusedLocalSymbols -jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { - this.finished = true; -}; - -//noinspection JSUnusedLocalSymbols -jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { -}; - -//noinspection JSUnusedLocalSymbols -jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { - this.results_[spec.id] = { - messages: spec.results().getItems(), - result: spec.results().failedCount > 0 ? "failed" : "passed" - }; -}; - -//noinspection JSUnusedLocalSymbols -jasmine.JsApiReporter.prototype.log = function(str) { -}; - -jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ - var results = {}; - for (var i = 0; i < specIds.length; i++) { - var specId = specIds[i]; - results[specId] = this.summarizeResult_(this.results_[specId]); - } - return results; -}; - -jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ - var summaryMessages = []; - var messagesLength = result.messages.length; - for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { - var resultMessage = result.messages[messageIndex]; - summaryMessages.push({ - text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, - passed: resultMessage.passed ? resultMessage.passed() : true, - type: resultMessage.type, - message: resultMessage.message, - trace: { - stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined - } - }); - } - - return { - result : result.result, - messages : summaryMessages - }; -}; - -/** - * @constructor - * @param {jasmine.Env} env - * @param actual - * @param {jasmine.Spec} spec - */ -jasmine.Matchers = function(env, actual, spec, opt_isNot) { - this.env = env; - this.actual = actual; - this.spec = spec; - this.isNot = opt_isNot || false; - this.reportWasCalled_ = false; -}; - -// todo: @deprecated as of Jasmine 0.11, remove soon [xw] -jasmine.Matchers.pp = function(str) { - throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); -}; - -// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] -jasmine.Matchers.prototype.report = function(result, failing_message, details) { - throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); -}; - -jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { - for (var methodName in prototype) { - if (methodName == 'report') continue; - var orig = prototype[methodName]; - matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); - } -}; - -jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { - return function() { - var matcherArgs = jasmine.util.argsToArray(arguments); - var result = matcherFunction.apply(this, arguments); - - if (this.isNot) { - result = !result; - } - - if (this.reportWasCalled_) return result; - - var message; - if (!result) { - if (this.message) { - message = this.message.apply(this, arguments); - if (jasmine.isArray_(message)) { - message = message[this.isNot ? 1 : 0]; - } - } else { - var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); - message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; - if (matcherArgs.length > 0) { - for (var i = 0; i < matcherArgs.length; i++) { - if (i > 0) message += ","; - message += " " + jasmine.pp(matcherArgs[i]); - } - } - message += "."; - } - } - var expectationResult = new jasmine.ExpectationResult({ - matcherName: matcherName, - passed: result, - expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], - actual: this.actual, - message: message - }); - this.spec.addMatcherResult(expectationResult); - return jasmine.undefined; - }; -}; - - - - -/** - * toBe: compares the actual to the expected using === - * @param expected - */ -jasmine.Matchers.prototype.toBe = function(expected) { - return this.actual === expected; -}; - -/** - * toNotBe: compares the actual to the expected using !== - * @param expected - * @deprecated as of 1.0. Use not.toBe() instead. - */ -jasmine.Matchers.prototype.toNotBe = function(expected) { - return this.actual !== expected; -}; - -/** - * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. - * - * @param expected - */ -jasmine.Matchers.prototype.toEqual = function(expected) { - return this.env.equals_(this.actual, expected); -}; - -/** - * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual - * @param expected - * @deprecated as of 1.0. Use not.toNotEqual() instead. - */ -jasmine.Matchers.prototype.toNotEqual = function(expected) { - return !this.env.equals_(this.actual, expected); -}; - -/** - * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes - * a pattern or a String. - * - * @param expected - */ -jasmine.Matchers.prototype.toMatch = function(expected) { - return new RegExp(expected).test(this.actual); -}; - -/** - * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch - * @param expected - * @deprecated as of 1.0. Use not.toMatch() instead. - */ -jasmine.Matchers.prototype.toNotMatch = function(expected) { - return !(new RegExp(expected).test(this.actual)); -}; - -/** - * Matcher that compares the actual to jasmine.undefined. - */ -jasmine.Matchers.prototype.toBeDefined = function() { - return (this.actual !== jasmine.undefined); -}; - -/** - * Matcher that compares the actual to jasmine.undefined. - */ -jasmine.Matchers.prototype.toBeUndefined = function() { - return (this.actual === jasmine.undefined); -}; - -/** - * Matcher that compares the actual to null. - */ -jasmine.Matchers.prototype.toBeNull = function() { - return (this.actual === null); -}; - -/** - * Matcher that boolean not-nots the actual. - */ -jasmine.Matchers.prototype.toBeTruthy = function() { - return !!this.actual; -}; - - -/** - * Matcher that boolean nots the actual. - */ -jasmine.Matchers.prototype.toBeFalsy = function() { - return !this.actual; -}; - - -/** - * Matcher that checks to see if the actual, a Jasmine spy, was called. - */ -jasmine.Matchers.prototype.toHaveBeenCalled = function() { - if (arguments.length > 0) { - throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); - } - - if (!jasmine.isSpy(this.actual)) { - throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); - } - - this.message = function() { - return [ - "Expected spy " + this.actual.identity + " to have been called.", - "Expected spy " + this.actual.identity + " not to have been called." - ]; - }; - - return this.actual.wasCalled; -}; - -/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ -jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; - -/** - * Matcher that checks to see if the actual, a Jasmine spy, was not called. - * - * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead - */ -jasmine.Matchers.prototype.wasNotCalled = function() { - if (arguments.length > 0) { - throw new Error('wasNotCalled does not take arguments'); - } - - if (!jasmine.isSpy(this.actual)) { - throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); - } - - this.message = function() { - return [ - "Expected spy " + this.actual.identity + " to not have been called.", - "Expected spy " + this.actual.identity + " to have been called." - ]; - }; - - return !this.actual.wasCalled; -}; - -/** - * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. - * - * @example - * - */ -jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { - var expectedArgs = jasmine.util.argsToArray(arguments); - if (!jasmine.isSpy(this.actual)) { - throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); - } - this.message = function() { - if (this.actual.callCount === 0) { - // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw] - return [ - "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.", - "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was." - ]; - } else { - return [ - "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall), - "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall) - ]; - } - }; - - return this.env.contains_(this.actual.argsForCall, expectedArgs); -}; - -/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ -jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; - -/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ -jasmine.Matchers.prototype.wasNotCalledWith = function() { - var expectedArgs = jasmine.util.argsToArray(arguments); - if (!jasmine.isSpy(this.actual)) { - throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); - } - - this.message = function() { - return [ - "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", - "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" - ]; - }; - - return !this.env.contains_(this.actual.argsForCall, expectedArgs); -}; - -/** - * Matcher that checks that the expected item is an element in the actual Array. - * - * @param {Object} expected - */ -jasmine.Matchers.prototype.toContain = function(expected) { - return this.env.contains_(this.actual, expected); -}; - -/** - * Matcher that checks that the expected item is NOT an element in the actual Array. - * - * @param {Object} expected - * @deprecated as of 1.0. Use not.toNotContain() instead. - */ -jasmine.Matchers.prototype.toNotContain = function(expected) { - return !this.env.contains_(this.actual, expected); -}; - -jasmine.Matchers.prototype.toBeLessThan = function(expected) { - return this.actual < expected; -}; - -jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { - return this.actual > expected; -}; - -/** - * Matcher that checks that the expected item is equal to the actual item - * up to a given level of decimal precision (default 2). - * - * @param {Number} expected - * @param {Number} precision - */ -jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { - if (!(precision === 0)) { - precision = precision || 2; - } - var multiplier = Math.pow(10, precision); - var actual = Math.round(this.actual * multiplier); - expected = Math.round(expected * multiplier); - return expected == actual; -}; - -/** - * Matcher that checks that the expected exception was thrown by the actual. - * - * @param {String} expected - */ -jasmine.Matchers.prototype.toThrow = function(expected) { - var result = false; - var exception; - if (typeof this.actual != 'function') { - throw new Error('Actual is not a function'); - } - try { - this.actual(); - } catch (e) { - exception = e; - } - if (exception) { - result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); - } - - var not = this.isNot ? "not " : ""; - - this.message = function() { - if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { - return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' '); - } else { - return "Expected function to throw an exception."; - } - }; - - return result; -}; - -jasmine.Matchers.Any = function(expectedClass) { - this.expectedClass = expectedClass; -}; - -jasmine.Matchers.Any.prototype.matches = function(other) { - if (this.expectedClass == String) { - return typeof other == 'string' || other instanceof String; - } - - if (this.expectedClass == Number) { - return typeof other == 'number' || other instanceof Number; - } - - if (this.expectedClass == Function) { - return typeof other == 'function' || other instanceof Function; - } - - if (this.expectedClass == Object) { - return typeof other == 'object'; - } - - return other instanceof this.expectedClass; -}; - -jasmine.Matchers.Any.prototype.toString = function() { - return ''; -}; - -/** - * @constructor - */ -jasmine.MultiReporter = function() { - this.subReporters_ = []; -}; -jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); - -jasmine.MultiReporter.prototype.addReporter = function(reporter) { - this.subReporters_.push(reporter); -}; - -(function() { - var functionNames = [ - "reportRunnerStarting", - "reportRunnerResults", - "reportSuiteResults", - "reportSpecStarting", - "reportSpecResults", - "log" - ]; - for (var i = 0; i < functionNames.length; i++) { - var functionName = functionNames[i]; - jasmine.MultiReporter.prototype[functionName] = (function(functionName) { - return function() { - for (var j = 0; j < this.subReporters_.length; j++) { - var subReporter = this.subReporters_[j]; - if (subReporter[functionName]) { - subReporter[functionName].apply(subReporter, arguments); - } - } - }; - })(functionName); - } -})(); -/** - * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults - * - * @constructor - */ -jasmine.NestedResults = function() { - /** - * The total count of results - */ - this.totalCount = 0; - /** - * Number of passed results - */ - this.passedCount = 0; - /** - * Number of failed results - */ - this.failedCount = 0; - /** - * Was this suite/spec skipped? - */ - this.skipped = false; - /** - * @ignore - */ - this.items_ = []; -}; - -/** - * Roll up the result counts. - * - * @param result - */ -jasmine.NestedResults.prototype.rollupCounts = function(result) { - this.totalCount += result.totalCount; - this.passedCount += result.passedCount; - this.failedCount += result.failedCount; -}; - -/** - * Adds a log message. - * @param values Array of message parts which will be concatenated later. - */ -jasmine.NestedResults.prototype.log = function(values) { - this.items_.push(new jasmine.MessageResult(values)); -}; - -/** - * Getter for the results: message & results. - */ -jasmine.NestedResults.prototype.getItems = function() { - return this.items_; -}; - -/** - * Adds a result, tracking counts (total, passed, & failed) - * @param {jasmine.ExpectationResult|jasmine.NestedResults} result - */ -jasmine.NestedResults.prototype.addResult = function(result) { - if (result.type != 'log') { - if (result.items_) { - this.rollupCounts(result); - } else { - this.totalCount++; - if (result.passed()) { - this.passedCount++; - } else { - this.failedCount++; - } - } - } - this.items_.push(result); -}; - -/** - * @returns {Boolean} True if everything below passed - */ -jasmine.NestedResults.prototype.passed = function() { - return this.passedCount === this.totalCount; -}; -/** - * Base class for pretty printing for expectation results. - */ -jasmine.PrettyPrinter = function() { - this.ppNestLevel_ = 0; -}; - -/** - * Formats a value in a nice, human-readable string. - * - * @param value - */ -jasmine.PrettyPrinter.prototype.format = function(value) { - if (this.ppNestLevel_ > 40) { - throw new Error('jasmine.PrettyPrinter: format() nested too deeply!'); - } - - this.ppNestLevel_++; - try { - if (value === jasmine.undefined) { - this.emitScalar('undefined'); - } else if (value === null) { - this.emitScalar('null'); - } else if (value === jasmine.getGlobal()) { - this.emitScalar(''); - } else if (value instanceof jasmine.Matchers.Any) { - this.emitScalar(value.toString()); - } else if (typeof value === 'string') { - this.emitString(value); - } else if (jasmine.isSpy(value)) { - this.emitScalar("spy on " + value.identity); - } else if (value instanceof RegExp) { - this.emitScalar(value.toString()); - } else if (typeof value === 'function') { - this.emitScalar('Function'); - } else if (typeof value.nodeType === 'number') { - this.emitScalar('HTMLNode'); - } else if (value instanceof Date) { - this.emitScalar('Date(' + value + ')'); - } else if (value.__Jasmine_been_here_before__) { - this.emitScalar(''); - } else if (jasmine.isArray_(value) || typeof value == 'object') { - value.__Jasmine_been_here_before__ = true; - if (jasmine.isArray_(value)) { - this.emitArray(value); - } else { - this.emitObject(value); - } - delete value.__Jasmine_been_here_before__; - } else { - this.emitScalar(value.toString()); - } - } finally { - this.ppNestLevel_--; - } -}; - -jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { - for (var property in obj) { - if (property == '__Jasmine_been_here_before__') continue; - fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && - obj.__lookupGetter__(property) !== null) : false); - } -}; - -jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; -jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; -jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; -jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; - -jasmine.StringPrettyPrinter = function() { - jasmine.PrettyPrinter.call(this); - - this.string = ''; -}; -jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); - -jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { - this.append(value); -}; - -jasmine.StringPrettyPrinter.prototype.emitString = function(value) { - this.append("'" + value + "'"); -}; - -jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { - this.append('[ '); - for (var i = 0; i < array.length; i++) { - if (i > 0) { - this.append(', '); - } - this.format(array[i]); - } - this.append(' ]'); -}; - -jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { - var self = this; - this.append('{ '); - var first = true; - - this.iterateObject(obj, function(property, isGetter) { - if (first) { - first = false; - } else { - self.append(', '); - } - - self.append(property); - self.append(' : '); - if (isGetter) { - self.append(''); - } else { - self.format(obj[property]); - } - }); - - this.append(' }'); -}; - -jasmine.StringPrettyPrinter.prototype.append = function(value) { - this.string += value; -}; -jasmine.Queue = function(env) { - this.env = env; - this.blocks = []; - this.running = false; - this.index = 0; - this.offset = 0; - this.abort = false; -}; - -jasmine.Queue.prototype.addBefore = function(block) { - this.blocks.unshift(block); -}; - -jasmine.Queue.prototype.add = function(block) { - this.blocks.push(block); -}; - -jasmine.Queue.prototype.insertNext = function(block) { - this.blocks.splice((this.index + this.offset + 1), 0, block); - this.offset++; -}; - -jasmine.Queue.prototype.start = function(onComplete) { - this.running = true; - this.onComplete = onComplete; - this.next_(); -}; - -jasmine.Queue.prototype.isRunning = function() { - return this.running; -}; - -jasmine.Queue.LOOP_DONT_RECURSE = true; - -jasmine.Queue.prototype.next_ = function() { - var self = this; - var goAgain = true; - - while (goAgain) { - goAgain = false; - - if (self.index < self.blocks.length && !this.abort) { - var calledSynchronously = true; - var completedSynchronously = false; - - var onComplete = function () { - if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { - completedSynchronously = true; - return; - } - - if (self.blocks[self.index].abort) { - self.abort = true; - } - - self.offset = 0; - self.index++; - - var now = new Date().getTime(); - if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { - self.env.lastUpdate = now; - self.env.setTimeout(function() { - self.next_(); - }, 0); - } else { - if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { - goAgain = true; - } else { - self.next_(); - } - } - }; - self.blocks[self.index].execute(onComplete); - - calledSynchronously = false; - if (completedSynchronously) { - onComplete(); - } - - } else { - self.running = false; - if (self.onComplete) { - self.onComplete(); - } - } - } -}; - -jasmine.Queue.prototype.results = function() { - var results = new jasmine.NestedResults(); - for (var i = 0; i < this.blocks.length; i++) { - if (this.blocks[i].results) { - results.addResult(this.blocks[i].results()); - } - } - return results; -}; - - -/** - * Runner - * - * @constructor - * @param {jasmine.Env} env - */ -jasmine.Runner = function(env) { - var self = this; - self.env = env; - self.queue = new jasmine.Queue(env); - self.before_ = []; - self.after_ = []; - self.suites_ = []; -}; - -jasmine.Runner.prototype.execute = function() { - var self = this; - if (self.env.reporter.reportRunnerStarting) { - self.env.reporter.reportRunnerStarting(this); - } - self.queue.start(function () { - self.finishCallback(); - }); -}; - -jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { - beforeEachFunction.typeName = 'beforeEach'; - this.before_.splice(0,0,beforeEachFunction); -}; - -jasmine.Runner.prototype.afterEach = function(afterEachFunction) { - afterEachFunction.typeName = 'afterEach'; - this.after_.splice(0,0,afterEachFunction); -}; - - -jasmine.Runner.prototype.finishCallback = function() { - this.env.reporter.reportRunnerResults(this); -}; - -jasmine.Runner.prototype.addSuite = function(suite) { - this.suites_.push(suite); -}; - -jasmine.Runner.prototype.add = function(block) { - if (block instanceof jasmine.Suite) { - this.addSuite(block); - } - this.queue.add(block); -}; - -jasmine.Runner.prototype.specs = function () { - var suites = this.suites(); - var specs = []; - for (var i = 0; i < suites.length; i++) { - specs = specs.concat(suites[i].specs()); - } - return specs; -}; - -jasmine.Runner.prototype.suites = function() { - return this.suites_; -}; - -jasmine.Runner.prototype.topLevelSuites = function() { - var topLevelSuites = []; - for (var i = 0; i < this.suites_.length; i++) { - if (!this.suites_[i].parentSuite) { - topLevelSuites.push(this.suites_[i]); - } - } - return topLevelSuites; -}; - -jasmine.Runner.prototype.results = function() { - return this.queue.results(); -}; -/** - * Internal representation of a Jasmine specification, or test. - * - * @constructor - * @param {jasmine.Env} env - * @param {jasmine.Suite} suite - * @param {String} description - */ -jasmine.Spec = function(env, suite, description) { - if (!env) { - throw new Error('jasmine.Env() required'); - } - if (!suite) { - throw new Error('jasmine.Suite() required'); - } - var spec = this; - spec.id = env.nextSpecId ? env.nextSpecId() : null; - spec.env = env; - spec.suite = suite; - spec.description = description; - spec.queue = new jasmine.Queue(env); - - spec.afterCallbacks = []; - spec.spies_ = []; - - spec.results_ = new jasmine.NestedResults(); - spec.results_.description = description; - spec.matchersClass = null; -}; - -jasmine.Spec.prototype.getFullName = function() { - return this.suite.getFullName() + ' ' + this.description + '.'; -}; - - -jasmine.Spec.prototype.results = function() { - return this.results_; -}; - -/** - * All parameters are pretty-printed and concatenated together, then written to the spec's output. - * - * Be careful not to leave calls to jasmine.log in production code. - */ -jasmine.Spec.prototype.log = function() { - return this.results_.log(arguments); -}; - -jasmine.Spec.prototype.runs = function (func) { - var block = new jasmine.Block(this.env, func, this); - this.addToQueue(block); - return this; -}; - -jasmine.Spec.prototype.addToQueue = function (block) { - if (this.queue.isRunning()) { - this.queue.insertNext(block); - } else { - this.queue.add(block); - } -}; - -/** - * @param {jasmine.ExpectationResult} result - */ -jasmine.Spec.prototype.addMatcherResult = function(result) { - this.results_.addResult(result); -}; - -jasmine.Spec.prototype.expect = function(actual) { - var positive = new (this.getMatchersClass_())(this.env, actual, this); - positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); - return positive; -}; - -/** - * Waits a fixed time period before moving to the next block. - * - * @deprecated Use waitsFor() instead - * @param {Number} timeout milliseconds to wait - */ -jasmine.Spec.prototype.waits = function(timeout) { - var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); - this.addToQueue(waitsFunc); - return this; -}; - -/** - * Waits for the latchFunction to return true before proceeding to the next block. - * - * @param {Function} latchFunction - * @param {String} optional_timeoutMessage - * @param {Number} optional_timeout - */ -jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { - var latchFunction_ = null; - var optional_timeoutMessage_ = null; - var optional_timeout_ = null; - - for (var i = 0; i < arguments.length; i++) { - var arg = arguments[i]; - switch (typeof arg) { - case 'function': - latchFunction_ = arg; - break; - case 'string': - optional_timeoutMessage_ = arg; - break; - case 'number': - optional_timeout_ = arg; - break; - } - } - - var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); - this.addToQueue(waitsForFunc); - return this; -}; - -jasmine.Spec.prototype.fail = function (e) { - var expectationResult = new jasmine.ExpectationResult({ - passed: false, - message: e ? jasmine.util.formatException(e) : 'Exception', - trace: { stack: e.stack } - }); - this.results_.addResult(expectationResult); -}; - -jasmine.Spec.prototype.getMatchersClass_ = function() { - return this.matchersClass || this.env.matchersClass; -}; - -jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { - var parent = this.getMatchersClass_(); - var newMatchersClass = function() { - parent.apply(this, arguments); - }; - jasmine.util.inherit(newMatchersClass, parent); - jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); - this.matchersClass = newMatchersClass; -}; - -jasmine.Spec.prototype.finishCallback = function() { - this.env.reporter.reportSpecResults(this); -}; - -jasmine.Spec.prototype.finish = function(onComplete) { - this.removeAllSpies(); - this.finishCallback(); - if (onComplete) { - onComplete(); - } -}; - -jasmine.Spec.prototype.after = function(doAfter) { - if (this.queue.isRunning()) { - this.queue.add(new jasmine.Block(this.env, doAfter, this)); - } else { - this.afterCallbacks.unshift(doAfter); - } -}; - -jasmine.Spec.prototype.execute = function(onComplete) { - var spec = this; - if (!spec.env.specFilter(spec)) { - spec.results_.skipped = true; - spec.finish(onComplete); - return; - } - - this.env.reporter.reportSpecStarting(this); - - spec.env.currentSpec = spec; - - spec.addBeforesAndAftersToQueue(); - - spec.queue.start(function () { - spec.finish(onComplete); - }); -}; - -jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { - var runner = this.env.currentRunner(); - var i; - - for (var suite = this.suite; suite; suite = suite.parentSuite) { - for (i = 0; i < suite.before_.length; i++) { - this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); - } - } - for (i = 0; i < runner.before_.length; i++) { - this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); - } - for (i = 0; i < this.afterCallbacks.length; i++) { - this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this)); - } - for (suite = this.suite; suite; suite = suite.parentSuite) { - for (i = 0; i < suite.after_.length; i++) { - this.queue.add(new jasmine.Block(this.env, suite.after_[i], this)); - } - } - for (i = 0; i < runner.after_.length; i++) { - this.queue.add(new jasmine.Block(this.env, runner.after_[i], this)); - } -}; - -jasmine.Spec.prototype.explodes = function() { - throw 'explodes function should not have been called'; -}; - -jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { - if (obj == jasmine.undefined) { - throw "spyOn could not find an object to spy upon for " + methodName + "()"; - } - - if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { - throw methodName + '() method does not exist'; - } - - if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { - throw new Error(methodName + ' has already been spied upon'); - } - - var spyObj = jasmine.createSpy(methodName); - - this.spies_.push(spyObj); - spyObj.baseObj = obj; - spyObj.methodName = methodName; - spyObj.originalValue = obj[methodName]; - - obj[methodName] = spyObj; - - return spyObj; -}; - -jasmine.Spec.prototype.removeAllSpies = function() { - for (var i = 0; i < this.spies_.length; i++) { - var spy = this.spies_[i]; - spy.baseObj[spy.methodName] = spy.originalValue; - } - this.spies_ = []; -}; - -/** - * Internal representation of a Jasmine suite. - * - * @constructor - * @param {jasmine.Env} env - * @param {String} description - * @param {Function} specDefinitions - * @param {jasmine.Suite} parentSuite - */ -jasmine.Suite = function(env, description, specDefinitions, parentSuite) { - var self = this; - self.id = env.nextSuiteId ? env.nextSuiteId() : null; - self.description = description; - self.queue = new jasmine.Queue(env); - self.parentSuite = parentSuite; - self.env = env; - self.before_ = []; - self.after_ = []; - self.children_ = []; - self.suites_ = []; - self.specs_ = []; -}; - -jasmine.Suite.prototype.getFullName = function() { - var fullName = this.description; - for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { - fullName = parentSuite.description + ' ' + fullName; - } - return fullName; -}; - -jasmine.Suite.prototype.finish = function(onComplete) { - this.env.reporter.reportSuiteResults(this); - this.finished = true; - if (typeof(onComplete) == 'function') { - onComplete(); - } -}; - -jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { - beforeEachFunction.typeName = 'beforeEach'; - this.before_.unshift(beforeEachFunction); -}; - -jasmine.Suite.prototype.afterEach = function(afterEachFunction) { - afterEachFunction.typeName = 'afterEach'; - this.after_.unshift(afterEachFunction); -}; - -jasmine.Suite.prototype.results = function() { - return this.queue.results(); -}; - -jasmine.Suite.prototype.add = function(suiteOrSpec) { - this.children_.push(suiteOrSpec); - if (suiteOrSpec instanceof jasmine.Suite) { - this.suites_.push(suiteOrSpec); - this.env.currentRunner().addSuite(suiteOrSpec); - } else { - this.specs_.push(suiteOrSpec); - } - this.queue.add(suiteOrSpec); -}; - -jasmine.Suite.prototype.specs = function() { - return this.specs_; -}; - -jasmine.Suite.prototype.suites = function() { - return this.suites_; -}; - -jasmine.Suite.prototype.children = function() { - return this.children_; -}; - -jasmine.Suite.prototype.execute = function(onComplete) { - var self = this; - this.queue.start(function () { - self.finish(onComplete); - }); -}; -jasmine.WaitsBlock = function(env, timeout, spec) { - this.timeout = timeout; - jasmine.Block.call(this, env, null, spec); -}; - -jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); - -jasmine.WaitsBlock.prototype.execute = function (onComplete) { - if (jasmine.VERBOSE) { - this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); - } - this.env.setTimeout(function () { - onComplete(); - }, this.timeout); -}; -/** - * A block which waits for some condition to become true, with timeout. - * - * @constructor - * @extends jasmine.Block - * @param {jasmine.Env} env The Jasmine environment. - * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. - * @param {Function} latchFunction A function which returns true when the desired condition has been met. - * @param {String} message The message to display if the desired condition hasn't been met within the given time period. - * @param {jasmine.Spec} spec The Jasmine spec. - */ -jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { - this.timeout = timeout || env.defaultTimeoutInterval; - this.latchFunction = latchFunction; - this.message = message; - this.totalTimeSpentWaitingForLatch = 0; - jasmine.Block.call(this, env, null, spec); -}; -jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); - -jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; - -jasmine.WaitsForBlock.prototype.execute = function(onComplete) { - if (jasmine.VERBOSE) { - this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); - } - var latchFunctionResult; - try { - latchFunctionResult = this.latchFunction.apply(this.spec); - } catch (e) { - this.spec.fail(e); - onComplete(); - return; - } - - if (latchFunctionResult) { - onComplete(); - } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { - var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); - this.spec.fail({ - name: 'timeout', - message: message - }); - - this.abort = true; - onComplete(); - } else { - this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; - var self = this; - this.env.setTimeout(function() { - self.execute(onComplete); - }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); - } -}; -// Mock setTimeout, clearTimeout -// Contributed by Pivotal Computer Systems, www.pivotalsf.com - -jasmine.FakeTimer = function() { - this.reset(); - - var self = this; - self.setTimeout = function(funcToCall, millis) { - self.timeoutsMade++; - self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); - return self.timeoutsMade; - }; - - self.setInterval = function(funcToCall, millis) { - self.timeoutsMade++; - self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); - return self.timeoutsMade; - }; - - self.clearTimeout = function(timeoutKey) { - self.scheduledFunctions[timeoutKey] = jasmine.undefined; - }; - - self.clearInterval = function(timeoutKey) { - self.scheduledFunctions[timeoutKey] = jasmine.undefined; - }; - -}; - -jasmine.FakeTimer.prototype.reset = function() { - this.timeoutsMade = 0; - this.scheduledFunctions = {}; - this.nowMillis = 0; -}; - -jasmine.FakeTimer.prototype.tick = function(millis) { - var oldMillis = this.nowMillis; - var newMillis = oldMillis + millis; - this.runFunctionsWithinRange(oldMillis, newMillis); - this.nowMillis = newMillis; -}; - -jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { - var scheduledFunc; - var funcsToRun = []; - for (var timeoutKey in this.scheduledFunctions) { - scheduledFunc = this.scheduledFunctions[timeoutKey]; - if (scheduledFunc != jasmine.undefined && - scheduledFunc.runAtMillis >= oldMillis && - scheduledFunc.runAtMillis <= nowMillis) { - funcsToRun.push(scheduledFunc); - this.scheduledFunctions[timeoutKey] = jasmine.undefined; - } - } - - if (funcsToRun.length > 0) { - funcsToRun.sort(function(a, b) { - return a.runAtMillis - b.runAtMillis; - }); - for (var i = 0; i < funcsToRun.length; ++i) { - try { - var funcToRun = funcsToRun[i]; - this.nowMillis = funcToRun.runAtMillis; - funcToRun.funcToCall(); - if (funcToRun.recurring) { - this.scheduleFunction(funcToRun.timeoutKey, - funcToRun.funcToCall, - funcToRun.millis, - true); - } - } catch(e) { - } - } - this.runFunctionsWithinRange(oldMillis, nowMillis); - } -}; - -jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { - this.scheduledFunctions[timeoutKey] = { - runAtMillis: this.nowMillis + millis, - funcToCall: funcToCall, - recurring: recurring, - timeoutKey: timeoutKey, - millis: millis - }; -}; - -/** - * @namespace - */ -jasmine.Clock = { - defaultFakeTimer: new jasmine.FakeTimer(), - - reset: function() { - jasmine.Clock.assertInstalled(); - jasmine.Clock.defaultFakeTimer.reset(); - }, - - tick: function(millis) { - jasmine.Clock.assertInstalled(); - jasmine.Clock.defaultFakeTimer.tick(millis); - }, - - runFunctionsWithinRange: function(oldMillis, nowMillis) { - jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); - }, - - scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { - jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); - }, - - useMock: function() { - if (!jasmine.Clock.isInstalled()) { - var spec = jasmine.getEnv().currentSpec; - spec.after(jasmine.Clock.uninstallMock); - - jasmine.Clock.installMock(); - } - }, - - installMock: function() { - jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; - }, - - uninstallMock: function() { - jasmine.Clock.assertInstalled(); - jasmine.Clock.installed = jasmine.Clock.real; - }, - - real: { - setTimeout: jasmine.getGlobal().setTimeout, - clearTimeout: jasmine.getGlobal().clearTimeout, - setInterval: jasmine.getGlobal().setInterval, - clearInterval: jasmine.getGlobal().clearInterval - }, - - assertInstalled: function() { - if (!jasmine.Clock.isInstalled()) { - throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); - } - }, - - isInstalled: function() { - return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; - }, - - installed: null -}; -jasmine.Clock.installed = jasmine.Clock.real; - -//else for IE support -jasmine.getGlobal().setTimeout = function(funcToCall, millis) { - if (jasmine.Clock.installed.setTimeout.apply) { - return jasmine.Clock.installed.setTimeout.apply(this, arguments); - } else { - return jasmine.Clock.installed.setTimeout(funcToCall, millis); - } -}; - -jasmine.getGlobal().setInterval = function(funcToCall, millis) { - if (jasmine.Clock.installed.setInterval.apply) { - return jasmine.Clock.installed.setInterval.apply(this, arguments); - } else { - return jasmine.Clock.installed.setInterval(funcToCall, millis); - } -}; - -jasmine.getGlobal().clearTimeout = function(timeoutKey) { - if (jasmine.Clock.installed.clearTimeout.apply) { - return jasmine.Clock.installed.clearTimeout.apply(this, arguments); - } else { - return jasmine.Clock.installed.clearTimeout(timeoutKey); - } -}; - -jasmine.getGlobal().clearInterval = function(timeoutKey) { - if (jasmine.Clock.installed.clearTimeout.apply) { - return jasmine.Clock.installed.clearInterval.apply(this, arguments); - } else { - return jasmine.Clock.installed.clearInterval(timeoutKey); - } -}; - -jasmine.version_= { - "major": 1, - "minor": 1, - "build": 0, - "revision": 1315677058 -}; diff --git a/test/lib/jasmine-1.1.0/jasmine_favicon.png b/test/lib/jasmine-1.1.0/jasmine_favicon.png deleted file mode 100644 index 218f3b43713598fa5a3e78b57aceb909c33f46df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 905 zcmV;419tq0P)Px#AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0008u zNkl3{fod28|PjmA)7fYg4w8-(2my9xtBGOs}K`n&t1VzxMO^X)M zrW+Ln1udc?q6TP)z5gAjt)P&D!M$+HJK#x<`xnD030zwD?KrxxY!2tlA zGc-58?0D7SsT)7Km=v+tNVNUk`?s@;^OxCF)y6P}_mL;~7;S<@b|MzmKq)m8l@yky zT1~ECpxZw@64!nkI34QLiUsA%i%N>-$&zGYR7WJyi9ERMyS(%kf z7A_r)X>!90&m(FwDQZ>q;+nOa*KR2+E6Fz)QwU=W1Oyo*4>_qlm|~joa|{4_A_3W8 z#FFZzRp-xMIx5a7D_Fj3&#r^TbIY@cND1d0f*^qDIs{!pw!IWGQ_%l4#ASm_D5Vet z0%ek7^)@xPihX_G0&hIc9*14ca=D!8oG}vW?H%~w^F?f_s>zU|fKrNJXJ_d6{v!t( zpEoqMws_yQws>3o?VW8Txq~#->dJG^ELW5irR!s`(_JvD^6;r+ho~eIK@ia8_lH(h zt*-p?CFC1_h2MV=?jP){uW!7WjLjCaO&c1D+tf582!XEaoB#xWAYcN5f$sLtf$koW zQs{{>)ZTq?FC6|J_%n}AWbiFK(Bo-%^-{H`*)E(ucjo-r%SYm)W5f6tN=xz=S646E fNXW#U{x?4WXWJ - - - Jasmine Spec Runner - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/spec/DocumentSpec.js b/test/spec/DocumentSpec.js deleted file mode 100644 index ecc7e34..0000000 --- a/test/spec/DocumentSpec.js +++ /dev/null @@ -1,26 +0,0 @@ -describe("CanvasTextEditor", function() { - var Document = require('Document'); - var testText = 'Line1\n\nLine3\nLine4' - - it("should have static method for parsing text into array of lines", function() { - var lines = Document.prepareText(testText); - expect(lines.length).toEqual(4); - expect(lines[0]).toEqual('Line1\n'); - expect(lines[1]).toEqual('\n'); - expect(lines[2]).toEqual('Line3\n'); - expect(lines[3]).toEqual('Line4'); - }); - - it("should support getting lines and characters at positions", function(){ - var doc = new Document(testText); - - expect(doc.getLineCount()).toEqual(4); - expect(doc.charAt(0,2)).toEqual('L'); - expect(doc.charAt(4,2)).toEqual('3'); - expect(doc.charAt(100,2)).toBeFalsy(); - - expect(doc.getLine(3)).toEqual('Line4'); - expect(doc.getLine(4)).toBeFalsy(); - }); - -}); \ No newline at end of file diff --git a/test/spec/EditorSpec.js b/test/spec/EditorSpec.js deleted file mode 100644 index 6b595dd..0000000 --- a/test/spec/EditorSpec.js +++ /dev/null @@ -1,7 +0,0 @@ -describe("CanvasTextEditor", function() { - var CanvasTextEditor = require('CanvasTextEditor'); - - it("should be possible to instatiate", function() { - var editor = new CanvasTextEditor; - }); -}); \ No newline at end of file diff --git a/test/spec/FontMetricsSpec.js b/test/spec/FontMetricsSpec.js deleted file mode 100644 index c08e482..0000000 --- a/test/spec/FontMetricsSpec.js +++ /dev/null @@ -1,27 +0,0 @@ -describe("CanvasTextEditor", function() { - var FontMetrics = require('FontMetrics'); - - it("should support getters for family and size", function() { - var family = 'Arial, sans-serif', - size = 18, - metrics = new FontMetrics(family, size); - - expect(metrics.getFamily()).toEqual(family); - expect(metrics.getSize()).toEqual(size); - - metrics = new FontMetrics; - - expect(metrics.getFamily()).toBeTruthy(); - expect(metrics.getSize()).toBeTruthy(); - }); - - it("should calculate font metrics", function(){ - - var metrics = new FontMetrics; - - expect(metrics.getHeight()).toBeTruthy(); - expect(metrics.getWidth()).toBeTruthy(); - expect(metrics.getBaseline()).toBeTruthy(); - - }); -}); \ No newline at end of file From 58bc84a7957c097dc28a21249d38be306da8454e Mon Sep 17 00:00:00 2001 From: Dmitriy Kubyshkin Date: Thu, 29 Mar 2012 16:22:43 +0700 Subject: [PATCH 2/6] Updated js to match part-6. --- canvas-text-editor.js | 482 ++++++++++++++++++++++++++++-------------- 1 file changed, 326 insertions(+), 156 deletions(-) diff --git a/canvas-text-editor.js b/canvas-text-editor.js index ad7b3ab..11d097c 100644 --- a/canvas-text-editor.js +++ b/canvas-text-editor.js @@ -52,7 +52,7 @@ var FontMetrics = require('FontMetrics'), Document = require('Document'), - Cursor = require('Cursor'); + Selection = require('Selection'); /** * Simple plain-text text editor using html5 canvas. @@ -64,7 +64,7 @@ var CanvasTextEditor = function(doc) { this._createWrapper(); this._createCanvas(); this._createInput(); - this._cursor = new Cursor(this); + this._selection = new Selection(this); }; module.exports = CanvasTextEditor; @@ -100,23 +100,33 @@ CanvasTextEditor.prototype._createCanvas = function() { this.canvas.style.display = 'block'; this.context = this.canvas.getContext('2d'); this.resize(640, 480); + this.render(); + this.wrapper.appendChild(this.canvas); +}; - // For now just very dumb implementation of rendering +/** + * Renders document onto the canvas + * @return {[type]} [description] + */ +CanvasTextEditor.prototype.render = function() { var baselineOffset = this._metrics.getBaseline(), lineHeight = this._metrics.getHeight(), characterWidth = this._metrics.getWidth(), maxHeight = Math.ceil(640 / lineHeight), lineCount = this._document.getLineCount(); + // Making sure we don't render somethign that we won't see if (lineCount < maxHeight) maxHeight = lineCount; + // Clearing previous iteration + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + + // Looping over document lines for(var i = 0; i < maxHeight; ++i) { this.context.fillText( this._document.getLine(i), 0, lineHeight * i + baselineOffset ); } - - this.wrapper.appendChild(this.canvas); }; /** @@ -126,9 +136,10 @@ CanvasTextEditor.prototype._createCanvas = function() { CanvasTextEditor.prototype._createInput = function() { this.inputEl = document.createElement('textarea'); this.inputEl.style.position = 'absolute'; - this.inputEl.style.top = '-100px'; + this.inputEl.style.top = '-10px'; this.inputEl.style.height = 0; this.inputEl.style.width = 0; + this.inputEl.addEventListener('input', this.handleInput.bind(this), false); this.inputEl.addEventListener('blur', this.blur.bind(this), false); this.inputEl.addEventListener('focus', this._inputFocus.bind(this), false); this.inputEl.addEventListener('keydown', this.keydown.bind(this), false); @@ -136,13 +147,50 @@ CanvasTextEditor.prototype._createInput = function() { this.wrapper.appendChild(this.inputEl); }; +/** + * Handles regular text input into our proxy field + * @param {Event} e + */ +CanvasTextEditor.prototype.handleInput = function(e) { + this.insertTextAtCurrentPosition(e.target.value); + e.target.value = ''; +}; + +/** + * Inserts text at the current cursor position + * @param {string} text + */ +CanvasTextEditor.prototype.insertTextAtCurrentPosition = function(text) { + var pos = this._selection.getPosition(); + // Inserting new text and changing position of cursor to a new one + this._selection.setPosition.apply( + this._selection, + this._document.insertText(text, pos[0], pos[1]) + ); + this.render(); +}; + +/** + * Deletes text at the current cursor position + * @param {string} text + */ +CanvasTextEditor.prototype.deleteCharAtCurrentPosition = function(forward) { + var pos = this._selection.getPosition(); + // Deleting text and changing position of cursor to a new one + this._selection.setPosition.apply( + this._selection, + this._document.deleteChar(forward, pos[0], pos[1]) + ); + this.render(); +}; + /** * Real handler code for editor gaining focus. * @private */ CanvasTextEditor.prototype._inputFocus = function() { this.wrapper.style.outline = '1px solid #09f'; - this._cursor.setVisible(true); + this._selection.setVisible(true); }; /** @@ -187,22 +235,33 @@ CanvasTextEditor.prototype.resize = function(width, height) { CanvasTextEditor.prototype.keydown = function(e) { var handled = true; switch(e.keyCode) { + case 8: // backspace + this.deleteCharAtCurrentPosition(false); + break; + case 46: // delete + this.deleteCharAtCurrentPosition(true); + break; + case 13: // Enter + this.insertTextAtCurrentPosition('\n'); + break; case 37: // Left arrow - this._cursor.moveLeft(); + this._selection.moveLeft(); break; case 38: // Up arrow - this._cursor.moveUp(); + this._selection.moveUp(); break; case 39: // Up arrow - this._cursor.moveRight(); + this._selection.moveRight(); break; case 40: // Down arrow - this._cursor.moveDown(); + this._selection.moveDown(); break; default: handled = false; } - return !handled; + if(handled) { + e.preventDefault(); + } }; /** @@ -211,7 +270,7 @@ CanvasTextEditor.prototype.keydown = function(e) { CanvasTextEditor.prototype.blur = function() { this.inputEl.blur(); this.wrapper.style.outline = 'none'; - this._cursor.setVisible(false); + this._selection.setVisible(false); }; /** @@ -221,149 +280,6 @@ CanvasTextEditor.prototype.focus = function() { this.inputEl.focus(); }; -}, "Cursor": function(exports, require, module) {/** - * Creates new cursor for the editor. - * @param {Editor} editor. - * @constructor - */ -Selection = function(editor) { - this.editor = editor; - this.blinkInterval = 500; - - this.start = { - line: 0, - character: 0 - }; - - this.end = { - line: 0, - character: 0 - }; - - this.el = document.createElement('div'); - this.el.style.position = 'absolute'; - this.el.style.width = '1px'; - this.el.style.height = this.editor.getFontMetrics().getHeight() + 'px'; - this.el.style.backgroundColor = '#000'; - - this.editor.getEl().appendChild(this.el); - this.setPosition(0, 0); -}; - -/** - * Responsible for cursor blinking - * @return {void} - */ -Selection.prototype.blink = function() { - if (parseInt(this.el.style.opacity, 10)) { - this.el.style.opacity = 0; - } else { - this.el.style.opacity = 1; - } -}; - -/** - * Moves cursor to a specified position inside document. - * @param {number} position Offset from the start of the document. - */ -Selection.prototype.setPosition = function(line, character) { - // Providing defaults for both line and character parts of position - if (typeof line === 'undefined') line = this.end.line - if (typeof character === 'undefined') character = this.end.character - - // Checking lower bounds - line >= 0 || (line = 0); - character >= 0 || (character = 0); - - // Checking upper bounds - var lineCount = this.editor.getDocument().getLineCount(); - line < lineCount || (line = lineCount - 1); - var characterCount = this.editor.getDocument().getLine(line).trim('\n').length; - character <= characterCount || (character = characterCount); - - // Saving new value - this.start.line = this.end.line = line; - this.start.character = this.end.character = character; - - // Calculating new position on the screen - var metrics = this.editor.getFontMetrics(), - offsetX = character * metrics.getWidth(), - offsetY = line * metrics.getHeight(); - this.el.style.left = offsetX + 'px'; - this.el.style.top = offsetY + 'px'; - - // This helps to see moving cursor when it is always in blink on - // state on a new position. Try to move cursror in any editor and you - // will see this in action. - if(this.isVisible()) { - this.el.style.opacity = 1; - clearInterval(this.interval); - this.interval = setInterval(this.blink.bind(this), this.blinkInterval); - } -}; - -/** - * Moves cursor up specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveUp = function(length) { - arguments.length || (length = 1); - var line = this.end.line - length; - this.setPosition(line); -}; - -/** - * Moves cursor down specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveDown = function(length) { - arguments.length || (length = 1); - this.setPosition(this.end.line + length); -}; - -/** - * Moves cursor up specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveLeft = function(length) { - arguments.length || (length = 1); - this.setPosition(undefined, this.end.character - length); -}; - -/** - * Moves cursor down specified amount of lines. - * @param {number} length - */ -Selection.prototype.moveRight = function(length) { - arguments.length || (length = 1); - this.setPosition(undefined, this.end.character + length); -}; - -/** - * Shows or hides cursor. - * @param {void} visible Whether cursor should be visible - */ -Selection.prototype.setVisible = function(visible) { - clearInterval(this.interval); - if(visible) { - this.el.style.display = 'block'; - this.el.style.opacity = 1; - this.interval = setInterval(this.blink.bind(this), this.blinkInterval); - } else { - this.el.style.display = 'none'; - } - this.visible = visible; -}; - -/** - * Returns visibility of the cursor. - * @return {Boolean} - */ -Selection.prototype.isVisible = function() { - return this.visible; -}; - -module.exports = Selection; }, "Document": function(exports, require, module) { /** * Creates new document from provided text. @@ -435,6 +351,109 @@ Document.prototype.getLength = function() { Document.prototype.charAt = function(column, row) { var row = this.storage[row]; if (row) return row.charAt(column); +}; + +/** + * Inserts text into arbitrary position in the document + * @param {string} text + * @param {number} column + * @param {number} row + * @return {Array} new position in the document + */ +Document.prototype.insertText = function(text, column, row) { + // First we need to split inserting text into array lines + text = Document.prepareText(text); + + // First we calculate new column position because + // text array will be changed in the process + var newColumn = text[text.length - 1].length; + if (text.length === 1) newColumn += column; + + // append remainder of the current line to last line in new text + text[text.length - 1] += this.storage[row].substr(column); + + // append first line of the new text to current line up to "column" position + this.storage[row] = this.storage[row].substr(0, column) + text[0]; + + // now we are ready to splice other new lines + // (not first and not last) into our storage + var args = [row + 1, 0].concat(text.slice(1)); + this.storage.splice.apply(this.storage, args); + + // Finally we calculate new position + column = newColumn; + row += text.length - 1; + + return [column, row]; +}; + +/** + * Deletes text with specified range from the document. + * @param {number} startColumn + * @param {number} startRow + * @param {number} endColumn + * @param {number} endRow + */ +Document.prototype.deleteRange = function(startColumn, startRow, endColumn, endRow) { + + // Check bounds + startRow >= 0 || (startRow = 0); + startColumn >= 0 || (startColumn = 0); + endRow < this.storage.length || (endRow = this.storage.length - 1); + endColumn <= this.storage[endRow].length || (endColumn = this.storage[endRow].length); + + // Little optimization that does nothing if there's nothing to delete + if(startColumn === endColumn && startRow === endRow) { + return [startColumn, startRow]; + } + + // Now we append start of start row to the remainder of endRow + this.storage[startRow] = this.storage[startRow].substr(0, startColumn) + + this.storage[endRow].substr(endColumn); + + // And remove everything inbetween + this.storage.splice(startRow + 1, endRow - startRow); + + // Return new position + return [startColumn, startRow]; +}; + +/** + * Deletes one char forward or backward + * @param {boolean} forward + * @param {number} column + * @param {number} row + * @return {Array} new position + */ +Document.prototype.deleteChar = function(forward, startColumn, startRow) { + var endRow = startRow, + endColumn = startColumn; + + if (forward) { + // If there are characters after cursor on this line we simple remove one + if (startColumn < this.storage[startRow].trim('\n').length) { + ++endColumn; + } + // if there are rows after this one we append it + else if (startRow < this.storage.length - 1) { + ++endRow; + endColumn = 0; + } + } + // Deleting backwards + else { + // If there are characters before the cursor on this line we simple remove one + if (startColumn > 0) { + --startColumn; + } + // if there are rwos before we append current to previous one + else if (startRow > 0) { + --startRow; + startColumn = this.storage[startRow].length - 1; + } + } + + return this.deleteRange(startColumn, startRow, endColumn, endRow); };}, "FontMetrics": function(exports, require, module) {"use strict"; /** @@ -516,4 +535,155 @@ FontMetrics.prototype.getWidth = function() { FontMetrics.prototype.getBaseline = function() { return this._baseline; }; +}, "Selection": function(exports, require, module) {/** + * Creates new selection for the editor. + * @param {Editor} editor. + * @constructor + */ +Selection = function(editor) { + this.editor = editor; + this.blinkInterval = 500; + + this.start = { + line: 0, + character: 0 + }; + + this.end = { + line: 0, + character: 0 + }; + + this.el = document.createElement('div'); + this.el.style.position = 'absolute'; + this.el.style.width = '1px'; + this.el.style.height = this.editor.getFontMetrics().getHeight() + 'px'; + this.el.style.backgroundColor = '#000'; + + this.editor.getEl().appendChild(this.el); + this.setPosition(0, 0); +}; + +/** + * Responsible for blinking + * @return {void} + */ +Selection.prototype.blink = function() { + if (parseInt(this.el.style.opacity, 10)) { + this.el.style.opacity = 0; + } else { + this.el.style.opacity = 1; + } +}; + +/** + * Moves both start and end to a specified position inside document. + * @param {number?} line + * @param {number?} character + */ +Selection.prototype.setPosition = function(character, line) { + // Providing defaults for both line and character parts of position + if (typeof line === 'undefined') line = this.end.line + if (typeof character === 'undefined') character = this.end.character + + // Checking lower bounds + line >= 0 || (line = 0); + character >= 0 || (character = 0); + + // Checking upper bounds + var lineCount = this.editor.getDocument().getLineCount(); + line < lineCount || (line = lineCount - 1); + var characterCount = this.editor.getDocument().getLine(line).trim('\n').length; + character <= characterCount || (character = characterCount); + + // Saving new value + this.start.line = this.end.line = line; + this.start.character = this.end.character = character; + + // Calculating new position on the screen + var metrics = this.editor.getFontMetrics(), + offsetX = character * metrics.getWidth(), + offsetY = line * metrics.getHeight(); + this.el.style.left = offsetX + 'px'; + this.el.style.top = offsetY + 'px'; + + // This helps to see moving cursor when it is always in blink on + // state on a new position. Try to move cursror in any editor and you + // will see this in action. + if(this.isVisible()) { + this.el.style.opacity = 1; + clearInterval(this.interval); + this.interval = setInterval(this.blink.bind(this), this.blinkInterval); + } +}; + +/** + * Returns current position of the end of the selection + * @return {Array} + */ +Selection.prototype.getPosition = function() { + return [this.end.character, this.end.line]; +} + +/** + * Moves up specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveUp = function(length) { + arguments.length || (length = 1); + this.setPosition(this.end.character, this.end.line - length); +}; + +/** + * Moves down specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveDown = function(length) { + arguments.length || (length = 1); + this.setPosition(this.end.character, this.end.line + length); +}; + +/** + * Moves up specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveLeft = function(length) { + arguments.length || (length = 1); + this.setPosition(this.end.character - length, this.end.line); +}; + +/** + * Moves down specified amount of lines. + * @param {number} length + */ +Selection.prototype.moveRight = function(length) { + arguments.length || (length = 1); + this.setPosition(this.end.character + length, this.end.line); +}; + +/** + * Shows or hides cursor. + * @param {void} visible Whether cursor should be visible + */ +Selection.prototype.setVisible = function(visible) { + clearInterval(this.interval); + if(visible) { + this.el.style.display = 'block'; + this.el.style.opacity = 1; + this.interval = setInterval(this.blink.bind(this), this.blinkInterval); + } else { + this.el.style.display = 'none'; + } + this.visible = visible; +}; + +/** + * Returns visibility of the cursor. + * @return {Boolean} + */ +Selection.prototype.isVisible = function() { + return this.visible; +}; + +module.exports = Selection; }}); From 18fca6d0f18f93bdd2178ff6e87ab76ba70f755f Mon Sep 17 00:00:00 2001 From: Dmitriy Kubyshkin Date: Sat, 7 Apr 2012 11:13:26 +0700 Subject: [PATCH 3/6] Updated demo to use code from part 7. --- canvas-text-editor.js | 377 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 327 insertions(+), 50 deletions(-) diff --git a/canvas-text-editor.js b/canvas-text-editor.js index 11d097c..31e8871 100644 --- a/canvas-text-editor.js +++ b/canvas-text-editor.js @@ -62,9 +62,14 @@ var CanvasTextEditor = function(doc) { this._document = doc || (new Document); this._metrics = new FontMetrics('"Courier New", Courier, monospace', 14); this._createWrapper(); + this._selection = new Selection(this); + this._selection.onchange = this.selectionChange.bind(this); this._createCanvas(); this._createInput(); - this._selection = new Selection(this); + document.addEventListener('keydown', this.addKeyModifier.bind(this), true); + document.addEventListener('keyup', this.removeKeyModfier.bind(this), true); + window.addEventListener('focus', this.clearKeyModifiers.bind(this), true); + window.addEventListener('focus', this.render.bind(this), true); }; module.exports = CanvasTextEditor; @@ -75,6 +80,74 @@ module.exports = CanvasTextEditor; */ CanvasTextEditor.prototype.className = 'canvas-text-editor'; +/** + * Determines if user holds shift key at the moment + * @type {Boolean} + */ +CanvasTextEditor.prototype.shiftPressed = false; + +/** + * Marks important for us key modfiers as pressed + * @param {Event} e + */ +CanvasTextEditor.prototype.addKeyModifier = function(e) { + if (e.keyCode === 16) { + this.shiftPressed = true; + } +}; + +/** + * Unmarks important for us key modfiers as pressed + * @param {Event} e + */ +CanvasTextEditor.prototype.removeKeyModfier = function(e) { + if (e.keyCode === 16) { + this.shiftPressed = false; + } +}; + +/** + * Clears all key modifiers + */ +CanvasTextEditor.prototype.clearKeyModifiers = function() { + this.shiftPressed = false; +}; + +/** + * Returns selection for this editor + * @return {Selection} + */ +CanvasTextEditor.prototype.getSelection = function() { + return this._selection; +}; + +/** + * Handles selection change + */ +CanvasTextEditor.prototype.selectionChange = function() { + // Assume that selection is empty + var selectedText = ''; + + // if it's not we put together selected text from document + if (!this._selection.isEmpty()) { + var ranges = this._selection.lineRanges(), + line; + for(var key in ranges) { + selectedText += this._document.getLine(parseInt(key)).slice( + ranges[key][0], ranges[key][1] === true ? undefined : ranges[key][1] + ); + } + } + + // Put selected text into our proxy + this.inputEl.value = selectedText; + this.inputEl.selectionStart = 0; + this.inputEl.selectionEnd = selectedText.length; + + // Updating canvas to show selection + this.render(); +}; + /** * Creates wrapper element for all parts of the editor * @private @@ -113,18 +186,48 @@ CanvasTextEditor.prototype.render = function() { lineHeight = this._metrics.getHeight(), characterWidth = this._metrics.getWidth(), maxHeight = Math.ceil(640 / lineHeight), - lineCount = this._document.getLineCount(); + lineCount = this._document.getLineCount(), + selectionRanges = this._selection.lineRanges(), + selectionWidth = 0; // Making sure we don't render somethign that we won't see if (lineCount < maxHeight) maxHeight = lineCount; // Clearing previous iteration - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.context.fillStyle = '#eee'; + this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); + this.context.fillStyle = '#000'; // Looping over document lines for(var i = 0; i < maxHeight; ++i) { + var topOffset = lineHeight * i; + + // Rendering selection for this line if one is present + if (selectionRanges[i]) { + this.context.fillStyle = '#cce6ff'; + + // Check whether we should select to the end of the line or not + if(selectionRanges[i][1] === true) { + selectionWidth = this.canvas.width; + } else { + selectionWidth = (selectionRanges[i][1] - selectionRanges[i][0]) * characterWidth; + } + + // Drawing selection + this.context.fillRect( + selectionRanges[i][0] * characterWidth, + i * lineHeight, + selectionWidth, + lineHeight + ) + + // Restoring fill color for the text + this.context.fillStyle = '#000'; + } + + // Drawing text this.context.fillText( - this._document.getLine(i), 0, lineHeight * i + baselineOffset + this._document.getLine(i), 0, topOffset + baselineOffset ); } }; @@ -136,9 +239,10 @@ CanvasTextEditor.prototype.render = function() { CanvasTextEditor.prototype._createInput = function() { this.inputEl = document.createElement('textarea'); this.inputEl.style.position = 'absolute'; - this.inputEl.style.top = '-10px'; - this.inputEl.style.height = 0; - this.inputEl.style.width = 0; + this.inputEl.style.top = '-25px'; + this.inputEl.style.left = '-25px'; + this.inputEl.style.height = '10px'; + this.inputEl.style.width = '10px'; this.inputEl.addEventListener('input', this.handleInput.bind(this), false); this.inputEl.addEventListener('blur', this.blur.bind(this), false); this.inputEl.addEventListener('focus', this._inputFocus.bind(this), false); @@ -161,7 +265,14 @@ CanvasTextEditor.prototype.handleInput = function(e) { * @param {string} text */ CanvasTextEditor.prototype.insertTextAtCurrentPosition = function(text) { + // If selection is not empty we need to "replace" selected text with inserted + // one which means deleting old selected text before inserting new one + if (!this._selection.isEmpty()) { + this.deleteCharAtCurrentPosition(); + } + var pos = this._selection.getPosition(); + // Inserting new text and changing position of cursor to a new one this._selection.setPosition.apply( this._selection, @@ -175,12 +286,23 @@ CanvasTextEditor.prototype.insertTextAtCurrentPosition = function(text) { * @param {string} text */ CanvasTextEditor.prototype.deleteCharAtCurrentPosition = function(forward) { - var pos = this._selection.getPosition(); - // Deleting text and changing position of cursor to a new one - this._selection.setPosition.apply( - this._selection, - this._document.deleteChar(forward, pos[0], pos[1]) - ); + // If there is a selection we just remove it no matter what direction is + if (!this._selection.isEmpty()) { + this._selection.setPosition.apply( + this._selection, + this._document.deleteRange( + this._selection.start.character, this._selection.start.line, + this._selection.end.character, this._selection.end.line + ) + ); + } else { + var pos = this._selection.getPosition(); + // Deleting text and changing position of cursor to a new one + this._selection.setPosition.apply( + this._selection, + this._document.deleteChar(forward, pos[0], pos[1]) + ); + } this.render(); }; @@ -230,7 +352,8 @@ CanvasTextEditor.prototype.resize = function(width, height) { }; /** - * Main keydown handler. + * Main keydown handler + * @param {Event} e */ CanvasTextEditor.prototype.keydown = function(e) { var handled = true; @@ -245,16 +368,16 @@ CanvasTextEditor.prototype.keydown = function(e) { this.insertTextAtCurrentPosition('\n'); break; case 37: // Left arrow - this._selection.moveLeft(); + this._selection.moveLeft(1, this.shiftPressed); break; case 38: // Up arrow - this._selection.moveUp(); + this._selection.moveUp(1, this.shiftPressed); break; case 39: // Up arrow - this._selection.moveRight(); + this._selection.moveRight(1, this.shiftPressed); break; case 40: // Down arrow - this._selection.moveDown(); + this._selection.moveDown(1, this.shiftPressed); break; default: handled = false; @@ -268,7 +391,6 @@ CanvasTextEditor.prototype.keydown = function(e) { * Blur handler. */ CanvasTextEditor.prototype.blur = function() { - this.inputEl.blur(); this.wrapper.style.outline = 'none'; this._selection.setVisible(false); }; @@ -400,7 +522,7 @@ Document.prototype.deleteRange = function(startColumn, startRow, endColumn, endR startRow >= 0 || (startRow = 0); startColumn >= 0 || (startColumn = 0); endRow < this.storage.length || (endRow = this.storage.length - 1); - endColumn <= this.storage[endRow].length || (endColumn = this.storage[endRow].length); + endColumn <= this.storage[endRow].trim('\n').length || (endColumn = this.storage[endRow].length); // Little optimization that does nothing if there's nothing to delete if(startColumn === endColumn && startRow === endRow) { @@ -430,14 +552,18 @@ Document.prototype.deleteChar = function(forward, startColumn, startRow) { endColumn = startColumn; if (forward) { + var characterCount = this.storage[startRow].trim('\n').length; // If there are characters after cursor on this line we simple remove one - if (startColumn < this.storage[startRow].trim('\n').length) { + if (startColumn < characterCount) { ++endColumn; } // if there are rows after this one we append it - else if (startRow < this.storage.length - 1) { - ++endRow; - endColumn = 0; + else { + startColumn = characterCount; + if (startRow < this.storage.length - 1) { + ++endRow; + endColumn = 0; + } } } // Deleting backwards @@ -475,8 +601,9 @@ var FontMetrics = function(family, size) { body.appendChild(line); // Now we can measure width and height of the letter - line.innerHTML = 'm'; // It doesn't matter what text goes here - this._width = line.offsetWidth; + var text = 'mmmmmmmmmm'; // 10 symbols to be more accurate with width + line.innerHTML = text; + this._width = line.offsetWidth / text.length; this._height = line.offsetHeight; // Now creating 1px sized item that will be aligned to baseline @@ -542,7 +669,6 @@ FontMetrics.prototype.getBaseline = function() { */ Selection = function(editor) { this.editor = editor; - this.blinkInterval = 500; this.start = { line: 0, @@ -564,6 +690,24 @@ Selection = function(editor) { this.setPosition(0, 0); }; +/** + * Hold blink interval for the cursor + * @type {Number} + */ +Selection.prototype.blinkInterval = 500; + +/** + * This callback called when selection size has changed + * @type {Function} + */ +Selection.prototype.onchange = null; + +/** + * If true that means that we currently manipulate right side of the selection + * @type {Boolean} + */ +Selection.prototype.activeEndSide = true; + /** * Responsible for blinking * @return {void} @@ -576,34 +720,112 @@ Selection.prototype.blink = function() { } }; +/** + * Returns selection split into line ranges + * @return {Array} + */ +Selection.prototype.lineRanges = function() { + if (this.isEmpty()) return {}; + var ranges = {}, + character = this.start.character, + line = this.start.line; + for (; line <= this.end.line ; line++) { + ranges[line] = ([character, line !== this.end.line || this.end.character]); + character = 0; + }; + return ranges; +}; + +/** + * Comparator for two cursor positions + * @return {number} + */ +Selection.prototype.comparePosition = function(one, two) { + if (one.line < two.line) { + return -1; + } else if (one.line > two.line) { + return 1; + } else { + if (one.character < two.character) { + return -1; + } else if (one.character > two.character) { + return 1; + } else { + return 0; + } + } +}; + +/** + * Determines if selection is emtpy (zero-length) + * @return {boolean} + */ +Selection.prototype.isEmpty = function() { + return this.comparePosition(this.start, this.end) === 0; +}; + /** * Moves both start and end to a specified position inside document. - * @param {number?} line - * @param {number?} character + * @param {number} line + * @param {number} character + */ +Selection.prototype.setPosition = function(character, line, keepSelection) { + + var position = this._forceBounds(character, line); + + // Calling private setter that does the heavy lifting + this._doSetPosition(position[0], position[1], keepSelection); + this._updateCursorStyle(); + + // Making a callback if necessary + if (typeof this.onchange === 'function') { + this.onchange(this, this.start, this.end); + } +}; + +/** + * Checks and forces bounds for proposed position updates + * @return {Array} */ -Selection.prototype.setPosition = function(character, line) { - // Providing defaults for both line and character parts of position - if (typeof line === 'undefined') line = this.end.line - if (typeof character === 'undefined') character = this.end.character +Selection.prototype._forceBounds = function(character, line) { + var position = this.getPosition(); // Checking lower bounds line >= 0 || (line = 0); - character >= 0 || (character = 0); + if (character < 0) { + // Wraparound for lines + if (line === position[1] && line > 0) { + --line; + character = this.editor.getDocument().getLine(line).trim('\n').length; + } else { + character = 0; + } + } // Checking upper bounds var lineCount = this.editor.getDocument().getLineCount(); line < lineCount || (line = lineCount - 1); var characterCount = this.editor.getDocument().getLine(line).trim('\n').length; - character <= characterCount || (character = characterCount); - - // Saving new value - this.start.line = this.end.line = line; - this.start.character = this.end.character = character; + if (character > characterCount) { + if (line === position[1] && line < this.editor.getDocument().getLineCount() - 1) { + ++line; + character = 0; + } else { + character = characterCount; + } + } + return [character, line]; +}; +/** + * Updates cursor styles so it matches current position + */ +Selection.prototype._updateCursorStyle = function() { // Calculating new position on the screen var metrics = this.editor.getFontMetrics(), - offsetX = character * metrics.getWidth(), - offsetY = line * metrics.getHeight(); + position = this.getPosition(), + offsetX = position[0] * metrics.getWidth(), + offsetY = position[1] * metrics.getHeight(); this.el.style.left = offsetX + 'px'; this.el.style.top = offsetY + 'px'; @@ -617,48 +839,103 @@ Selection.prototype.setPosition = function(character, line) { } }; +/** + * Private unconditional setter for cursor position + * @param {number} character + * @param {number} line + * @param {boolean} keepSelection + */ +Selection.prototype._doSetPosition = function(character, line, keepSelection) { + // Saving new value + if (keepSelection) { + + compare = this.comparePosition({ + line: line, + character: character + }, this.start); + + // If selection is empty and we are moving left we set active side to start + if (compare === -1 && (this.isEmpty() || line < this.start.line)) { + this.activeEndSide = false; + } + + if (this.activeEndSide) { + this.end.line = line; + this.end.character = character; + } else { + this.start.line = line; + this.start.character = character; + } + + // Making sure that end is further than start + if (this.comparePosition(this.start, this.end) > 0) { + this.activeEndSide = !this.activeEndSide; + var temp = { + line: this.start.line, + character: this.start.character + } + this.start.line = this.end.line; + this.start.character = this.end.character; + this.end.line = temp.line; + this.end.character = temp.character; + } + } else { + this.activeEndSide = true; + this.start.line = this.end.line = line; + this.start.character = this.end.character = character; + } +}; + /** * Returns current position of the end of the selection * @return {Array} */ Selection.prototype.getPosition = function() { - return [this.end.character, this.end.line]; + if (this.activeEndSide) { + return [this.end.character, this.end.line]; + } else { + return [this.start.character, this.start.line]; + } } /** * Moves up specified amount of lines. * @param {number} length */ -Selection.prototype.moveUp = function(length) { +Selection.prototype.moveUp = function(length, keepSelection) { arguments.length || (length = 1); - this.setPosition(this.end.character, this.end.line - length); + var position = this.getPosition(); + this.setPosition(position[0], position[1] - length, keepSelection); }; /** * Moves down specified amount of lines. * @param {number} length */ -Selection.prototype.moveDown = function(length) { +Selection.prototype.moveDown = function(length, keepSelection) { arguments.length || (length = 1); - this.setPosition(this.end.character, this.end.line + length); + var position = this.getPosition(); + this.setPosition(position[0], position[1] + length, keepSelection); }; /** * Moves up specified amount of lines. * @param {number} length */ -Selection.prototype.moveLeft = function(length) { +Selection.prototype.moveLeft = function(length, keepSelection) { arguments.length || (length = 1); - this.setPosition(this.end.character - length, this.end.line); + var position = this.getPosition(); + this.setPosition(position[0] - length, position[1], keepSelection); }; /** * Moves down specified amount of lines. * @param {number} length */ -Selection.prototype.moveRight = function(length) { +Selection.prototype.moveRight = function(length, keepSelection) { arguments.length || (length = 1); - this.setPosition(this.end.character + length, this.end.line); + var position = this.getPosition(); + this.setPosition(position[0] + length, position[1], keepSelection); }; /** From 2d09559be683853a3998fa9316e1ba3edc5e4e8d Mon Sep 17 00:00:00 2001 From: Dmitriy Kubyshkin Date: Sat, 7 Apr 2012 14:24:55 +0700 Subject: [PATCH 4/6] Fixed some browser issues. --- canvas-text-editor.js | 44 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/canvas-text-editor.js b/canvas-text-editor.js index 31e8871..0b3febf 100644 --- a/canvas-text-editor.js +++ b/canvas-text-editor.js @@ -74,6 +74,12 @@ var CanvasTextEditor = function(doc) { module.exports = CanvasTextEditor; +/** + * Determines if current browser is Opera + * @type {Boolean} + */ +CanvasTextEditor.prototype.isOpera = ('opera' in window) && ('version' in window.opera); + /** * CSS class that is assigned to the wrapper. * @type {String} @@ -131,7 +137,7 @@ CanvasTextEditor.prototype.selectionChange = function() { // if it's not we put together selected text from document if (!this._selection.isEmpty()) { var ranges = this._selection.lineRanges(), - line; + line = ''; for(var key in ranges) { selectedText += this._document.getLine(parseInt(key)).slice( ranges[key][0], ranges[key][1] === true ? undefined : ranges[key][1] @@ -139,10 +145,7 @@ CanvasTextEditor.prototype.selectionChange = function() { } } - // Put selected text into our proxy - this.inputEl.value = selectedText; - this.inputEl.selectionStart = 0; - this.inputEl.selectionEnd = selectedText.length; + this.setInputText(selectedText, true); // Updating canvas to show selection this.render(); @@ -247,8 +250,10 @@ CanvasTextEditor.prototype._createInput = function() { this.inputEl.addEventListener('blur', this.blur.bind(this), false); this.inputEl.addEventListener('focus', this._inputFocus.bind(this), false); this.inputEl.addEventListener('keydown', this.keydown.bind(this), false); + this.inputEl.addEventListener('keypress', this.setInputText.bind(this, ''), false); this.inputEl.tabIndex = -1; // we don't want input to get focus by tabbing this.wrapper.appendChild(this.inputEl); + this.setInputText('', true); }; /** @@ -256,8 +261,33 @@ CanvasTextEditor.prototype._createInput = function() { * @param {Event} e */ CanvasTextEditor.prototype.handleInput = function(e) { - this.insertTextAtCurrentPosition(e.target.value); - e.target.value = ''; + var value = e.target.value; + if (this.isOpera) { + // Opera doesn't need a placeholder + value = value.substring(0, value.length); + } else { + // Compensate for placeholder + value = value.substring(0, value.length - 1); + } + this.insertTextAtCurrentPosition(value); + this.needsClearing = true; +}; + +/** + * Makes input contain only placeholder character and places cursor at start + */ +CanvasTextEditor.prototype.setInputText = function(text, force) { + if(this.needsClearing || force === true) { + if (this.isOpera) { + this.inputEl.value = text; + this.inputEl.select(); + } else { + this.inputEl.value = text + '#'; + this.inputEl.selectionStart = 0; + this.inputEl.selectionEnd = text.length; + } + } + this.needsClearing = false; }; /** From c8a6ab7fed8df33b9e45567abdaa77eb9dab216d Mon Sep 17 00:00:00 2001 From: Dmitriy Kubyshkin Date: Mon, 9 Apr 2012 10:03:03 +0700 Subject: [PATCH 5/6] Updated code to the latest version (part-8). --- canvas-text-editor.js | 145 +++++++++++++++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 31 deletions(-) diff --git a/canvas-text-editor.js b/canvas-text-editor.js index 0b3febf..4a4b922 100644 --- a/canvas-text-editor.js +++ b/canvas-text-editor.js @@ -1,4 +1,14 @@ - +/**! + * Canvas Text Editor + * + * Copyright (c) 2012 Dmitriy Kubyshkin (http://kubyshkin.ru) + * + * Version: 0.1.0 (04/09/2012) + * Requires: Browser with canvas support + * + * Dual licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ (function(/*! Stitch !*/) { if (!this.require) { var modules = {}, cache = {}, require = function(name, root) { @@ -58,11 +68,30 @@ var FontMetrics = require('FontMetrics'), * Simple plain-text text editor using html5 canvas. * @constructor */ -var CanvasTextEditor = function(doc) { +var CanvasTextEditor = function(doc, options) { this._document = doc || (new Document); - this._metrics = new FontMetrics('"Courier New", Courier, monospace', 14); + + this.options = { + textColor: 'WindowText', + backgroundColor: 'Window', + selectionColor: 'Highlight', + focusColor: '#09f', + fontFamily: '"Courier New", Courier, monospace', + fontSize: 14, + padding: 5, + width: 640, + height: 480 + }; + + if (typeof options === 'object') { + for(key in options) { + this.options[key] = options[key]; + } + } + + this._metrics = new FontMetrics(this.options.fontFamily, this.options.fontSize); this._createWrapper(); - this._selection = new Selection(this); + this._selection = new Selection(this, this.options.textColor); this._selection.onchange = this.selectionChange.bind(this); this._createCanvas(); this._createInput(); @@ -74,6 +103,18 @@ var CanvasTextEditor = function(doc) { module.exports = CanvasTextEditor; +/** + * Top offset in lines + * @type {Number} + */ +CanvasTextEditor.prototype._scrollTop = 0; + +/** + * Left offset in characters + * @type {Number} + */ +CanvasTextEditor.prototype._scrollLeft = 0; + /** * Determines if current browser is Opera * @type {Boolean} @@ -127,6 +168,22 @@ CanvasTextEditor.prototype.getSelection = function() { return this._selection; }; +/** + * Returns current top offset + * @return {number} + */ +CanvasTextEditor.prototype.scrollTop = function() { + return this._scrollTop; +}; + +/** + * Returns current left offset + * @return {number} + */ +CanvasTextEditor.prototype.scrollLeft = function() { + return this._scrollLeft; +}; + /** * Handles selection change */ @@ -145,6 +202,7 @@ CanvasTextEditor.prototype.selectionChange = function() { } } + this._checkScroll(); this.setInputText(selectedText, true); // Updating canvas to show selection @@ -160,8 +218,8 @@ CanvasTextEditor.prototype._createWrapper = function() { this.wrapper.className = this.className; this.wrapper.style.display = 'inline-block'; this.wrapper.style.position = 'relative'; - this.wrapper.style.backgroundColor = '#eee'; - this.wrapper.style.border = '5px solid #eee'; + this.wrapper.style.backgroundColor = this.options.backgroundColor; + this.wrapper.style.border = this.options.padding + 'px solid ' + this.options.backgroundColor; this.wrapper.style.overflow = 'hidden'; this.wrapper.tabIndex = 0; // tabindex is necessary to get focus this.wrapper.addEventListener('focus', this.focus.bind(this), false); @@ -175,11 +233,32 @@ CanvasTextEditor.prototype._createCanvas = function() { this.canvas = document.createElement('canvas'); this.canvas.style.display = 'block'; this.context = this.canvas.getContext('2d'); - this.resize(640, 480); + this.resize(this.options.width, this.options.height); this.render(); this.wrapper.appendChild(this.canvas); }; +/** + * Makes sure that cursor is visible + * @return {[type]} [description] + */ +CanvasTextEditor.prototype._checkScroll = function() { + var maxHeight = Math.ceil(this.canvas.height / this._metrics.getHeight()) - 1, + maxWidth = Math.ceil(this.canvas.width / this._metrics.getWidth()) - 1, + cursorPosition = this._selection.getPosition(); + if (cursorPosition[0] > this._scrollLeft + maxWidth ) { + this._scrollLeft = cursorPosition[0] - maxWidth; + } else if (cursorPosition[0] < this._scrollLeft) { + this._scrollLeft = cursorPosition[0]; + } + if (cursorPosition[1] > this._scrollTop + maxHeight) { + this._scrollTop = cursorPosition[1] - maxHeight; + } else if (cursorPosition[1] < this._scrollTop) { + this._scrollTop = cursorPosition[1]; + } + this._selection.updateCursorStyle(); +}; + /** * Renders document onto the canvas * @return {[type]} [description] @@ -188,7 +267,7 @@ CanvasTextEditor.prototype.render = function() { var baselineOffset = this._metrics.getBaseline(), lineHeight = this._metrics.getHeight(), characterWidth = this._metrics.getWidth(), - maxHeight = Math.ceil(640 / lineHeight), + maxHeight = Math.ceil(this.canvas.height / lineHeight), lineCount = this._document.getLineCount(), selectionRanges = this._selection.lineRanges(), selectionWidth = 0; @@ -197,17 +276,17 @@ CanvasTextEditor.prototype.render = function() { if (lineCount < maxHeight) maxHeight = lineCount; // Clearing previous iteration - this.context.fillStyle = '#eee'; + this.context.fillStyle = this.options.backgroundColor; this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); - this.context.fillStyle = '#000'; + this.context.fillStyle = this.options.textColor; // Looping over document lines - for(var i = 0; i < maxHeight; ++i) { - var topOffset = lineHeight * i; + for(var i = this._scrollTop; i < maxHeight + this._scrollTop; ++i) { + var topOffset = lineHeight * (i - this._scrollTop); // Rendering selection for this line if one is present if (selectionRanges[i]) { - this.context.fillStyle = '#cce6ff'; + this.context.fillStyle = this.options.selectionColor; // Check whether we should select to the end of the line or not if(selectionRanges[i][1] === true) { @@ -218,19 +297,19 @@ CanvasTextEditor.prototype.render = function() { // Drawing selection this.context.fillRect( - selectionRanges[i][0] * characterWidth, - i * lineHeight, + (selectionRanges[i][0] - this._scrollLeft) * characterWidth, + topOffset, selectionWidth, lineHeight ) // Restoring fill color for the text - this.context.fillStyle = '#000'; + this.context.fillStyle = this.options.textColor; } // Drawing text this.context.fillText( - this._document.getLine(i), 0, topOffset + baselineOffset + this._document.getLine(i).slice(this._scrollLeft), 0, topOffset + baselineOffset ); } }; @@ -341,7 +420,7 @@ CanvasTextEditor.prototype.deleteCharAtCurrentPosition = function(forward) { * @private */ CanvasTextEditor.prototype._inputFocus = function() { - this.wrapper.style.outline = '1px solid #09f'; + this.wrapper.style.outline = '1px solid ' + this.options.focusColor; this._selection.setVisible(true); }; @@ -388,10 +467,10 @@ CanvasTextEditor.prototype.resize = function(width, height) { CanvasTextEditor.prototype.keydown = function(e) { var handled = true; switch(e.keyCode) { - case 8: // backspace + case 8: // Backspace this.deleteCharAtCurrentPosition(false); break; - case 46: // delete + case 46: // Delete this.deleteCharAtCurrentPosition(true); break; case 13: // Enter @@ -403,7 +482,7 @@ CanvasTextEditor.prototype.keydown = function(e) { case 38: // Up arrow this._selection.moveUp(1, this.shiftPressed); break; - case 39: // Up arrow + case 39: // Right arrow this._selection.moveRight(1, this.shiftPressed); break; case 40: // Down arrow @@ -697,8 +776,9 @@ FontMetrics.prototype.getBaseline = function() { * @param {Editor} editor. * @constructor */ -Selection = function(editor) { +Selection = function(editor, color) { this.editor = editor; + color || (color = '#000'); this.start = { line: 0, @@ -714,7 +794,7 @@ Selection = function(editor) { this.el.style.position = 'absolute'; this.el.style.width = '1px'; this.el.style.height = this.editor.getFontMetrics().getHeight() + 'px'; - this.el.style.backgroundColor = '#000'; + this.el.style.backgroundColor = color; this.editor.getEl().appendChild(this.el); this.setPosition(0, 0); @@ -805,7 +885,6 @@ Selection.prototype.setPosition = function(character, line, keepSelection) { // Calling private setter that does the heavy lifting this._doSetPosition(position[0], position[1], keepSelection); - this._updateCursorStyle(); // Making a callback if necessary if (typeof this.onchange === 'function') { @@ -837,6 +916,7 @@ Selection.prototype._forceBounds = function(character, line) { line < lineCount || (line = lineCount - 1); var characterCount = this.editor.getDocument().getLine(line).trim('\n').length; if (character > characterCount) { + // Wraparound for lines if (line === position[1] && line < this.editor.getDocument().getLineCount() - 1) { ++line; character = 0; @@ -850,12 +930,12 @@ Selection.prototype._forceBounds = function(character, line) { /** * Updates cursor styles so it matches current position */ -Selection.prototype._updateCursorStyle = function() { +Selection.prototype.updateCursorStyle = function() { // Calculating new position on the screen var metrics = this.editor.getFontMetrics(), position = this.getPosition(), - offsetX = position[0] * metrics.getWidth(), - offsetY = position[1] * metrics.getHeight(); + offsetX = (position[0] - this.editor.scrollLeft()) * metrics.getWidth(), + offsetY = (position[1] - this.editor.scrollTop()) * metrics.getHeight(); this.el.style.left = offsetX + 'px'; this.el.style.top = offsetY + 'px'; @@ -876,7 +956,7 @@ Selection.prototype._updateCursorStyle = function() { * @param {boolean} keepSelection */ Selection.prototype._doSetPosition = function(character, line, keepSelection) { - // Saving new value + // If this is a selection range if (keepSelection) { compare = this.comparePosition({ @@ -884,11 +964,14 @@ Selection.prototype._doSetPosition = function(character, line, keepSelection) { character: character }, this.start); - // If selection is empty and we are moving left we set active side to start + // Determining whether we should make the start side of the range active + // (have a cursor). This happens when we start the selection be moving + // left, or moving up. if (compare === -1 && (this.isEmpty() || line < this.start.line)) { this.activeEndSide = false; } + // Assign new value to the side that is active if (this.activeEndSide) { this.end.line = line; this.end.character = character; @@ -897,7 +980,7 @@ Selection.prototype._doSetPosition = function(character, line, keepSelection) { this.start.character = character; } - // Making sure that end is further than start + // Making sure that end is further than start and swap if necessary if (this.comparePosition(this.start, this.end) > 0) { this.activeEndSide = !this.activeEndSide; var temp = { @@ -909,7 +992,7 @@ Selection.prototype._doSetPosition = function(character, line, keepSelection) { this.end.line = temp.line; this.end.character = temp.character; } - } else { + } else { // Simple cursor move this.activeEndSide = true; this.start.line = this.end.line = line; this.start.character = this.end.character = character; From b93ef19ff5e2d787463cce1ef3b3a3aa2ad4653b Mon Sep 17 00:00:00 2001 From: Dmitriy Kubyshkin Date: Mon, 9 Apr 2012 10:28:35 +0700 Subject: [PATCH 6/6] Fixed issue with removing lines at the end of the document. --- canvas-text-editor.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/canvas-text-editor.js b/canvas-text-editor.js index 4a4b922..99fb4db 100644 --- a/canvas-text-editor.js +++ b/canvas-text-editor.js @@ -1,14 +1,4 @@ -/**! - * Canvas Text Editor - * - * Copyright (c) 2012 Dmitriy Kubyshkin (http://kubyshkin.ru) - * - * Version: 0.1.0 (04/09/2012) - * Requires: Browser with canvas support - * - * Dual licensed under the MIT license: - * http://www.opensource.org/licenses/mit-license.php - */ + (function(/*! Stitch !*/) { if (!this.require) { var modules = {}, cache = {}, require = function(name, root) { @@ -267,12 +257,12 @@ CanvasTextEditor.prototype.render = function() { var baselineOffset = this._metrics.getBaseline(), lineHeight = this._metrics.getHeight(), characterWidth = this._metrics.getWidth(), - maxHeight = Math.ceil(this.canvas.height / lineHeight), + maxHeight = Math.ceil(this.canvas.height / lineHeight) + this._scrollTop, lineCount = this._document.getLineCount(), selectionRanges = this._selection.lineRanges(), selectionWidth = 0; - // Making sure we don't render somethign that we won't see + // Making sure we don't render something that we won't see if (lineCount < maxHeight) maxHeight = lineCount; // Clearing previous iteration @@ -281,7 +271,7 @@ CanvasTextEditor.prototype.render = function() { this.context.fillStyle = this.options.textColor; // Looping over document lines - for(var i = this._scrollTop; i < maxHeight + this._scrollTop; ++i) { + for(var i = this._scrollTop; i < maxHeight; ++i) { var topOffset = lineHeight * (i - this._scrollTop); // Rendering selection for this line if one is present