Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
AngularJs-twitterBootstrap-wysiHtml5
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;
});
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;
}
!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);
<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>
@statico

This comment has been minimized.

Show comment Hide comment
@statico

statico May 24, 2013

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
  }

statico commented May 24, 2013

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
  }
@robertklep

This comment has been minimized.

Show comment Hide comment
@robertklep

robertklep Jun 3, 2013

Just what I needed!

Just what I needed!

@ffky

This comment has been minimized.

Show comment Hide comment
@ffky

ffky Jun 5, 2013

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.

ffky commented Jun 5, 2013

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.

@srinivasupadhya

This comment has been minimized.

Show comment Hide comment
@srinivasupadhya

srinivasupadhya Jun 14, 2013

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();
        }
    };
});

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();
        }
    };
});
@arnoutaertgeerts

This comment has been minimized.

Show comment Hide comment
@arnoutaertgeerts

arnoutaertgeerts Aug 22, 2013

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

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

@esgy

This comment has been minimized.

Show comment Hide comment
@esgy

esgy Dec 24, 2013

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

esgy commented Dec 24, 2013

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

@esgy

This comment has been minimized.

Show comment Hide comment
@esgy

esgy Dec 30, 2013

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>

esgy commented Dec 30, 2013

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>
@NenadP

This comment has been minimized.

Show comment Hide comment
@NenadP

NenadP Feb 6, 2014

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 commented Feb 6, 2014

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.

@pavelnikolov

This comment has been minimized.

Show comment Hide comment
@pavelnikolov

pavelnikolov Mar 14, 2014

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

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

@SergeyNarozhnyy

This comment has been minimized.

Show comment Hide comment
@SergeyNarozhnyy

SergeyNarozhnyy Apr 5, 2015

@joshkurz, what about Serv injection in the MainCtrl? I don't see any provider code...

@joshkurz, what about Serv injection in the MainCtrl? I don't see any provider code...

@anishmm

This comment has been minimized.

Show comment Hide comment
@anishmm

anishmm Apr 7, 2015

Templates text style making problem , the hash tag redirect the root url ("<a class='btn dropdown-toggle' data-toggle='dropdown' href='#'>" ) please share a solution

anishmm commented Apr 7, 2015

Templates text style making problem , the hash tag redirect the root url ("<a class='btn dropdown-toggle' data-toggle='dropdown' href='#'>" ) please share a solution

@yokiardzian

This comment has been minimized.

Show comment Hide comment
@yokiardzian

yokiardzian Oct 26, 2016

i Got error :

angular.js:13236 TypeError: **Cannot read property 'Editor' of undefined**
    at Wysihtml5.createEditor (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:141:34)
    at new Wysihtml5 (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:120:28)
    at HTMLTextAreaElement.<anonymous> (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:296:37)
    at Function.each (http://localhost:42336/Scripts/jquery-2.2.0.js:360:19)
    at jQuery.each (http://localhost:42336/Scripts/jquery-2.2.0.js:137:17)
    at jQuery.$.fn.wysihtml5 (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:294:21)
    at link (http://localhost:42336/Scripts/App/app.js:127:70)
    at http://localhost:42336/Scripts/angular.min.js:78:461
    at ka (http://localhost:42336/Scripts/angular.min.js:79:16)
    at u (http://localhost:42336/Scripts/angular.min.js:66:326) <div rich-text-editor="" class="ng-isolate-scope">

yokiardzian commented Oct 26, 2016

i Got error :

angular.js:13236 TypeError: **Cannot read property 'Editor' of undefined**
    at Wysihtml5.createEditor (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:141:34)
    at new Wysihtml5 (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:120:28)
    at HTMLTextAreaElement.<anonymous> (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:296:37)
    at Function.each (http://localhost:42336/Scripts/jquery-2.2.0.js:360:19)
    at jQuery.each (http://localhost:42336/Scripts/jquery-2.2.0.js:137:17)
    at jQuery.$.fn.wysihtml5 (http://localhost:42336/Scripts/bootstrap-wysihtml5.js:294:21)
    at link (http://localhost:42336/Scripts/App/app.js:127:70)
    at http://localhost:42336/Scripts/angular.min.js:78:461
    at ka (http://localhost:42336/Scripts/angular.min.js:79:16)
    at u (http://localhost:42336/Scripts/angular.min.js:66:326) <div rich-text-editor="" class="ng-isolate-scope">
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment