public
Last active

AngularJs-twitterBootstrap-wysiHtml5

  • Download Gist
app.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
var demoApp = angular.module('demoApp', ['ngResource'], function($locationProvider) {
$locationProvider.hashPrefix('');
});
 
function MainCtrl($scope, Serv) {
$scope.selectedItem = {
value: 0,
label: ''
};
$scope.Wrapper = Serv;
}
demoApp.directive('richTextEditor', function( $log, $location ) {
 
var self = this;
var directive = {
restrict : "A",
replace : true,
transclude : true,
scope : {
 
},
template :
 
"<div>" +
 
"<textarea id=\"richtexteditor-content\" style=\"height:300px;width:100%\"></textarea>"+
 
"</div>",
 
link : function( $scope, $element, $attrs ) {
$scope.editor = $('#richtexteditor-content').wysihtml5();
 
 
// $scope.editor = new wysihtml5.Editor( "richtexteditor-content", {
 
// toolbar : "richtexteditor-toolbar",
// parserRules: wysihtml5ParserRules
// });
$scope.$parent.$watch( $attrs.content, function( newValue, oldValue ) {
$scope.editor.innerHTML = newValue;
$scope.editor.composer.setValue( newValue );
});
$scope.cancel = function() {
$scope.$parent.cancel();
}
/* $scope.save = function() {
var currentTemplateContent = $encryption.encodeHtml( $scope.editor.getValue() );
$scope.$parent.currentTemplate.content = currentTemplateContent;
$scope.$parent.save();
}
*/
$scope.isClean = function() {
$scope.$parent.isClean();
}
}
}
return directive;
});
bootstrap-wysiHtml5.css
CSS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
ul.wysihtml5-toolbar {
margin: 0;
padding: 0;
display: block;
}
 
ul.wysihtml5-toolbar::after {
clear: both;
display: table;
content: "";
}
 
ul.wysihtml5-toolbar > li {
float: left;
display: list-item;
list-style: none;
margin: 0 5px 10px 0;
}
 
ul.wysihtml5-toolbar a[data-wysihtml5-command=bold] {
font-weight: bold;
}
 
ul.wysihtml5-toolbar a[data-wysihtml5-command=italic] {
font-style: italic;
}
 
ul.wysihtml5-toolbar a[data-wysihtml5-command=underline] {
text-decoration: underline;
}
 
ul.wysihtml5-toolbar a.btn.wysihtml5-command-active {
background-image: none;
-webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
-moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);
background-color: #E6E6E6;
background-color: #D9D9D9 9;
outline: 0;
}
 
