Compare commits

...

46 Commits

Author SHA1 Message Date
Chakib Benziane 7571d66905 fixes #34 9 years ago
Dmitri Akatov 4cc8e467bd Merge pull request #32 from kathygit/master
add ";" at the end of the js file so that the minified version works
10 years ago
Kathy Laird 9eb48567ab add ; at the end of the js file so that the minified version works 10 years ago
Dmitri Akatov 9a1471d23c 0.3.7 10 years ago
Dmitri Akatov 18691f1441 don't need karma-mocha 10 years ago
Dmitri Akatov 06ae4fc77a simpler karma usage (there are no unit tests) 10 years ago
Dmitri Akatov d7771af3c7 move bower dependency on angular to development, and use angular instead of angular unstable 10 years ago
Dmitri Akatov 8a3ae75892 moved angular-unstable to development dependency 10 years ago
Dmitri Akatov 8c7ad58a05 bumped grunt and bower versions 10 years ago
Dmitri Akatov 543c491b5f commands are run from local bin directory anyways 10 years ago
Dmitri Akatov f8da2a127e use shields.io images 10 years ago
Dmitri Akatov ac284284ca added License 11 years ago
Dmitri Akatov 6197aca95c Merge pull request #18 from Narretz/patch-1
added note that directive is broken in IE and Opera < 15
11 years ago
Narretz 60dde289a3 added note that directive is broken in IE and Opera < 15
This might save some people some time that need to support IE, but that only test it when they are finished.
11 years ago
Dmitri Akatov 64fd235450 only move caret to end on model change when the corresponding attribute is set 11 years ago
Dmitri Akatov dd8ac4dc44 pull out options first 11 years ago
Dmitri Akatov 52d84c973e these variables aren't injections 11 years ago
Dmitri Akatov 76e3d515bf 0.3.6 11 years ago
Dmitri Akatov 14b0659205 better code formatting in the README 11 years ago
Dmitri Akatov 5cfa6cb537 bumped grunt-contrib-jshint 11 years ago
Dmitri Akatov 2cd8d711e6 added coderwall badge 11 years ago
Dmitri Akatov 5da962fa23 typo in test 11 years ago
Dmitri Akatov cc52856cd9 in post-commit hook first remove all examples 11 years ago
Dmitri Akatov fac8a4543c instead of trying to access the scope, change it through an input element (verbatim) 11 years ago
Dmitri Akatov 61c22697e6 updated npm dependencies 11 years ago
Dmitri Akatov c40f303002 added gemnasium badge 11 years ago
Dmitri Akatov 94121dae35 updated description and added repository to package.json 11 years ago
Dmitri Akatov bef2d32443 added Bitdeli Badge 11 years ago
Dmitri Akatov 60d6382192 Merge pull request #5 from solidspark/master
Point to the first object of $element's array
11 years ago
Dmitri Akatov 9a274bb465 Merge pull request #6 from boneskull/master
removes jQuery as prerequisite:
11 years ago
Dmitri Akatov 3e64e14a22 Travis-CI 11 years ago
Dmitri Akatov b6d7233be3 finished post-commit hook 11 years ago
Dmitri Akatov f773f5ff94 preparing post-commit hook to create gh-pages 11 years ago
Christopher Hiller cbf28d9225 removes jQuery as prerequisite:
- jQuery was used in two places in the code: a get() call and a click() call, both of which are unsupported by jqLite, but have workarounds.
- jQuery is still used for functional testing.
11 years ago
Ryan Katkov d0cd823932 Point to the first object of $element's array 11 years ago
Dmitri Akatov 7ba9e4abcb 0.3.5 - don't do anything for contenteditable without an ngModel 11 years ago
Dmitri Akatov d8d7bf29e9 add a scenario dsl extension (mostly copied from the standard angular scenario dsl) 11 years ago
Dmitri Akatov d2715b92f6 use 2 spaces per tab 11 years ago
Dmitri Akatov ba76a3749d avoid calling $scope.$apply twice in the same digest cycle if anything is listening on blur() or focus() 11 years ago
Dmitri Akatov ccdece14ae i don't like semicolons any more 11 years ago
Dmitri Akatov 2c8dea244c only use coffeescript for configuration and in tests 11 years ago
Dmitri Akatov 4fa785b175 version 0.3.1: don't rerender unless we actually replaced some divs in no-line-break mode 11 years ago
Dmitri Akatov 0da3b6c473 bump angular-bootstrap 11 years ago
Dmitri Akatov 232a698a83 bumped karma, updated test config to run with new version of karma 11 years ago
Dmitri Akatov dab97313db no-line-breaks attribute to prevent line breaks 11 years ago
Dmitri Akatov b939d4f9ac some explanation about caret behaviour in contenteditable elements. 11 years ago

