├── .github └── workflows │ └── codeql.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bower.help ├── bower.json ├── dev └── index.html ├── dist ├── editor.min.css ├── images │ ├── header-bg.gif │ ├── header-bg.orig.gif │ ├── icons.old.png │ ├── icons.png │ ├── logo.png │ └── resize.gif └── wysiwyg.min.js ├── gulpfile.js ├── package.json └── src ├── css └── editor.sass ├── images ├── header-bg.gif ├── header-bg.orig.gif ├── icons.old.png ├── icons.png ├── logo.png └── resize.gif ├── js ├── ngpColorsGrid.js ├── ngpContentFrame.js ├── ngpImageResizer.js ├── ngpResizable.js ├── ngpSymbolsGrid.js ├── ngpUtils.js ├── wysiwyg.js └── wysiwygEdit.js └── tests ├── conf.js └── load-spec.js /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | branches: [ "master" ] 19 | schedule: 20 | - cron: '30 8 * * 3' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Add any setup steps before running the `github/codeql-action/init` action. 61 | # This includes steps like installing compilers or runtimes (`actions/setup-node` 62 | # or others). This is typically only required for manual builds. 63 | # - name: Setup runtime (example) 64 | # uses: actions/setup-example@v1 65 | 66 | # Initializes the CodeQL tools for scanning. 67 | - name: Initialize CodeQL 68 | uses: github/codeql-action/init@v3 69 | with: 70 | languages: ${{ matrix.language }} 71 | build-mode: ${{ matrix.build-mode }} 72 | # If you wish to specify custom queries, you can do so here or in a config file. 73 | # By default, queries listed here will override any specified in a config file. 74 | # Prefix the list here with "+" to use these queries and those in the config file. 75 | 76 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 77 | # queries: security-extended,security-and-quality 78 | 79 | # If the analyze step fails for one of the languages you are analyzing with 80 | # "We were unable to automatically build your code", modify the matrix above 81 | # to set the build mode to "manual" for that language. Then modify this step 82 | # to build your code. 83 | # ℹ️ Command-line programs to run using the OS shell. 84 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 85 | - if: matrix.build-mode == 'manual' 86 | shell: bash 87 | run: | 88 | echo 'If you are using a "manual" build mode for one or more of the' \ 89 | 'languages you are analyzing, replace this with the commands to build' \ 90 | 'your code, for example:' 91 | echo ' make bootstrap' 92 | echo ' make release' 93 | exit 1 94 | 95 | - name: Perform CodeQL Analysis 96 | uses: github/codeql-action/analyze@v3 97 | with: 98 | category: "/language:${{matrix.language}}" 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .idea/* 3 | dev/editor.css 4 | dev/wysiwyg.js 5 | dev/images/* 6 | build/* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Bug fixes 4 | 5 | 0.3.0 XSS vulnerability fix from (2015-09-23) happen to be replacing inline styles because $sanitize was utilized. I will have to think some good workaround for this but for now 6 | I recommend using configuration for the XSS. In other words if you want to sanitize the user's input, use sanitize: true in configuration 7 | 8 | ## Imprevements 9 | 10 | - added configuration for the editor 11 | - added configurable toolbar 12 | 13 | 14 | 0.2.1 XSS vulnerability fix (2015-09-23) 15 | 16 | ## Bug fixes 17 | 18 | - used $sanitize to prevent XSS when render from the model to view. Gret read about it: https://www.blackhat.com/docs/eu-14/materials/eu-14-Javed-Revisiting-XSS-Sanitization-wp.pdf 19 | 20 | ## Imprevements 21 | 22 | - added changelog 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sergey Petrenko 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ngWYSIWYG 2 | ========= 3 | 4 | Folks, for your judgement and, hopefully, contributions, here is the true angular WYSIWYG. 5 | I took images and layout from the tinyeditor, so kudos to Michael Leigeber. 6 | 7 | Here is the Demo 8 | 9 | ### Why iFrame? 10 | 11 | A real rich text editor must reflect the true stage of the editing content. Any CSS and/or Javascript on the host page must not overide the specifics of the content. 12 | Moreover, iframe allows to isolate your security issues (any possible Javascript code in the content may polute your window's scope). 13 | 14 | 15 | Installation 16 | ========================= 17 | 18 | ## Requirements 19 | 20 | 1. `AngularJS` ≥ `1.2.x` 21 | 2. `Angular Sanitize` ≥ `1.2.x` 22 | 23 | ### Bower 24 | 25 | ````Shell 26 | $ bower install ngWYSIWYG --save 27 | ``` 28 | 29 | Include the ngWYSIWYG files in your index.html: 30 | ````HTML 31 | 32 | 33 | ``` 34 | 35 | Add it as module to your app.js: 36 | 37 | ````JavaScript 38 | ['ngWYSIWYG'] 39 | ```` 40 | 41 | Use it wherever you want: 42 | 43 | ```HTML 44 | 45 | ``` 46 | 47 | ## Configuration 48 | 49 | You can configure the editor for two options (will extend l8r). First option is if you want to sanitize input from the user and prevent XSS attacks. This option uses angular's 50 | $sanitize. The second option will allow to configure toolbar buttons. You will be able to configure which buttons you want to show. Please see example. 51 | 52 | ````JavaScript 53 | angular.module('myApp', ['ngWYSIWYG']). 54 | controller('demoController', ['$scope', '$q', '$timeout', function($scope, $q, $timeout) { 55 | $scope.your_variable = 'some HTML text here'; 56 | $scope.api = { 57 | scope: $scope, 58 | $scope.editorConfig = { 59 | sanitize: false, 60 | toolbar: [ 61 | { name: 'basicStyling', items: ['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', '-', 'leftAlign', 'centerAlign', 'rightAlign', 'blockJustify', '-'] }, 62 | { name: 'paragraph', items: ['orderedList', 'unorderedList', 'outdent', 'indent', '-'] }, 63 | { name: 'doers', items: ['removeFormatting', 'undo', 'redo', '-'] }, 64 | { name: 'colors', items: ['fontColor', 'backgroundColor', '-'] }, 65 | { name: 'links', items: ['image', 'hr', 'symbols', 'link', 'unlink', '-'] }, 66 | { name: 'tools', items: ['print', '-'] }, 67 | { name: 'styling', items: ['font', 'size', 'format'] }, 68 | ] 69 | }; 70 | }; 71 | }]); 72 | ```` 73 | 74 | ```HTML 75 | 76 | ``` 77 | 78 | ## Custom content style 79 | 80 | This option enables you to specify a custom CSS file to be used within the editor (the editable area). 81 | 82 | ````HTML 83 | 84 | ``` 85 | 86 | If you specify a relative path, it is resolved in relation to the URL of the (HTML) file that includes ngWYSIWYG, 87 | NOT relative to ngWYSIWYG itself. In the example above, if the HTML file is hosted at http://www.example.com/wysiwyg.html, 88 | then the css URL will be resolved to: http://www.example.com/some_style.css. 89 | 90 | ### Use case 91 | 92 | This configuration is useful when you want your editor's content area to show the content exactly like its going to be 93 | show in the destination, without adding inline css to it. For example, let's say that the destination has a black background color 94 | with a white font-color. In this case your some_style.css file would have the following properties: 95 | 96 | ```CSS 97 | html, body { 98 | background-color: black; 99 | color: #ffffff; 100 | } 101 | ``` 102 | 103 | ## API 104 | 105 | There is an idea on the api functions to delegate some responsibilities to the customer's scope. 106 | The first thing which is implemented is insert image delegation. By default the directive uses a simple prompt function to accept image's url. However, 107 | there is a way to bring up a custom dialog box on the customer's side and return promise. 108 | 109 | ````JavaScript 110 | angular.module('myApp', ['ngWYSIWYG']). 111 | controller('demoController', ['$scope', '$q', '$timeout', function($scope, $q, $timeout) { 112 | $scope.your_variable = 'some HTML text here'; 113 | $scope.api = { 114 | scope: $scope, 115 | $scope.editorConfig = { 116 | sanitize: false, 117 | toolbar: [ 118 | { name: 'basicStyling', items: ['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', '-', 'leftAlign', 'centerAlign', 'rightAlign', 'blockJustify', '-'] }, 119 | { name: 'paragraph', items: ['orderedList', 'unorderedList', 'outdent', 'indent', '-'] }, 120 | { name: 'doers', items: ['removeFormatting', 'undo', 'redo', '-'] }, 121 | { name: 'colors', items: ['fontColor', 'backgroundColor', '-'] }, 122 | { name: 'links', items: ['image', 'hr', 'symbols', 'link', 'unlink', '-'] }, 123 | { name: 'tools', items: ['print', '-'] }, 124 | { name: 'styling', items: ['font', 'size', 'format'] }, 125 | ] 126 | }; 127 | insertImage: function() { 128 | var deferred = $q.defer(); 129 | $timeout(function() { 130 | var val = prompt('Enter image url', 'http://'); 131 | if(val) { 132 | deferred.resolve(''); 133 | } 134 | else { 135 | deferred.reject(null); 136 | } 137 | }, 1000); 138 | return deferred.promise; 139 | } 140 | }; 141 | }]); 142 | ```` 143 | Make sure you feed the api object to the directive like this: 144 | 145 | ```HTML 146 | 147 | ``` 148 | 149 | ### Simple download (aka git clone/fork) 150 | 151 | 1. Include dist/wysiwyg.min.js in your project using script tag. 152 | 1. Include dist/editor.min.js in your project using link tag. 153 | 2. Add dependency to `ngWYSIWYG` to your app module. Example: ```angular.module('myApp', ['ngWYSIWYG'])```. 154 | 3. Add element ``````. 155 | 156 | Maintenance 157 | ========================= 158 | 159 | ### Roadmap 160 | 161 | - Current cursor/caret position style reflection on the toolbar 162 | - Material Design 163 | - Implement tests 164 | - Look for the Angular 2.0 165 | 166 | ### Issues? 167 | 168 | If you find any, please let me know by sumbitting an issue request. I will be working on it actively. 169 | 170 | ## Contributers 171 | 172 | Contributions are welcome and special thanks to all the contributions! 173 | 174 | ## License 175 | 176 | [MIT license](http://opensource.org/licenses/MIT) 177 | -------------------------------------------------------------------------------- /bower.help: -------------------------------------------------------------------------------- 1 | http://briantford.com/blog/angular-bower 2 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngWYSIWYG", 3 | "version": "0.6.2", 4 | "homepage": "https://github.com/psergus/ngWYSIWYG", 5 | "authors": [ 6 | "Sergey Petrenko " 7 | ], 8 | "description": "Angular WYSIWYG directive", 9 | "main": [ 10 | "dist/editor.min.css", 11 | "dist/wysiwyg.min.js", 12 | "dist/images/header-bg.gif", 13 | "dist/images/icons.png", 14 | "dist/images/resize.gif" 15 | ], 16 | "keywords": [ 17 | "angular", 18 | "wysiwyg", 19 | "texteditor", 20 | "angular", 21 | "iframe", 22 | "angular", 23 | "directive", 24 | "angualr", 25 | "rich", 26 | "text", 27 | "angular", 28 | "wysiwyg", 29 | "angular", 30 | "angular", 31 | "editor" 32 | ], 33 | "license": "MIT", 34 | "ignore": [ 35 | "**/.*", 36 | "node_modules", 37 | "bower_components", 38 | "test", 39 | "tests" 40 | ], 41 | "dependencies": { 42 | "angular": "~1.2.0", 43 | "angular-sanitize": "~1.2.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular WYSIWYG Demo 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |

ngWYSIWYG - Angular real rich text editor.

16 | 17 |
18 |
19 |

ng-model's value

