├── .editorconfig ├── .gitattributes ├── .gitignore ├── README.md ├── examples └── index.html ├── gulpfile.js ├── index.html ├── package.json ├── source ├── cursor.coffee ├── helpers.coffee ├── keyboard.coffee ├── textarea.coffee └── word.coffee └── spec └── textareaSpec.coffee /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | test/bower_components 7 | 8 | *.sublime-project 9 | *.sublime-workspace 10 | 11 | # Windows image file caches 12 | Thumbs.db 13 | ehthumbs.db 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SVG Input Elements 2 | ================== 3 | 4 | Work in progress with the aim of creating a simple editable textarea in SVG. 5 | 6 | Examples can be found the example folder. 7 | 8 | To set up the development environment you need npm. Then run 9 | 10 | `npm install` 11 | 12 | from the command line. 13 | 14 | To start the tests and build the project, just run this in the command line: 15 | 16 | `gulp` 17 | 18 | How to use 19 | ---------- 20 | 21 | The features documented here are fairly stable and I will try not to change their behaviour (unless it is incorrect). 22 | 23 | Create a textarea: 24 | 25 | var textarea = SVGIE.textarea(svg, {width: 600}); 26 | 27 | Createa a textarea with initial text: 28 | 29 | var textarea = SVGIE.textarea(svg, {width: 600}, "This is a textarea."); 30 | 31 | Get the width of the textarea: 32 | 33 | textarea("width"); 34 | 35 | Change the width of the textarea: 36 | 37 | textarea("width", 600); 38 | 39 | Get the value of the textarea: 40 | 41 | textarea("val"); 42 | 43 | Change the value of the textarea: 44 | 45 | textarea("val", "Some new text for the textarea."); 46 | 47 | Goals and limits for the new version 48 | ------------------------------------ 49 | 50 | * Faster rendering! 51 | * No jQuery dependency 52 | * No dependencies at all 53 | * Fewer features (No CSS subset, no lists and no image boxes. Just a textarea.) 54 | * Modern browsers only (no IE9) 55 | * Tests! 56 | 57 | Old README 58 | ========== 59 | 60 | _This readme is outdated. Work is being done on rewriting this library. Only 61 | the future will tell if the rewrite is successful or not... _ 62 | 63 | ~~_We are soon ready for a 1.0 version. Help coding a better and more general 64 | 2.0 version would be greatly appreciated!_~~ 65 | 66 | The current state can be considered an alpha or beta version of 1.0. It is 67 | feature complete but buggy. 68 | 69 | Better documentation will be included in the 1.0 release. For now, use the 70 | instructions in [Getting Started](#getting-started). A demo (updated 5 June 71 | 2012) can be found at 72 | [josf.se/svg-input-elements/](http://josf.se/svg-input-elements/). 73 | 74 | This project started out as a sub-project to a master thesis project, 75 | [Personas in Real Life](http://personasinreallife.tumblr.com). 76 | 77 | __Go to:__ [Features](#features-), [Requirements](#requirements), 78 | [Getting Started](#getting-started), [Events](#events), [Versions](#versions) 79 | or [Development](#development) 80 | 81 | Features 82 | -------- 83 | * Line wrapping 84 | * Word wrapping of long words 85 | * Copy/cut/paste 86 | * Text selection with mouse and keyboard 87 | * Change cursor position with left/right/up/down/home/end etc 88 | * Handles both paragraphs (_enter_) and manual line breaks (_shift+enter_) 89 | * Undo/redo 90 | * ... 91 | 92 | Requirements 93 | ------------ 94 | This project requires [jQuery](http://docs.jquery.com/Downloading_jQuery) 95 | (we're using 1.7.2) and the 96 | [jQuery SVG plugin](http://keith-wood.name/svg.html) 97 | 98 | Getting Started 99 | --------------- 100 | You can download the latest examples and test yourself, just change the path 101 | "../tools/build.js" to "../jquery.svg.input.js" unless you're on an Apache 102 | server with PHP. 103 | 104 | _Note:_ In Chrome (and Safari?) the script will fail if you 105 | run it from your local file system 106 | [because of a bug](http://code.google.com/p/chromium/issues/detail?id=49001). 107 | 108 | _Note:_ The generated files in the root might not always be 100% up to date. 109 | 110 | [Download jQuery](http://jquery.com/) (we've tested on version 1.7.2) and 111 | [jQuery SVG](http://keith-wood.name/svg.html) (tested on version 1.4.5) and 112 | include these libraries, and SVG Input Elements: 113 | ``` 114 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 26 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var concat = require('gulp-concat'); 3 | var coffee = require('gulp-coffee'); 4 | var karma = require('karma').server; 5 | var uglify = require('gulp-uglify'); 6 | var rename = require('gulp-rename'); 7 | var browserSync = require('browser-sync'); 8 | 9 | var karmaConf = { 10 | browsers: ['PhantomJS'], 11 | //browsers: ['Chrome'], 12 | preprocessors: { 13 | '**/*.coffee': ['coffee'], 14 | }, 15 | frameworks: ['jasmine'], 16 | coffeePreprocessor: { 17 | // options passed to the coffee compiler 18 | options: { 19 | bare: true, 20 | sourceMap: true 21 | }, 22 | // transforming the filenames 23 | transformPath: function(path) { 24 | return path.replace(/\.coffee$/, '.js'); 25 | } 26 | }, 27 | files: [ 28 | 'gh/*.js', 29 | 'spec/*.coffee' 30 | ] 31 | }; 32 | 33 | var sourceGlob = [ 34 | 'source/*.coffee' 35 | ]; 36 | 37 | gulp.task('browser-sync', function() { 38 | browserSync.init(null, { 39 | server: { 40 | baseDir: './' 41 | } 42 | }); 43 | }); 44 | 45 | gulp.task('watch', ['browser-sync'], function(done) { 46 | karma.start(karmaConf, done); 47 | gulp.watch(sourceGlob, ['gh']); 48 | }); 49 | 50 | gulp.task('gh', function() { 51 | return gulp.src(sourceGlob) 52 | .pipe(coffee()) 53 | .pipe(concat('svg-input-elements.js')) 54 | .pipe(gulp.dest('gh/')); 55 | }); 56 | 57 | gulp.task('default', ['watch']); 58 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SVG Input Elements, an SVG textarea 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 |
22 |
23 |