@ -0,0 +1,3 @@
{
"directory": "bower_components"
}

3
.gitignore vendored

@ -1,4 +1,3 @@
node_modules
app/components
components
bower_components
.idea

@ -0,0 +1,7 @@
language: node_js
node_js:
- '0.10'
notifications:
email: false

@ -5,46 +5,20 @@ module.exports = (grunt) ->
grunt.initConfig
pkg: grunt.file.readJSON 'package.json'
meta:
src: 'src'
test: 'test'
target: '<%= pkg.name %>.js'
banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
'<%= grunt.template.today("yyyy-mm-dd") %>\n' +
'<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' +
'* Copyright (c) <%= grunt.template.today("yyyy") %>' +
' <%= pkg.author.name %>;' +
' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n'
clean: target: src: ['<%= pkg.title %>.js']
coffeelint:
src:
files: src: ['<%= meta.src %>/**/*.coffee']
options: max_line_length: level: 'warn'
test:
files: src: ['<%= meta.src %>/**/*.coffee']
options: max_line_length: level: 'warn'
gruntfile:
files: src: ['Gruntfile.coffee']
coffee: src:
files:
'<%= meta.target %>': [
'<%= meta.src %>/**/*.coffee'
]
karma:
unit: configFile: 'test/config-unit.coffee'
e2e: configFile: 'test/config-e2e.coffee'
unit_ci:
configFile: 'test/config-unit.coffee'
singleRun: true
browsers: ['PhantomJS']
e2e: configFile: 'karma.coffee'
e2e_ci:
configFile: 'test/config-e2e.coffee'
configFile: 'karma.coffee'
singleRun: true
browsers: ['PhantomJS']
jshint: target: ['<%= meta.target %>']
jshint:
src: ['angular-contenteditable.js']
options:
asi: true
require('matchdep').filterDev('grunt-*').forEach grunt.loadNpmTasks
grunt.registerTask 'test', ['karma:e2e_ci']
grunt.registerTask 'lint', ['coffeelint']
grunt.registerTask 'build', ['clean', 'coffee']
grunt.registerTask 'default', ['lint' , 'test', 'build', 'jshint']
grunt.registerTask 'lint', ['jshint']
grunt.registerTask 'default', ['lint' , 'test']

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Dmitri Akatov
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.

