├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── angular-contenteditable-scenario.js ├── angular-contenteditable.js ├── bower.json ├── karma.coffee ├── package.json ├── test ├── e2e │ └── scenarios.coffee └── fixtures │ ├── img │ ├── gb.gif │ ├── ru.gif │ └── us.gif │ ├── no-line-breaks.html │ ├── select-non-editable.html │ ├── simple.html │ ├── states.json │ ├── strip-br.html │ ├── typeahead1.html │ ├── typeahead2.html │ └── typeahead3.html └── util └── post-commit /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | bower_components 3 | .idea 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "es5": true, 5 | "esnext": true, 6 | "bitwise": true, 7 | "camelcase": true, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "quotmark": "single", 16 | "regexp": true, 17 | "undef": true, 18 | "unused": true, 19 | "strict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "globals": { 23 | "angular": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '0.10' 5 | 6 | notifications: 7 | email: false 8 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | # global module:false 2 | 3 | module.exports = (grunt) -> 4 | 5 | grunt.initConfig 6 | pkg: grunt.file.readJSON 'package.json' 7 | meta: 8 | test: 'test' 9 | karma: 10 | e2e: configFile: 'karma.coffee' 11 | e2e_ci: 12 | configFile: 'karma.coffee' 13 | singleRun: true 14 | browsers: ['PhantomJS'] 15 | jshint: 16 | src: ['angular-contenteditable.js'] 17 | options: 18 | asi: true 19 | 20 | require('matchdep').filterDev('grunt-*').forEach grunt.loadNpmTasks 21 | 22 | grunt.registerTask 'test', ['karma:e2e_ci'] 23 | grunt.registerTask 'lint', ['jshint'] 24 | grunt.registerTask 'default', ['lint' , 'test'] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Dmitri Akatov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-contenteditable 2 | [![Build Status](https://img.shields.io/travis/akatov/angular-contenteditable.svg)](https://travis-ci.org/akatov/angular-contenteditable) 3 | [![Dependency Status](https://img.shields.io/gemnasium/akatov/angular-contenteditable.svg)](https://gemnasium.com/akatov/angular-contenteditable) 4 | [![endorse](https://api.coderwall.com/akatov/endorsecount.png)](https://coderwall.com/akatov) 5 | 6 | An AngularJS directive to bind html tags with the `contenteditable` attribute to models. 7 | 8 | ## Install 9 | 10 | ```bash 11 | bower install angular-contenteditable 12 | ``` 13 | 14 | ## Usage 15 | 16 | ```javascript 17 | angular.module('myapp', ['contenteditable']) 18 | .controller('Ctrl', ['$scope', function($scope) { 19 | $scope.model="interesting stuff" 20 | }]) 21 | ``` 22 | 23 | ```html 24 |
25 | 30 | 31 |
32 | ``` 33 | 34 | ## Notice 35 | 36 | The directive currently does not work in any version of Internet Explorer or Opera < 15. 37 | Both browsers don't fire the `input` event for contenteditable fields. 38 | 39 | In Chrome, when a contenteditable element X contains a non-contenteditable 40 | element Y as the last element, then the behaviour of the caret is the following: 41 | 42 | * When X has style `display` set to `block` or `inline-block`, then the caret 43 | moves to the very far right edge of X when it is _immediately_ at the end of X 44 | (inserting spaces moves the caret back). 45 | 46 | * When X has style `display` set to `inline`, then the caret disappears instead. 47 | 48 | ## Development 49 | 50 | ```bash 51 | npm install 52 | bower install 53 | grunt 54 | ``` 55 | -------------------------------------------------------------------------------- /angular-contenteditable-scenario.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * we copied most of the 'element' scenario dsl so we can keep the old actions 4 | * and also add the 'enter' and modified 'html' actions. 5 | * @see https://github.com/angular/angular.js/blob/master/src/ngScenario/dsl.js 6 | * @see http://stackoverflow.com/questions/12575199/how-to-test-a-contenteditable-field-based-on-a-td-using-angularjs-e2e-testing 7 | * 8 | * Usage: 9 | * element(selector, label).count() get the number of elements that match selector 10 | * element(selector, label).click() clicks an element 11 | * element(selector, label).mouseover() mouseover an element 12 | * element(selector, label).mousedown() mousedown an element 13 | * element(selector, label).mouseup() mouseup an element 14 | * element(selector, label).query(fn) executes fn(selectedElements, done) 15 | * element(selector, label).{method}() gets the value (as defined by jQuery, ex. val) 16 | * element(selector, label).{method}(value) sets the value (as defined by jQuery, ex. val) 17 | * element(selector, label).{method}(key) gets the value (as defined by jQuery, ex. attr) 18 | * element(selector, label).{method}(key, value) sets the value (as defined by jQuery, ex. attr) 19 | * element(selector, label).enter(value) sets the text if the element is contenteditable 20 | */ 21 | angular.scenario.dsl('element', function() { 22 | var KEY_VALUE_METHODS = ['attr', 'css', 'prop']; 23 | var VALUE_METHODS = [ 24 | 'val', 'text', 'html', 'height', 'innerHeight', 'outerHeight', 'width', 25 | 'innerWidth', 'outerWidth', 'position', 'scrollLeft', 'scrollTop', 'offset' 26 | ]; 27 | var chain = {}; 28 | 29 | chain.count = function() { 30 | return this.addFutureAction("element '" + this.label + "' count", function($window, $document, done) { 31 | try { 32 | done(null, $document.elements().length); 33 | } catch (e) { 34 | done(null, 0); 35 | } 36 | }); 37 | }; 38 | 39 | chain.click = function() { 40 | return this.addFutureAction("element '" + this.label + "' click", function($window, $document, done) { 41 | var elements = $document.elements(); 42 | var href = elements.attr('href'); 43 | var eventProcessDefault = elements.trigger('click')[0]; 44 | 45 | if (href && elements[0].nodeName.toUpperCase() === 'A' && eventProcessDefault) { 46 | this.application.navigateTo(href, function() { 47 | done(); 48 | }, done); 49 | } else { 50 | done(); 51 | } 52 | }); 53 | }; 54 | 55 | chain.dblclick = function() { 56 | return this.addFutureAction("element '" + this.label + "' dblclick", function($window, $document, done) { 57 | var elements = $document.elements(); 58 | var href = elements.attr('href'); 59 | var eventProcessDefault = elements.trigger('dblclick')[0]; 60 | 61 | if (href && elements[0].nodeName.toUpperCase() === 'A' && eventProcessDefault) { 62 | this.application.navigateTo(href, function() { 63 | done(); 64 | }, done); 65 | } else { 66 | done(); 67 | } 68 | }); 69 | }; 70 | 71 | chain.mouseover = function() { 72 | return this.addFutureAction("element '" + this.label + "' mouseover", function($window, $document, done) { 73 | var elements = $document.elements(); 74 | elements.trigger('mouseover'); 75 | done(); 76 | }); 77 | }; 78 | 79 | chain.mousedown = function() { 80 | return this.addFutureAction("element '" + this.label + "' mousedown", function($window, $document, done) { 81 | var elements = $document.elements(); 82 | elements.trigger('mousedown'); 83 | done(); 84 | }); 85 | }; 86 | 87 | chain.mouseup = function() { 88 | return this.addFutureAction("element '" + this.label + "' mouseup", function($window, $document, done) { 89 | var elements = $document.elements(); 90 | elements.trigger('mouseup'); 91 | done(); 92 | }); 93 | }; 94 | 95 | chain.query = function(fn) { 96 | return this.addFutureAction('element ' + this.label + ' custom query', function($window, $document, done) { 97 | fn.call(this, $document.elements(), done); 98 | }); 99 | }; 100 | 101 | angular.forEach(KEY_VALUE_METHODS, function(methodName) { 102 | chain[methodName] = function(name, value) { 103 | var args = arguments, 104 | futureName = (args.length == 1) 105 | ? "element '" + this.label + "' get " + methodName + " '" + name + "'" 106 | : "element '" + this.label + "' set " + methodName + " '" + name + "' to " + "'" + value + "'"; 107 | 108 | return this.addFutureAction(futureName, function($window, $document, done) { 109 | var element = $document.elements(); 110 | done(null, element[methodName].apply(element, args)); 111 | }); 112 | }; 113 | }); 114 | 115 | angular.forEach(VALUE_METHODS, function(methodName) { 116 | chain[methodName] = function(value) { 117 | var args = arguments, 118 | futureName = (args.length == 0) 119 | ? "element '" + this.label + "' " + methodName 120 | : futureName = "element '" + this.label + "' set " + methodName + " to '" + value + "'"; 121 | 122 | return this.addFutureAction(futureName, function($window, $document, done) { 123 | var element = $document.elements(); 124 | done(null, element[methodName].apply(element, args)); 125 | }); 126 | }; 127 | }); 128 | 129 | // =============== These are the methods ================ \\ 130 | chain.enter = function(value) { 131 | return this.addFutureAction("element '" + this.label + "' enter '" + value + "'", function($window, $document, done) { 132 | var element = $document.elements() 133 | if (element.is('[contenteditable=""]') 134 | || (element.attr('contenteditable') 135 | && element.attr('contenteditable').match(/true/i))) { 136 | element.text(value) 137 | element.trigger('input') 138 | } 139 | done() 140 | }) 141 | } 142 | 143 | chain.html = function(value) { 144 | var args = arguments, 145 | futureName = (args.length == 0) 146 | ? "element '" + this.label + "' html" 147 | : futureName = "element '" + this.label + "' set html to '" + value + "'"; 148 | return this.addFutureAction(futureName, function($window, $document, done) { 149 | var element = $document.elements(); 150 | element.html.apply(element, args) 151 | if (args.length > 0 152 | && (element.is('[contenteditable=""]') 153 | || (element.attr('contenteditable') 154 | && element.attr('contenteditable').match(/true/i)))) { 155 | element.trigger('input') 156 | } 157 | done(null, element.html.apply(element, args)); 158 | }); 159 | }; 160 | 161 | return function(selector, label) { 162 | this.dsl.using(selector, label); 163 | return chain; 164 | }; 165 | }); 166 | -------------------------------------------------------------------------------- /angular-contenteditable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see http://docs.angularjs.org/guide/concepts 3 | * @see http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController 4 | * @see https://github.com/angular/angular.js/issues/528#issuecomment-7573166 5 | */ 6 | 7 | angular.module('contenteditable', []) 8 | .directive('contenteditable', ['$timeout', function($timeout) { return { 9 | restrict: 'A', 10 | require: '?ngModel', 11 | link: function(scope, element, attrs, ngModel) { 12 | // don't do anything unless this is actually bound to a model 13 | if (!ngModel) { 14 | return 15 | } 16 | 17 | // options 18 | var opts = {} 19 | angular.forEach([ 20 | 'stripBr', 21 | 'noLineBreaks', 22 | 'selectNonEditable', 23 | 'moveCaretToEndOnChange', 24 | 'stripTags' 25 | ], function(opt) { 26 | var o = attrs[opt] 27 | opts[opt] = o && o !== 'false' 28 | }) 29 | 30 | // view -> model 31 | element.bind('input', function(e) { 32 | scope.$apply(function() { 33 | var html, html2, rerender 34 | html = element.html() 35 | rerender = false 36 | if (opts.stripBr) { 37 | html = html.replace(/
$/, '') 38 | } 39 | if (opts.noLineBreaks) { 40 | html2 = html.replace(/
/g, '').replace(/
/g, '').replace(/<\/div>/g, '') 41 | if (html2 !== html) { 42 | rerender = true 43 | html = html2 44 | } 45 | } 46 | if (opts.stripTags) { 47 | rerender = true 48 | html = html.replace(/<\S[^><]*>/g, '') 49 | } 50 | ngModel.$setViewValue(html) 51 | if (rerender) { 52 | ngModel.$render() 53 | } 54 | if (html === '') { 55 | // the cursor disappears if the contents is empty 56 | // so we need to refocus 57 | $timeout(function(){ 58 | element[0].blur() 59 | element[0].focus() 60 | }) 61 | } 62 | }) 63 | }) 64 | 65 | // model -> view 66 | var oldRender = ngModel.$render 67 | ngModel.$render = function() { 68 | var el, el2, range, sel 69 | if (!!oldRender) { 70 | oldRender() 71 | } 72 | var html = ngModel.$viewValue || '' 73 | if (opts.stripTags) { 74 | html = html.replace(/<\S[^><]*>/g, '') 75 | } 76 | 77 | element.html(html) 78 | if (opts.moveCaretToEndOnChange) { 79 | el = element[0] 80 | range = document.createRange() 81 | sel = window.getSelection() 82 | if (el.childNodes.length > 0) { 83 | el2 = el.childNodes[el.childNodes.length - 1] 84 | range.setStartAfter(el2) 85 | } else { 86 | range.setStartAfter(el) 87 | } 88 | range.collapse(true) 89 | sel.removeAllRanges() 90 | sel.addRange(range) 91 | } 92 | } 93 | if (opts.selectNonEditable) { 94 | element.bind('click', function(e) { 95 | var range, sel, target 96 | target = e.toElement 97 | if (target !== this && angular.element(target).attr('contenteditable') === 'false') { 98 | range = document.createRange() 99 | sel = window.getSelection() 100 | range.setStartBefore(target) 101 | range.setEndAfter(target) 102 | sel.removeAllRanges() 103 | sel.addRange(range) 104 | } 105 | }) 106 | } 107 | } 108 | }}]); 109 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-contenteditable", 3 | "version": "0.3.8", 4 | "main": "angular-contenteditable.js", 5 | "ignore": [ 6 | ".*", 7 | "README.md", 8 | "Gruntfile.coffee", 9 | "package.json", 10 | "components", 11 | "node_modules", 12 | "src", 13 | "test" 14 | ], 15 | "devDependencies": { 16 | "angular": "*", 17 | "angular-mocks": "~1.0.5", 18 | "angular-scenario": "~1.0.5", 19 | "expect": "~0.2.0", 20 | "jquery": "~2.0.2", 21 | "bootstrap-css": "~2.3.2", 22 | "angular-bootstrap": "~0.4.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /karma.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (karma) -> 2 | toServe = for file in [ 3 | 'bower_components/**/*.css' 4 | 'bower_components/*/*.js' 5 | 'test/fixtures/**/*' 6 | ] 7 | pattern: file 8 | watched: false 9 | included: false 10 | served: true 11 | 12 | karma.set 13 | frameworks: ['ng-scenario'] 14 | 15 | preprocessors: '**/*.coffee': 'coffee' 16 | 17 | files: [ 18 | 'test/e2e/**/*.coffee' 19 | 'angular-contenteditable.js' 20 | 'angular-contenteditable-scenario.js' 21 | ].concat toServe 22 | 23 | exclude: [] 24 | 25 | reporters: ['progress'] 26 | 27 | port: 9876 28 | 29 | runnerPort: 9100 30 | 31 | colors: true 32 | 33 | logLevel: karma.LOG_INFO 34 | 35 | autoWatch: true 36 | 37 | browsers: ['Chrome'] 38 | 39 | captureTimeout: 60000 40 | 41 | singleRun: false 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-contenteditable", 3 | "version": "0.3.8", 4 | "description": "angular model for the 'contenteditable' html attribute", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/akatov/angular-contenteditable.git" 8 | }, 9 | "main": "angular-contenteditable.js", 10 | "directories": { 11 | "test": "test" 12 | }, 13 | "scripts": { 14 | "install": "bower install", 15 | "test": "grunt test" 16 | }, 17 | "repository": "", 18 | "keywords": [ 19 | "angular", 20 | "extension" 21 | ], 22 | "author": "Dmitri Akatov", 23 | "license": "BSD", 24 | "devDependencies": { 25 | "matchdep": "~0.3.0", 26 | "grunt": "~0.4.1", 27 | "grunt-cli": "~0.1.13", 28 | "grunt-contrib-jshint": "~0.7.1", 29 | "grunt-karma": "~0.6.2", 30 | "bower": "~1.3.5", 31 | "karma": "~0.10.2", 32 | "karma-ng-scenario": "0.1.0", 33 | "karma-coffee-preprocessor": "0.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/e2e/scenarios.coffee: -------------------------------------------------------------------------------- 1 | describe 'module contenteditable', -> 2 | describe 'directive contenteditable', -> 3 | describe 'simple application', -> 4 | beforeEach -> 5 | browser().navigateTo 'base/test/fixtures/simple.html' 6 | 7 | it 'should update the model from the view (simple text)', -> 8 | element('#input').enter('abc') 9 | expect(element('#input').html()).toBe 'abc' 10 | expect(element('#output').html()).toBe 'abc' 11 | 12 | it 'should update the model from the view (text with spans)', -> 13 | element('#input').html('abc red') 14 | expect(element('#input span').html()).toBe 'red' 15 | expect(element('#output').html()).toBe 'abc <span style="color:red">red</span>' 16 | 17 | it 'should update the view from the model', -> 18 | input('model').enter('oops') 19 | expect(element('#input').html()).toBe 'oops' 20 | expect(element('#output').html()).toBe 'oops' 21 | input('model').enter('a red b') 22 | expect(element('#input').html()).toBe 'a red b' 23 | expect(element('#input span').html()).toBe 'red' 24 | expect(element('#output').html()).toBe 'a <span style="color:red">red</span> b' 25 | -------------------------------------------------------------------------------- /test/fixtures/img/gb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akatov/angular-contenteditable/886324c6adc128fae290db5cc3425bb2adcc18fe/test/fixtures/img/gb.gif -------------------------------------------------------------------------------- /test/fixtures/img/ru.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akatov/angular-contenteditable/886324c6adc128fae290db5cc3425bb2adcc18fe/test/fixtures/img/ru.gif -------------------------------------------------------------------------------- /test/fixtures/img/us.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akatov/angular-contenteditable/886324c6adc128fae290db5cc3425bb2adcc18fe/test/fixtures/img/us.gif -------------------------------------------------------------------------------- /test/fixtures/no-line-breaks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 25 | something 26 | 27 |
28 |
29 | 30 |
{{ model }}
31 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /test/fixtures/select-non-editable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 | 23 |
24 | something 25 |
26 |
27 |
28 | 29 |
{{ model }}
30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /test/fixtures/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple 5 | 6 | 7 | 14 | 15 | 16 |
17 | 18 |
{{ model }}
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 | 34 |
35 | 36 |
Edit me - I don't affect anything
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /test/fixtures/states.json: -------------------------------------------------------------------------------- 1 | [ "Russia " 2 | , "United States of America " 3 | , "United Kingdom "] 4 | -------------------------------------------------------------------------------- /test/fixtures/strip-br.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 | 23 |
24 | something 25 |
26 |
27 |
28 | 29 |
{{ model }}
30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /test/fixtures/typeahead1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 |
21 |
Model: {{selected| json}}
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/typeahead2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 42 | 43 | 44 |
45 |
Model: {{ selected | json }}
46 |
47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /test/fixtures/typeahead3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 43 | 44 | 45 |
46 |
Model: {{ selected | json }}
47 |
48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /util/post-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'tmpdir' 4 | require 'json' 5 | 6 | puts 'running post-commit hook' 7 | 8 | BOWER = JSON.parse File.read 'bower.json' 9 | 10 | def commit 11 | @commit ||= `git log | head -1 | cut -d' ' -f2` 12 | end 13 | 14 | # get version of library from bower.json 15 | def bower_version(library) 16 | BOWER["dependencies"][library] || BOWER["devDependencies"][library] 17 | end 18 | 19 | # return the base url for a library from bower / github 20 | def library_base_url(library) 21 | # TODO: figure out how to clean the cache so we can use `bower --offline` 22 | (@library_base_url ||= {})[library] ||= 23 | JSON.parse(`bower lookup #{library} --json`)["url"] 24 | .sub(/^git:\/\/github.com/, 'https://rawgithub.com') 25 | .sub(/\.git$/, "/#{bower_version(library).sub(/~/, '')}/") 26 | end 27 | 28 | # transform script ref to bower URL 29 | def script_url(src) 30 | if src =~ /\/bower_components\// 31 | parts = src.split('/bower_components/')[1].split('/') 32 | library_base_url(parts[0]) + parts.drop(1).join('/') 33 | else 34 | src.sub(/^\.\.\/\.\./, 35 | 'https://rawgithub.com/akatov/angular-contenteditable/master') 36 | end 37 | end 38 | 39 | # link href 40 | # script src 41 | def replace_script_and_link(contents) 42 | ["script src", "link href"].reduce(contents) do |c, tag| 43 | c.gsub /#{tag}="([^"]*)"/ do 44 | "#{tag}=\"#{script_url($1)}\"" 45 | end 46 | end 47 | end 48 | 49 | def index_header 50 | < 52 | 53 | angular-contenteditable 54 | 55 | 56 |