SVG Input Elements

24 |

An SVG textarea implemented in JavaScript. This is a work in progress.

25 | 26 | View the project on GitHub 27 |
28 |
29 | 30 |
31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |

Rough roadmap to v 1.0

43 |
44 |
Basic text input
45 |
Get the basic structures in place and get line wrapping, the cursor and basic text input working.
46 |
Advanced text input
47 |
Select text and copy, cut, delete and replace it. Select text with keyboard.
48 |
Horizontal scrolling
49 |
Vertical scrolling and other necessary features to manage area constraints.
50 |
Touch compatible
51 |
Make sure text input, selection, scrolling etc. works on touch devices.
52 |
53 |
54 |
55 | 56 |
57 | 58 | 61 |
62 | 63 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SVG-Input-Elements", 3 | "version": "0.3.1", 4 | "description": "A JavaScript implementation of an inline SVG textarea", 5 | "main": "gulpfile.js", 6 | "directories": { 7 | "example": "examples", 8 | "lib": "dist" 9 | }, 10 | "scripts": { 11 | "test": "gulp" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/silence150/SVG-Input-Elements.git" 16 | }, 17 | "keywords": [ 18 | "svg", 19 | "js", 20 | "textarea", 21 | "input", 22 | "edit" 23 | ], 24 | "author": { 25 | "name": "Josef Engelfrost", 26 | "email": "josef@josf.se" 27 | }, 28 | "contributors": [ 29 | { 30 | "name": "Tim Brandin", 31 | "email": "info@sypreme.se" 32 | }, 33 | { 34 | "name": "Josef Engelfrost", 35 | "email": "josef@josf.se" 36 | } 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/silence150/SVG-Input-Elements/issues" 41 | }, 42 | "devDependencies": { 43 | "gulp": "^3.6.2", 44 | "gulp-concat": "~2.2.0", 45 | "gulp-cached": "~0.0.3", 46 | "gulp-remember": "~0.2.0", 47 | "gulp-coffee": "~2.0.0", 48 | "karma": "~0.12.16", 49 | "karma-jasmine": "~0.1.5", 50 | "karma-coffee-preprocessor": "~0.2.1", 51 | "karma-phantomjs-launcher": "~0.1.4", 52 | "karma-chrome-launcher": "~0.1.4", 53 | "gulp-uglify": "~0.3.0", 54 | "gulp-rename": "~1.2.0", 55 | "browser-sync": "~1.1.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /source/cursor.coffee: -------------------------------------------------------------------------------- 1 | this.SVGIE ?= {} 2 | 3 | svgNS = 'http://www.w3.org/2000/svg' 4 | 5 | controllerPrototype = 6 | set: (word, pos, cursorPoint) -> 7 | @model.word = word 8 | @model.pos = pos 9 | @model.point = cursorPoint if cursorPoint? 10 | @model.view.setAttributeNS null, "transform", "translate(" + cursorPoint.x + ", " + word("dy") + ")" 11 | @facet 12 | word: -> 13 | @model.word 14 | #@facet 15 | pos: -> 16 | @model.pos 17 | #@facet 18 | # char: (char) -> 19 | # console.log char 20 | # @model.word("insert", char, @model.pos) 21 | 22 | SVGIE.cursor = (textarea, word, pos) -> 23 | controller = Object.create controllerPrototype 24 | controller.facet = (method, args...) -> 25 | if method is "facet" or method is "model" or not controller[method]? 26 | return undefined 27 | controller[method].apply controller, args 28 | controller.model = 29 | word: word 30 | pos: pos 31 | point: null 32 | view: do => 33 | v = document.createElementNS svgNS, "line" 34 | v.setAttributeNS null, "x1", 0 35 | v.setAttributeNS null, "y1", 0 36 | v.setAttributeNS null, "x2", 0 37 | v.setAttributeNS null, "y2", -1 * textarea("lineheight") 38 | v.setAttributeNS null, "stroke-width", 1.5 39 | v.setAttributeNS null, "stroke", "black" 40 | v.setAttributeNS null, "transform", "translate(" + (word("dx") + word("width")) + ", " + word("dy") + ")" 41 | textarea("view").appendChild v 42 | v 43 | controller.facet 44 | -------------------------------------------------------------------------------- /source/helpers.coffee: -------------------------------------------------------------------------------- 1 | #@SVGIE ?= {} 2 | 3 | window.addEventListener "click", (e) -> 4 | paintPoint(e.clientX, e.clientY) 5 | paintPoint = (x, y) -> 6 | div = document.createElement("div") 7 | div.style.position = "absolute" 8 | div.style.width = "2px" 9 | div.style.height = "2px" 10 | div.style.left = x + "px" 11 | div.style.top = y + "px" 12 | div.style.backgroundColor = "red" 13 | document.body.appendChild(div) 14 | console.log "red dot at", x, y 15 | -------------------------------------------------------------------------------- /source/keyboard.coffee: -------------------------------------------------------------------------------- 1 | this.SVGIE ?= {} 2 | 3 | svgNS = 'http://www.w3.org/2000/svg' 4 | 5 | focusedTextarea = null 6 | 7 | controllerPrototype = 8 | focusedTextarea: (textarea) -> 9 | if textarea is undefined 10 | return 11 | 12 | SVGIE.keyboard = (textarea, cursor) -> 13 | controller = Object.create controllerPrototype 14 | 15 | controller.facet = (method, args...) -> 16 | if method is "facet" or method is "model" or not controller[method]? 17 | return undefined 18 | controller[method].apply controller, args 19 | controller.model = 20 | cursor: cursor 21 | 22 | window.addEventListener "keypress", (e) -> 23 | if e.which? 24 | s = String.fromCharCode e.keyCode 25 | else if e.which isnt 0 and e.charCode isnt 0 26 | s = String.fromCharCode e.which 27 | else 28 | s = "" 29 | console.log e 30 | controller.model.cursor "char", s 31 | controller.facet 32 | -------------------------------------------------------------------------------- /source/textarea.coffee: -------------------------------------------------------------------------------- 1 | @SVGIE ?= {} 2 | 3 | svgNS = 'http://www.w3.org/2000/svg' 4 | 5 | focusedTextarea = null 6 | 7 | # Keyboard events 8 | window.addEventListener "keypress", (e) -> 9 | if focusedTextarea? 10 | if e.which? 11 | s = String.fromCharCode e.keyCode 12 | else if e.which isnt 0 and e.charCode isnt 0 13 | s = String.fromCharCode e.which 14 | else 15 | s = "" 16 | 17 | focusedTextarea "insert", s 18 | # word = focusedTextarea "cursor", "word" 19 | # char = focusedTextarea "cursor", "char" 20 | # word "insert", s, char 21 | # char += 1 22 | # focusedTextarea "cursor", word, char 23 | 24 | controllerPrototype = 25 | val: (s) -> 26 | if s? 27 | while @model.view.firstChild 28 | @model.view.removeChild @model.view.firstChild 29 | @model.words = SVGIE.word @facet, null, s 30 | else 31 | s = "" 32 | word = @model.words 33 | if word? then loop 34 | s += word "val" 35 | break if word "isEnd" 36 | word = word "next" 37 | s 38 | width: (w) -> 39 | unless w is undefined # allow setting width to null 40 | @model.width = w 41 | @model.background.setAttributeNS null, "width", w 42 | @model.words("repos") if @model.words? 43 | @model.width 44 | height: (h) -> 45 | unless h is undefined 46 | @model.height = h 47 | @model.background.setAttributeNS null, "height", h 48 | @model.height 49 | focus: -> 50 | focusedTextarea = @facet 51 | @facet is focusedTextarea 52 | focused: -> 53 | @facet is focusedTextarea 54 | lineheight: -> 55 | @model.lineheight 56 | words: -> 57 | @model.words 58 | cursor: -> 59 | @model.cursor 60 | insert: (s) -> 61 | word = @facet("cursor")("word") 62 | pos = @facet("cursor")("pos") 63 | word "insert", s, pos 64 | 65 | # @facet("cursor")("") 66 | view: -> 67 | @model.view 68 | svgPoint: (x, y) -> 69 | p = @model.svg.createSVGPoint() 70 | p.x = x 71 | p.y = y 72 | # visualPoint = @model.svg.createElementNS svgNS, "rect" 73 | # visualPoint.style.position = "absolute" 74 | # visualPoint.style.width = "2" 75 | # visualPoint.style.height = "2" 76 | # visualPoint.style. 77 | p 78 | 79 | SVGIE.textarea = (el, options, s) -> 80 | unless el? and (el.nodeName is "svg" or el.nodeName is "g") 81 | throw "Missing first argument, no or passed" 82 | unless typeof options is 'object' 83 | if options is undefined 84 | options = {} 85 | else 86 | throw "Options object must be of type object" 87 | unless s? 88 | s = "" 89 | 90 | # Set the group element and svg element 91 | if el.nodeName is 'g' 92 | g = el 93 | svg = g.ownerSVGElement 94 | else 95 | svg = el 96 | g = document.createElementNS svgNS, "g" 97 | el.appendChild g 98 | 99 | # Make sure a width is set 100 | unless options.width? 101 | options.width = svg.getBoundingClientRect().width 102 | 103 | background = document.createElementNS svgNS, "rect" 104 | background.setAttributeNS null, "x", 0 105 | background.setAttributeNS null, "y", 0 106 | background.setAttributeNS null, "width", options.width 107 | background.setAttributeNS null, "fill", "white" 108 | # set height later, since we only support "auto-height" at the moment 109 | 110 | g.appendChild background 111 | 112 | controller = Object.create controllerPrototype 113 | controller.facet = (method, args...) -> 114 | if method is "facet" or method is "model" or not controller[method]? 115 | return undefined 116 | controller[method].apply controller, args 117 | controller.model = 118 | height: unless options.height? then null else options.height 119 | width: unless options.width? then null else options.width 120 | view: g 121 | background: background 122 | lineheight: do -> 123 | testWord = document.createElementNS svgNS, "text" 124 | g.appendChild testWord 125 | testTextNode = document.createTextNode "SVGIE" 126 | testWord.appendChild testTextNode 127 | rect = testWord.getBoundingClientRect() 128 | g.removeChild testWord 129 | rect.height 130 | facet: controller.facet 131 | svg: svg 132 | 133 | # Watch out with dependencies! controller.facet needs controller.model to be defined 134 | controller.model.words = SVGIE.word controller.facet, null, s 135 | controller.model.cursor = SVGIE.cursor controller.facet, controller.model.words("prev"), -1 136 | # controller.model.keyboard = SVGIE.keyboard controller.facet, controller.model.cursor 137 | 138 | # Set this textarea to focused when the background is clicked 139 | background.addEventListener "click", (e) -> 140 | focusedTextarea = controller.facet 141 | word = controller.facet "words" 142 | loop 143 | break if word "isEnd" 144 | break if (word("next")("dy") - focusedTextarea("lineheight")) > e.offsetY 145 | word = word "next" 146 | 147 | pos = word("wordLength") - 1 148 | cursorPoint = word("view").getEndPositionOfChar pos 149 | focusedTextarea("cursor") "set", word, pos, cursorPoint 150 | 151 | controller.facet 152 | -------------------------------------------------------------------------------- /source/word.coffee: -------------------------------------------------------------------------------- 1 | this.SVGIE ?= {} 2 | 3 | svgNS = 'http://www.w3.org/2000/svg' 4 | spaceNS = "http://www.w3.org/XML/1998/namespace" 5 | 6 | # Define how words should be split 7 | wordRegexp = /^(\S+|\r\n|\s)((\r|\n|.)*)$/ 8 | # Define whitespace. Must be the same definition as in the wordsplit regexp 9 | whitespaceRegexp = /\s/ 10 | newlinesRegexp = /(\r\n|\r|\n)/ 11 | 12 | controllerPrototype = 13 | val: (s) -> 14 | if s? 15 | # Change text 16 | @model.s = s 17 | @model.view.textContent = s 18 | .replace(newlinesRegexp, "") 19 | .replace(/\t/, " ") 20 | # Recalculate width 21 | @model.width = @model.view.getBoundingClientRect().width # What about that other word width property/method? 22 | # Repos next word unless next word is the first word 23 | @model.next("repos") unless @isEnd() 24 | @model.s 25 | wordLength: -> 26 | @model.s.length 27 | prev: (prev) -> 28 | if prev? 29 | @model.prev = prev 30 | else 31 | @model.prev 32 | next: (next) -> 33 | if next? 34 | @model.next = next 35 | else 36 | @model.next 37 | isBeginning: -> 38 | @model.beginning 39 | isEnd: -> 40 | @model.next "isBeginning" 41 | dx: -> 42 | @model.dx 43 | dy: -> 44 | @model.line * @model.textarea "lineheight" 45 | line: -> 46 | @model.line 47 | width: -> 48 | @model.width = @model.view.getBoundingClientRect().width 49 | textarea: -> 50 | @model.textarea 51 | view: -> 52 | @model.view 53 | whitespace: -> 54 | whitespace = no 55 | if whitespaceRegexp.test @model.s 56 | whitespace = switch 57 | when @model.s is " " then "space" 58 | when @model.s is "\t" then "tab" 59 | when newlinesRegexp.test @model.s then "newline" #what about \r and \r\n? 60 | else true 61 | whitespace 62 | firstInLine: -> 63 | prev = @prev() 64 | if prev? 65 | prev("line") < @line() 66 | true 67 | autoWrapped: -> 68 | unless @model.prev? 69 | return false 70 | unless @model.textarea("width") isnt null 71 | return false 72 | unless (@model.prev("dx") + @model.prev("width") + @width()) < @model.textarea("width") 73 | return @model.prev("whitespace") isnt "linebreak" 74 | repos: -> 75 | # Get new dx value 76 | dx = do => 77 | if @isBeginning() 78 | 0 79 | else 80 | # Ignore a single leading space 81 | if not @whitespace() and @model.prev("whitespace") is "space" and @model.prev("autoWrapped") 82 | 0 83 | else 84 | @model.prev("dx") + @model.prev("width") 85 | # Can we stay on the same line? 86 | if @whitespace() isnt "newline" and (@model.textarea("width") is null or @model.textarea("width") >= (dx + @model.width)) 87 | # This will break if word is wider than textarea 88 | @model.dx = dx 89 | @model.line = @model.prev "line" 90 | else 91 | # Start a new line 92 | @model.dx = 0 93 | @model.line = @model.prev("line") + 1 94 | @model.view.setAttributeNS null, "x", @model.dx 95 | @model.view.setAttributeNS null, "y", @model.line * @model.textarea("lineheight") 96 | if @isEnd() 97 | # Extend textarea's height if necessary 98 | @model.textarea "height", Math.max @facet("dy"), @model.textarea("height") 99 | else 100 | @model.next("repos") 101 | @model.dx 102 | insert: (s, pos) -> 103 | #TODO: Reposition cursor 104 | #TODO: Handele out of range pos values 105 | unless pos? and pos <= @model.s.length and pos >= 0 106 | throw "The position '" + pos + "' is not set or out of range" 107 | s = @model.s.substr(0, pos) + s + @model.s.substr(pos) 108 | next = @model.next 109 | parsedS = wordRegexp.exec s 110 | @model.s = parsedS[1] 111 | rest = parsedS[2] 112 | 113 | @val @model.s 114 | 115 | if rest? 116 | SVGIE.word @model.textarea, @facet, rest 117 | @val() 118 | 119 | 120 | SVGIE.word = (textarea, prev, s) -> 121 | unless typeof textarea is 'function' 122 | throw "Textarea must be a textarea function" 123 | unless prev is null or typeof prev is 'function' 124 | throw "Second argument should be a word controller or null" 125 | unless typeof s is 'string' 126 | throw "Third argument must be a string" 127 | 128 | if s.length is 0 and prev? 129 | return null 130 | else if s.length is 0 and not prev? 131 | s = "" 132 | rest = "" 133 | else 134 | parsedS = wordRegexp.exec s 135 | s = parsedS[1] 136 | rest = parsedS[2] 137 | 138 | controller = Object.create controllerPrototype 139 | 140 | leftWord = prev if prev? 141 | rightWord = prev("next") if prev? 142 | 143 | controller.facet = (method, args...) -> 144 | if method is "facet" or method is "model" or !controller[method]? 145 | return undefined 146 | controller[method].apply controller, args 147 | controller.model = 148 | s: s 149 | prev: do -> 150 | if leftWord? 151 | leftWord "next", controller.facet 152 | leftWord 153 | else 154 | controller.facet 155 | next: do -> 156 | if rightWord? 157 | rightWord "prev", controller.facet 158 | rightWord 159 | else 160 | controller.facet 161 | dx: -1 162 | line: unless prev? then 1 else prev "line" 163 | textarea: textarea 164 | width: 0 165 | facet: controller.facet 166 | atChar: 0 167 | beginning: not prev? 168 | view: do -> 169 | v = document.createElementNS svgNS, "text" 170 | v.setAttributeNS spaceNS, "xml:space", "preserve" 171 | textarea("view").appendChild v 172 | v.addEventListener "click", (e) -> 173 | # Make this textarea "focused" 174 | textarea "focus" 175 | 176 | textareaDimensions = textarea("view").getBoundingClientRect() 177 | 178 | x = e.clientX - textareaDimensions.left 179 | y = e.clientY - textareaDimensions.top 180 | 181 | p = textarea "svgPoint", x, y 182 | charNum = v.getCharNumAtPosition p 183 | charRect = v.getExtentOfChar charNum 184 | 185 | if x < (charRect.x + (charRect.width / 2)) 186 | cursorPoint = v.getStartPositionOfChar charNum 187 | else 188 | cursorPoint = v.getEndPositionOfChar charNum 189 | charNum += 1 190 | cursor = textarea("cursor") 191 | cursor("set", controller.facet, charNum, cursorPoint) 192 | 193 | #if e.offsetX > (charRect.x + (charRect.width / 2)) 194 | # char += 1 195 | #closestGap = if e.offsetX < (charRect.x + (charRect.width / 2)) then clickedCharRect.x else clickedCharRect.x + clickedCharRect.width 196 | #console.log e 197 | v 198 | 199 | controller.val controller.model.s 200 | controller.width() # Calculate width with new "val" 201 | controller.repos() 202 | 203 | #next("prev", controller.facet) if next? 204 | 205 | if rest? 206 | SVGIE.word textarea, controller.facet, rest 207 | 208 | controller.facet 209 | -------------------------------------------------------------------------------- /spec/textareaSpec.coffee: -------------------------------------------------------------------------------- 1 | svgNS = 'http://www.w3.org/2000/svg' 2 | xlinkNS = 'http://www.w3.org/1999/xlink' 3 | 4 | describe "The textarea controller", -> 5 | textarea = null 6 | svg = -> 7 | svgElement = document.createElementNS svgNS, "svg" 8 | svgElement.setAttributeNS null, "version", "1.1" 9 | svgElement.setAttributeNS null, "width", "100px" 10 | svgElement.setAttributeNS null, "height", "100px" 11 | # document.appendChild svgElement 12 | svgElement 13 | beforeEach -> 14 | textarea = SVGIE.textarea svg(), {}, "" 15 | 16 | it "has a 'width' action which returns the svg width if no width was set", -> 17 | expect(textarea "width").toBe 0 # this is a useless test at the moment 18 | it "has a 'width' action which returns a numer if a width was set", -> 19 | textarea2 = SVGIE.textarea svg(), { width: 100 } 20 | expect(textarea2 "width").toBe 100 21 | it "has a 'width' action which sets the width to the value of the second argument", -> 22 | textarea "width", 200 23 | expect(textarea "width").toBe 200 24 | it "has a 'height' action which returns null if no height was set", -> 25 | expect(textarea "height").toBe 0 26 | it "has a 'height' action which returns a number if a height was set", -> 27 | textarea = SVGIE.textarea svg(), { height: 100 } 28 | expect(textarea "height").toBe 100 29 | it "has a 'lineheight' action which returns a number", -> 30 | expect(textarea "lineheight").toEqual jasmine.any Number 31 | it "has a 'words' action which returns s word function even if no string was passed to SVGIE.textarea", -> 32 | expect(textarea "words").not.toBe null 33 | it "has a 'words' action which returns a word controller if a string was passed to SVGIE.textarea", -> 34 | textarea = SVGIE.textarea svg(), {}, "string" 35 | expect(textarea "words").toEqual jasmine.any Function 36 | it "has a 'val' action which returns the textareas string value", -> 37 | textarea = SVGIE.textarea svg(), {}, "one two three" 38 | expect(textarea "val").toBe "one two three" 39 | it "has a 'val' action which can set the value of the textarea", -> 40 | textarea "val", "one two three" 41 | expect(textarea "val").toBe "one two three" 42 | it "has a 'focus' action which sets the focused textarea to textarea", -> 43 | expect(textarea "focus", textarea).toBe true 44 | it "has a 'focused' action which returns true or false if the textarea is focused", -> 45 | expect(textarea "focused").toEqual false 46 | textarea "focus", textarea 47 | expect(textarea "focused").toEqual true 48 | --------------------------------------------------------------------------------