@ -1,4 +1,7 @@
# angular-contenteditable
[![Build Status](https://img.shields.io/travis/akatov/angular-contenteditable.svg)](https://travis-ci.org/akatov/angular-contenteditable)
[![Dependency Status](https://img.shields.io/gemnasium/akatov/angular-contenteditable.svg)](https://gemnasium.com/akatov/angular-contenteditable)
[![endorse](https://api.coderwall.com/akatov/endorsecount.png)](https://coderwall.com/akatov)
An AngularJS directive to bind html tags with the `contenteditable` attribute to models.
@ -12,8 +15,9 @@ bower install angular-contenteditable
```javascript
angular.module('myapp', ['contenteditable'])
.controller('Ctrl', function($scope){
$scope.model="<i>interesting</i> stuff" })
.controller('Ctrl', ['$scope', function($scope) {
$scope.model="<i>interesting</i> stuff"
}])
```
```html
@ -21,13 +25,29 @@ angular.module('myapp', ['contenteditable'])
<span contenteditable="true"
ng-model="model"
strip-br="true"
select-non-editable="true"></span></div>
select-non-editable="true">
</span>
</div>
```
## Notice
The directive currently does not work in any version of Internet Explorer or Opera < 15.
Both browsers don't fire the `input` event for contenteditable fields.
In Chrome, when a contenteditable element X contains a non-contenteditable
element Y as the last element, then the behaviour of the caret is the following:
* When X has style `display` set to `block` or `inline-block`, then the caret
moves to the very far right edge of X when it is _immediately_ at the end of X
(inserting spaces moves the caret back).
* When X has style `display` set to `inline`, then the caret disappears instead.
## Development
```bash
npm install
grunt build
grunt karma:e2e
bower install
grunt
```

@ -0,0 +1,165 @@
/**
* we copied most of the 'element' scenario dsl so we can keep the old actions
* and also add the 'enter' and modified 'html' actions.
* @see https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js
* @see http://stackoverflow.com/questions/12575199/how-to-test-a-contenteditable-field-based-on-a-td-using-angularjs-e2e-testing
*
* Usage:
* element(selector, label).count() get the number of elements that match selector
* element(selector, label).click() clicks an element
* element(selector, label).mouseover() mouseover an element
* element(selector, label).mousedown() mousedown an element
* element(selector, label).mouseup() mouseup an element
* element(selector, label).query(fn) executes fn(selectedElements, done)
* element(selector, label).{method}() gets the value (as defined by jQuery, ex. val)
* element(selector, label).{method}(value) sets the value (as defined by jQuery, ex. val)
* element(selector, label).{method}(key) gets the value (as defined by jQuery, ex. attr)
* element(selector, label).{method}(key, value) sets the value (as defined by jQuery, ex. attr)
* element(selector, label).enter(value) sets the text if the element is contenteditable
*/
angular.scenario.dsl('element', function() {
var KEY_VALUE_METHODS = ['attr', 'css', 'prop'];
var VALUE_METHODS = [
'val', 'text', 'html', 'height', 'innerHeight', 'outerHeight', 'width',
'innerWidth', 'outerWidth', 'position', 'scrollLeft', 'scrollTop', 'offset'
];
var chain = {};
chain.count = function() {
return this.addFutureAction("element '" + this.label + "' count", function($window, $document, done) {
try {
done(null, $document.elements().length);
} catch (e) {
done(null, 0);
}
});
};
chain.click = function() {
return this.addFutureAction("element '" + this.label + "' click", function($window, $document, done) {
var elements = $document.elements();
var href = elements.attr('href');
var eventProcessDefault = elements.trigger('click')[0];
if (href && elements[0].nodeName.toUpperCase() === 'A' && eventProcessDefault) {
this.application.navigateTo(href, function() {
done();
}, done);
} else {
done();
}
});
};
chain.dblclick = function() {
return this.addFutureAction("element '" + this.label + "' dblclick", function($window, $document, done) {
var elements = $document.elements();
var href = elements.attr('href');
var eventProcessDefault = elements.trigger('dblclick')[0];
if (href && elements[0].nodeName.toUpperCase() === 'A' && eventProcessDefault) {
this.application.navigateTo(href, function() {
done();
}, done);
} else {
done();
}
});
};
chain.mouseover = function() {
return this.addFutureAction("element '" + this.label + "' mouseover", function($window, $document, done) {
var elements = $document.elements();
elements.trigger('mouseover');
done();
});
};
chain.mousedown = function() {
return this.addFutureAction("element '" + this.label + "' mousedown", function($window, $document, done) {
var elements = $document.elements();
elements.trigger('mousedown');
done();
});
};
chain.mouseup = function() {
return this.addFutureAction("element '" + this.label + "' mouseup", function($window, $document, done) {
var elements = $document.elements();
elements.trigger('mouseup');
done();
});
};
chain.query = function(fn) {
return this.addFutureAction('element ' + this.label + ' custom query', function($window, $document, done) {
fn.call(this, $document.elements(), done);
});
};
angular.forEach(KEY_VALUE_METHODS, function(methodName) {
chain[methodName] = function(name, value) {
var args = arguments,
futureName = (args.length == 1)
? "element '" + this.label + "' get " + methodName + " '" + name + "'"
: "element '" + this.label + "' set " + methodName + " '" + name + "' to " + "'" + value + "'";
return this.addFutureAction(futureName, function($window, $document, done) {
var element = $document.elements();
done(null, element[methodName].apply(element, args));
});
};
});
angular.forEach(VALUE_METHODS, function(methodName) {
chain[methodName] = function(value) {
var args = arguments,
futureName = (args.length == 0)
? "element '" + this.label + "' " + methodName
: futureName = "element '" + this.label + "' set " + methodName + " to '" + value + "'";
return this.addFutureAction(futureName, function($window, $document, done) {
var element = $document.elements();
done(null, element[methodName].apply(element, args));
});
};
});
// =============== These are the methods ================ \\
chain.enter = function(value) {
return this.addFutureAction("element '" + this.label + "' enter '" + value + "'", function($window, $document, done) {
var element = $document.elements()
if (element.is('[contenteditable=""]')
|| (element.attr('contenteditable')
&& element.attr('contenteditable').match(/true/i))) {
element.text(value)
element.trigger('input')
}
done()
})
}
chain.html = function(value) {
var args = arguments,
futureName = (args.length == 0)
? "element '" + this.label + "' html"
: futureName = "element '" + this.label + "' set html to '" + value + "'";
return this.addFutureAction(futureName, function($window, $document, done) {
var element = $document.elements();
element.html.apply(element, args)
if (args.length > 0
&& (element.is('[contenteditable=""]')
|| (element.attr('contenteditable')
&& element.attr('contenteditable').match(/true/i)))) {
element.trigger('input')
}
done(null, element.html.apply(element, args));
});
};
return function(selector, label) {
this.dsl.using(selector, label);
return chain;
};
});

@ -1,55 +1,98 @@
(function() {
angular.module('contenteditable', []).directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, elmt, attrs, ngModel) {
var old_render;
elmt.bind('input', function(e) {
return scope.$apply(function() {
var html;
html = elmt.html();
if (attrs.stripBr && attrs.stripBr !== "false" && html === '<br>') {
html = '';
/**
* @see http://docs.angularjs.org/guide/concepts
* @see http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController
* @see https://github.com/angular/angular.js/issues/528#issuecomment-7573166
*/
angular.module('contenteditable', [])
.directive('contenteditable', ['$timeout', function($timeout) { return {
restrict: 'A',
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
// don't do anything unless this is actually bound to a model
if (!ngModel) {
return
}
// options
var opts = {}
angular.forEach([
'stripBr',
'noLineBreaks',
'selectNonEditable',
'moveCaretToEndOnChange',
], function(opt) {
var o = attrs[opt]
opts[opt] = o && o !== 'false'
})
// view -> model
element.bind('input', function(e) {
scope.$apply(function() {
var html, html2, rerender
html = element.text()
rerender = false
if (opts.stripBr) {
html = html.replace(/<br>$/, '')
}
if (opts.noLineBreaks) {
html2 = html.replace(/<div>/g, '').replace(/<br>/g, '').replace(/<\/div>/g, '')
if (html2 !== html) {
rerender = true
html = html2
}
return ngModel.$setViewValue(html);
});
});
old_render = ngModel.$render;
ngModel.$render = function() {
var el, el2, range, sel;
if (old_render != null) {
old_render();
}
elmt.html(ngModel.$viewValue || '');
el = elmt.get(0);
range = document.createRange();
sel = window.getSelection();
ngModel.$setViewValue(html)
if (rerender) {
ngModel.$render()
}
if (html === '') {
// the cursor disappears if the contents is empty
// so we need to refocus
$timeout(function(){
element[0].blur()
element[0].focus()
})
}
})
})
// model -> view
var oldRender = ngModel.$render
ngModel.$render = function() {
var el, el2, range, sel
if (!!oldRender) {
oldRender()
}
element.html(ngModel.$viewValue || '')
if (opts.moveCaretToEndOnChange) {
el = element[0]
range = document.createRange()
sel = window.getSelection()
if (el.childNodes.length > 0) {
el2 = el.childNodes[el.childNodes.length - 1];
range.setStartAfter(el2);
el2 = el.childNodes[el.childNodes.length - 1]
range.setStartAfter(el2)
} else {
range.setStartAfter(el);
range.setStartAfter(el)
}
range.collapse(true);
sel.removeAllRanges();
return sel.addRange(range);
};
if (attrs.selectNonEditable && attrs.selectNonEditable !== "false") {
return elmt.click(function(e) {
var range, sel, target;
target = e.toElement;
if (target !== this && angular.element(target).attr('contenteditable') === 'false') {
range = document.createRange();
sel = window.getSelection();
range.setStartBefore(target);
range.setEndAfter(target);
sel.removeAllRanges();
return sel.addRange(range);
}
});
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}
}
};
});
}).call(this);
if (opts.selectNonEditable) {
element.bind('click', function(e) {
var range, sel, target
target = e.toElement
if (target !== this && angular.element(target).attr('contenteditable') === 'false') {
range = document.createRange()
sel = window.getSelection()
range.setStartBefore(target)
range.setEndAfter(target)
sel.removeAllRanges()
sel.addRange(range)
}
})
}
}
}}]);

@ -1,25 +1,24 @@
{ "name": "angular-contenteditable"
, "version": "0.2.0"
, "main": "angular-contenteditable.js"
, "ignore":
[ ".*"
, "README.md"
, "Gruntfile.coffee"
, "package.json"
, "components"
, "node_modules"
, "src"
, "test"
]
, "dependencies":
{ "angular-unstable": "~1.1.5"
}
, "devDependencies":
{ "angular-mocks": "~1.0.5"
, "angular-scenario": "~1.0.5"
, "angular-bootstrap": "~0.3.0"
, "expect": "~0.2.0"
, "jquery": "~2.0.2"
, "bootstrap-css": "~2.3.2"
{
"name": "angular-contenteditable",
"version": "0.3.7",
"main": "angular-contenteditable.js",
"ignore": [
".*",
"README.md",
"Gruntfile.coffee",
"package.json",
"components",
"node_modules",
"src",
"test"
],
"devDependencies": {
"angular": "*",
"angular-mocks": "~1.0.5",
"angular-scenario": "~1.0.5",
"expect": "~0.2.0",
"jquery": "~2.0.2",
"bootstrap-css": "~2.3.2",
"angular-bootstrap": "~0.4.0"
}
}

@ -0,0 +1,41 @@
module.exports = (karma) ->
toServe = for file in [
'bower_components/**/*.css'
'bower_components/*/*.js'
'test/fixtures/**/*'
]
pattern: file
watched: false
included: false
served: true
karma.set
frameworks: ['ng-scenario']
preprocessors: '**/*.coffee': 'coffee'
files: [
'test/e2e/**/*.coffee'
'angular-contenteditable.js'
'angular-contenteditable-scenario.js'
].concat toServe
exclude: []
reporters: ['progress']
port: 9876
runnerPort: 9100
colors: true
logLevel: karma.LOG_INFO
autoWatch: true
browsers: ['Chrome']
captureTimeout: 60000
singleRun: false

@ -1,12 +1,17 @@
{
"name": "angular-contenteditable",
"version": "0.2.0",
"description": "angular extensions",
"version": "0.3.7",
"description": "angular model for the 'contenteditable' html attribute",
"repository": {
"type": "git",
"url": "https://github.com/akatov/angular-contenteditable.git"
},
"main": "angular-contenteditable.js",
"directories": {
"test": "test"
},
"scripts": {
"install": "bower install",
"test": "grunt test"
},
"repository": "",
@ -17,19 +22,14 @@
"author": "Dmitri Akatov",
"license": "BSD",
"devDependencies": {
"matchdep": "~0.1.2",
"matchdep": "~0.3.0",
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-mocha-cli": "~1.0.6",
"should": "~1.2.2",
"grunt-coffee-redux": "~0.2.3",
"grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-clean": "~0.4.1",
"grunt-contrib-watch": "~0.4.4",
"grunt-contrib-jshint": "~0.6.0",
"grunt-coffeelint": "0.0.6",
"coffee-script": "~1.6.3",
"karma": "~0.8.5",
"grunt-karma": "~0.4.4"
"grunt-cli": "~0.1.13",
"grunt-contrib-jshint": "~0.7.1",
"grunt-karma": "~0.6.2",
"bower": "~1.3.5",
"karma": "~0.10.2",
"karma-ng-scenario": "0.1.0",
"karma-coffee-preprocessor": "0.1.0"
}
}

@ -1,41 +0,0 @@
angular.module('contenteditable', [])
.directive('contenteditable', ->
require: 'ngModel',
link: (scope, elmt, attrs, ngModel) ->
# view -> model
elmt.bind 'input', (e) ->
scope.$apply ->
html = elmt.html()
html = '' if attrs.stripBr && attrs.stripBr != "false" && html == '<br>'
ngModel.$setViewValue(html)
# model -> view
old_render = ngModel.$render # save for later
ngModel.$render = ->
old_render() if old_render?
elmt.html(ngModel.$viewValue || '')
# move cursor to the end
el = elmt.get(0)
range = document.createRange()
sel = window.getSelection()
if el.childNodes.length > 0
el2 = el.childNodes[el.childNodes.length - 1]
range.setStartAfter(el2)
else
range.setStartAfter(el)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
# select whole sub-span if it has contenteditable="false"
if attrs.selectNonEditable && attrs.selectNonEditable != "false"
elmt.click (e) ->
target = e.toElement
if target != @ && angular.element(target).attr('contenteditable') == 'false'
range = document.createRange()
sel = window.getSelection()
range.setStartBefore(target)
range.setEndAfter(target)
sel.removeAllRanges()
sel.addRange(range)
)

@ -1,52 +0,0 @@
basePath = '..'
toServe = [
'components/bootstrap-css/css/bootstrap.css'
'components/jquery/jquery.js'
'components/angular-unstable/angular.js'
'components/angular-bootstrap/ui-bootstrap.js'
'components/angular-bootstrap/ui-bootstrap-tpls.js'
'angular-contenteditable.js'
'test/fixtures/simple.html'
'test/fixtures/typeahead1.html'
'test/fixtures/typeahead2.html'
'test/fixtures/typeahead3.html'
'test/fixtures/states.json'
'test/fixtures/img/ru.gif'
'test/fixtures/img/gb.gif'
'test/fixtures/img/us.gif'
]
toServe =
for file in toServe
pattern: file
watched: false
included: false
served: true
files = [
ANGULAR_SCENARIO
ANGULAR_SCENARIO_ADAPTER
'src/**/*.coffee'
'test/e2e/**/*.coffee'
].concat toServe
exclude = []
reporters = ['progress']
port = 9876
runnerPort = 9100
colors = true
logLevel = LOG_INFO
autoWatch = true
browsers = ['Chrome']
captureTimeout = 60000
singleRun = false

@ -1,30 +0,0 @@
basePath = '..'
files =
[ MOCHA
, MOCHA_ADAPTER
, 'components/expect/expect.js'
, 'components/angular/angular.js'
, 'src/**/*.coffee'
, 'test/unit/*.coffee'
]
exclude = []
reporters = ['progress']
port = 9876
runnerPort = 9100
colors = true
logLevel = LOG_INFO
autoWatch = true
browsers = ['Chrome']
captureTimeout = 60000
singleRun = false

@ -1,78 +1,24 @@
angular.scenario.dsl 'contenteditable', ->
(name) ->
@name = name
enter: (value) ->
@addFutureAction "contenteditable '#{@name}' enter '#{value}'"
, ($window, $document, done) ->
elmt = $document.elements @name
elmt.text value
elmt.trigger 'input'
done()
html: (args...) ->
futureName =
if args.length == 0
"contenteditable '#{@name}' html"
else
"contenteditable '#{@name}' set html to '#{args[0]}'"
@addFutureAction futureName
, ($window, $document, done) ->
elmt = $document.elements @name
elmt.html args
elmt.trigger 'input'
done(null, elmt.html(args))
angular.scenario.dsl 'scope', ->
(ctrl, arg = null) ->
futureName =
if !arg?
"scope in Controller '#{ctrl}'"
else if typeof arg == 'function'
"executing 'scope.$apply(#{arg})' in Controller '#{ctrl}'"
else
"scope variable '#{arg}' in Controller '#{ctrl}'"
@addFutureAction futureName
, ($window, $document, done) ->
elmt = $window.$ "[ng-controller='#{ctrl}']"
return done("No Controller #{ctrl}") unless elmt?
sc = elmt.scope()
return done(null, sc) unless arg?
if typeof arg == 'string'
parts = arg.split '.'
for p in parts
sc = sc[p]
done(null, sc)
else if typeof arg == 'function'
sc.$apply -> arg(sc)
done()
else
done "don't understand argument #{arg}"
describe 'radians', ->
describe 'contenteditable', ->
describe 'module contenteditable', ->
describe 'directive contenteditable', ->
describe 'simple application', ->
beforeEach ->
browser().navigateTo 'base/test/fixtures/simple.html'
it 'should update the model from the view (simple text)', ->
contenteditable('#input').enter('abc')
element('#input').enter('abc')
expect(element('#input').html()).toBe 'abc'
expect(scope('Ctrl', 'model')).toBe 'abc'
expect(element('#output').html()).toBe 'abc'
it 'should update the model from the view (text with spans)', ->
contenteditable('#input').html('abc <span style="color:red">red</span>')
expect(scope('Ctrl', 'model')).toBe 'abc <span style="color:red">red</span>'
element('#input').html('abc <span style="color:red">red</span>')
expect(element('#input span').html()).toBe 'red'
expect(element('#output').html()).toBe 'abc &lt;span style="color:red"&gt;red&lt;/span&gt;'
it 'should update the view from the model', ->
expect(scope('Ctrl', 'model')).toBe 'Initial stuff <b>with bold</b> <em>and italic</em> yay'
scope('Ctrl', ($scope) -> $scope.model = 'oops')
expect(scope('Ctrl', 'model')).toBe 'oops'
input('model').enter('oops')
expect(element('#input').html()).toBe 'oops'
scope('Ctrl', ($scope) -> $scope.model = 'a <span style="color:red">red</span> b')
expect(element('#output').html()).toBe 'oops'
input('model').enter('a <span style="color:red">red</span> b')
expect(element('#input').html()).toBe 'a <span style="color:red">red</span> b'
expect(element('#input span').html()).toBe 'red'
## Why doesn't it work on this one??!!
# expect(element('#output').html()).toBe 'oops'
expect(element('#output').html()).toBe 'a &lt;span style="color:red"&gt;red&lt;/span&gt; b'

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html ng-app="simple">
<head>
<title>Simple</title>
<!-- we need jquery for e2e testing -->
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../bower_components/angular/angular.js"></script>
<script src="../../angular-contenteditable.js"></script>
<script>
angular.module('simple', ['contenteditable'])
.controller('Ctrl', function($scope) {
// $scope.model2 = 'hello'
$scope.model = "Initial stuff <b>with bold</b> <em>and italic</em> yay"
window.scope = $scope
})
.controller('Ctrl2', function($scope) {})
angular.bootstrap(document, ['simple'])
</script>
</head>
<body>
<div ng-controller="Ctrl">
<label>Contenteditable (View):</label>
<span id="input" contenteditable="true" ng-model="model" no-line-breaks="true" style="display:block">
something
</span>
<hr>
<div>
<label>Model:</label>
<pre id="output" onclick="console.log('clicked the output')">{{ model }}</pre>
</div>
</div>
</body>
</html>

@ -3,8 +3,8 @@
<head>
<title>Simple</title>
<!-- we need jquery for e2e testing -->
<script src="../../components/jquery/jquery.js"></script>
<script src="../../components/angular-unstable/angular.js"></script>
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../bower_components/angular/angular.js"></script>
<script src="../../angular-contenteditable.js"></script>
<script>
angular.module('simple', ['contenteditable'])

@ -2,33 +2,38 @@
<html ng-app="simple">
<head>
<title>Simple</title>
<!-- we need jquery for e2e testing -->
<script src="../../components/jquery/jquery.js"></script>
<script src="../../components/angular-unstable/angular.js"></script>
<script src="../../bower_components/angular/angular.js"></script>
<script src="../../angular-contenteditable.js"></script>
<script>
angular.module('simple', ['contenteditable'])
.controller('Ctrl', function($scope) {
$scope.model = "Initial stuff <b>with bold</b> <em>and italic</em> yay"
})
.controller('Ctrl2', function($scope) {})
.controller('Ctrl', function($scope) {
$scope.model = "Initial stuff <b>with bold</b> <em>and italic</em> yay"
})
angular.bootstrap(document, ['simple'])
</script>
</head>
<body>
<div ng-controller="Ctrl">
<label>Contenteditable (View):</label>
<div id="input" contenteditable ng-model="model">
</div>
<label>Model (non-editable):</label>
<pre id="output">{{ model }}</pre>
<hr>
<div>
<label>Model:</label>
<pre id="output" onclick="console.log('clicked the output')">{{ model }}</pre>
</div>
</div>
<div ng-controller="Ctrl2">
The other controller
<label>Contenteditable View (1):</label>
<div id="input" contenteditable ng-model="model"></div>
<hr>
<label>Contenteditable View (2):</label>
<div id="input2" contenteditable="true" ng-model="model"></div>
<hr>
<label>Contenteditable View (3):</label>
<div id="input3" contenteditable="TRUE" ng-model="model"></div>
<hr>
<label>non-contenteditable View:</label>
<div id="non-editable" contenteditable="false" ng-model="model"></div>
<hr>
<label>Input (edit verbatim)</label>
<input ng-model="model" style="width:100%">
<hr>
<label>Contenteditable without Model</label>
<div contenteditable>Edit me - I don't affect anything</div>
</div>
</body>
</html>

@ -3,8 +3,8 @@
<head>
<title>Simple</title>
<!-- we need jquery for e2e testing -->
<script src="../../components/jquery/jquery.js"></script>
<script src="../../components/angular-unstable/angular.js"></script>
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../bower_components/angular/angular.js"></script>
<script src="../../angular-contenteditable.js"></script>
<script>
angular.module('simple', ['contenteditable'])

@ -2,11 +2,11 @@
<html lang="en" ng-app="typeahead1">
<head>
<meta charset="utf-8">
<link href="../../components/bootstrap-css/css/bootstrap.css" rel="stylesheet">
<script src="../../components/jquery/jquery.js"></script>
<script src="../../components/angular-unstable/angular.js"></script>
<script src="../../components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="../../components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<link href="../../bower_components/bootstrap-css/css/bootstrap.css" rel="stylesheet">
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../bower_components/angular/angular.js"></script>
<script src="../../bower_components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="../../bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="../../angular-contenteditable.js"></script>
<script>
angular.module('typeahead1', ['ui.bootstrap', 'contenteditable'])

@ -2,11 +2,11 @@
<html lang="en" ng-app="typeahead2">
<head>
<meta charset="utf-8">
<link href="../../components/bootstrap-css/css/bootstrap.css" rel="stylesheet">
<script src="../../components/jquery/jquery.js"></script>
<script src="../../components/angular-unstable/angular.js"></script>
<script src="../../components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="../../components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<link href="../../bower_components/bootstrap-css/css/bootstrap.css" rel="stylesheet">
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../bower_components/angular/angular.js"></script>
<script src="../../bower_components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="../../bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="../../angular-contenteditable.js"></script>
<script>
angular.module('typeahead2', ['ui.bootstrap', 'contenteditable'])

@ -2,11 +2,11 @@
<html lang="en" ng-app="typeahead3">
<head>
<meta charset="utf-8">
<link href="../../components/bootstrap-css/css/bootstrap.css" rel="stylesheet">
<script src="../../components/jquery/jquery.js"></script>
<script src="../../components/angular-unstable/angular.js"></script>
<script src="../../components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="../../components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<link href="../../bower_components/bootstrap-css/css/bootstrap.css" rel="stylesheet">
<script src="../../bower_components/jquery/jquery.js"></script>
<script src="../../bower_components/angular/angular.js"></script>
<script src="../../bower_components/angular-bootstrap/ui-bootstrap.js"></script>
<script src="../../bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="../../angular-contenteditable.js"></script>
<script>
angular.module('typeahead3', ['ui.bootstrap', 'contenteditable'])

@ -1,3 +0,0 @@
describe 'radians', ->
describe 'contenteditable', ->
it 'passes', ->

@ -0,0 +1,97 @@
#!/usr/bin/env ruby
require 'tmpdir'
require 'json'
puts 'running post-commit hook'
BOWER = JSON.parse File.read 'bower.json'
def commit
@commit ||= `git log | head -1 | cut -d' ' -f2`
end
# get version of library from bower.json
def bower_version(library)
BOWER["dependencies"][library] || BOWER["devDependencies"][library]
end
# return the base url for a library from bower / github
def library_base_url(library)
# TODO: figure out how to clean the cache so we can use `bower --offline`
(@library_base_url ||= {})[library] ||=
JSON.parse(`bower lookup #{library} --json`)["url"]
.sub(/^git:\/\/github.com/, 'https://rawgithub.com')
.sub(/\.git$/, "/#{bower_version(library).sub(/~/, '')}/")
end
# transform script ref to bower URL
def script_url(src)
if src =~ /\/bower_components\//
parts = src.split('/bower_components/')[1].split('/')
library_base_url(parts[0]) + parts.drop(1).join('/')
else
src.sub(/^\.\.\/\.\./,
'https://rawgithub.com/akatov/angular-contenteditable/master')
end
end
# link href
# script src
def replace_script_and_link(contents)
["script src", "link href"].reduce(contents) do |c, tag|
c.gsub /#{tag}="([^"]*)"/ do
"#{tag}=\"#{script_url($1)}\""
end
end
end
def index_header
<<EOF
<html>
<head>
<title>angular-contenteditable</title>
</head>
<body>
<h1>angular contenteditable</h1>
<h2>examples<h2>
<ul>
EOF
end
def index_footer
<<EOF
</ul>
</body>
</html>
EOF
end
puts commit
def execute
Dir.mktmpdir do |temp|
FileUtils.cp_r 'test/fixtures/', temp
FileUtils.mv "#{temp}/fixtures", "#{temp}/examples"
File.open("#{temp}/index.html", File::CREAT | File::WRONLY) do |index_file|
index_file.write index_header
Dir.glob("#{temp}/examples/*.html").each do |file_name|
bn = File.basename file_name
puts "changing references in #{bn}"
File.write file_name, replace_script_and_link(File.read file_name)
index_file.write " <li><a href='examples/#{bn}'>#{bn}</a></li>\n"
end
index_file.write index_footer
end
`git checkout gh-pages`
`git rm -r examples`
['index.html', 'examples'].each do |f|
FileUtils.cp_r "#{temp}/#{f}", '.'
`git add #{f}`
end
`git commit --message "updating gh-pages for commit #{commit}"`
`git checkout master`
end
end
execute