angular contenteditable

57 |

examples

58 |
    59 | EOF 60 | end 61 | 62 | def index_footer 63 | < 65 | 66 | 67 | EOF 68 | end 69 | 70 | puts commit 71 | 72 | def execute 73 | Dir.mktmpdir do |temp| 74 | FileUtils.cp_r 'test/fixtures/', temp 75 | FileUtils.mv "#{temp}/fixtures", "#{temp}/examples" 76 | File.open("#{temp}/index.html", File::CREAT | File::WRONLY) do |index_file| 77 | index_file.write index_header 78 | Dir.glob("#{temp}/examples/*.html").each do |file_name| 79 | bn = File.basename file_name 80 | puts "changing references in #{bn}" 81 | File.write file_name, replace_script_and_link(File.read file_name) 82 | index_file.write "
  • #{bn}
  • \n" 83 | end 84 | index_file.write index_footer 85 | end 86 | `git checkout gh-pages` 87 | `git rm -r examples` 88 | ['index.html', 'examples'].each do |f| 89 | FileUtils.cp_r "#{temp}/#{f}", '.' 90 | `git add #{f}` 91 | end 92 | `git commit --message "updating gh-pages for commit #{commit}"` 93 | `git checkout master` 94 | end 95 | end 96 | 97 | execute 98 | --------------------------------------------------------------------------------