diff --git a/angular-contenteditable.js b/angular-contenteditable.js index 49b59ee..ff05f1b 100644 --- a/angular-contenteditable.js +++ b/angular-contenteditable.js @@ -5,30 +5,34 @@ */ 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 - } + .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' - }) + // options + var opts = {} + angular.forEach([ + 'stripBr', + 'noLineBreaks', + 'selectNonEditable', + 'moveCaretToEndOnChange', + 'placeholder' + ], function(opt) { + var o = attrs[opt] + opts[opt] = o && o !== 'false' + }) + if (opts.placeholder) { + opts.placeholder = attrs.placeholder || 'Empty' + } - // view -> model - element.bind('input', function(e) { - scope.$apply(function() { + // view -> model + function readViewValue() { var html, html2, rerender html = element.html() rerender = false @@ -44,55 +48,91 @@ angular.module('contenteditable', []) } ngModel.$setViewValue(html) if (rerender) { - ngModel.$render() + ngModel.$render(true) } - if (html === '') { - // the cursor disappears if the contents is empty - // so we need to refocus - $timeout(function(){ - element[0].blur() - element[0].focus() - }) - } - }) - }) + return html; + } - // model -> view - var oldRender = ngModel.$render - ngModel.$render = function() { - var el, el2, range, sel - if (!!oldRender) { - oldRender() + element.bind('input', function(e) { + scope.$apply(function() { + var empty = '' === readViewValue() + if (empty) { + // the cursor disappears if the contents is empty + // so we need to keep focus + selectAll(element[0]) + } else if (opts.placeholder) { + element.removeClass('placeholder') + } + }) + }) + if (opts.placeholder) { + element.bind('blur', function (e) { + scope.$apply(function () { + if (readViewValue() === '') { + element.html(opts.placeholder) + element.addClass('placeholder') + } + }); + }) } - 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) - } else { - range.setStartAfter(el) + + // model -> view + var oldRender = ngModel.$render + ngModel.$render = function(ignorePlaceholder) { + var el, el2, range, sel + if (!!oldRender) { + oldRender() } - range.collapse(true) - sel.removeAllRanges() - sel.addRange(range) - } - } - if (opts.selectNonEditable) { - element.bind('click', function(e) { - var range, sel, target - target = e.toElement - if (target !== this && angular.element(target).attr('contenteditable') === 'false') { + var value = ngModel.$viewValue || ''; + if (!ignorePlaceholder && opts.placeholder) { + element.toggleClass('placeholder', value === ''); + if (value === '') { + value = opts.placeholder; + } + } + element.html(value); + + if (opts.moveCaretToEndOnChange) { + el = element[0] range = document.createRange() sel = window.getSelection() - range.setStartBefore(target) - range.setEndAfter(target) + 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) } - }) + } + if (opts.placeholder) { + element.bind('focus', function () { + if (!ngModel.$viewValue) { + element.html('') + selectAll(element[0]) + } + }) + } + + if (opts.selectNonEditable) { + element.bind('click', function(e) { + var range, sel, target + target = e.toElement + if (target !== this && angular.element(target).attr('contenteditable') === 'false') { + selectAll(target) + } + }) + } } } - }}]); + + function selectAll(node) { + var range = document.createRange() + range.selectNodeContents(node) + var sel = window.getSelection() + sel.removeAllRanges() + sel.addRange(range) + } + }]); diff --git a/test/fixtures/no-line-breaks.html b/test/fixtures/no-line-breaks.html index d200c05..5d3edbe 100644 --- a/test/fixtures/no-line-breaks.html +++ b/test/fixtures/no-line-breaks.html @@ -14,8 +14,6 @@ window.scope = $scope }) .controller('Ctrl2', function($scope) {}) - -angular.bootstrap(document, ['simple']) diff --git a/test/fixtures/placeholder.html b/test/fixtures/placeholder.html new file mode 100644 index 0000000..9511159 --- /dev/null +++ b/test/fixtures/placeholder.html @@ -0,0 +1,22 @@ + + + + + + + + + + +
+
+ + + diff --git a/test/fixtures/select-non-editable.html b/test/fixtures/select-non-editable.html index 78090ec..9e40f8a 100644 --- a/test/fixtures/select-non-editable.html +++ b/test/fixtures/select-non-editable.html @@ -13,8 +13,6 @@ $scope.model = "Initial stuff with bold and italic and non-editable stuff" }) .controller('Ctrl2', function($scope) {}) - -angular.bootstrap(document, ['simple']) diff --git a/test/fixtures/simple.html b/test/fixtures/simple.html index 1b3d0bf..43172b0 100644 --- a/test/fixtures/simple.html +++ b/test/fixtures/simple.html @@ -9,7 +9,6 @@ .controller('Ctrl', function($scope) { $scope.model = "Initial stuff with bold and italic yay" }) -angular.bootstrap(document, ['simple']) diff --git a/test/fixtures/strip-br.html b/test/fixtures/strip-br.html index 0081e15..0282f75 100644 --- a/test/fixtures/strip-br.html +++ b/test/fixtures/strip-br.html @@ -13,8 +13,6 @@ $scope.model = "Initial stuff with bold and italic yay" }) .controller('Ctrl2', function($scope) {}) - -angular.bootstrap(document, ['simple'])