20 |
21 | {{content}} 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 47 | 48 | -------------------------------------------------------------------------------- /dist/editor.min.css: -------------------------------------------------------------------------------- 1 | wysiwyg-edit .tinyeditor{border:1px solid #bbb;padding:0 1px 1px;font:12px Verdana,Arial}wysiwyg-edit .tinyeditor iframe{border:none;background:#fff;overflow-x:hidden}wysiwyg-edit .tinyeditor .sizer{min-height:200px;height:400px;position:relative}wysiwyg-edit .tinyeditor .resizer{background:url(images/resize.gif) 15px 15px no-repeat;float:right;height:32px;width:32px;cursor:ns-resize}wysiwyg-edit .tinyeditor .tinyeditor-header{height:auto;border-bottom:1px solid #bbb;background:url(images/header-bg.gif);padding-top:1px}wysiwyg-edit .tinyeditor .tinyeditor-header select{float:left;width:220px;border:1px solid #ccc;background-color:#fff;height:30px;line-height:30px}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group{float:left;height:31px}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-size{margin:0 3px}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-style{margin-right:12px}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-divider{float:left;width:1px;height:30px;background:#ccc}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control{float:left;width:34px;height:30px;cursor:pointer;background-image:url(images/icons.png);background-position-x:0}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control:hover{background-color:#fff;background-position-x:34px}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control.pressed{background-color:#D0CFCF}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control-fa{background-image:none;position:relative}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control-fa i{font-size:16px;margin:8px 5px 0 10px;color:#555}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control-fa div.hr{border:1px solid #555;margin:15px 10px}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group:after{clear:left}wysiwyg-edit .tinyeditor .tinyeditor-footer{height:32px;border-top:1px solid #bbb;background:#f5f5f5;margin-top:10px}wysiwyg-edit .tinyeditor .tinyeditor-footer .toggle{float:left;background:url(images/icons.png) -34px 2px no-repeat;padding:9px 13px 0 31px;height:23px;border-right:1px solid #ccc;cursor:pointer;color:#666}wysiwyg-edit .tinyeditor .tinyeditor-footer .toggle:hover{background-color:#fff}wysiwyg-edit .tinyeditor .resize{float:right;height:32px;width:32px;background:url(images/resize.gif) 15px 15px no-repeat;cursor:s-resize}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control .ngp-colors-grid{position:absolute;left:0;top:30px;background-color:#fff;width:192px;border:2px solid #000;padding:0;margin:0;z-index:100}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control .ngp-colors-grid>li{list-style:none;border:2px solid #fff;float:left;width:20px;height:20px}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control .ngp-colors-grid>li:hover{opacity:.7}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control .ngp-symbols-grid{position:absolute;left:0;top:30px;background-color:#fff;width:384px;border:2px solid #000;padding:0;margin:0;z-index:100}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control .ngp-symbols-grid>li{list-style:none;border:1px solid #e7e7e7;float:left;width:30px;height:30px;text-align:center;font-size:1.3em}wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control .ngp-symbols-grid>li:hover{opacity:.7;font-size:1.7em}@media only screen and (max-width:500px){wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-font,wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-size,wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-style{width:80px}}@media only screen and (max-width:768px){wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-font,wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-size,wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-style{width:80px}} -------------------------------------------------------------------------------- /dist/images/header-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/dist/images/header-bg.gif -------------------------------------------------------------------------------- /dist/images/header-bg.orig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/dist/images/header-bg.orig.gif -------------------------------------------------------------------------------- /dist/images/icons.old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/dist/images/icons.old.png -------------------------------------------------------------------------------- /dist/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/dist/images/icons.png -------------------------------------------------------------------------------- /dist/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/dist/images/logo.png -------------------------------------------------------------------------------- /dist/images/resize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/dist/images/resize.gif -------------------------------------------------------------------------------- /dist/wysiwyg.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports?module.exports=t(require("angular")):"function"==typeof define&&define.amd?define(["angular"],t):t(e.angular)}(this,function(e){"use strict";e.module("ngWYSIWYG",["ngSanitize"]),e.module("ngWYSIWYG").config(["$provide",function(e){e.decorator("$sanitize",["$delegate","$log",function(e,t){return function(t,n){var o=e(t,n);return o}}])}]),e.module("ngWYSIWYG").constant("NGP_EVENTS",{ELEMENT_CLICKED:"ngp-element-clicked",CLICK_AWAY:"ngp-click-away"}),e.module("ngWYSIWYG").directive("ngpColorsGrid",["NGP_EVENTS",function(e){var t=function(t,n){t.$on(e.CLICK_AWAY,function(){t.$apply(function(){t.show=!1})}),n.parent().bind("click",function(e){e.stopPropagation()}),t.colors=["#000000","#993300","#333300","#003300","#003366","#000080","#333399","#333333","#800000","#FF6600","#808000","#008000","#008080","#0000FF","#666699","#808080","#FF0000","#FF9900","#99CC00","#339966","#33CCCC","#3366FF","#800080","#999999","#FF00FF","#FFCC00","#FFFF00","#00FF00","#00FFFF","#00CCFF","#993366","#C0C0C0","#FF99CC","#FFCC99","#FFFF99","#CCFFCC","#CCFFFF","#99CCFF","#CC99FF","#FFFFFF"],t.pick=function(e){t.onPick({color:e})},n.ready(function(){function e(t){1==t.nodeType&&(t.setAttribute("unselectable","on"),t.unselectable="on");for(var n=t.firstChild;n;)e(n),n=n.nextSibling}for(var t=0;t
  • '}}]),e.module("ngWYSIWYG").directive("ngpSymbolsGrid",["NGP_EVENTS",function(e){var t=function(t,n){t.$on(e.CLICK_AWAY,function(){t.$apply(function(){t.show=!1})}),n.parent().bind("click",function(e){e.stopPropagation()}),t.symbols=["¡","¿","–","—","»","«","©","÷","µ","¶","±","¢","€","£","®","§","™","¥","°","∀","∂","∃","∅","∇","∈","∉","∋","∏","∑","↑","→","↓","♠","♣","♥","♦","á","à","â","å","ã","ä","æ","ç","é","è","ê","ë","í","ì","î","ï","ñ","ó","ò","ô","ø","õ","ö","ß","ú","ù","û","ü","ÿ"],t.pick=function(e){t.onPick({symbol:e})},n.ready(function(){function e(t){1==t.nodeType&&(t.setAttribute("unselectable","on"),t.unselectable="on");for(var n=t.firstChild;n;)e(n),n=n.nextSibling}for(var t=0;t
  • '}}]),e.module("ngWYSIWYG").service("ngpImageResizer",["NGP_EVENTS",function(e){function t(e){e.preventDefault()}function n(e){e.preventDefault(),e.stopPropagation(),v.style.height="",v.style.width="",l()}function o(e){e.preventDefault(),e.stopPropagation(),v.style.width="100%",v.style.height="",l()}function i(e){return e.target!=y?(c.removeEventListener("mousemove",r),void(h=!1)):(e.stopPropagation(),e.preventDefault(),c.addEventListener("mousemove",r),void(h=!0))}function r(e){e.stopPropagation(),e.preventDefault();var t=e.pageY,n=t-(v.getBoundingClientRect().top+d.pageYOffset);v.style.height=n+"px",v.style.width="",g&&e.clientY>g&&d.innerHeight-e.clientY<=45&&d.scrollTo(0,d.innerHeight),g=e.clientY,l()}function a(e,t){return t==p||h?void c.removeEventListener("mousemove",r):"IMG"!==t.tagName?s():(p.parentNode||u.appendChild(p),v=t,void l())}function l(){var e=d.getComputedStyle(v);p.style.height=e.getPropertyValue("height"),p.style.width=e.getPropertyValue("width"),p.style.top=v.getBoundingClientRect().top+d.pageYOffset+"px",p.style.left=v.getBoundingClientRect().left+d.pageXOffset+"px",p.style.display="block"}function s(e){p.parentNode&&(e&&"IMG"===e.target.tagName||(p.style.display="none",g=null))}var c,d,u,p,g,m,y,f,h,v,b=this;b.setup=function(r,l){d=l.defaultView,c=l,u=c.querySelector("body"),m=r,p=c.createElement("div"),p.className="ngp-image-resizer",p.style.position="absolute",p.style.border="1px dashed black",p.style.display="none",p.setAttribute("contenteditable",!1),y=c.createElement("div"),y.style.position="absolute",y.style.height="10px",y.style.width="10px",y.style.bottom="-5px",y.style.right="-5px",y.style.border="1px solid black",y.style.backgroundColor="#fff",y.style.cursor="se-resize",y.setAttribute("contenteditable",!1),p.appendChild(y),f=c.createElement("div"),f.style.position="absolute",f.style.height="30px",f.style.width="150px",f.style.bottom="-30px",f.style.left="0",p.appendChild(f);var g=c.createElement("button");g.addEventListener("click",n),g.innerHTML="Auto",f.appendChild(g);var h=c.createElement("button");h.addEventListener("click",o),h.innerHTML="100%",f.appendChild(h),c.addEventListener("mousedown",i),c.addEventListener("mouseup",i),d.parent.document.addEventListener("mouseup",i),u.addEventListener("mscontrolselect",t),m.$on(e.ELEMENT_CLICKED,a),m.$on(e.CLICK_AWAY,s)}}]);var t='
    {toolbar}
    ';return e.module("ngWYSIWYG").directive("wysiwygEdit",["ngpUtils","NGP_EVENTS","$rootScope","$compile","$timeout","$q",function(n,o,i,r,a,l){var s=function(a,s,c,d){function u(){null==m&&(m=document.querySelector("wysiwyg-edit").querySelector("iframe"),y=m.contentDocument,f=y.defaultView)}function p(e){a.$broadcast("insertElement",e)}function g(e){var t="<"+e.type;if(t+=' class="'+e["class"],h&&(t+=" tinyeditor-control-fa"),t+='" ',"div"==e.type){if(e.title&&(t+='title="'+e.title+'" '),e.backgroundPos&&!h&&(t+='style="background-position: '+e.backgroundPos+'; position: relative;" '),e.pressed&&(t+="ng-class=\"{'pressed': cursorStyle."+e.pressed+'}" '),e.command){var n="'"+e.command+"'";e.commandParameter&&(n+=", '"+e.commandParameter+"'"),t+='ng-click="execCommand('+n+')" '}else e.specialCommand&&(t+='ng-click="'+e.specialCommand+'" ');t+=">",e.faIcon&&h&&"-"!=e.faIcon&&(t+=''),e.faIcon&&h&&"-"==e.faIcon&&(t+='
    '),e.inner&&(t+=e.inner)}else"select"==e.type&&(t+='ng-model="'+e.model+'" ',t+='ng-options="'+e.options+'" ',t+='ng-change="'+e.change+'" ',t+='");return t+=""}a.editMode=!1,a.cursorStyle={},document.addEventListener("click",function(){i.$broadcast(o.CLICK_AWAY)});var m=null,y=null,f=null;a.panelButtons={"-":{type:"div","class":"tinyeditor-divider"},bold:{type:"div",title:"Bold","class":"tinyeditor-control",faIcon:"bold",backgroundPos:"34px -120px",pressed:"bold",command:"bold"},italic:{type:"div",title:"Italic","class":"tinyeditor-control",faIcon:"italic",backgroundPos:"34px -150px",pressed:"italic",command:"italic"},underline:{type:"div",title:"Underline","class":"tinyeditor-control",faIcon:"underline",backgroundPos:"34px -180px",pressed:"underline",command:"underline"},strikethrough:{type:"div",title:"Strikethrough","class":"tinyeditor-control",faIcon:"strikethrough",backgroundPos:"34px -210px",pressed:"strikethrough",command:"strikethrough"},subscript:{type:"div",title:"Subscript","class":"tinyeditor-control",faIcon:"subscript",backgroundPos:"34px -240px",pressed:"sub",command:"subscript"},superscript:{type:"div",title:"Superscript","class":"tinyeditor-control",faIcon:"superscript",backgroundPos:"34px -270px",pressed:"super",command:"superscript"},leftAlign:{type:"div",title:"Left Align","class":"tinyeditor-control",faIcon:"align-left",backgroundPos:"34px -420px",pressed:"alignmet == 'left'",command:"justifyleft"},centerAlign:{type:"div",title:"Center Align","class":"tinyeditor-control",faIcon:"align-center",backgroundPos:"34px -450px",pressed:"alignment == 'center'",command:"justifycenter"},rightAlign:{type:"div",title:"Right Align","class":"tinyeditor-control",faIcon:"align-right",backgroundPos:"34px -480px",pressed:"alignment == 'right'",command:"justifyright"},blockJustify:{type:"div",title:"Block Justify","class":"tinyeditor-control",faIcon:"align-justify",backgroundPos:"34px -510px",pressed:"alignment == 'justify'",command:"justifyfull"},orderedList:{type:"div",title:"Insert Ordered List","class":"tinyeditor-control",faIcon:"list-ol",backgroundPos:"34px -300px",command:"insertorderedlist"},unorderedList:{type:"div",title:"Insert Unordered List","class":"tinyeditor-control",faIcon:"list-ul",backgroundPos:"34px -330px",command:"insertunorderedlist"},outdent:{type:"div",title:"Outdent","class":"tinyeditor-control",faIcon:"outdent",backgroundPos:"34px -360px",command:"outdent"},indent:{type:"div",title:"Indent","class":"tinyeditor-control",faIcon:"indent",backgroundPos:"34px -390px",command:"indent"},removeFormatting:{type:"div",title:"Remove Formatting","class":"tinyeditor-control",faIcon:"eraser",backgroundPos:"34px -720px",command:"removeformat"},undo:{type:"div",title:"Undo","class":"tinyeditor-control",faIcon:"undo",backgroundPos:"34px -540px",command:"undo"},redo:{type:"div",title:"Redo","class":"tinyeditor-control",faIcon:"repeat",backgroundPos:"34px -570px",command:"redo"},fontColor:{type:"div",title:"Font Color","class":"tinyeditor-control",faIcon:"font",backgroundPos:"34px -779px",specialCommand:"showFontColors = !showFontColors",inner:''},backgroundColor:{type:"div",title:"Background Color","class":"tinyeditor-control",faIcon:"paint-brush",backgroundPos:"34px -808px",specialCommand:"showBgColors = !showBgColors",inner:''},image:{type:"div",title:"Insert Image","class":"tinyeditor-control",faIcon:"picture-o",backgroundPos:"34px -600px",specialCommand:"insertImage()"},hr:{type:"div",title:"Insert Horizontal Rule","class":"tinyeditor-control",faIcon:"-",backgroundPos:"34px -630px",command:"inserthorizontalrule"},symbols:{type:"div",title:"Insert Special Symbol","class":"tinyeditor-control",faIcon:"cny",backgroundPos:"34px -838px",specialCommand:"showSpecChars = !showSpecChars",inner:''},link:{type:"div",title:"Insert Hyperlink","class":"tinyeditor-control",faIcon:"link",backgroundPos:"34px -660px",specialCommand:"insertLink()"},unlink:{type:"div",title:"Remove Hyperlink","class":"tinyeditor-control",faIcon:"chain-broken",backgroundPos:"34px -690px",command:"unlink"},print:{type:"div",title:"Print","class":"tinyeditor-control",faIcon:"print",backgroundPos:"34px -750px",command:"print"},font:{type:"select",title:"Font","class":"tinyeditor-font",model:"font",options:"a as a for a in fonts",change:"fontChange()"},size:{type:"select",title:"Size","class":"tinyeditor-size",model:"fontsize",options:"a.key as a.name for a in fontsizes",change:"sizeChange()"},format:{type:"select",title:"Style","class":"tinyeditor-size",model:"textstyle",options:"s.key as s.name for s in styles",change:"styleChange()"}};var h=a.config&&a.config.fontAwesome;a.toolbar=a.config&&a.config.toolbar?a.config.toolbar:[{name:"basicStyling",items:["bold","italic","underline","strikethrough","subscript","superscript","leftAlign","centerAlign","rightAlign","blockJustify","-"]},{name:"paragraph",items:["orderedList","unorderedList","outdent","indent","-"]},{name:"doers",items:["removeFormatting","undo","redo","-"]},{name:"colors",items:["fontColor","backgroundColor","-"]},{name:"links",items:["image","hr","symbols","link","unlink","-"]},{name:"tools",items:["print","-"]},{name:"styling",items:["font","size","format"]}];var v=[];e.forEach(a.toolbar,function(t,n){var o=[];e.forEach(t.items,function(e,t){var n=a.panelButtons[e];n||(n=a.config.buttons[e]),this.push(g(n))},o),this.push('
    '+o.join("")+"
    ")},v);var b=t.replace("{toolbar}",v.join(""));b=b.replace("{contentStyle}",c.contentStyle||""),s.html(b),r(s.contents())(a),a.execCommand=function(e,t){switch(e){case"bold":a.cursorStyle.bold=!a.cursorStyle.bold;break;case"italic":a.cursorStyle.italic=!a.cursorStyle.italic;break;case"underline":a.cursorStyle.underline=!a.cursorStyle.underline;break;case"strikethrough":a.cursorStyle.strikethrough=!a.cursorStyle.strikethrough;break;case"subscript":a.cursorStyle.sub=!a.cursorStyle.sub;break;case"superscript":a.cursorStyle["super"]=!a.cursorStyle["super"];break;case"justifyleft":a.cursorStyle.alignment="left";break;case"justifycenter":a.cursorStyle.alignment="center";break;case"justifyright":a.cursorStyle.alignment="right";break;case"justifyfull":a.cursorStyle.alignment="justify"}a.$broadcast("execCommand",{command:e,arg:t})},a.fonts=["Verdana","Arial","Arial Black","Arial Narrow","Courier New","Century Gothic","Comic Sans MS","Georgia","Impact","Tahoma","Times","Times New Roman","Webdings","Trebuchet MS"],a.fontChange=function(){a.execCommand("fontname",a.font)},a.fontsizes=[{key:1,name:"x-small"},{key:2,name:"small"},{key:3,name:"normal"},{key:4,name:"large"},{key:5,name:"x-large"},{key:6,name:"xx-large"},{key:7,name:"xxx-large"}],a.mapFontSize={10:1,13:2,16:3,18:4,24:5,32:6,48:7},a.sizeChange=function(){a.execCommand("fontsize",a.fontsize)},a.styles=[{name:"Paragraph",key:"

    "},{name:"Header 1",key:"

    "},{name:"Header 2",key:"

    "},{name:"Header 3",key:"

    "},{name:"Header 4",key:"

    "},{name:"Header 5",key:"

    "},{name:"Header 6",key:"
    "}],a.styleChange=function(){a.execCommand("formatblock",a.textstyle)},a.showFontColors=!1,a.setFontColor=function(e){a.execCommand("foreColor",e)},a.showBgColors=!1,a.setBgColor=function(e){a.execCommand("hiliteColor",e)},a.showSpecChars=!1,a.insertSpecChar=function(e){p(e)},a.insertLink=function(){if(u(),null!=f.getSelection().focusNode){var t=n.getSelectionBoundaryElement(f,!0),o="http://";if(t&&"A"==t.nodeName){o=t.href;var i=y.createRange();i.setStart(t.firstChild,0),i.setEnd(t.firstChild,t.firstChild.length);var r=f.getSelection();r.removeAllRanges(),r.addRange(i)}var s;s=a.api&&a.api.insertLink&&e.isFunction(a.api.insertLink)?a.api.insertLink.apply(a.api.scope||null,[o]):prompt("Please enter the URL","http://"),l.when(s).then(function(e){a.execCommand("createlink",e)})}},a.insertImage=function(){var t;a.api&&a.api.insertImage&&e.isFunction(a.api.insertImage)?t=a.api.insertImage.apply(a.api.scope||null):(t=prompt("Please enter the picture URL","http://"),t=''),l.when(t).then(function(e){p(e)})},s.ready(function(){function e(t){1==t.nodeType&&(t.setAttribute("unselectable","on"),t.unselectable="on");for(var n=t.firstChild;n;)e(n),n=n.nextSibling}for(var t=0;t'),d.close(),d.designMode="On",t.setup(i,d);var u=e.element(l[0].contentDocument.body),p=e.element(l[0].contentDocument.head);u.attr("contenteditable","true"),d.addEventListener("click",function(e){"HTML"===e.target.tagName&&e.target.querySelector("body").focus(),i.$emit(o.ELEMENT_CLICKED,e.target)}),s.contentStyle&&p.append(''),c.$render=function(){u[0].innerHTML=c.$viewValue?i.config&&i.config.sanitize?a(c.$viewValue):c.$viewValue:""},i.sync=function(){i.$evalAsync(function(e){c.$setViewValue(u.html())})};var g=null;u.bind("click keyup change paste",function(){g&&r.cancel(g),g=r(function(){var e=u[0].ownerDocument,t=e.querySelector(".ngp-image-resizer"),o=u[0].innerHTML;t&&(o=o.replace(t.outerHTML,"")),c.$setViewValue(o);var r=n.getSelectionBoundaryElement(l[0].contentWindow,!0);if(r){var a=l[0].contentWindow.getComputedStyle(r),s={bold:"bold"==a.getPropertyValue("font-weight")||parseInt(a.getPropertyValue("font-weight"))>=700,italic:"italic"==a.getPropertyValue("font-style"),underline:"underline"==a.getPropertyValue("text-decoration"),strikethrough:"line-through"==a.getPropertyValue("text-decoration"),font:a.getPropertyValue("font-family"),size:parseInt(a.getPropertyValue("font-size")),color:a.getPropertyValue("color"),sub:"sub"==a.getPropertyValue("vertical-align"),"super":"super"==a.getPropertyValue("vertical-align"),background:a.getPropertyValue("background-color"),alignment:a.getPropertyValue("text-align")};i.$emit("cursor-position",s)}},100,!0)}),i.range=null,i.getSelection=function(){if(d.getSelection){var e=d.getSelection();e.getRangeAt&&e.rangeCount&&(i.range=e.getRangeAt(0))}},i.restoreSelection=function(){if(i.range&&d.getSelection){var e=d.getSelection();e.removeAllRanges(),e.addRange(i.range)}},i.$on("execCommand",function(e,t){console.log("execCommand: "),console.log(t),l[0].contentDocument.body.focus();var n=d.selection;if(n){var o=n.createRange();d.execCommand(t.command,0,t.arg),o.collapse(!1),o.select()}else d.execCommand(t.command,0,t.arg);d.body.focus(),i.sync()}),i.$on("insertElement",function(e,t){var n,o;if(d.defaultView.getSelection){if(n=d.defaultView.getSelection(),n.getRangeAt&&n.rangeCount){o=n.getRangeAt(0),o.deleteContents();var r=d.createElement("div");r.innerHTML=t;for(var a,l,s=d.createDocumentFragment();a=r.firstChild;)l=s.appendChild(a);s.firstChild;o.insertNode(s),l&&(o=o.cloneRange(),o.setStartAfter(l),o.collapse(!0),n.removeAllRanges(),n.addRange(o))}}else d.selection&&"Control"!=d.selection.type&&d.selection.createRange().pasteHTML(t);i.sync()}),i.$on("$destroy",function(){});try{d.execCommand("styleWithCSS",0,0),d.execCommand("enableObjectResizing",!1,"false"),d.execCommand("contentReadOnly",0,"false")}catch(m){try{d.execCommand("useCSS",0,1)}catch(m){}}};return{link:l,require:"ngModel",scope:{config:"=ngpContentFrame"},replace:!0,restrict:"AE"}}]),e.module("ngWYSIWYG").directive("ngpResizable",["$document",function(e){return function(t,n){var o=e[0],i=n[0],r=o.createElement("span");r.className="resizer",i.appendChild(r),r.addEventListener("mousedown",function(){function e(e){e.preventDefault();var t=e.pageY;e.view!=o.defaultView&&(t=e.pageY+e.view.frameElement.getBoundingClientRect().top+o.defaultView.pageYOffset);var n=t-(i.getBoundingClientRect().top+o.defaultView.pageYOffset),r=i.style.height.replace("px","");r&&n>r&&window.innerHeight-e.clientY<=45&&o.defaultView.scrollBy(0,n-r),i.style.height=n+"px"}function t(){o.removeEventListener("mousemove",e),o.removeEventListener("mouseup",t);for(var n=o.querySelectorAll("iframe"),i=0;i0&&(n=o.getRangeAt(0),i=n[t?"startContainer":"endContainer"],3===i.nodeType&&(i=i.parentNode))):e.getSelection&&(o=e.getSelection(),o.rangeCount>0&&(n=o.getRangeAt(0),i=n[t?"startContainer":"endContainer"],3===i.nodeType&&(i=i.parentNode))),i)}}]),"ngWYSIWYG"}); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var git = require('gulp-git'); 3 | var filter = require('gulp-filter'); 4 | var tag_version = require('gulp-tag-version'); 5 | var gulpif = require('gulp-if'); 6 | var sass = require('gulp-sass'); 7 | var minifyCss = require('gulp-minify-css'); 8 | var clean = require('gulp-clean'); 9 | var concat = require('gulp-concat'); 10 | var uglify = require('gulp-uglify'); 11 | var webserver = require('gulp-webserver'); 12 | var bump = require('gulp-bump'); 13 | var rename = require('gulp-rename'); 14 | var protractor = require("gulp-protractor").protractor; 15 | var _ = require('underscore'); 16 | var umd = require('gulp-umd'); 17 | 18 | var development = false; 19 | var webserverInstance; 20 | 21 | gulp.task('set-development-mode', function() { 22 | development = true; 23 | }); 24 | 25 | gulp.task('watch', function() { 26 | gulp.watch(['./src/**/*'], ['minify', 'uglify', 'copy-images']); 27 | }); 28 | 29 | gulp.task('webserver', function() { 30 | webserverInstance = gulp.src('./dev').pipe(webserver({ host: '0.0.0.0', port: 8000 })); 31 | }); 32 | 33 | gulp.task('develop', ['set-development-mode', 'minify', 'uglify', 'copy-images', 'watch', 'webserver']); 34 | 35 | function getDestination() { 36 | if (development) { 37 | return './dev'; 38 | } 39 | return './dist'; 40 | } 41 | 42 | function renameMin(path) { 43 | path.basename += ".min"; 44 | return path; 45 | } 46 | 47 | gulp.task('sass', function() { 48 | return gulp.src('./src/css/**/*.sass') 49 | .pipe(sass().on('error', sass.logError)) 50 | .pipe(gulp.dest(getDestination())); 51 | }); 52 | 53 | gulp.task('minify', ['sass'], function() { 54 | return gulp.src(getDestination() + '/editor.css') 55 | .pipe(gulpif(development === false, rename(renameMin))) 56 | .pipe(minifyCss({compatibility: 'ie8'})) 57 | .pipe(gulp.dest(getDestination())); 58 | }); 59 | 60 | gulp.task('clean-css', ['minify'], function () { 61 | return gulp.src(getDestination() + '/editor.css', {read: false}) 62 | .pipe(clean()); 63 | }); 64 | 65 | gulp.task('concat-js', function() { 66 | return gulp.src([ 67 | './src/js/wysiwyg.js', 68 | './src/js/ngpColorsGrid.js', 69 | './src/js/ngpSymbolsGrid.js', 70 | './src/js/ngpImageResizer.js', 71 | './src/js/wysiwygEdit.js', 72 | './src/js/ngpContentFrame.js', 73 | './src/js/ngpResizable.js', 74 | './src/js/ngpUtils.js' 75 | ]) 76 | .pipe(concat('wysiwyg.js')) 77 | .pipe(umd({ 78 | dependencies: function (file) { 79 | return [{ 80 | name: 'angular', 81 | amd: 'angular', 82 | cjs: 'angular', 83 | global: 'angular', 84 | param: 'angular' 85 | }]; 86 | }, 87 | exports: function (file) { 88 | return "'ngWYSIWYG'"; 89 | }, 90 | //template: umdTemplates.returnExportsNoNamespace.path, 91 | templateSource: '(function(root, factory) {\r\n' + 92 | 'if (typeof exports === "object") {\r\n' + 93 | 'module.exports = factory(<%= cjs %>);\r\n' + 94 | '} else if (typeof define === "function" && define.amd) {\r\n' + 95 | 'define(<%= amd %>, factory);\r\n' + 96 | '} else{\r\n' + 97 | 'factory(<%= global %>);\r\n' + 98 | '}\r\n' + 99 | '}(this, function(<%= param %>) {\r\n' + 100 | '<%= contents %>\r\n' + 101 | 'return <%= exports %>;\r\n' + 102 | '}));' 103 | })) 104 | .pipe(gulp.dest('./dev')); 105 | }); 106 | 107 | gulp.task('uglify', ['concat-js'], function() { 108 | return gulp.src('./dev/wysiwyg.js') 109 | .pipe(gulpif(development === false, uglify({ mangle: true }))) 110 | .pipe(gulpif(development === false, rename(renameMin))) 111 | .pipe(gulp.dest(getDestination())); 112 | }); 113 | 114 | gulp.task('copy-images', function() { 115 | return gulp.src('./src/images/**/*') 116 | .pipe(gulp.dest(getDestination() + '/images/')); 117 | }); 118 | 119 | gulp.task('run-tests', ['webserver'], function() { 120 | return gulp.src(['./src/tests/*.js', '!./src/tests/conf.js']) 121 | .pipe(protractor({ 122 | configFile: './src/tests/conf.js', 123 | args: ['--baseUrl', 'http://127.0.0.1:8000'] 124 | })) 125 | .on('error', function(e) { throw e; }); 126 | }); 127 | 128 | gulp.task('tests', ['run-tests'], function() { 129 | webserverInstance.emit('kill'); 130 | }); 131 | 132 | gulp.task('build', ['clean-css', 'uglify', 'copy-images']); 133 | 134 | function inc(importance) { 135 | // get all the files to bump version in 136 | return gulp.src(['./package.json', './bower.json']) 137 | // bump the version number in those files 138 | .pipe(bump({type: importance})) 139 | // save it back to filesystem 140 | .pipe(gulp.dest('./')) 141 | // commit the changed version number 142 | .pipe(git.commit('bumps package version')) 143 | 144 | // read only one file to get the version number 145 | .pipe(filter('package.json')) 146 | // **tag it in the repository** 147 | .pipe(tag_version()); 148 | } 149 | 150 | gulp.task('patch', function() { return inc('patch'); }); 151 | gulp.task('feature', function() { return inc('minor'); }); 152 | gulp.task('release', function() { return inc('major'); }); 153 | 154 | gulp.task('push', function() { 155 | var packageJson = require('./package.json'); 156 | git.push('origin', 'v' + packageJson.version, function (err) { 157 | if (err) throw err; 158 | }); 159 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-wysiwyg", 3 | "version": "0.6.2", 4 | "main": "dist/wysiwyg.min.js", 5 | "description": "true angular WYSIWYG", 6 | "devDependencies": { 7 | "gulp": "^3.9.0", 8 | "gulp-bump": "^1.0.0", 9 | "gulp-clean": "^0.3.1", 10 | "gulp-concat": "^2.6.0", 11 | "gulp-filter": "^3.0.1", 12 | "gulp-git": "^1.6.1", 13 | "gulp-if": "^2.0.0", 14 | "gulp-minify": "0.0.5", 15 | "gulp-minify-css": "^1.2.3", 16 | "gulp-protractor": "^2.1.0", 17 | "gulp-rename": "^1.2.2", 18 | "gulp-sass": "^2.1.1", 19 | "gulp-tag-version": "^1.3.0", 20 | "gulp-uglify": "^1.5.1", 21 | "gulp-umd": "~0.2", 22 | "gulp-webserver": "^0.9.1", 23 | "underscore": "^1.8.3" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/psergus/ngWYSIWYG.git" 28 | }, 29 | "keywords": [ 30 | "angular", 31 | "wysiwyg", 32 | "editor" 33 | ], 34 | "author": "Sergey Petrenko", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/psergus/ngWYSIWYG/issues" 38 | }, 39 | "homepage": "https://github.com/psergus/ngWYSIWYG#readme" 40 | } 41 | -------------------------------------------------------------------------------- /src/css/editor.sass: -------------------------------------------------------------------------------- 1 | wysiwyg-edit .tinyeditor 2 | border: 1px solid #bbb 3 | padding: 0 1px 1px 4 | font: 12px Verdana,Arial 5 | iframe 6 | border: none 7 | background: white 8 | overflow-x: hidden 9 | .sizer 10 | min-height: 200px 11 | height: 400px 12 | position: relative 13 | .resizer 14 | background: url('images/resize.gif') 15px 15px no-repeat 15 | float: right 16 | height: 32px 17 | width: 32px 18 | cursor: ns-resize 19 | .tinyeditor-header 20 | height: auto 21 | border-bottom: 1px solid #bbb 22 | background: url('images/header-bg.gif') repeat 23 | padding-top: 1px 24 | select 25 | float: left 26 | width: 220px 27 | border: 1px solid #cccccc 28 | background-color: #ffffff 29 | height: 30px 30 | line-height: 30px 31 | /*margin-top:5px 32 | .tinyeditor-buttons-group 33 | .tinyeditor-font 34 | /*margin-left:12px 35 | .tinyeditor-size 36 | margin: 0 3px 37 | .tinyeditor-style 38 | margin-right: 12px 39 | .tinyeditor-divider 40 | float: left 41 | width: 1px 42 | height: 30px 43 | background: #ccc 44 | .tinyeditor-control 45 | float: left 46 | width: 34px 47 | height: 30px 48 | cursor: pointer 49 | background-image: url('images/icons.png') 50 | background-position-x: 0px 51 | &:hover 52 | background-color: #fff 53 | background-position-x: 34px 54 | &.pressed 55 | background-color: #D0CFCF 56 | .tinyeditor-control-fa 57 | background-image: none 58 | position: relative 59 | i 60 | font-size: 16px 61 | margin: 8px 5px 0 10px 62 | color: #555 63 | div.hr 64 | border: 1px solid #555 65 | margin: 15px 10px 66 | float: left 67 | height: 31px 68 | &:after 69 | clear: left 70 | .tinyeditor-footer 71 | height: 32px 72 | border-top: 1px solid #bbb 73 | background: #f5f5f5 74 | margin-top: 10px 75 | .toggle 76 | float: left 77 | background: url('images/icons.png') -34px 2px no-repeat 78 | padding: 9px 13px 0 31px 79 | height: 23px 80 | border-right: 1px solid #ccc 81 | cursor: pointer 82 | color: #666 83 | &:hover 84 | background-color: #fff 85 | .resize 86 | float: right 87 | height: 32px 88 | width: 32px 89 | background: url('images/resize.gif') 15px 15px no-repeat 90 | cursor: s-resize 91 | .tinyeditor-header .tinyeditor-buttons-group .tinyeditor-control 92 | .ngp-colors-grid 93 | position: absolute 94 | left: 0 95 | top: 30px 96 | background-color: white 97 | width: 192px 98 | border: black solid 2px 99 | padding: 0 100 | margin: 0 101 | z-index: 100 102 | > li 103 | list-style: none 104 | border: white solid 2px 105 | float: left 106 | width: 20px 107 | height: 20px 108 | &:hover 109 | opacity: 0.7 110 | .ngp-symbols-grid 111 | position: absolute 112 | left: 0 113 | top: 30px 114 | background-color: white 115 | width: 384px 116 | border: black solid 2px 117 | padding: 0 118 | margin: 0 119 | z-index: 100 120 | > li 121 | list-style: none 122 | border: rgb(231, 231, 231) solid 1px 123 | float: left 124 | width: 30px 125 | height: 30px 126 | text-align: center 127 | font-size: 1.3em 128 | &:hover 129 | opacity: 0.7 130 | font-size: 1.7em 131 | 132 | @media only screen and (max-width: 500px) 133 | wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group 134 | .tinyeditor-font, .tinyeditor-size, .tinyeditor-style 135 | width: 80px 136 | 137 | @media only screen and (max-width: 768px) 138 | wysiwyg-edit .tinyeditor .tinyeditor-header .tinyeditor-buttons-group 139 | .tinyeditor-font, .tinyeditor-size, .tinyeditor-style 140 | width: 80px 141 | -------------------------------------------------------------------------------- /src/images/header-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/src/images/header-bg.gif -------------------------------------------------------------------------------- /src/images/header-bg.orig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/src/images/header-bg.orig.gif -------------------------------------------------------------------------------- /src/images/icons.old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/src/images/icons.old.png -------------------------------------------------------------------------------- /src/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/src/images/icons.png -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/src/images/logo.png -------------------------------------------------------------------------------- /src/images/resize.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psergus/ngWYSIWYG/2310e757188e2da0270f050051848ed000aba6ad/src/images/resize.gif -------------------------------------------------------------------------------- /src/js/ngpColorsGrid.js: -------------------------------------------------------------------------------- 1 | angular.module('ngWYSIWYG').directive('ngpColorsGrid', ['NGP_EVENTS', function(NGP_EVENTS) { 2 | var linker = function (scope, element) { 3 | 4 | //click away 5 | scope.$on(NGP_EVENTS.CLICK_AWAY, function() { 6 | scope.$apply(function() { 7 | scope.show = false; 8 | }); 9 | }); 10 | 11 | element.parent().bind('click', function(e) { 12 | e.stopPropagation(); 13 | }); 14 | 15 | scope.colors = [ 16 | '#000000', '#993300', '#333300', '#003300', '#003366', '#000080', '#333399', '#333333', 17 | '#800000', '#FF6600', '#808000', '#008000', '#008080', '#0000FF', '#666699', '#808080', 18 | '#FF0000', '#FF9900', '#99CC00', '#339966', '#33CCCC', '#3366FF', '#800080', '#999999', 19 | '#FF00FF', '#FFCC00', '#FFFF00', '#00FF00', '#00FFFF', '#00CCFF', '#993366', '#C0C0C0', 20 | '#FF99CC', '#FFCC99', '#FFFF99', '#CCFFCC', '#CCFFFF', '#99CCFF', '#CC99FF', '#FFFFFF' 21 | ]; 22 | 23 | scope.pick = function( color ) { 24 | scope.onPick({color: color}); 25 | }; 26 | 27 | element.ready(function() { 28 | //real deal for IE 29 | function makeUnselectable(node) { 30 | if (node.nodeType == 1) { 31 | node.setAttribute("unselectable", "on"); 32 | node.unselectable = 'on'; 33 | } 34 | var child = node.firstChild; 35 | while (child) { 36 | makeUnselectable(child); 37 | child = child.nextSibling; 38 | } 39 | } 40 | //IE fix 41 | for(var i = 0; i < document.getElementsByClassName('ngp-colors-grid').length; i += 1) { 42 | makeUnselectable(document.getElementsByClassName("ngp-colors-grid")[i]); 43 | } 44 | }); 45 | }; 46 | return { 47 | link: linker, 48 | scope: { 49 | show: '=', 50 | onPick: '&' 51 | }, 52 | restrict: 'AE', 53 | template: '
    ' 54 | }; 55 | }]); -------------------------------------------------------------------------------- /src/js/ngpContentFrame.js: -------------------------------------------------------------------------------- 1 | angular.module('ngWYSIWYG').directive('ngpContentFrame', ['ngpImageResizer', 'ngpUtils', 'NGP_EVENTS', '$compile', 2 | '$timeout', '$sanitize', function(ngpImageResizer, ngpUtils, NGP_EVENTS, $compile, $timeout, $sanitize) { 3 | 4 | //kudos http://stackoverflow.com/questions/13881834/bind-angular-cross-iframes-possible 5 | var linker = function( scope, $element, attrs, ctrl ) { 6 | var $document = $element[0].contentDocument; 7 | $document.open(); //damn Firefox. kudos: http://stackoverflow.com/questions/15036514/why-can-i-not-set-innerhtml-of-an-iframe-body-in-firefox 8 | $document.write(''); 9 | $document.close(); 10 | $document.designMode = 'On'; 11 | ngpImageResizer.setup(scope, $document); 12 | var $body = angular.element($element[0].contentDocument.body); 13 | var $head = angular.element($element[0].contentDocument.head); 14 | $body.attr('contenteditable', 'true'); 15 | 16 | // fixing issue that makes caret disappear on chrome (https://github.com/psergus/ngWYSIWYG/issues/22) 17 | $document.addEventListener('click', function(event) { 18 | if (event.target.tagName === 'HTML') { 19 | event.target.querySelector('body').focus(); 20 | } 21 | scope.$emit(NGP_EVENTS.ELEMENT_CLICKED, event.target); 22 | }); 23 | 24 | // this option enables you to specify a custom CSS to be used within the editor (the editable area) 25 | if (attrs.contentStyle) { 26 | $head.append(''); 27 | } 28 | 29 | //model --> view 30 | ctrl.$render = function() { 31 | //sanitize the input only if defined through config 32 | $body[0].innerHTML = ctrl.$viewValue? ( (scope.config && scope.config.sanitize)? $sanitize(ctrl.$viewValue) : ctrl.$viewValue) : ''; 33 | }; 34 | 35 | scope.sync = function() { 36 | scope.$evalAsync(function(scope) { 37 | ctrl.$setViewValue($body.html()); 38 | }); 39 | }; 40 | 41 | var debounce = null; //we will debounce the event in case of the rapid movement. Overall, we are intereseted in the last cursor/caret position 42 | //view --> model 43 | $body.bind('click keyup change paste', function() { //we removed 'blur' event 44 | //lets debounce it 45 | if(debounce) { 46 | $timeout.cancel(debounce); 47 | } 48 | debounce = $timeout(function blurkeyup() { 49 | var contentDocument = $body[0].ownerDocument; 50 | var imageResizer = contentDocument.querySelector('.ngp-image-resizer'); 51 | var html = $body[0].innerHTML; 52 | if (imageResizer) { 53 | html = html.replace(imageResizer.outerHTML, ''); 54 | } 55 | ctrl.$setViewValue(html); 56 | //check the caret position 57 | //http://stackoverflow.com/questions/14546568/get-parent-element-of-caret-in-iframe-design-mode 58 | var el = ngpUtils.getSelectionBoundaryElement($element[0].contentWindow, true); 59 | if(el) { 60 | var computedStyle = $element[0].contentWindow.getComputedStyle(el); 61 | var elementStyle = { 62 | 'bold': (computedStyle.getPropertyValue("font-weight") == 'bold' || parseInt(computedStyle.getPropertyValue("font-weight")) >= 700), 63 | 'italic': (computedStyle.getPropertyValue("font-style") == 'italic'), 64 | 'underline': (computedStyle.getPropertyValue("text-decoration") == 'underline'), 65 | 'strikethrough': (computedStyle.getPropertyValue("text-decoration") == 'line-through'), 66 | 'font': computedStyle.getPropertyValue("font-family"), 67 | 'size': parseInt(computedStyle.getPropertyValue("font-size")), 68 | 'color': computedStyle.getPropertyValue("color"), 69 | 'sub': (computedStyle.getPropertyValue("vertical-align") == 'sub'), 70 | 'super': (computedStyle.getPropertyValue("vertical-align") == 'super'), 71 | 'background': computedStyle.getPropertyValue("background-color"), 72 | 'alignment': computedStyle.getPropertyValue("text-align") 73 | }; 74 | //dispatch upward the through the scope chain 75 | scope.$emit('cursor-position', elementStyle); 76 | //console.log( JSON.stringify(elementStyle) ); 77 | } 78 | }, 79 | 100/*ms*/, true /*invoke apply*/); 80 | }); 81 | 82 | 83 | scope.range = null; 84 | scope.getSelection = function() { 85 | if($document.getSelection) { 86 | var sel = $document.getSelection(); 87 | if(sel.getRangeAt && sel.rangeCount) { 88 | scope.range = sel.getRangeAt(0); 89 | } 90 | } 91 | }; 92 | scope.restoreSelection = function() { 93 | if(scope.range && $document.getSelection) { 94 | var sel = $document.getSelection(); 95 | sel.removeAllRanges(); 96 | sel.addRange(scope.range); 97 | } 98 | }; 99 | 100 | scope.$on('execCommand', function(e, cmd) { 101 | console.log('execCommand: '); 102 | console.log(cmd); 103 | $element[0].contentDocument.body.focus(); 104 | //scope.getSelection(); 105 | var sel = $document.selection; //http://stackoverflow.com/questions/11329982/how-refocus-when-insert-image-in-contenteditable-divs-in-ie 106 | if (sel) { 107 | var textRange = sel.createRange(); 108 | $document.execCommand(cmd.command, 0, cmd.arg); 109 | textRange.collapse(false); 110 | textRange.select(); 111 | } 112 | else { 113 | $document.execCommand(cmd.command, 0, cmd.arg); 114 | } 115 | //scope.restoreSelection(); 116 | $document.body.focus(); 117 | scope.sync(); 118 | }); 119 | 120 | scope.$on('insertElement', function(event, html) { 121 | var sel, range; 122 | if ($document.defaultView.getSelection) { 123 | sel = $document.defaultView.getSelection(); 124 | if (sel.getRangeAt && sel.rangeCount) { 125 | range = sel.getRangeAt(0); 126 | range.deleteContents(); 127 | 128 | // Range.createContextualFragment() would be useful here but is 129 | // only relatively recently standardized and is not supported in 130 | // some browsers (IE9, for one) 131 | var el = $document.createElement("div"); 132 | el.innerHTML = html; 133 | var frag = $document.createDocumentFragment(), node, lastNode; 134 | while ((node = el.firstChild)) { 135 | lastNode = frag.appendChild(node); 136 | } 137 | var firstNode = frag.firstChild; 138 | range.insertNode(frag); 139 | 140 | // Preserve the selection 141 | if (lastNode) { 142 | range = range.cloneRange(); 143 | range.setStartAfter(lastNode); 144 | range.collapse(true); 145 | sel.removeAllRanges(); 146 | sel.addRange(range); 147 | } 148 | } 149 | } else if ($document.selection && $document.selection.type != "Control") { 150 | // IE < 9 151 | $document.selection.createRange().pasteHTML(html); 152 | } 153 | scope.sync(); 154 | }); 155 | 156 | scope.$on('$destroy', function() { 157 | //clean after myself 158 | 159 | }); 160 | 161 | //init 162 | try { 163 | $document.execCommand("styleWithCSS", 0, 0); // <-- want the Old Schoold elements like or , comment this line. kudos to: http://stackoverflow.com/questions/3088993/webkit-stylewithcss-contenteditable-not-working 164 | $document.execCommand('enableObjectResizing', false, 'false'); 165 | $document.execCommand('contentReadOnly', 0, 'false'); 166 | } 167 | catch(e) { 168 | try { 169 | $document.execCommand("useCSS", 0, 1); 170 | } 171 | catch(e) { 172 | } 173 | } 174 | }; 175 | return { 176 | link: linker, 177 | require: 'ngModel', 178 | scope: { 179 | config: '=ngpContentFrame' 180 | }, 181 | replace: true, 182 | restrict: 'AE' 183 | } 184 | } 185 | ]); -------------------------------------------------------------------------------- /src/js/ngpImageResizer.js: -------------------------------------------------------------------------------- 1 | angular.module('ngWYSIWYG').service('ngpImageResizer', ['NGP_EVENTS', function(NGP_EVENTS) { 2 | var service = this; 3 | var iframeDoc, iframeWindow, iframeBody, resizerContainer, lastVerticalCursorPosition, 4 | iframeScope, keepRatioButton, resizerOptionsContainer, resizing, elementBeingResized; 5 | 6 | service.setup = function(scope, document) { 7 | iframeWindow = document.defaultView; 8 | iframeDoc = document; 9 | iframeBody = iframeDoc.querySelector('body'); 10 | iframeScope = scope; 11 | 12 | // creating resizer container 13 | resizerContainer = iframeDoc.createElement('div'); 14 | resizerContainer.className = 'ngp-image-resizer'; 15 | resizerContainer.style.position = 'absolute'; 16 | resizerContainer.style.border = '1px dashed black'; 17 | resizerContainer.style.display = 'none'; 18 | resizerContainer.setAttribute('contenteditable', false); 19 | 20 | // creating bottom-right resizer button 21 | keepRatioButton = iframeDoc.createElement('div'); 22 | keepRatioButton.style.position = 'absolute'; 23 | keepRatioButton.style.height = '10px'; 24 | keepRatioButton.style.width = '10px'; 25 | keepRatioButton.style.bottom = '-5px'; 26 | keepRatioButton.style.right = '-5px'; 27 | keepRatioButton.style.border = '1px solid black'; 28 | keepRatioButton.style.backgroundColor = '#fff'; 29 | keepRatioButton.style.cursor = 'se-resize'; 30 | keepRatioButton.setAttribute('contenteditable', false); 31 | resizerContainer.appendChild(keepRatioButton); 32 | 33 | // resizer options container 34 | resizerOptionsContainer = iframeDoc.createElement('div'); 35 | resizerOptionsContainer.style.position = 'absolute'; 36 | resizerOptionsContainer.style.height = '30px'; 37 | resizerOptionsContainer.style.width = '150px'; 38 | resizerOptionsContainer.style.bottom = '-30px'; 39 | resizerOptionsContainer.style.left = '0'; 40 | resizerContainer.appendChild(resizerOptionsContainer); 41 | 42 | // resizer options 43 | var resizerReset = iframeDoc.createElement('button'); 44 | resizerReset.addEventListener('click', resetImageSize); 45 | resizerReset.innerHTML = 'Auto'; 46 | resizerOptionsContainer.appendChild(resizerReset); 47 | 48 | var resizer100 = iframeDoc.createElement('button'); 49 | resizer100.addEventListener('click', size100); 50 | resizer100.innerHTML = '100%'; 51 | resizerOptionsContainer.appendChild(resizer100); 52 | 53 | // resizer listener 54 | iframeDoc.addEventListener('mousedown', startResizing); 55 | iframeDoc.addEventListener('mouseup', startResizing); 56 | iframeWindow.parent.document.addEventListener('mouseup', startResizing); 57 | 58 | iframeBody.addEventListener('mscontrolselect', disableIESelect); 59 | 60 | // listening to events 61 | iframeScope.$on(NGP_EVENTS.ELEMENT_CLICKED, createResizer); 62 | iframeScope.$on(NGP_EVENTS.CLICK_AWAY, removeResizer); 63 | }; 64 | 65 | function disableIESelect(event) { 66 | event.preventDefault(); 67 | } 68 | 69 | function resetImageSize(event) { 70 | event.preventDefault(); 71 | event.stopPropagation(); 72 | elementBeingResized.style.height = ''; 73 | elementBeingResized.style.width = ''; 74 | updateResizer(); 75 | } 76 | 77 | function size100(event) { 78 | event.preventDefault(); 79 | event.stopPropagation(); 80 | elementBeingResized.style.width = '100%'; 81 | elementBeingResized.style.height = ''; 82 | updateResizer(); 83 | } 84 | 85 | function startResizing(event) { 86 | if (event.target != keepRatioButton) { 87 | iframeDoc.removeEventListener('mousemove', updateImageSize); 88 | resizing = false; 89 | return; 90 | } 91 | event.stopPropagation(); 92 | event.preventDefault(); 93 | iframeDoc.addEventListener('mousemove', updateImageSize); 94 | resizing = true; 95 | } 96 | 97 | function updateImageSize(event) { 98 | event.stopPropagation(); 99 | event.preventDefault(); 100 | 101 | var cursorVerticalPosition = event.pageY; 102 | var newHeight = cursorVerticalPosition - 103 | (elementBeingResized.getBoundingClientRect().top + iframeWindow.pageYOffset); 104 | elementBeingResized.style.height = newHeight + 'px'; 105 | elementBeingResized.style.width = ''; 106 | 107 | if (lastVerticalCursorPosition && event.clientY > lastVerticalCursorPosition 108 | && iframeWindow.innerHeight - event.clientY <= 45) { 109 | iframeWindow.scrollTo(0, iframeWindow.innerHeight); 110 | } 111 | lastVerticalCursorPosition = event.clientY; 112 | updateResizer(); 113 | } 114 | 115 | function createResizer(event, element) { 116 | if (element == resizerContainer || resizing) { 117 | iframeDoc.removeEventListener('mousemove', updateImageSize); 118 | return; 119 | } 120 | if (element.tagName !== 'IMG') { 121 | return removeResizer(); 122 | } 123 | if (!resizerContainer.parentNode) { 124 | iframeBody.appendChild(resizerContainer); 125 | } 126 | elementBeingResized = element; 127 | updateResizer(); 128 | } 129 | 130 | function updateResizer() { 131 | var elementStyle = iframeWindow.getComputedStyle(elementBeingResized); 132 | resizerContainer.style.height = elementStyle.getPropertyValue('height'); 133 | resizerContainer.style.width = elementStyle.getPropertyValue('width'); 134 | resizerContainer.style.top = (elementBeingResized.getBoundingClientRect().top + iframeWindow.pageYOffset) + 'px'; 135 | resizerContainer.style.left = (elementBeingResized.getBoundingClientRect().left + iframeWindow.pageXOffset) + 'px'; 136 | resizerContainer.style.display = 'block'; 137 | } 138 | 139 | function removeResizer(event) { 140 | if (!resizerContainer.parentNode) { 141 | return; 142 | } 143 | if (event && event.target.tagName === 'IMG') { 144 | return; 145 | } 146 | resizerContainer.style.display = 'none'; 147 | lastVerticalCursorPosition = null; 148 | } 149 | }]); -------------------------------------------------------------------------------- /src/js/ngpResizable.js: -------------------------------------------------------------------------------- 1 | angular.module('ngWYSIWYG').directive('ngpResizable', ['$document', function($document) { 2 | return function($scope, $element) { 3 | var doc = $document[0]; 4 | var element = $element[0]; 5 | 6 | var resizeButton = doc.createElement('span'); 7 | resizeButton.className = 'resizer'; 8 | element.appendChild(resizeButton); 9 | 10 | resizeButton.addEventListener('mousedown', function() { 11 | doc.addEventListener('mousemove', resize); 12 | doc.addEventListener('mouseup', stopResizing); 13 | 14 | var iframes = doc.querySelectorAll('iframe'); 15 | for (var i = 0; i < iframes.length; i++) { 16 | iframes[i].contentWindow.document.addEventListener('mouseup', stopResizing); 17 | iframes[i].contentWindow.document.addEventListener('mousemove', resize); 18 | } 19 | 20 | function resize(event) { 21 | event.preventDefault(); 22 | 23 | // Function to manage resize down event 24 | var cursorVerticalPosition = event.pageY; 25 | if (event.view != doc.defaultView) { 26 | // we are hover our iframe 27 | cursorVerticalPosition = event.pageY + 28 | event.view.frameElement.getBoundingClientRect().top + 29 | doc.defaultView.pageYOffset; 30 | } 31 | var height = cursorVerticalPosition - (element.getBoundingClientRect().top + doc.defaultView.pageYOffset); 32 | 33 | var currentHeight = element.style.height.replace('px', ''); 34 | if (currentHeight && currentHeight < height && 35 | window.innerHeight - event.clientY <= 45) { 36 | // scrolling to improve resize usability 37 | doc.defaultView.scrollBy(0, height - currentHeight); 38 | } 39 | 40 | element.style.height = height + 'px'; 41 | } 42 | 43 | function stopResizing() { 44 | doc.removeEventListener('mousemove', resize); 45 | doc.removeEventListener('mouseup', stopResizing); 46 | 47 | var iframes = doc.querySelectorAll('iframe'); 48 | for (var i = 0; i < iframes.length; i++) { 49 | iframes[i].contentWindow.document.removeEventListener('mouseup', stopResizing); 50 | iframes[i].contentWindow.document.removeEventListener('mousemove', resize); 51 | } 52 | } 53 | }); 54 | }; 55 | }]); -------------------------------------------------------------------------------- /src/js/ngpSymbolsGrid.js: -------------------------------------------------------------------------------- 1 | angular.module('ngWYSIWYG').directive('ngpSymbolsGrid', ['NGP_EVENTS', function(NGP_EVENTS) { 2 | var linker = function (scope, element) { 3 | 4 | scope.$on(NGP_EVENTS.CLICK_AWAY, function() { 5 | scope.$apply(function() { 6 | scope.show = false; 7 | }); 8 | }); 9 | 10 | element.parent().bind('click', function(e) { 11 | e.stopPropagation(); 12 | }); 13 | 14 | scope.symbols = [ 15 | '¡', '¿', '–', '—', '»', '«', '©', 16 | '÷', 'µ', '¶', '±', '¢', '€', '£', '®', 17 | '§', '™', '¥', '°', '∀', '∂', '∃', '∅', 18 | '∇', '∈', '∉', '∋', '∏', '∑', '↑', '→', '↓', 19 | '♠', '♣', '♥', '♦', 'á', 'à', 'â', 'å', 20 | 'ã', 'ä', 'æ', 'ç', 'é', 'è', 'ê', 'ë', 21 | 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 22 | 'ô', 'ø', 'õ', 'ö', 'ß', 'ú', 'ù', 23 | 'û', 'ü', 'ÿ' 24 | ]; 25 | 26 | scope.pick = function( symbol ) { 27 | scope.onPick({symbol: symbol}); 28 | }; 29 | 30 | element.ready(function() { 31 | //real deal for IE 32 | function makeUnselectable(node) { 33 | if (node.nodeType == 1) { 34 | node.setAttribute("unselectable", "on"); 35 | node.unselectable = 'on'; 36 | } 37 | var child = node.firstChild; 38 | while (child) { 39 | makeUnselectable(child); 40 | child = child.nextSibling; 41 | } 42 | } 43 | //IE fix 44 | for(var i = 0; i < document.getElementsByClassName('ngp-symbols-grid').length; i += 1) { 45 | makeUnselectable(document.getElementsByClassName("ngp-symbols-grid")[i]); 46 | } 47 | }); 48 | }; 49 | return { 50 | link: linker, 51 | scope: { 52 | show: '=', 53 | onPick: '&' 54 | }, 55 | restrict: 'AE', 56 | template: '
    ' 57 | } 58 | }]); -------------------------------------------------------------------------------- /src/js/ngpUtils.js: -------------------------------------------------------------------------------- 1 | angular.module('ngWYSIWYG').service('ngpUtils', [function() { 2 | var service = this; 3 | 4 | service.getSelectionBoundaryElement = function(win, isStart) { 5 | var range, sel, container = null; 6 | var doc = win.document; 7 | if (doc.selection) { 8 | // IE branch 9 | range = doc.selection.createRange(); 10 | range.collapse(isStart); 11 | return range.parentElement(); 12 | } 13 | else if (doc.getSelection) { 14 | //firefox 15 | sel = doc.getSelection(); 16 | if (sel.rangeCount > 0) { 17 | range = sel.getRangeAt(0); 18 | //console.log(range); 19 | container = range[isStart ? "startContainer" : "endContainer"]; 20 | if (container.nodeType === 3) { 21 | container = container.parentNode; 22 | } 23 | //console.log(container); 24 | } 25 | } 26 | else if (win.getSelection) { 27 | // Other browsers 28 | sel = win.getSelection(); 29 | if (sel.rangeCount > 0) { 30 | range = sel.getRangeAt(0); 31 | container = range[isStart ? "startContainer" : "endContainer"]; 32 | 33 | // Check if the container is a text node and return its parent if so 34 | if (container.nodeType === 3) { 35 | container = container.parentNode; 36 | } 37 | } 38 | } 39 | return container; 40 | }; 41 | }]); -------------------------------------------------------------------------------- /src/js/wysiwyg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('ngWYSIWYG', ['ngSanitize']); 4 | 5 | //debug sanitize 6 | angular.module('ngWYSIWYG').config(['$provide', 7 | //http://odetocode.com/blogs/scott/archive/2014/09/10/a-journey-with-trusted-html-in-angularjs.aspx 8 | function($provide) { 9 | $provide.decorator("$sanitize",['$delegate', '$log', function($delegate, $log) { 10 | return function(text, target) { 11 | var result = $delegate(text, target); 12 | //$log.info("$sanitize input: " + text); 13 | //$log.info("$sanitize output: " + result); 14 | return result; 15 | }; 16 | }]); 17 | } 18 | ]); 19 | 20 | angular.module('ngWYSIWYG').constant('NGP_EVENTS', { 21 | ELEMENT_CLICKED: 'ngp-element-clicked', 22 | CLICK_AWAY: 'ngp-click-away' 23 | }); 24 | -------------------------------------------------------------------------------- /src/js/wysiwygEdit.js: -------------------------------------------------------------------------------- 1 | var editorTemplate = "
    " + 2 | "
    " + 3 | "{toolbar}" + // <-- we gonna replace it with the configured toolbar 4 | "
    " + 5 | "
    " + 6 | "
    " + 7 | "" + 8 | "" + 9 | "
    " + 10 | "
    " + 11 | "
    wysiwygsource
    " + 12 | "
    " + 13 | "
    "; 14 | 15 | angular.module('ngWYSIWYG').directive('wysiwygEdit', ['ngpUtils', 'NGP_EVENTS', '$rootScope', '$compile', '$timeout', '$q', 16 | function(ngpUtils, NGP_EVENTS, $rootScope, $compile, $timeout, $q) { 17 | var linker = function( scope, $element, attrs, ctrl ) { 18 | scope.editMode = false; 19 | scope.cursorStyle = {}; //current cursor/caret position style 20 | 21 | document.addEventListener('click', function() { 22 | $rootScope.$broadcast(NGP_EVENTS.CLICK_AWAY); 23 | }); 24 | 25 | var iframe = null; 26 | var iframeDocument = null; 27 | var iframeWindow = null; 28 | 29 | function loadVars() { 30 | if (iframe != null) return; 31 | iframe = document.querySelector('wysiwyg-edit').querySelector('iframe'); 32 | iframeDocument = iframe.contentDocument; 33 | iframeWindow = iframeDocument.defaultView; 34 | } 35 | 36 | function insertElement(html) { 37 | scope.$broadcast('insertElement', html); 38 | } 39 | 40 | scope.panelButtons = { 41 | '-': { type: 'div', class: 'tinyeditor-divider' }, 42 | bold: { type: 'div', title: 'Bold', class: 'tinyeditor-control', faIcon: 'bold', backgroundPos: '34px -120px', pressed: 'bold', command: 'bold' }, 43 | italic:{type: 'div', title: 'Italic', class: 'tinyeditor-control', faIcon: 'italic', backgroundPos: '34px -150px', pressed: 'italic', command: 'italic' }, 44 | underline:{ type: 'div', title: 'Underline', class: 'tinyeditor-control', faIcon: 'underline', backgroundPos: '34px -180px', pressed: 'underline', command: 'underline' }, 45 | strikethrough:{ type: 'div', title: 'Strikethrough', class: 'tinyeditor-control', faIcon: 'strikethrough', backgroundPos: '34px -210px', pressed: 'strikethrough', command: 'strikethrough' }, 46 | subscript:{ type: 'div', title: 'Subscript', class: 'tinyeditor-control', faIcon: 'subscript', backgroundPos: '34px -240px', pressed: 'sub', command: 'subscript' }, 47 | superscript:{ type: 'div', title: 'Superscript', class: 'tinyeditor-control', faIcon: 'superscript', backgroundPos: '34px -270px', pressed: 'super', command: 'superscript' }, 48 | leftAlign:{ type: 'div', title: 'Left Align', class: 'tinyeditor-control', faIcon: 'align-left', backgroundPos: '34px -420px', pressed: 'alignmet == \'left\'', command: 'justifyleft' }, 49 | centerAlign:{ type: 'div', title: 'Center Align', class: 'tinyeditor-control', faIcon: 'align-center', backgroundPos: '34px -450px', pressed: 'alignment == \'center\'', command: 'justifycenter' }, 50 | rightAlign:{ type: 'div', title: 'Right Align', class: 'tinyeditor-control', faIcon: 'align-right', backgroundPos: '34px -480px', pressed: 'alignment == \'right\'', command: 'justifyright' }, 51 | blockJustify:{ type: 'div', title: 'Block Justify', class: 'tinyeditor-control', faIcon: 'align-justify', backgroundPos: '34px -510px', pressed: 'alignment == \'justify\'', command: 'justifyfull' }, 52 | orderedList:{ type: 'div', title: 'Insert Ordered List', class: 'tinyeditor-control', faIcon: 'list-ol', backgroundPos: '34px -300px', command: 'insertorderedlist' }, 53 | unorderedList:{ type: 'div', title: 'Insert Unordered List', class: 'tinyeditor-control', faIcon: 'list-ul', backgroundPos: '34px -330px', command: 'insertunorderedlist' }, 54 | outdent:{ type: 'div', title: 'Outdent', class: 'tinyeditor-control', faIcon: 'outdent', backgroundPos: '34px -360px', command: 'outdent' }, 55 | indent:{ type: 'div', title: 'Indent', class: 'tinyeditor-control', faIcon: 'indent', backgroundPos: '34px -390px', command: 'indent' }, 56 | removeFormatting:{ type: 'div', title: 'Remove Formatting', class: 'tinyeditor-control', faIcon: 'eraser', backgroundPos: '34px -720px', command: 'removeformat' }, 57 | undo:{ type: 'div', title: 'Undo', class: 'tinyeditor-control', faIcon: 'undo', backgroundPos: '34px -540px', command: 'undo' }, 58 | redo:{ type: 'div', title: 'Redo', class: 'tinyeditor-control', faIcon: 'repeat', backgroundPos: '34px -570px', command: 'redo' }, 59 | fontColor:{ type: 'div', title: 'Font Color', class: 'tinyeditor-control', faIcon: 'font', backgroundPos: '34px -779px', specialCommand: 'showFontColors = !showFontColors', inner: '' }, 60 | backgroundColor:{ type: 'div', title: 'Background Color', class: 'tinyeditor-control', faIcon: 'paint-brush', backgroundPos:'34px -808px', specialCommand: 'showBgColors = !showBgColors', inner: '' }, 61 | image:{ type: 'div', title: 'Insert Image', class: 'tinyeditor-control', faIcon: 'picture-o', backgroundPos: '34px -600px', specialCommand: 'insertImage()' }, 62 | hr:{ type: 'div', title: 'Insert Horizontal Rule', class: 'tinyeditor-control', faIcon: '-', backgroundPos: '34px -630px', command: 'inserthorizontalrule' }, 63 | symbols:{ type: 'div', title: 'Insert Special Symbol', class: 'tinyeditor-control', faIcon: 'cny', backgroundPos: '34px -838px', specialCommand: 'showSpecChars = !showSpecChars', inner: '' }, 64 | link:{ type: 'div', title: 'Insert Hyperlink', class: 'tinyeditor-control', faIcon: 'link', backgroundPos: '34px -660px', specialCommand: 'insertLink()' }, 65 | unlink:{ type: 'div', title: 'Remove Hyperlink', class: 'tinyeditor-control', faIcon: 'chain-broken', backgroundPos: '34px -690px', command: 'unlink' }, 66 | print:{ type: 'div', title: 'Print', class: 'tinyeditor-control', faIcon: 'print', backgroundPos: '34px -750px', command: 'print' }, 67 | font:{ type: 'select', title: 'Font', class: 'tinyeditor-font', model: 'font', options: 'a as a for a in fonts', change: 'fontChange()' }, 68 | size:{ type: 'select', title: 'Size', class: 'tinyeditor-size', model: 'fontsize', options: 'a.key as a.name for a in fontsizes', change: 'sizeChange()' }, 69 | format:{ type: 'select', title: 'Style', class: 'tinyeditor-size', model: 'textstyle', options: 's.key as s.name for s in styles', change: 'styleChange()' } 70 | }; 71 | 72 | var usingFontAwesome = scope.config && scope.config.fontAwesome; 73 | 74 | function getButtonHtml(button) { 75 | var html = '<' + button.type; 76 | html += ' class="' + button.class; 77 | if (usingFontAwesome) { 78 | html += ' tinyeditor-control-fa'; 79 | } 80 | html += '" '; 81 | if (button.type == 'div') { 82 | if (button.title) { 83 | html += 'title="' + button.title + '" '; 84 | } 85 | if (button.backgroundPos && !usingFontAwesome) { 86 | html += 'style="background-position: ' + button.backgroundPos + '; position: relative;" '; 87 | } 88 | if (button.pressed) { 89 | html += 'ng-class="{\'pressed\': cursorStyle.' + button.pressed + '}" '; 90 | } 91 | if (button.command) { 92 | var executable = '\'' + button.command + '\''; 93 | if (button.commandParameter) { 94 | executable += ', \'' + button.commandParameter + '\''; 95 | } 96 | html += 'ng-click="execCommand(' + executable + ')" '; 97 | } else if (button.specialCommand) { 98 | html += 'ng-click="' + button.specialCommand + '" '; 99 | } 100 | html += '>'; // this closes
    101 | if (button.faIcon && usingFontAwesome && button.faIcon != '-') { 102 | html += ''; 103 | } 104 | if (button.faIcon && usingFontAwesome && button.faIcon == '-') { 105 | html += '
    '; 106 | } 107 | if (button.inner) { 108 | html+= button.inner; 109 | } 110 | } else if (button.type == 'select') { 111 | html += 'ng-model="' + button.model + '" '; 112 | html += 'ng-options="' + button.options + '" '; 113 | html += 'ng-change="' + button.change + '" '; 114 | html += ''; 115 | } 116 | html += ''; 117 | return html; 118 | } 119 | 120 | //show all panels by default 121 | scope.toolbar = (scope.config && scope.config.toolbar)? scope.config.toolbar : [ 122 | { name: 'basicStyling', items: ['bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'leftAlign', 'centerAlign', 'rightAlign', 'blockJustify', '-'] }, 123 | { name: 'paragraph', items: ['orderedList', 'unorderedList', 'outdent', 'indent', '-'] }, 124 | { name: 'doers', items: ['removeFormatting', 'undo', 'redo', '-'] }, 125 | { name: 'colors', items: ['fontColor', 'backgroundColor', '-'] }, 126 | { name: 'links', items: ['image', 'hr', 'symbols', 'link', 'unlink', '-'] }, 127 | { name: 'tools', items: ['print', '-'] }, 128 | { name: 'styling', items: ['font', 'size', 'format'] } 129 | ]; 130 | //compile the template 131 | var toolbarGroups = []; 132 | angular.forEach(scope.toolbar, function(buttonGroup, index) { 133 | var buttons = []; 134 | angular.forEach(buttonGroup.items, function(button, index) { 135 | var newButton = scope.panelButtons[button]; 136 | if (!newButton) { 137 | // checks if it is a button defined by the user 138 | newButton = scope.config.buttons[button]; 139 | } 140 | this.push( getButtonHtml(newButton) ); 141 | }, buttons); 142 | this.push( 143 | "
    " + 144 | buttons.join('') + 145 | "
    " 146 | ); 147 | }, toolbarGroups); 148 | 149 | var template = editorTemplate.replace('{toolbar}', toolbarGroups.join('')); 150 | template = template.replace('{contentStyle}', attrs.contentStyle || ''); 151 | //$element.replaceWith( angular.element($compile( editorTemplate.replace('{toolbar}', toolbarGroups.join('') ) )(scope)) ); 152 | $element.html( template ); 153 | $compile($element.contents())(scope); 154 | 155 | /* 156 | * send the event to the iframe's controller to exec the command 157 | */ 158 | scope.execCommand = function(cmd, arg) { 159 | //console.log('execCommand'); 160 | //scope.$emit('execCommand', {command: cmd, arg: arg}); 161 | switch(cmd) { 162 | case 'bold': 163 | scope.cursorStyle.bold = !scope.cursorStyle.bold; 164 | break; 165 | case 'italic': 166 | scope.cursorStyle.italic = !scope.cursorStyle.italic; 167 | break; 168 | case 'underline': 169 | scope.cursorStyle.underline = !scope.cursorStyle.underline; 170 | break; 171 | case 'strikethrough': 172 | scope.cursorStyle.strikethrough = !scope.cursorStyle.strikethrough; 173 | break; 174 | case 'subscript': 175 | scope.cursorStyle.sub = !scope.cursorStyle.sub; 176 | break; 177 | case 'superscript': 178 | scope.cursorStyle.super = !scope.cursorStyle.super; 179 | break; 180 | case 'justifyleft': 181 | scope.cursorStyle.alignment = 'left'; 182 | break; 183 | case 'justifycenter': 184 | scope.cursorStyle.alignment = 'center'; 185 | break; 186 | case 'justifyright': 187 | scope.cursorStyle.alignment = 'right'; 188 | break; 189 | case 'justifyfull': 190 | scope.cursorStyle.alignment = 'justify'; 191 | break; 192 | } 193 | //console.log(scope.cursorStyle); 194 | scope.$broadcast('execCommand', {command: cmd, arg: arg}); 195 | }; 196 | 197 | 198 | scope.fonts = ['Verdana','Arial', 'Arial Black', 'Arial Narrow', 'Courier New', 'Century Gothic', 'Comic Sans MS', 'Georgia', 'Impact', 'Tahoma', 'Times', 'Times New Roman', 'Webdings','Trebuchet MS']; 199 | /* 200 | scope.$watch('font', function(newValue) { 201 | if(newValue) { 202 | scope.execCommand( 'fontname', newValue ); 203 | scope.font = ''; 204 | } 205 | }); 206 | */ 207 | scope.fontChange = function() { 208 | scope.execCommand( 'fontname', scope.font ); 209 | //scope.font = ''; 210 | }; 211 | scope.fontsizes = [{key: 1, name: 'x-small'}, {key: 2, name: 'small'}, {key: 3, name: 'normal'}, {key: 4, name: 'large'}, {key: 5, name: 'x-large'}, {key: 6, name: 'xx-large'}, {key: 7, name: 'xxx-large'}]; 212 | scope.mapFontSize = { 10: 1, 13: 2, 16: 3, 18: 4, 24: 5, 32: 6, 48: 7}; 213 | scope.sizeChange = function() { 214 | scope.execCommand( 'fontsize', scope.fontsize ); 215 | }; 216 | /* 217 | scope.$watch('fontsize', function(newValue) { 218 | if(newValue) { 219 | scope.execCommand( 'fontsize', newValue ); 220 | scope.fontsize = ''; 221 | } 222 | }); 223 | */ 224 | scope.styles = [{name: 'Paragraph', key: '

    '}, {name: 'Header 1', key: '

    '}, {name: 'Header 2', key: '

    '}, {name: 'Header 3', key: '

    '}, {name: 'Header 4', key: '

    '}, {name: 'Header 5', key: '

    '}, {name: 'Header 6', key: '
    '}]; 225 | scope.styleChange = function() { 226 | scope.execCommand( 'formatblock', scope.textstyle ); 227 | }; 228 | /* 229 | scope.$watch('textstyle', function(newValue) { 230 | if(newValue) { 231 | scope.execCommand( 'formatblock', newValue ); 232 | scope.fontsize = ''; 233 | } 234 | }); 235 | */ 236 | scope.showFontColors = false; 237 | scope.setFontColor = function( color ) { 238 | scope.execCommand('foreColor', color); 239 | }; 240 | scope.showBgColors = false; 241 | scope.setBgColor = function( color ) { 242 | scope.execCommand('hiliteColor', color); 243 | }; 244 | 245 | scope.showSpecChars = false; 246 | scope.insertSpecChar = function(symbol) { 247 | insertElement(symbol); 248 | }; 249 | scope.insertLink = function() { 250 | loadVars(); 251 | if (iframeWindow.getSelection().focusNode == null) return; // user should at least click the editor 252 | var elementBeingEdited = ngpUtils.getSelectionBoundaryElement(iframeWindow, true); 253 | var defaultUrl = 'http://'; 254 | if (elementBeingEdited && elementBeingEdited.nodeName == 'A') { 255 | defaultUrl = elementBeingEdited.href; 256 | 257 | // now we select the whole a tag since it makes no sense to add a link inside another link 258 | var selectRange = iframeDocument.createRange(); 259 | selectRange.setStart(elementBeingEdited.firstChild, 0); 260 | selectRange.setEnd(elementBeingEdited.firstChild, elementBeingEdited.firstChild.length); 261 | var selection = iframeWindow.getSelection(); 262 | selection.removeAllRanges(); 263 | selection.addRange(selectRange); 264 | } 265 | var val; 266 | if(scope.api && scope.api.insertLink && angular.isFunction(scope.api.insertLink)) { 267 | val = scope.api.insertLink.apply( scope.api.scope || null, [defaultUrl]); 268 | } else { 269 | val = prompt('Please enter the URL', 'http://'); 270 | } 271 | //resolve the promise if any 272 | $q.when(val).then(function(data) { 273 | scope.execCommand('createlink', data); 274 | }); 275 | }; 276 | /* 277 | * insert 278 | */ 279 | scope.insertImage = function() { 280 | var val; 281 | if(scope.api && scope.api.insertImage && angular.isFunction(scope.api.insertImage)) { 282 | val = scope.api.insertImage.apply( scope.api.scope || null ); 283 | } 284 | else { 285 | val = prompt('Please enter the picture URL', 'http://'); 286 | val = ''; //we convert into HTML element. 287 | } 288 | //resolve the promise if any 289 | $q.when(val).then(function(data) { 290 | insertElement(data); 291 | }); 292 | }; 293 | $element.ready(function() { 294 | function makeUnselectable(node) { 295 | if (node.nodeType == 1) { 296 | node.setAttribute("unselectable", "on"); 297 | node.unselectable = 'on'; 298 | } 299 | var child = node.firstChild; 300 | while (child) { 301 | makeUnselectable(child); 302 | child = child.nextSibling; 303 | } 304 | } 305 | //IE fix 306 | for(var i = 0; i < document.getElementsByClassName('tinyeditor-header').length; i += 1) { 307 | makeUnselectable(document.getElementsByClassName("tinyeditor-header")[i]); 308 | } 309 | }); 310 | //catch the cursort position style 311 | scope.$on('cursor-position', function(event, data) { 312 | //console.log('cursor-position', data); 313 | scope.cursorStyle = data; 314 | scope.font = data.font.replace(/(')/g, ''); //''' replace single quotes 315 | scope.fontsize = scope.mapFontSize[data.size]? scope.mapFontSize[data.size] : 0; 316 | }); 317 | }; 318 | return { 319 | link: linker, 320 | scope: { 321 | content: '=', //this is our content which we want to edit 322 | api: '=', //this is our api object 323 | config: '=' 324 | }, 325 | restrict: 'AE', 326 | replace: true 327 | } 328 | } 329 | ]); -------------------------------------------------------------------------------- /src/tests/conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | seleniumAddress: 'http://localhost:4444/wd/hub' 3 | }; -------------------------------------------------------------------------------- /src/tests/load-spec.js: -------------------------------------------------------------------------------- 1 | describe('ngWYSIWYG', function() { 2 | var iframeEditor, modelContent; 3 | 4 | beforeEach(function() { 5 | browser.get('http://localhost:8000/'); 6 | iframeEditor = element(by.tagName('iframe')); 7 | modelContent = element(by.binding('content')); 8 | }); 9 | 10 | it('should load the editor', function() { 11 | expect(iframeEditor).not.toBe(null); 12 | expect(modelContent.getText()).not.toBe(null); 13 | expect(modelContent.getText()).toBe('

    Hello world!

    '); 14 | }); 15 | 16 | it('should accept input', function() { 17 | iframeEditor.click(); 18 | iframeEditor.sendKeys(' We are ngWYSIWYG'); 19 | expect(modelContent.getText()).toBe('

    Hello world! We are ngWYSIWYG

    '); 20 | }); 21 | 22 | it('should accept image insertion', function() { 23 | // create a new line 24 | iframeEditor.click(); 25 | iframeEditor.sendKeys(protractor.Key.ENTER); 26 | 27 | // click on insertImage button 28 | var button = element(by.css('[ng-click="insertImage()"]')); 29 | button.click(); 30 | 31 | // fulfill prompt 32 | browser.wait(protractor.ExpectedConditions.alertIsPresent(), 1000); 33 | var imagePrompt = browser.switchTo().alert(); 34 | imagePrompt.sendKeys('https://www.codementor.io/assets/page_img/learn-javascript.png'); 35 | imagePrompt.accept(); 36 | 37 | // check if it was added 38 | expect(modelContent.getText()).toBe('

    Hello world!

    ' + 39 | '

    '); 40 | }); 41 | }); --------------------------------------------------------------------------------