ul.wysihtml5-commands-disabled .dropdown-menu {
display: none !important;
}
bootstrap-wysihtml5.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
!function($, wysi) {
"use strict";
 
var templates = {
"font-styles": "<li class='dropdown'>" +
"<a class='btn dropdown-toggle' data-toggle='dropdown' href='#'>" +
"<i class='icon-font'></i>&nbsp;<span class='current-font'>Normal text</span>&nbsp;<b class='caret'></b>" +
"</a>" +
"<ul class='dropdown-menu'>" +
"<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='div'>Normal text</a></li>" +
"<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h1'>Heading 1</a></li>" +
"<li><a data-wysihtml5-command='formatBlock' data-wysihtml5-command-value='h2'>Heading 2</a></li>" +
"</ul>" +
"</li>",
"emphasis": "<li>" +
"<div class='btn-group'>" +
"<a class='btn' data-wysihtml5-command='bold' title='CTRL+B'>Bold</a>" +
"<a class='btn' data-wysihtml5-command='italic' title='CTRL+I'>Italic</a>" +
"<a class='btn' data-wysihtml5-command='underline' title='CTRL+U'>Underline</a>" +
"</div>" +
"</li>",
"lists": "<li>" +
"<div class='btn-group'>" +
"<a class='btn' data-wysihtml5-command='insertUnorderedList' title='Unordered List'><i class='icon-list'></i></a>" +
"<a class='btn' data-wysihtml5-command='insertOrderedList' title='Ordered List'><i class='icon-th-list'></i></a>" +
"<a class='btn' data-wysihtml5-command='Outdent' title='Outdent'><i class='icon-indent-right'></i></a>" +
"<a class='btn' data-wysihtml5-command='Indent' title='Indent'><i class='icon-indent-left'></i></a>" +
"</div>" +
"</li>",
"link": "<li>" +
"<div class='bootstrap-wysihtml5-insert-link-modal modal hide fade'>" +
"<div class='modal-header'>" +
"<a class='close' data-dismiss='modal'>&times;</a>" +
"<h3>Insert Link</h3>" +
"</div>" +
"<div class='modal-body'>" +
"<input value='http://' class='bootstrap-wysihtml5-insert-link-url input-xlarge'>" +
"</div>" +
"<div class='modal-body'>" +
"<h5>Anchor Text</h5>" +
"<input value='' class='anchorText input-xlarge'>" +
"</div>" +
"<div class='modal-footer'>" +
"<a href='#' class='btn' data-dismiss='modal'>Cancel</a>" +
"<a href='#' class='btn btn-primary2' data-dismiss='modal'>Insert link</a>" +
"</div>" +
"</div>" +
"<a class='btn' data-wysihtml5-command='createLink' title='Link'><i class='icon-share'></i></a>" +
"</li>",
"image": "<li>" +
"<div class='bootstrap-wysihtml5-insert-image-modal modal hide fade'>" +
"<div class='modal-header'>" +
"<a class='close' data-dismiss='modal'>&times;</a>" +
"<h3>Insert Image</h3>" +
"</div>" +
"<div class='modal-body'>" +
"<input value='http://' class='bootstrap-wysihtml5-insert-image-url input-xlarge'>" +
"</div>" +
"<div class='modal-footer'>" +
"<a href='#' class='btn' data-dismiss='modal'>Cancel</a>" +
"<a href='#' class='btn btn-primary' data-dismiss='modal'>Insert image</a>" +
"</div>" +
"</div>" +
"<a class='btn' data-wysihtml5-command='insertImage' title='Insert image'><i class='icon-picture'></i></a>" +
"</li>",
 
"html":
"<li>" +
"<div class='btn-group'>" +
"<a class='btn' data-wysihtml5-action='change_view' title='Edit HTML'><i class='icon-pencil'></i></a>" +
"</div>" +
"</li>"
};
 
var defaultOptions = {
"font-styles": true,
"emphasis": true,
"lists": true,
"html": true,
"link": true,
"image": true,
events: {},
parserRules: {
tags: {
"b": {},
"i": {},
"br": {},
"ol": {},
"ul": {},
"li": {},
"h1": {},
"h2": {},
"blockquote": {},
"u": 1,
"img": {
"check_attributes": {
"width": "numbers",
"alt": "alt",
"src": "url",
"height": "numbers"
}
},
"a": {
set_attributes: {
target: "_blank",
rel: "nofollow"
},
check_attributes: {
href: "url" // important to avoid XSS
}
}
}
},
stylesheets: []
};
 
var Wysihtml5 = function(el, options) {
this.el = el;
this.toolbar = this.createToolbar(el, options || defaultOptions);
this.editor = this.createEditor(options);
 
window.editor = this.editor;
 
$('iframe.wysihtml5-sandbox').each(function(i, el){
$(el.contentWindow).off('focus.wysihtml5').on({
'focus.wysihtml5' : function(){
$('li.dropdown').removeClass('open');
}
});
});
};
 
Wysihtml5.prototype = {
 
constructor: Wysihtml5,
 
createEditor: function(options) {
options = $.extend(defaultOptions, options || {});
options.toolbar = this.toolbar[0];
 
var editor = new wysi.Editor(this.el[0], options);
 
if(options && options.events) {
for(var eventName in options.events) {
editor.on(eventName, options.events[eventName]);
}
}
 
return editor;
},
 
createToolbar: function(el, options) {
var self = this;
var toolbar = $("<ul/>", {
'class' : "wysihtml5-toolbar",
'style': "display:none"
});
 
for(var key in defaultOptions) {
var value = false;
 
if(options[key] !== undefined) {
if(options[key] === true) {
value = true;
}
} else {
value = defaultOptions[key];
}
 
if(value === true) {
toolbar.append(templates[key]);
 
if(key == "html") {
this.initHtml(toolbar);
}
 
if(key == "link") {
this.initInsertLink(toolbar);
}
 
if(key == "image") {
this.initInsertImage(toolbar);
}
}
}
 
toolbar.find("a[data-wysihtml5-command='formatBlock']").click(function(e) {
var el = $(e.srcElement);
self.toolbar.find('.current-font').text(el.html());
});
 
this.el.before(toolbar);
 
return toolbar;
},
 
initHtml: function(toolbar) {
var changeViewSelector = "a[data-wysihtml5-action='change_view']";
toolbar.find(changeViewSelector).click(function(e) {
toolbar.find('a.btn').not(changeViewSelector).toggleClass('disabled');
});
},
 
initInsertImage: function(toolbar) {
var self = this;
var insertImageModal = toolbar.find('.bootstrap-wysihtml5-insert-image-modal');
var urlInput = insertImageModal.find('.bootstrap-wysihtml5-insert-image-url');
var insertButton = insertImageModal.find('a.btn-primary2');
var initialValue = urlInput.val();
 
var insertImage = function() {
var url = urlInput.val();
urlInput.val(initialValue);
self.editor.composer.commands.exec("insertImage", url);
};
 
urlInput.keypress(function(e) {
if(e.which == 13) {
insertImage();
insertImageModal.modal('hide');
}
});
 
insertButton.click(insertImage);
 
insertImageModal.on('shown', function() {
urlInput.focus();
});
 
insertImageModal.on('hide', function() {
self.editor.currentView.element.focus();
});
 
toolbar.find('a[data-wysihtml5-command=insertImage]').click(function() {
insertImageModal.modal('show');
insertImageModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) {
e.stopPropagation();
});
return false;
});
},
 
initInsertLink: function(toolbar) {
var self = this;
var insertLinkModal = toolbar.find('.bootstrap-wysihtml5-insert-link-modal');
var urlInput = insertLinkModal.find('.bootstrap-wysihtml5-insert-link-url');
var anchorText = insertLinkModal.find('.anchorText');
var insertButton = insertLinkModal.find('a.btn-primary2');
var initialValue = urlInput.val();
 
var insertLink = function() {
var url = urlInput.val();
urlInput.val(anchorText.val());
self.editor.composer.commands.exec("createLink", {
href: url,
target: "_blank",
rel: "nofollow",
value: anchorText.val(),
});
};
var pressedEnter = false;
 
urlInput.keypress(function(e) {
if(e.which == 13) {
insertLink();
insertLinkModal.modal('hide');
}
});
 
insertButton.click(insertLink);
 
insertLinkModal.on('shown', function() {
urlInput.focus();
});
 
insertLinkModal.on('hide', function() {
self.editor.currentView.element.focus();
});
 
toolbar.find('a[data-wysihtml5-command=createLink]').click(function() {
insertLinkModal.modal('show');
insertLinkModal.on('click.dismiss.modal', '[data-dismiss="modal"]', function(e) {
e.stopPropagation();
});
return false;
});
 
 
}
};
 
$.fn.wysihtml5 = function (options) {
return this.each(function () {
var $this = $(this);
$this.data('wysihtml5', new Wysihtml5($this, options));
});
};
 
$.fn.wysihtml5.Constructor = Wysihtml5;
 
}(window.jQuery, window.wysihtml5);
index.html
HTML
1 2 3 4 5 6 7 8 9 10 11
<script type="text/javascript" src="http://docs.angularjs.org/angular-resource-1.0.1.min.js"></script>
<script type="text/javascript" src="bootstrap-wysiHtml5.js"></script>
<link rel="stylesheet" type="text/css" href="http://angular-ui.github.com/lib/bootstrap/docs/assets/css/bootstrap.css" >
 
<link rel="stylesheet" type="text/css" href="bootstrap-wysiHtml5.css" >
<div lang="en" ng-app="demoApp" ng-controller="MainCtrl">
<div rich-text-editor></div>
<button class="btn btn-primary">Click Me</button>
</div>
 
‚Äč

This was super helpful, thanks!

I've found I can shorten it to:

.directive 'richTextEditor', ->
  return {
    restrict: 'A'
    replace: true
    require: '?ngModel'
    transclude: true
    template: '<div><textarea></textarea></div>' # Wrapper <div> required.
    link: (scope, element, attrs, controller) ->
      textarea = element.find('textarea').wysihtml5(
        stylesheets: null # Prevents a console error, likely a bug.
      )
      editor = textarea.data('wysihtml5').editor

      # Sync view -> model (took me 2 hours to figure this out)
      editor.on 'change', ->
        controller.$setViewValue editor.getValue()

      # Sync model -> view
      scope.$watch attrs.ngModel, (newValue, oldValue) ->
        textarea.html(newValue)
        editor.setValue newValue
  }

Just what I needed!

Thanks for the snippets! Works great. @statico I had another part of my page that was displaying the model's data so I added scope.$apply in the change listener for it to update the view.

Thanks a lot! that helped :)
@statico -

stylesheets: null # Prevents a console error, likely a bug.

its the path to wysiwyg-color.css on your server if the default ./lib/wysiwyg-color.css does not work for you

Now i have written the following code for data-bound bootstrap-wysihtml5. Seems to be working fine. I see the following issues.
1. the content change is reflected only on change. that is fired only on blur. can we use keyup and paste events somehow?
2. mine is a single page app with multiple editors in same page. when i try to change order of editors the content goes missing :( on refresh though the content is back. there is no error message on console. anyone faced this issue?
3. on trying to remove the editor from dom you get an error message. the editor tries to keep the div & textarea in sync which breaks because it isnt there anymore. anyone tried fixing this?

app.directive('richTextEditor', function() {
    return {
        restrict : "A",
        require : 'ngModel',
        replace : true,
        transclude : true,
        template : "<div><textarea></textarea></div>",
        link : function(scope, element, attrs, ctrl) {
            var textarea = $(element.find('textarea')).wysihtml5();
            var editor = textarea.data('wysihtml5').editor;

            // view -> model
            editor.on('change', function() {
                scope.$apply(function() {
                    ctrl.$setViewValue(editor.getValue());
                });
            });

            // model -> view
            ctrl.$render = function() {
                textarea.html(ctrl.$viewValue);
                editor.setValue(ctrl.$viewValue);
            };

            /* - similar to above
            scope.$watch(attrs.ngModel, function(newValue, oldValue) {
                textarea.html(newValue);
                editor.setValue(newValue);
            });
            */

            // load init value from DOM
            ctrl.$render();
        }
    };
});

I can't seem to get this working, here is my plunk! http://plnkr.co/edit/ezzDZy190ozvN1TQUt2d

activate the html mode then change to it and you will see that all the content in the attached model is lost.. it's back again after you exit the html mode and write something in the textarea

This version works with the HTML mode.

App.directive('richTextEditor', function() {
    return {
        restrict : "A",
        require : 'ngModel',
        //replace : true,
        transclude : true,
        //template : '<div><textarea></textarea></div>',
        link : function(scope, element, attrs, ctrl) {

          var textarea = element.wysihtml5({"html": true});

          var editor = textarea.data('wysihtml5').editor;

          // view -> model
          editor.on('change', function() {
              if(editor.getValue())
              scope.$apply(function() {
                  ctrl.$setViewValue(editor.getValue());
              });
          });

          // model -> view
          ctrl.$render = function() {
            textarea.html(ctrl.$viewValue);
            editor.setValue(ctrl.$viewValue);
          };

          ctrl.$render();
        }
    };
});
<textarea rich-text-editor ng-model="data"></textarea>

Really great stuff! Is it possible to prevent user to paste rich html ? I would like on paste to get only plain text inside editor.

@NenadP - I have the same problem with similar directive - the users paste from Word and everything stops working.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.